Skip to content

Repository files navigation

🌙 Noct Programming Language

Noct is a tiny yet mighty programming language for:

  • Application integration such as sandboxed game scripting (ANSI C, no external dependencies)
  • Small systems such as embedded Linux in flash memory

Its syntax is lightweight, but the runtime is built for high-end performance.

  • Automatic loop-vectorizing SIMD acceleration
  • Automatic loop-parallelizing GPU acceleration

Small enough to learn today, powerful enough to ship tomorrow!


Feature Highlights

Small, Fast, and Robust

Only about 200 KB — with a fast JIT compiler, a robust generational GC, and a clean C/JS-like syntax featuring a novel Dictionary-based OOP model.

  • L0: 100KB with Interpreter
  • L1: 200KB with Interpreter + JIT
  • L2: 400KB with Interpreter + JIT + Optimization
  • L3: 660KB with Interpreter + JIT + Optimization + SIMD
  • L4: 770KB with Interpreter + JIT + Optimization + SIMD + GPU

The baseline JIT execution (L1) is typically 4-13x faster than interpreter execution (L0). SIMD (L3) and GPU (L4) optimizations are 100-350x faster than L0 in the image processing area.

Portable

Written in portable ANSI C with no external dependencies, it runs virtually everywhere — from desktop PCs down to Raspberry Pi.

Remarkably, Noct runs well on MS-DOS, even with the JIT compiler enabled!

Embeddable

Noct can be easily embedded into your applications.

By adding your own APIs, you can build a customized scripting runtime, and you are free to rebrand Noct as your own scripting language.

For example, in Playfield Engine, and Suika3, they integrate Noct with game-specific APIs and refer to it as Ray scripting.

High Performance Optimization

Automatic SIMD vectorization backed by target-neutral loop, DOALL, and reduction analyses.

Also, GPU parallelzation is available for Direct3D 12, OpenGL ES, Vulkan, and Metal.


Status

Stable, the current version is 2.0.x.

The core virtual machine is completed, and is already being used through integration with other projects.

  • Playfield Engine

    • A framework to build custom 2D game engines that run everywhere.
  • Suika3.

    • A visual novel engine for the mobile era.
  • zedBSD.

    • A re-implemented modern BSD OS.

The primary objective of this project, "embedded sandbox scripting", has been achieved. However, we are continuing to develop this software in the hope of finding new applications, e.g.,:

  • Declative UI framework for use with C/C++/Rust, inspired by ArkTS.
  • Modern scripting host for retro computers.

Our current roadmap is:

  • Expanding the standard library with modular, opt-in API components.

Platform Support

JIT Backends (optional):

  • x86, x86_64
  • ARMv7, Arm64
  • RISC-V 32/64
  • PowerPC 32/64
  • MIPS 32/64

Supported OSes:

  • Desktop: Windows, macOS, Linux, FreeBSD
  • Mobile: iOS, Android, OpenHarmony
  • Exotic: zedBSD, Solaris 10/11, NetBSD, OpenBSD, Haiku
  • Retro: DPMI (DOS, OS/2, Windows 3.1-XP)
  • Consoles: Switch, PlayStation 4/5, Xbox Series X|S
  • Any POSIX compliant OS

Note: On major smartphones and consoles, runtime code generation (JIT) is generally prohibited or tightly restricted by platform policies. Noct runs there with interpreter or AOT compilation.

Speedup

Our JIT compiler achieves speedups of upto 300 times for SIMD and 50 times for scalar CPU.

A synthetic benchmark shows that our JIT compiler speeds up execution time by 4.1-13.5 times.

func main() {
    var sum = 0;
    for(i in 0..10000) {
        for(j in 0..100000) {
            sum = sum + 1;
        }
    }
}

L1 Speedup:

CPU Arch JIT (s) Interpreter (s) Scaling (JIT vs Interpreter)
PowerPC 970FX ppc64 29.47 397.22 13.5x
Ingenic JZ4770 mips32 129.75 1447.36 11.2x
Intel Core Ultra 5 228V x86_64 5.93 34.95 5.9x
Apple M5 arm64 2.76 11.34 4.1x

Core Design & Features

Noct combines simplicity, speed, and portability — traits rarely found together in scripting languages:

  • Familiar Syntax — C/JS-like and easy to learn.
  • Lightweight JIT — Fast execution in a tiny runtime.
  • Generational GC — Young semi-space copying + old mark-sweep-compact.
  • Portable ANSI C — No dependencies; runs everywhere.
  • Tiny Footprint — Runtime fits in ~200 KB.
  • AOT Compilation — Translate to C for JIT-restricted platforms. (e.g. iOS, Android)
  • Easy HPC — Automatic vectorization.

While most languages compromise on at least one of these,
Noct delivers all without sacrificing clarity or speed.

In addition, Noct introduces Dictionary-based OOP (D-OOP) — a novel paradigm for object-oriented programming, achieved through dictionary merging instead of prototype chains or heavyweight class hierarchies. Thanks to this design, Noct programs can even be translated into Emacs Lisp code, reflecting the language's Lisp-inspired roots.


Why Noct?

"What if a programming language could be learned in a single afternoon — and used the next day to create real games?"

Noct was born from this question: a desire to create a language that's minimal yet meaningful — simple enough for beginners, fast enough for production. It bridges the gap between play and production, letting you focus on making games, not fighting tools.

At the same time, Noct brings commercial-grade VM technology — once limited to large industrial runtimes — into a form small enough for game projects, built on proven techniques from engines like Java and .NET.


Try it!

Your First Program

Noct is simple enough to try right now — no setup, no hassle.

Save the following as first.noct, and just run noct first.noct.

Source:

func main() {
    Person = class {
        name: ""
    };

    var jessie = new Person { name: "Jessie" };
    var tom = new Person { name: "Tom" };

    var people = [jessie, tom];
    for (person in people) {
        print("Hello, " + person.name + "!");
    }
}

Run:

$ noct first.noct

Output:

Hello, Jessie!
Hello, Tom!

That's it. You've written your first Noct program.


Installation

Download Prebuild Binaries

Visit the release page to obtain the latest prebuilt binaries.

Manually Build from Source

Clone the repository, build it with CMake, and you're ready to go:

git clone https://github.com/awemorris/NoctLang.git
cd NoctLang
cmake --preset static
cd build-static
make
make install

Build for zedBSD

The amd64 zedBSD target is a static cross build. Set the one required target root variable to an absolute zedBSD source-tree path, then use the matching configure and build presets:

export ZEDBSD_SOURCE_DIR=/absolute/path/to/zedBSD
cmake --preset zedbsd
cmake --build --preset zedbsd --parallel 16

The executable is written to build-zedbsd/noct. The target uses zedBSD's headers, startup objects, libc, and linker policy through the integration file owned by the zedBSD tree. Configure fails when ZEDBSD_SOURCE_DIR is absent or does not identify a complete integration tree; it never falls back to host headers or host libc.

Run

To run a script:

noct script.noct

To disable the JIT compiler:

noct -j0 script.noct

JIT compilation is eager and enabled by default; -j states it explicitly:

noct -j script.noct

To start the interactive REPL, run noct without a script. The reusable REPL session API for embedded hosts is documented in docs/repl.md.

Compile into Bytecode

To compile a script into a sibling .nbc bytecode file:

noct --compile script.noct

The .nbc file contains the module's CPU bytecode and its declared require module-name list. Required module bodies remain separate and are resolved by the host when the bytecode is loaded. Noct continues to read legacy Noct Bytecode 1.0 input, but the compiler writes Noct Bytecode 1.1 and does not create new .nb files.

To create one self-contained CPU application containing every root module and its transitive dependencies:

noct --compile --app --path=lib:vendor application.nap main.noct

A .nap application does not need source files, sidecar .nbc files, or a module resolver at run time.

Source and .nbc files can load a module from the current directory or a colon-separated search path. In each directory, framework.noct is preferred over framework.nct, which is preferred over framework.nbc:

require framework;

func main() {
	framework_main();
}
noct --path=lib:vendor main.noct

Required modules are loaded once, and their initializers run in dependency order. Module discovery is a CLI policy; embedded hosts choose their own source resolver through NoctConfig.require_resolver.

Optional GPU Optimization

__accel func marks an ordinary CPU-executable function as a candidate for accelerator optimization. Without --gpu, at -O0, in a build without the accelerator, or when a function is not eligible, its checked CPU body runs normally. The annotation is an optimization hint, not a distinct function kind or a requirement that a backend accept the function.

Build the optional accelerator with -DNOCT_ENABLE_ACCEL=ON. One option selects the platform backends automatically: Windows uses Direct3D 12, Linux and FreeBSD build Vulkan and OpenGL ES, and macOS uses Metal. List the available devices before selecting one if necessary:

noct --gpu-list
noct --gpu program.noct
noct --gpu=vulkan:EXACT_DEVICE_NAME program.noct

--gpu chooses the default suitable device. The canonical selectors printed by --gpu-list begin with vulkan:, opengles:, d3d12:, or metal:. An exact plain device name is accepted for compatibility only when it is unique across the available backends; use the qualified selector when a name is ambiguous.

On Linux and FreeBSD, the accelerator component is the CLI-private shared library libnoctaccel.so; it is not a public accelerator ABI.

--gpu is valid only when running source, including source supplied with -e. It cannot be combined with .nbc, .nap, --compile, or --app. Source is the GPU distribution format: bytecode and app files contain neither GPU code nor backend metadata.

After GPU execution has started, a GPU error terminates that invocation. The removed CPU loop is not replayed after a partial or failed GPU execution.

Eligible loop groups are executed synchronously. CPU statements between two eligible groups end the first GPU session before the CPU statements run and start a new session for the later group. Integer additive-zero reductions and some provably device-only local Packed buffers can be optimized; unsupported forms simply keep their ordinary CPU implementation. These are multiple GPU regions within one function, not execution on multiple physical GPUs. Currently, reductions are limited to 32-bit wrapped addition from zero with an int or u32 accumulator. Device-only direct return, dirty subrange transfer, cross-region device persistence, and floating-point, minimum, maximum, or product reductions are not optimized. The generated runtime calls use a private, backend-specific __Accel package. There is no public Accel.* accelerator API.

Compile into Emacs Lisp

To compile a script into an Emacs Lisp file:

noct --elisp script.el script.noct

Optimization Options

  -O, -O[0-3], -O9     ... optimization preset (`-O` is the default)
  -j, -j0              ... eager JIT (default), or interpreter only

-O and -O1 enable weak typing, typed operations and CSE. -O2 adds ABCE/SIMD. -O3 permits fused FMA semantics. -O9 currently uses the same compiler optimization set as -O3; it does not select a parallel execution backend.

Garbage Collection Options

  --gc-nursery-size=N  ... first GC space size in bytes (default: 2MB = 2097152)
  --gc-graduate-size=N ... second GC space size in bytes (default: 256KB = 262144)
  --gc-tenure-size=N   ... final GC space size in bytes (default: 256MB = 268435456)
  --gc-lop-threshold=N ... move objects larger than N-bytes to final GC space (default: 32KB = 32768)

Examples

Noct programs consist of functions, expressions, and control structures similar to C and JavaScript. The main function is the entry point.

Arrays

func main() {
    var array = [1, 2, 3];
    for (v in array) {
        print(v);
    }
}

Dictionaries

func main() {
    var dict = {name: "Apple", price: 100};
    for (key, value in dict) {
        print(key + "=" + value);
    }
}

Lambda Functions

func main() {
    // Lambda notation.
    var f = (x) => { return x + 1; }
    print(f(1));

    // No closures. Use the 'with' argument explicitly.
    var g = (x, with) => {
        return x + with.y;
    };
    var y = 2;
    var z = g(1, {y: y});
}

Typed Array (Packed)

func main() {
    // Allocate uint8.
    // (Available Types: int8/int16/int32/int64/uint8/uint16/uint32/uint64/float32/float64)
    var a = Packed.uint8(1024);

    // Access via [] notation.
    for(i in 0..Packed.size(a))
        a[i] = i;
}

File I/O

Text input.

func main() {
    // Read the entire text.
    var text = File.readText("text.txt");

    // Read lines.
    File.readForEachLine("text.txt", (line) => { print(line); });
}

Text output.

func main() {
    // Write a string.
    File.writeText("text.txt", "aaa\nbbb");

    // Write lines.
    File.writeForEachLine("text.txt", ["aaa", "bbb"]);
}

Binary file I/O.

var in_file = File.open("test1.bin", "r");
var data = File.read(in_file, 100);  # data => uint8[]
for (i in 0..100)
    print(data[i]);
File.close(in_file);

var out_file = File.open("test2.bin", "w");
File.write(out_file, data, 0, Packed.size(data));
File.close(out_file);

Object-Oriented Model

The object-oriented model in Noct is a lightweight variation of prototype-based OOP.

  • Classes are simply dictionary templates
  • Inheritance and instantiation are realized by dictionary copying and merging
  • There is no prototype chain, and modifying a class does not affect existing instances

This design treats dictionaries as first-class objects, and the author refers to it as Dictionary-based OOP (D-OOP).

// Base class definition. (A class is just a dictionary.)
let Animal = class {
    name: "Animal",
    cry: (this) => {
    }
};

// Subclass definition. (This is just a dictionary merging.)
let Cat = extend Animal {
    name: "Cat",
    voice: "meow",
    cry: (this) => {
        print(this.name + " cries like " + this.voice);
    }
};

func main() {
    // Instance generation. (This is just a dictionary merging.)
    var myCat = new Cat {
        voice: "neee"
    };

    // This-call uses the ". ()" syntax.
    myCat.cry();
}

Why D-OOP?

  • Moving Beyond "Implementation Inheritance":
    • Traditional class-based inheritance is increasingly viewed as outdated, with modern languages like Rust proving that composition is often a superior alternative.
    • Noct embraces this shift by focusing on the "composition of dictionaries" rather than maintaining complex, dynamic inheritance hierarchies.
  • Eliminating Prototype Chains for O(1) Access:
    • In conventional prototype-based languages, resolving properties through a chain is a high-overhead operation that frequently triggers cache misses, requiring heavy optimizations like Inline Caching.
    • Noct eliminates the prototype chain entirely.
    • By merging dictionaries at the time of creation, it achieves guaranteed O(1) access to all properties.
  • Memory Locality and Hardware-Aware Design:
    • By creating flattened copies of dictionaries, Noct ensures superior memory locality.
    • This approach is specifically designed to align with modern hardware architectures, maximizing CPU cache hit rates and ensuring high affinity with NUMA (Non-Uniform Memory Access) environments.

JIT Pipeline

Intermediate Representations

Noct employs two distinct intermediate representations (IRs) to balance high-level program analysis with efficient execution:

  • HIR (High-level Intermediate Representation)
    • Structured control flow graph (CFG) for program analysis.
    • Expression DAG for algebraic simplification.
    • Basis for future advanced optimizations.
CFG for "func foo(a) { if (a > 0) { return a; } else { return -a; } }"

  +---------------+
  | 0: Func Block |         -- pred: none, succ: 1
  +---------------+
     +-------------+
     | 1: IF Block |        -- pred: 0, succ: 2 (true), 3 (false)
     +-------------+
        +----------------+
        | 2: Basic Block |  -- pred 1, succ 5
        +----------------+
     +---------------+
     | 3: Else Block |      -- pred 1, succ 4
     +---------------+
        +----------------+
        | 4: Basic Block |  -- pred 3, succ 5
        +----------------+
  +--------------+
  | 5: End Block |          -- pred 2, 4
  +--------------+
                                 (pred = predecessor, succ = successor)
     
  DAG for "a = 1 + 2"


     LHS   ---- ASSIGN  ----   RHS
      |                         |
     term                      ADD
      |                        / \
   symbol a                 term  term
                             |       |
                           int 1   int 2
  • LIR (Low-level Intermediate Representation)
    • VM bytecode, serving as the primary format for both interpretation and JIT codegen input.
    • High abstraction level to achieve fast, portable interpretation.
    • Compact enough for efficient machine code lowering in the JIT.
  LIR for "a = 1 + 2"

    ICONST       %0, 1               ; Load constant 1
    ICONST       %1, 2               ; Load constant 2
    ADD          %2, %0, %1          ; Compute sum
    STORESYMBOL  "a", %2             ; Store result into global variable "a"

Compilation Pipeline

 +-----+     +-----+     +-----+     +-----+
 | SRC | --> | AST | --> | HIR | --> | LIR | ----> <<Interpreter>> Crescente Interpreter Backend
 +-----+     +-----+     +-----+     +-----+
                                        |
                                        +--------> <<JIT Codegen>> Piena JIT Backend
                                        |
                                        +--------> <<AOT Codegen>> Nuova C AOT Backend

  • The AST captures the syntactic structure.
  • The HIR provides an analyzable, optimization-friendly form.
  • The LIR bridges execution, serving both the interpreter and JIT.

Design Rationale

The separation of HIR and LIR enables:

  • A lightweight JIT pipeline: minimal overhead from analysis to code generation.
  • Clarity in architecture: each stage has a well-defined role, simplifying maintenance.
  • Portability: the same LIR can be interpreted directly or lowered into optimized machine code.

As shown above, HIR expresses structure, while LIR expresses execution. This split allows Noct to keep the JIT pipeline lightweight without sacrificing optimization opportunities.

Because all JIT backends translate from the same LIR, portability across architectures comes naturally. This unified approach is what makes Noct both portable and maintainable.


Native API

The Noct runtime can be embedded in C applications. This allows you to load, compile, and execute scripts and bytecode directly within your software.

void call_noct(const char *file_name, const char *file_text)
{
    // Create a VM.
    NoctVM *vm;
    NoctEnv *env;
    noct_create_vm(&vm, &env);

    // Compile source.
    noct_register_source(env, file_name, file_text);

    // Call the main() function with no arguments.
    NoctValue ret = NOCT_ZERO;
    noct_enter_vm(env, "main", 0, NULL, &ret);

    // Destroy the runtime.
    noct_destroy_vm(vm);
}

This API requires linking against the Noct runtime and including the appropriate header (noct/noct.h).

Error handling and result introspection are left to the host application, giving full control over integration.

For more details, see the Native API document.


Test and CI

Noct is tested on Windows, macOS, Linux, and FreeBSD.

Our CI is running on GitHub Actions. Each push to the main branch triggers builds and binary releases, ensuring stability across supported platforms.

Testing Machines and Environments

Vendor Machine Processor Architecture ABI OS JIT
Apple MacBook Pro M5 Apple M5 Armv9 arm64 macOS 26 OK
MacBook Air Intel Core i5 Intel64 x86_64 macOS 10.13 OK
PowerMac G5 PowerPC G5 970FX PowerPC 64 ppc64 (ELFv1) Debian forky OK
ppc32 Debian forky OK
IBM Power8 S814 Power8 Power8 ppc64le (ELFv2) Debian trixie OK
Fujitsu Primergy TX2550 M5 Intel Xeon Gold 6130 x2 Intel64 Skylake-SP x86_64 Debian trixie OK
x86 Debian trixie OK
Lenovo ThinkPad X9 Intel Core Ultra 5 228V Intel64 Lunar-Lake x64 (MS ABI) Windows 11 OK
x86_64 Debian trixie OK
ThinkPad X260 Intel Core i5-6500U Intel64 Skylake x86_64 Solaris 11 OK
Panasonic Let's Note LX6 Intel Core i5-7200U Intel64 Kaby Lake x86_64 Haiku OK
Let's Note SV8 Intel Core i5-8250U Intel64 Coffee Lake x86_64 Windows 11 OK
Dell Latitude 5330 Intel Core i5-1240P Intel64 Alder Lake x86_64 Debian trixie OK
Latitude 5230 Intel Core i5-1135G7 Intel64 Tiger Lake x86_64 FreeBSD 15 OK
RPi Raspberry Pi 4B Broadcom BCM2711 Arm Cortex-A72 aarch64 Raspberry Pi OS 64-bit OK
armv7-eabihf Raspberry Pi OS 32-bit OK
Raspberry Pi Zero 2 Broadcom RP3A0 Arm Cortex-A53 aarch64 Raspberry Pi OS 64-bit OK
armv7-eabihf Raspberry Pi OS 32-bit OK
Anbernic RG350 Ingenic JZ4770 MIPS32r2 mipsel Debian forky OK
Cobalt Networks Cobalt Qube 2800 QED RM5231 MIPS-IV mipseb NetBSD OK
StarFive VisionFive 2 JH7110 RISC-V RV64GC riscv64 Debian Trixie OK
qemu qemu RISC-V 32 RV32GC riscv32 qemu-user OK
68040 680x0 m68k qemu-user N/A

Now preparing...

Vendor Machine Processor Architecture ABI OS
Apple Macintosh LC630 Motorola 68040 680x0 m68k Debian forky
Fujitsu SPARC Enterprise M3000 SPARC64 VII+ SPARC V9 sparc64 Solaris 10
Imagination MIPS Creator CI 40 MIPS interAptive MIPS32r3 mips Debian forky
Generic Loongson PC Loongson 3A4000 MIPS64 mips64 Debian forky

Documentation


License

Noct is free/libre software, released under the permissive zlib license.

This means you can use it freely — for personal, educational, or commercial purposes. You're also free to modify, redistribute, and build upon it, with minimal restrictions.


Contributing

While the core sandboxed virtual machine of Noct is already stable, the Standard API is under active development. We welcome all kinds of contributions — bug fixes, examples, documentation, ideas, or new features.

We're also building the broader NoctVM family, including a game engine designed to empower creators.

Whether you're here to code, teach, test, or explore — we'd love to have you with us.

About

Small JIT and AOT language with multicore, SIMD, and GPU acceleration support.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages