AI Decision System
Replayable, offline-verifiable records for AI-assisted go/no-go decisions: experiment promotions, typed model answers and human-approved actions.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기Nova: a memory-safe systems programming language compiled to portable C
저장소 README에서 가져옴
README 전체를 불러오는 중입니다.
README 전문 보기 ↗Nova is a memory-safe systems programming language with C#/TypeScript-style syntax. It is compiled ahead of time to portable C11, so it can go wherever a C11 compiler can, freestanding kernels included. It has no garbage collector and no managed runtime.
Safety comes from compile-time ownership and borrowing, plus bounds and integer-overflow checks that stay on in every build. Allocation failure is always a value the program handles. Memory placement is a separate choice. Nova offers the stack, owned heap objects, arenas, pools and user-defined allocators, and the borrow checker treats them all the same way.
Its first target platform is CosmoOS: Nova programs run in its userland, and Nova components run inside its kernel. Nova is also meant for other C codebases, such as Linux (ADR 0008).
Status: early development, not production-ready. The compiler, its checker and the C backend are complete for the language as specified so far. That covers ownership, borrowing, regions, generics, collections, unsafe code, the C FFI and a freestanding profile (milestones M0–M8), and a first round of performance work (M9). Several features most languages take for granted are not there yet; see What Nova does not have yet. Documentation: vkuttyp.github.io/Nova, a guide, Nova for Rust, Zig, C, C# and Go programmers, and the reference. Current status: docs/README.md and docs/roadmap.md.
borrow T and borrow mut T follow the usual rules: one writer or many readers.-> borrow(a, b) T).struct Lexer<region src>) or a function over such values (fn split<region r>(…)).
(ownership.md, lifetimes.md)drop blocks run exactly once: at scope exit, on early return and on error paths.using and defer cover scoped cleanup. There is no unwinding: a panic aborts.Box on SystemHeap, Arena, FixedBufferAllocator, Pool, scratch frames, user-defined allocators,
and in the kernel KernelHeap.Result, handled or propagated with ?.--no-placement turns it off, and the drop timing is the same either way
(optimisations.md §4).Vector, String, StringView, Slice, SlotMap (generational handles) and StableVector (stable addresses).Every example below is a test in tests/corpus (the readme-* files). The programs are built and run
in every C configuration and in the interpreter, and the output shown is what they print. The rejected programs are
functions of check-fail/readme-errors.nova and readme-effects.nova, and the errors shown are the compiler's own.
fn main() {
Console.writeLine("Hello, Nova");
}
novac build hello.nova -o hello && ./hello # through C, to a native executable
novac run --interp hello.nova # or in the reference interpreter
struct Account {
id: u64;
balance: i64;
public fn deposit(borrow mut this, amount: i64) {
this.balance += amount;
}
drop { // runs exactly once, when the owner goes away
Console.write("closing ");
Console.writeLine(this.id);
}
}
fn open(id: u64) -> Result<Box<Account>, AllocError> => Box.create(Account { id: id, balance: 0 });
fn transfer(from: borrow mut Account, to: borrow mut Account, amount: i64) {
from.balance -= amount;
to.balance += amount;
}
fn main() {
var a = open(1)!; // `!`: panic if the allocation fails
var b = open(2)!;
a.deposit(100);
transfer(mut a, mut b, 30);
Console.writeLine(a.balance);
Console.writeLine(b.balance);
} // b, then a: each Account dropped, each box freed
It prints 70, 30, closing 2, closing 1. There is no free and no reference count. Each Box owns its
Account, and transfer borrows both exclusively for the length of the call.
A reference into a vector must not survive the vector's growth:
fn main() {
var numbers = Vector<i32>.create();
numbers.push(1)!;
let first = borrow numbers[0];
numbers.push(2)!; // may reallocate the storage `first` points into
Console.writeLine(first);
}
invalidation.nova:5:5: error[N0241]: cannot borrow 'numbers' as mutable while 'numbers[0]' is borrowed
|
5 | numbers.push(2)!; // may reallocate the storage `first` points into
| ^^^^^^^
= note: 'numbers[0]' is borrowed here (4:5)
= note: the borrow is used later here (6:5)
The same rule protects arenas. Resetting one frees everything allocated in it, so it waits until no allocation is in use:
fn main() {
var arena = Arena.create(SystemHeap {}, 4096u64);
let table = arena.allocArray(16usize, 0u32)!;
arena.reset(); // frees every allocation, while `table` is still in use
table[0] = 1;
}
arena-reset.nova:4:5: error[N0241]: cannot borrow 'arena' as mutable while 'arena' is borrowed
|
4 | arena.reset(); // frees every allocation, while `table` is still in use
| ^^^^^
= note: 'arena' is borrowed here (3:17)
= note: the borrow is used later here (5:11)
enum ParseError { Empty, BadDigit(u8), TooLarge }
fn parse(text: StringView) -> Result<u32, ParseError> {
if (text.length == 0) { return Err(ParseError.Empty); }
var value: u64 = 0;
for (c in text.bytes()) {
if (c < 48 || c > 57) { return Err(ParseError.BadDigit(c)); }
value = value * 10 + (c - 48) as u64;
if (value > 4294967295) { return Err(ParseError.TooLarge); }
}
return Ok(truncate<u32>(value));
}
fn sum(a: StringView, b: StringView) -> Result<u32, ParseError> {
return Ok(parse(a)? + parse(b)?); // `?` returns the error to the caller
}
fn main() {
match (sum("120", "3x")) {
Ok(total) => { Console.writeLine(total); }
Err(ParseError.BadDigit(c)) => { Console.write("bad digit: "); Console.writeLine(c); }
Err(_) => { Console.writeLine("cannot parse"); }
}
Console.writeLine(sum("120", "3")!);
}
It prints bad digit: 120 (the byte x), then 123. ? passes an error up; ! panics on one. Patterns nest
(Err(ParseError.BadDigit(c))), and the compiler checks that the arms cover every value.
fn distinctBytes(input: Slice<u8>) -> Result<u64, AllocError> {
using arena = Arena.create(SystemHeap {}, 65536u64);
let counts = arena.allocArray(256usize, 0u32)?; // lives in `arena`, not on the heap
for (b in input) { counts[b as usize] += 1; }
var distinct: u64 = 0;
for (c in counts) { if (c != 0) { distinct += 1; } }
return Ok(distinct); // returning `counts` itself would be a compile error
} // the whole arena is released here
fn main() {
Console.writeLine(distinctBytes("hello, world".bytes())!);
}
It prints 9. The table lives in the arena, and leaving the function releases the arena and everything in it at
once. Returning counts would not compile, since nothing may outlive its arena.
interface Shape {
fn area(borrow this) -> u64;
}
struct Square { side: u64; }
struct Rect { w: u64; h: u64; }
impl Shape for Square { fn area(borrow this) -> u64 => this.side * this.side; }
impl Shape for Rect { fn area(borrow this) -> u64 => this.w * this.h; }
fn total<S: Shape>(shapes: Slice<S>) -> u64 {
var sum: u64 = 0;
for (shape in shapes) { sum += shape.area(); } // shape: borrow S
return sum;
}
fn main() {
let squares = [Square { side: 2 }, Square { side: 3 }];
var rects = Vector<Rect>.create();
rects.push(Rect { w: 4, h: 5 })!;
Console.writeLine(total(squares[..]) + total(rects.view()));
}
It prints 33. total is compiled once for each shape type it is used with, so the call is resolved at compile time.
For a collection of different types, dyn Shape dispatches at run time instead (Vector<Box<dyn Shape>>;
type-system.md §37).
struct Reading { sensor: u32; celsius: f64; }
fn main() {
var readings = Vector<Reading>.create();
for (i in 0..8u32) {
readings.push(Reading { sensor: i % 3, celsius: 18.5 + (i as f64) * 0.75 })!;
}
let limit = 20.0;
readings.retain((r) => r.celsius > limit); // captures `limit`
readings.sortBy((a, b) => if (a.celsius > b.celsius) -1 else if (a.celsius < b.celsius) 1 else 0);
var sum = 0.0;
readings.forEach((r) => { sum += r.celsius; }); // writes `sum`: a `borrow mut fn`
Console.writeLine(readings.length);
Console.writeLine(readings[0].celsius);
Console.writeLine(sum / round<f64>(readings.length));
}
It prints 5, 23.75 and 22.25. A closure borrows what it captures, so it cannot outlive the block it is written
in. retain, sortBy and forEach are written in Nova (lib/core/slices.nova) and allocate nothing; sortBy is not
stable. There are no implicit numeric conversions: as converts where every value fits exactly (u32 as f64),
round<f64> rounds a wider integer such as a usize, and truncate<T> turns a float into an integer, panicking if it
is out of range.
split returns a vector of views into its input, not copies. The region parameter r ties them to the text they view:
/// Splits a line at commas without copying: each field is a view into `line`.
fn split<region r>(line: StringView<r>) -> Result<Vector<StringView<r>>, AllocError> {
var fields = Vector<StringView<r>>.create();
var start: usize = 0;
for (i in 0..line.length) {
if (line[i] == 44) { // ','
fields.push(line[start..i])?;
start = i + 1;
}
}
fields.push(line[start..])?;
return Ok(fields); // the result borrows from `line`
}
fn main() {
var text = String.create();
text.append("id,name,,score")!;
let fields = split(text.view())!;
// text.clear(); // error: `text` is borrowed by `fields`
Console.writeLine(fields.length);
for (field in fields) { Console.writeLine(field); }
}
It prints 4, then the fields id, name, an empty line and score. Uncommenting text.clear() is a compile error,
because fields still views text.
Calling C is unsafe. The function that does it must say why it is sound:
extern "C" {
fn strlen(s: *CChar) -> usize;
}
/// The length of `text` up to its zero byte, or null if it has none.
/// Safety: `strlen` is called only after finding a zero byte inside `text`, so it cannot read past the end.
fn cLength(text: StringView) -> usize? {
for (i in 0..text.length) {
if (text.bytes()[i] == 0) {
return unsafe { strlen(text.ptr as *CChar) };
}
}
return null; // no terminator: refuse rather than overrun
}
fn main() {
match (cLength("nova\0")) {
Some(n) => { Console.writeLine(n); }
null => { Console.writeLine("not terminated"); }
}
match (cLength("nova")) {
Some(n) => { Console.writeLine(n); }
null => { Console.writeLine("not terminated"); }
}
}
It prints 4, then not terminated. Without the /// Safety: comment, the compiler rejects the function (N0283).
A spin lock's guard makes the code that runs while it is held atomic. The compiler follows calls to find what may sleep:
@header("kernel/sched.h")
extern "C" {
@effects(sleep)
fn cosmo_sleep_ms(ms: u32);
}
/// Holding a spin-lock guard makes the code that runs meanwhile atomic: it must not sleep.
@context(atomic)
struct SpinGuard { cpu: u32; drop {} }
/// Safety: cosmo_sleep_ms has no preconditions; it may sleep.
fn backOff() { unsafe { cosmo_sleep_ms(1); } }
fn update(counter: borrow mut u64) {
let guard = SpinGuard { cpu: 0 };
counter += 1;
backOff(); // sleeps while the guard is held
}
effects.nova:17:5: error[N0295]: a 'SpinGuard' is live here, which makes this an atomic context (type-system.md §32.3), but this may sleep: it calls 'backOff', which calls 'cosmo_sleep_ms', declared @effects(sleep)
|
17 | backOff(); // sleeps while the guard is held
| ^^^^^^^^^^
The check runs at compile time and across functions. It complements the kernel's lockdep rather than replacing it.
fn main() {
var level: u8 = 250;
for (i in 0..10) { level += 1; } // u8 arithmetic is checked: this panics at 256
Console.writeLine(level);
}
nova panic: integer overflow at overflow.nova:3:24
The program aborts with exit status 134. In Nova an overflow is never a silent wrap or undefined behaviour, in any build mode.
Nova borrows the most from Rust (ownership and borrowing), Zig (explicit allocators), Cyclone (regions) and Goose (compiler-managed stacks, a C backend). The table compares the languages most often chosen for memory-safe systems work, with C for reference. "Memory-safe" here means safe code cannot cause use-after-free, double free, out-of-bounds access or reads of uninitialised memory.
| Nova | Rust | Swift | Go | Zig | Ada / SPARK | C | |
|---|---|---|---|---|---|---|---|
| Memory-safe (safe subset) | yes | yes | yes | yes | spatial checks only1 | Ada partly2; SPARK yes | no |
| How | ownership, borrowing, regions | ownership, borrowing | automatic reference counting (ARC) | tracing garbage collector | runtime checks, by build mode | runtime checks; SPARK proof | — |
| Garbage collector or reference counting | none | none (opt-in Rc/Arc) | ARC on classes | GC | none | none | none |
| Lifetime syntax | none for plain borrows; region parameters where references are stored or passed on | lifetime parameters ('a), often elided | none | none | none | none (accessibility rules) | — |
| Choice of allocator | per allocation (Box, Arena, pools, user-defined) | global; per-collection allocators unstable | no | no | per call (explicit Allocator) | storage pools | malloc or your own |
| Allocation failure | a Result | aborts by default (try_reserve for some types) | aborts | fatal error | an error value | Storage_Error | NULL |
| Integer overflow | panic, every build | panic in debug, wraps in release by default | trap | wraps | checked in Debug/ReleaseSafe, undefined in ReleaseFast |
What sets Nova apart:
sqrt, abs, floor, ceil, min and max: no trigonometry or logarithms yet.for over arrays, slices and vectors.Each is either planned (roadmap.md) or an open decision (open-decisions.md).
There is no performance claim without a benchmark (benchmarks.md). On the sixteen-workload
suite in benchmarks/suite, the same algorithms are written in Nova, C, C++, Rust, Zig and Goose.
Measured at the end of M11 (2026-10-26), with every check that can fail still in place:
recursion, which gcc
rewrites algorithmically, and nbody, where C keeps sqrt's errno path and Nova's C does not (0.61).So Nova is not yet as fast as C on most workloads. String and vector growth were the slowest, at 2–3.5 times C's time.
M13's G1 (2026-10-28) grows them with realloc and keeps the call out of push and append's fast path. On the same
Ryzen machine, vector-growth went from 2.08 to 1.03 times C's time under clang (2.02 to 0.99 under gcc), and
string-building from 2.42 to 1.61 (3.53 to 2.64). The rest of string-building's gap is the UTF-8 boundary checks of
one-byte slices. The suite-wide ranges above are from before G1.
Results and their causes are in milestone-9.md, milestone-11.md and
milestone-13.md.
curl -fsSL https://vkuttyp.github.io/Nova/install.sh | sh # macOS and Linux
irm https://vkuttyp.github.io/Nova/install.ps1 | iex # Windows (PowerShell)
This installs the latest release of novac, a single native executable
(no .NET needed; the macOS builds are signed and notarized), in ~/.nova/bin, after checking its SHA-256. novac build also needs a C compiler (clang, gcc or MSVC); check, run --interp, fmt, lsp and mcp do not.
To build from source instead (to work on Nova, or on another platform), with the .NET 10 SDK:
git clone https://github.com/vkuttyp/Nova.git && cd Nova
dotnet build src/Nova.Cli -c Release # src/Nova.Cli/bin/Release/net10.0/novac
and put that novac on your PATH, for example with a wrapper script in ~/.local/bin/novac:
#!/bin/sh
exec /path/to/Nova/src/Nova.Cli/bin/Release/net10.0/novac "$@"
novac --version prints the version, and novac --help every command.
novac new hello # nova.toml, src/main.nova, .gitignore, and AGENTS.md for AI coding agents
cd hello
novac run --interp # run it in the interpreter: no C compiler involved
novac build # compile through C to out/hello
./out/hello
Inside a package (a directory with nova.toml, or below one), commands without file arguments use the package.
novac new lib --lib makes a library instead.
novac check # parse, type-check and borrow-check: the fastest way to find errors
novac fix # apply the fixes the compiler is sure of, then check again
novac explain N0249 # what a diagnostic code means
novac fmt # rewrite in the one canonical style; novac fmt --check only lists files
A diagnostic names its file, line and column, a code such as N0249, and often the specification section that
explains the rule. novac fix repairs the mechanical ones: a missing mut before an argument, let that should be
var, a narrowing as that should be truncate<T>(…), a missing ;.
Every command also takes files directly, without a package:
novac check hello.nova
novac run --interp hello.nova -- first second # arguments after -- go to the program
novac build hello.nova -o hello && ./hello
Name them in nova.toml, by path or by git tag, commit or branch:
[dependencies]
geometry = { path = "../geometry" }
lz4 = { git = "https://example.org/lz4-nova.git", tag = "v1.2.0" }
novac fetch resolves them and pins each in nova.lock (commit it); novac fetch --offline uses only what is
already fetched. A library's public items are used by its name, geometry.Point, or imported with
import geometry.Point;. See
tooling.md §4.
| Command | Shows |
|---|---|
novac build --emit c -o out/c | the generated C, readable and warning-free |
novac build --explain-allocations | where each box lives (stack or heap) and why |
novac build --explain-bounds-checks, --explain-overflow-checks | which checks were removed, with the reason |
novac build -g | a debug build: the C mapped back to Nova lines for lldb and gdb (editors/debug) |
novac build --profile freestanding | no C library, for kernels and firmware (--emit obj for one object) |
novac doc src -o api.md | a Markdown page of the public items and their doc comments |
novac lsp is a language server: errors as you type, hover types, go to definition, document symbols, formatting and
quick fixes. editors/vscode is a VS Code extension that starts it.
Nova is new, so models have seen little of it; the tools are built to correct an agent quickly:
claude mcp add nova -- novac mcp # Nova's MCP server, for Claude Code (any MCP client works)
novac new hello --agent claude # AGENTS.md, CLAUDE.md and .mcp.json in a new package
novac check --diagnostics-format=json # every diagnostic, with its fixes, as one JSON document
novac mcp gives an agent the checker, the fixes, a sandboxed interpreter, the C generator and the whole
specification as tools. novac new writes AGENTS.md; --agent adds Claude Code's, Gemini CLI's, Copilot's or
Cursor's files, and novac agents refreshes them after a novac update. The guide for agents, with the mistakes
models make in Nova: Nova for AI agents.
| Command | Does |
|---|---|
novac new <name> [--lib] [--agent <names>] | make a package |
novac check, build, run --interp | check; compile through C; run in the interpreter |
novac fix [--dry-run], novac explain <code> | apply the compiler's fixes; explain a diagnostic |
novac fmt [--check | --stdout], novac doc | format; document the public items |
novac fetch [--offline] | resolve dependencies into nova.lock |
novac lsp, novac mcp | the language server; the MCP server for AI agents |
novac agents [--agent <names>] | write or refresh the package's agent instructions |
novac exits with 0 on success, 1 when the program has errors, and 2 for a usage error. The full reference is
The novac command in the guide.
dotnet build Nova.slnx # the compiler and its tools; warnings are errors
dotnet test Nova.slnx # about 3,300 tests: units, the corpus, the guide's examples
src/Nova.Compiler, src/Nova.Cli the compiler (C#, .NET 10) and the novac command
runtime/ the C runtime: core (freestanding), hosted
lib/core/ Nova's core library
programs/ utilities written in Nova
tests/ unit tests, the corpus, the random-program fuzzer
benchmarks/ the benchmark suite; LZ4, fbvalid and GPT against CosmoOS's C
integrations/cosmoos/ CosmoOS userland and kernel integration (milestones C1–C9)
docs/ the specification, design records and milestone reports
Start with docs/README.md, the index of the specification, decisions and milestone reports. Contributors and coding agents: read AGENTS.md first.
Copyright (C) 2026 Veeran Puthumkara.
Nova is free software, licensed under the GNU General Public License, version 2 only
(GPL-2.0-only).
The parts of Nova that end up inside the programs it compiles, the C runtime (runtime/), the core library
(lib/core/) and the code novac writes into its output, also carry a runtime exception
(GPL-2.0-only WITH Classpath-exception-2.0): compiling a program with Nova does not place that program under the
GPL. You may license your own programs as you choose.
Zig checks bounds, overflow and null in its Debug and ReleaseSafe modes. Nothing prevents use-after-free or dangling pointers. ↩
Ada checks bounds and ranges at run time, but Unchecked_Deallocation can leave dangling access values. SPARK
restricts pointers with ownership rules and proves the absence of run-time errors. ↩
wrappingAdd, checkedAdd (which returns T?) and
saturatingAdd, and the same for Sub and Mul.as converts only where no value can be lost (checked at compile time); truncate<T> makes narrowing explicit.T: Shape), and
Result<T, E>, T? and match on enums with payloads.f32 and f64 follow IEEE 754, with sqrt, abs, floor, ceil, min and max; conversions to integers
are explicit. Kernel code compiles with --no-float, which rejects them.values[..].sortBy((a, b) => a - b),
v.retain((x) => x > limit). An effect list on a function type, @effects() fn(i32), lets kernel code take a
callback that cannot sleep. A move closure owns its captures and can be returned or stored, boxed as a
Box<dyn fn(…) -> R>. (type-system.md §35–§36)dyn Interface behind a reference or a box (§37). Threads with Thread.spawn,
Arc, Mutex and atomics, where only Send values cross and only Share values are shared, so safe code has no
data races (§38).unsafe blocks with raw pointers, MMIO and C function pointers.unsafe must state why it is sound in a /// Safety: comment, which the compiler
enforces (unsafe.md).@context(atomic) and @effects(sleep, alloc, …) let the compiler reject, for example, sleeping while a spin
lock's guard is live, across calls.KRef<T>) and grace periods. (type-system.md §32–§34)#line source maps. The backend is designed so that its C has no undefined
behaviour: every operation that could have some goes through a checked helper.Constraint_Error (suppressible) |
| undefined (signed) |
| Bounds checks | always (proved ones removed) | always (proved ones removed) | always | always | Debug/ReleaseSafe | yes (suppressible) | no |
| Data races in safe code | prevented (Send/Share); kernel code: context checks | prevented (Send/Sync) | prevented in Swift 6 mode | possible (runtime race detector) | possible | protected objects; SPARK proves absence | possible |
| Compiles to | C11 (clang, gcc; MSVC best-effort) | LLVM | LLVM | its own backend (also gccgo) | LLVM, own backends, C | GCC (GNAT), LLVM | — |
| Freestanding / kernel | yes: profile with no C library | yes (no_std) | experimental (Embedded Swift) | not without its runtime | yes | yes | yes |
| Panic | abort | unwind or abort | trap | panic/recover | panic handler (aborts by default) | exceptions | — |
| Maturity | early; one implementation | production | production | production | pre-1.0 | production | production |