Skip to content

Latest commit

 

History

History
281 lines (235 loc) · 15.1 KB

FAQ.md

File metadata and controls

281 lines (235 loc) · 15.1 KB

Nish FAQ

Short answers with pointers into the reference. See LANGUAGE.md for the rules and ARCHITECTURE.md for the machinery.

Why is there no any?

Because every Nish value has exactly one fixed LLVM type and memory layout, decided at compile time. any (and unknown, unions other than T | null, typeof, instanceof, prototypes, eval, ...) would need runtime type tags, boxing, and dynamic dispatch, which is what a JavaScript engine provides and what Nish deliberately leaves out. The full list, with the guarantee each construct would break, is in LANGUAGE.md: Forbidden constructs and wp0-validator.md. If it compiles, the layout is known.

Why is number a 32-bit integer by default?

Loop and index code is the common case in the programs Nish targets, and i32 is what C and Rust use for it: one machine word, exact, vectorisable, and the natural type for array indices and exit codes. Treating every number as a double would make i % 2, a[i], and for (let i ...) carry conversions and lose exactness above 2^53. Measured on the benchmark suite, compiling the same program --number-mode f64 costs 1.36x on fib, 1.80x on sieve and 3.4x on an array reduction (14x when the array is cache-resident, where a float accumulator cannot be split into parallel partial sums), plus about 12 KB of binary for the double formatter that console.log then needs: wp9-optimisation.md has the tables and the three mechanisms. The cost is that number does not behave like JavaScript's number: signed integer overflow panics rather than wrapping (see below), and 1.5 is rejected in i32 mode (LANGUAGE.md: Semantics decisions). The explicit types i32, i64, and f64 are always available whatever the mode.

How do I get JavaScript number semantics?

Compile with --number-mode f64: number becomes double, literals may be fractional, x / 0 is Infinity, and Math.sqrt(n) works directly on a number. Array indices are truncated toward zero, .length is a double, and main must be declared main(): i32 because the exit code is an integer (tests/cases/f64_mode, arr_f64, math_intrinsics). In i32 mode you can mix: const x: f64 = 1.5 and Math.sqrt(toF64(n)). Two things are still not JavaScript even in f64 mode: toI32 saturates instead of wrapping modulo 2^32, and Math.min/max with a NaN operand return the other operand (LANGUAGE.md: Builtins).

Why is there no garbage collector?

A collector needs a runtime that knows where every pointer is, and it costs binary size, memory, and unpredictable pauses; the whole point of compiling ahead of time is to avoid that. Nish uses one bump-allocated arena (runtime/runtime.c, the core half of the C runtime, about 3.5 KB of machine code): allocation is a load, an add, a compare and a store inlined into the caller; nothing is freed individually; the entry wrapper frees everything when main returns, and a C or Node host can call nish_reset_arena() between batches to recycle memory in O(1). Objects, arrays, and strings built at run time live there; string literals are constant data. On top of that the compiler places memory statically (LANGUAGE.md: Memory model, wp6-memory.md): an object, object literal, or array literal that provably never leaves its function is an alloca, and a function whose arena temporaries all die with it marks the arena on entry and releases it before returning, so a hot loop keeps the arena flat. using a = arena() frees a batch explicitly, and Arena.mark() / Arena.used() read the arena (Arena.release(m) and Arena.reset() still work but are deprecated). Optional reference counting for objects that must outlive an arena reset is still on the plan; it has not been built.

How do I keep memory flat in a long-running loop?

Usually by doing nothing: a temporary new, [...], or { ... } that does not escape is a stack slot reused every iteration, and a helper function whose strings or dynamic arrays die with it gets an automatic arena scope. Two things defeat that: returning or storing the object (the caller now owns arena memory), and calling the deprecated Arena.reset / Arena.release inside the callee (the compiler then never scopes it). If a batch does accumulate, open a block with using a = arena() around it: the block releases everything allocated after that line on every exit, and the compiler refuses the block if anything allocated in it could outlive it (LANGUAGE.md: using a = arena()). Arena.used() tells you whether it worked (tests/cases/mem_scope_dynamic_array prints it before and after 100000 calls). --no-stack-alloc shows the arena-only IR for comparison.

Does a tail-recursive function use constant stack?

Yes, when its arguments are all scalars, and in every build profile — --profile debug included, where no optimiser runs at all. return f(...) as the whole of a return is emitted as a tail call, which tells LLVM the callee cannot reach the caller's stack frame, so the frame is reused instead of stacked. Measured on a recursion with one string temporary, built --profile debug: 1,840 KB of peak RSS at a million levels and 1,836 KB at a hundred million, against a segfault at a million without the marker. tests/link/tail_call_depth_debug is the checked-in one: a million levels at -O0 in a function with no arena scope at all, which is the shape the release on its own never reached.

Two things go with it. A function with an automatic arena scope also moves its nish_arena_release ahead of the call — a release between the call and the ret is work after the call, and a call with work after it is not the last thing the function does (LANGUAGE.md: Memory model, item 2). And at -O1 and above LLVM's own pass rewrites the self-recursion as a loop before the backend ever sees the marker; the marker is what makes debug behave like the rest.

Two things do not follow. Nothing in the language guarantees any of this, so a program that must iterate a hundred million times should say so with a loop. And the shape that looks most like it should qualify is refused: a string accumulator is memory the frame owns, so step(n - 1, acc + piece) over strings is a plain call, one frame per level.

What happens on integer division by zero?

The program panics: attempt to divide by zero on stderr and exit status 1, and INT_MIN / -1 (also %) panics with attempt to divide with overflow, exactly as Rust does. JavaScript would give Infinity | 0, that is 0, and C leaves it undefined. The check is two compares and a branch to a cold block, and LLVM folds it away for constant divisors (LANGUAGE.md: Checked integer division). f64 division is IEEE: x / 0 is Infinity, 0 / 0 is NaN.

Do integers overflow?

Not silently. A signed i32/i64 +, -, *, negation, ++, -- or op= the compiler cannot prove fits is checked, and an overflow stops the program with attempt to add with overflow (or its siblings) and exit 1, the rule of a Rust debug build kept in every build. One the bounds walk proves fits — a loop counter below a length, a value built from two bytes — is a plain add nsw with no check, so the common loops cost nothing.

Code that overflows on purpose — a hash, a linear congruential generator, a wrap-around counter — must say so:

  • wrappingAdd, wrappingSub and wrappingMul from nish:unsafe wrap two's-complement at the one site that means it: wrappingAdd(2147483647, 1) is -2147483648.
  • Write it in an unsigned type. u8, u16, u32 and u64 are defined to wrap and are never checked, which is what they are for; an FNV-1a round in u32 needs nothing more.
  • The deprecated --wrapping makes every operator of your own package wrap (never a dependency's or the standard library's).

Earlier versions made signed overflow undefined behaviour, as C does; that is gone, and no flag brings it back (--nsw is refused) (LANGUAGE.md: Semantics decisions).

How do I know the compiled program behaves like Node?

npm run test:diff compiles every whole program in tests/cases and a 71-program corpus, runs each binary, runs the same program's JavaScript rewrite under Node with runtime/shim.mjs, and compares stdout and exit status byte for byte. The rewrite was made from the compiler's own recorded types ((a + b) | 0 for i32, BigInt for i64, a bounds-checked index helper, byte lengths) by a rewriter that was deleted with stage0, so it is frozen in tests/differential/goldens/. node tests/differential/fuzz.js --count 200 compiles random integer programs with the last release and with the current compiler and compares the IR. Everything that still differs is listed with a reproducer in wp13-differential.md and tests/differential/known-failures.txt: 1-ulp libm differences in sin/cos/log/pow, Math.min/max with NaN, Math.round(-0.3), and the division panics above.

How do I call Nish code from Node?

Two ways, both generated from the same signatures as the IR (wp8-interop.md):

  • WebAssembly: scripts/build.sh a.ll -o a.wasm --profile wasm builds a freestanding module; WebAssembly.instantiate loads it and every exported scalar function is callable directly (examples/node-host.mjs). --emit-dts a.d.ts writes the typings and a.mjs, a loader that passes Int32Array / Float64Array / BigInt64Array arguments by copying them into the module's memory (link runtime/runtime-wasm.c). Strings are not available in this profile because it has no WASI runtime.
  • Native addon: --emit-napi a_napi.c writes an N-API shim, scripts/build.sh a.ll runtime/runtime.c a_napi.c -o a.node --profile napi builds the addon, and require("./a.node") loads it (examples/node-addon.mjs). Numbers, booleans, i64 (bigint), strings and typed arrays are bridged with argument type checks; a typed array is borrowed zero-copy, so writes through it are visible in JS.

Design the boundary around batches: one call that processes a whole buffer, not one call per element. node bench/ffi.mjs measures the difference (about 30 ns per N-API call, 2 ns per wasm call, before any work is done; 0.5 ns per element when a 1M-element Float64Array crosses in one call).

Why not embed a JavaScript engine for npm packages?

Calling npm packages from an Nish binary would mean embedding a JS engine (QuickJS adds about 2 MB, V8 tens of MB), marshalling every value across the boundary at hundreds of cycles per call, and running a second, garbage-collected heap next to the arena. That erases the reasons to compile in the first place, so Nish deliberately has no such bridge. The supported direction is the reverse: Node does I/O, HTTP, and npm; Nish does math, parsing, data transforms, and hot loops; they meet once per batch.

What does an error look like?

Every rejection is one line file:line:col: error: message followed by the source line and a caret marking the offending node, and the process exits with status 1:

tests/cases/reject_type_mismatch.ts:1:40: error: Operator `+` requires two operands of the same numeric type or two strings, got i32 and boolean
  1 | function f(a: number): number { return a + true; }
    |                                        ^~~~~~~~

Syntax errors use the same layout with syntax error:. The compiler reports every error it finds, in source order, up to 20 before ...and N more errors, and --json prints each as one object with a stable code. The message texts are catalogued in LANGUAGE.md and the format in wp10-ci.md.

Which exit codes does nish use?

0 success; 1 your program was rejected (or an input path is missing); 2 usage error (unknown flag, no inputs); 3 toolchain error (--link found no clang, or scripts/build.sh failed; the .ll files are still written and named); 70 internal compiler error. Details in INSTALL.md and wp12-release.md.

How do I report a bug?

  • Exit code 70 means the compiler itself crashed, not that your program is wrong. It prints nish <version>: internal compiler error while compiling <files> and the exception; re-run with NISH_DEBUG=1 for the stack trace, then open an issue at https://github.com/amritk/nish/issues with the input file, the command line, and that output.
  • Wrong IR or wrong native output for a program that compiles: attach the .ts, the .ll (-o out.ll), the flags, and what you expected. --plain gives the smallest IR to look at. Behaviour that is known to disagree with JavaScript or the design notes is listed in LANGUAGE.md: Known inconsistencies; check there first.
  • A rejection you think is wrong: quote the full three-line diagnostic. The reference marks every rule with the test case that fixes it, so a report can point at the rule.

Why does clang warn about "overriding the module target triple"?

By default the IR is target-neutral (no target triple line), so clang fills in the host triple and warns. It is harmless; scripts/build.sh passes -Wno-override-module. --target host (or a triple such as aarch64-unknown-linux-gnu or wasm32-wasi) writes the target datalayout and target triple lines and the warning goes away. Cross-compile with --target <triple> plus clang --target=<triple>, or with llc -mtriple=....

Why does opt -O2 on my .ll not vectorise anything?

Because a target-neutral module gives opt a generic data layout with no vector registers. --link is unaffected (clang supplies the layout), but when you inspect optimised IR by hand compile with --target host and opt -O2 -S will vectorise the same loops the binary gets (tests/cases/opt_target_triple is checked for exactly that).

Why does my function not get willreturn / readnone?

Because the compiler could not prove it. A while loop, a for loop that is not a counted loop, a checked a[i], an integer / or % (the divisor check can panic), a signed +, - or * not proven to fit (the overflow check can panic), or a call to process.exit drops willreturn; any arena allocation, console.log, string concatenation, field store through a non-stack object, integer division, a checked signed operation, or Math.random makes the function impure. An object that lives in a stack slot is the function's own memory, so reading and writing its fields keeps readnone (tests/cases/mem_stack_struct, swapped). The exact rules are in ARCHITECTURE.md: Attribute soundness rules; the attributes are guarantees, so the compiler never guesses.

Can I use this on Windows?

Not natively yet: scripts/build.sh is bash and the runtime is built with a POSIX clang. Use WSL with Ubuntu (INSTALL.md).