Skip to content

Repository files navigation

nanoscript (nscript)

current version 0.2.0

This is a Work in Progress

Purpose

nanoscript (nscript) is a lightweight, JavaScript-inspired programming language designed for simple computational tasks in the browser or on the server. Built with TypeScript, it draws inspiration from an earlier C++-based project, clx, and is tailored to address security concerns that arise with JavaScript's eval. nscript ensures safe execution of code by limiting access to only explicitly included features through developer-defined modules.

nanoscript operates on a lightweight engine that runs seamlessly in both browser and server environments. Built with TypeScript, nscript code can be executed anywhere JavaScript is supported. A typical use case for nanoscript is enabling power-users to write custom logic or advanced configurations to interact with an application safely. Executing external code on a server using eval or similar methods can expose the application to significant security risks, as eval enables the execution of fully-featured JavaScript, potentially allowing malicious actions.

There are two execution backends. The default one is a portable JavaScript stack machine that runs anywhere. The second compiles each program to a purpose-built WebAssembly module and is 1.4x-10x faster depending on the workload — see docs/wasm-engine.md.

Javascript functions and objects can be passed into the engine and called by name to allow integration with your application code. Nanoscript is a convenient way to write code that glues together your more performant application code

Key Features

  • JavaScript-inspired Syntax: Familiar syntax for JavaScript developers.
  • Secure Execution: No access to features outside explicitly defined modules.
  • Strictly Typed by Default: Every binding resolves to a concrete type via annotation or inference; opt back into dynamic typing with allowAny: true.
  • High-Level Constructs: Includes variables, loops, functions, and more.
  • Customizable Modules: Extendable via developer-defined modules for specific use cases.
  • First-Class Functions and Closures: Functions are values — store, pass and return them; arrows ((x: int): int => x * 2) and by-reference capture included, on both backends.
  • Parameterized Container Types: list<int>, set<string>, object<float>, function types (int, int) => int; uniform literals infer their element type.
  • Python-Flavoured Sugar: Slicing (a[1:3], a[::-1]), negative indexing, a lazy range(start, end, step), typed container methods (arr.filter(f).map(g)), and the membership operators in / not in.
  • Advanced Loop Control: Nested loop control with break x;.
  • String Interpolation: Template strings with syntax: hello ${world}.
  • WebAssembly Backend: Optional WASM execution engine for compute-heavy scripts.
  • Structured Errors: All pipeline stages throw NSError with line and character offsets, so tooling can map failures to source ranges.
  • VS Code Extension: Syntax highlighting, live diagnostics, completion, and in-editor runs on either backend — see editors/vscode.
  • Upcoming Features: Class declarations

Table of Contents

  1. Installation
  2. Usage
  3. Nanoscript Syntax Overview
  4. Examples
  5. The WebAssembly Backend
  6. Editor Support
  7. Documentation
  8. Future Features
  9. License

Installation

Install using NPM

npm install @creation-wasteland/nanoscript

Usage

Import NSEngine from the module

// typescript

import { NSEngine, NenvExport, NenvModule } from "@creation-wasteland/nanoscript";

Add any required modules so that your API can be called from the engine.

To create a nenv module, define a new nenv.NenvModule with a name and exports

The exports property should contain a list of NenvExports. Each export needs to have a name, type, and object.

// typescript

const myModule = {
    name: "myModule",
    exports: [
        { name: "myObject", type: "constant", object: {x: 1, y: 2, z: 3} },
        { name: "myFunction", type: "function", object: (a: number, b: number) => a + b },
        ...
    ] as NenvExport[]
} as NenvModule;

To add your module to the engine, use the addModules method

// typescript

const engine = new NSEngine();
engine.addModules([
    myModule,
    ...
])

To compile and execute nanoscript code, use the compileAndRun method

// typescript

const output = engine.compileAndRun(code);

Nanoscript Syntax Overview

Nanoscript is a language that is similar to the most basic elements of javascript/c with some slight syntax modifications. If you are used to javascript, you should instantly be able to use nanoscript.

Variable and const declarations

Variable declarations are similar to javascript. Let and const both have block scoping (similar to how it would work in C)

// nanoscript

let x = 0;              // inferred int
const y = 'hello';      // inferred string
let z: float = 0;       // annotated

Nanoscript is strictly typed by default: every variable, function parameter and return type must resolve to a concrete type, from a name: type annotation or from inference, and compilation fails where one cannot. Values read out of untyped containers (object/list/set members and elements) are any and must be bound through an annotation, which acts as a type assertion:

// nanoscript

let o = {'a': 1};
let n: int = o.a;                    // assert the member's type
function add(a: int, b: int): int {  // annotated signature
    return a + b;
}

Unlike JavaScript, there are no implicit conversions to string — the only implicit conversion is the lossless intfloat widening. Converting to a string is always explicit, with the builtin str() cast or a template string:

// nanoscript

let n = 5;
let bad  = 'n=' + n;        // compile error
let good = 'n=' + str(n);   // 'n=5'
let also = `n=${n}`;        // 'n=5'

To opt back into fully dynamic typing, construct the engine with new NSEngine({ allowAny: true }). See docs/type-system.md for the rules and docs/language-reference.md for the syntax.

For and while loops

// nanoscript

for (let i = 0; i < 100; i++) {
    ...
}

let c = 10;
while (c >= 0) {
    ...
}

One notable "new" feature, is the ability to break directly to different levels.

// nanoscript

for (let i = 0; i < 100; i++) {
    for (let j = 0; j < 100; j++) {
        if (i + j == 50) {
            break 2; // break out of the enclosing loop
        }
    }
}

For-in Style Loops

// nanoscript

const arr = [0,1,2,3,4,5,6];

for (x: int in arr) {
    ...
}

// Same thing
for (let x: int in arr) {
    ...
}

// You can also use the ∈ symbol if you want to be extra fancy
for (x: int  arr) {
    ...
}

You can precede the variable name with const or let if you want, it does not make a difference. The : type annotation is required under strict typing (list elements are any), except over strings, whose characters are known to be strings; with allowAny: true the bare for (x in arr) works.

Strings

You can use ", ', or ` to denote string literals

// nanoscript

let s1 = "Hello";
let s2 = 'Hello';
let s3 = `Hello`;

String Builder Strings (Template Strings, Formatted Strings, whatever you want to call it...)

There is support for string templating using the ` character. Template elements must be surrounded by ${}

// nanoscript

let s1 = "World";
let s2 = `Hello ${s1}`;     // Hello World

Internal Functions

Functions can be defined using the function keyword. Functions can then be called by name while in scope.

// nanoscript

function func(a: int, b: int): int {
    return a + b;
}

func(1,2);      // 3
 

Parameter annotations are required under strict typing; the return type can be annotated or inferred from the body (recursion needs the annotation).

Functions are first-class values — store them, pass them, return them, close over enclosing variables, and write them as arrows:

// nanoscript

function makeCounter(): () => int {
    let n = 0;
    return (): int => { n += 1; return n; };
}
let next = makeCounter();
next();                              // 1
next();                              // 2

[1, 2, 3, 4, 5]
    .filter((x: int): bool => x % 2 == 1)
    .map((x: int): int => x * x);    // [1, 9, 25]

List Literals

Lists can be declared with list literal syntax, similar to javascript

// nanoscript

let arr = [0,1,2,3,4,5];    // js list with 6 elements
let arr2 = [];              // empty list

Note: List literals that can be fully determined at compile-time, will be pre-compiled and result in a massive performance increase.

Set Literals

Sets can be declared with list literal syntax, similar to javascript

// nanoscript

let set1 = {0,1,2,3,4,5};    // a set with 6 elements
let notASet = {};            // NOT an empty set, this is an empty object

Note: Set literals that can be fully determined at compile-time, will be pre-compiled and result in a massive performance increase.

Object Literals

Objects can be created with a familiar syntax as well.

Note: Object keys must be enclosed with single or double quotes like strings.

// nanoscript

let empty = {};

let o = {
    'x': 1.0,
    'y': 2.5
};

let dog = {
    'name': 'Dogmeat',
    'level': 42
};

Note: Object literals that can be fully determined at compile-time, will be pre-compiled and result in a massive performance increase.

Examples

// typescript

const code = ... // Your nanoscript code (below)

const myModule = {
    name: "myModule",
    exports: [
        { name: "addNumbers", type: "function", object: (a: number, b: number) => a + b },
        ...
    ] as nenv.NenvExport[]
} as nenv.NenvModule;

const engine = new NSEngine();
engine.addModules([
    myModule,
    ...
]);

const output = engine.compileAndRun(code);
// nanoscript

let a = 100;

// use the imported function addNumbers from the module
console.log(addNumbers(a, 10));     // prints 110 to the console

The console object and addNumbers functions are pulled in from the nenv and can be called by name inside of the nanoscript script.

The WebAssembly Backend

The default backend is a JavaScript stack machine: portable, no build step, works everywhere. For compute-heavy scripts there is a second backend that translates each compiled program into a purpose-built WebAssembly module.

// Node, or inside a Web Worker
const engine = new NSEngine({ backend: "wasm" });
const result = engine.compileAndRun(code);

On a browser's main thread, use the async entry point (browsers reject synchronous WebAssembly compilation of modules over 4 KiB there):

const engine = new NSEngine({ backend: "auto" });
const result = await engine.compileAndRunAsync(code);

When one program runs many times, translate it once and reuse it:

import { WasmExecutor } from "@creation-wasteland/nanoscript/wasm";

const program  = engine.compile(userScript);
const compiled = await new WasmExecutor().compileAsync(program);

for (const row of rows) {
    console.log(compiled.run());
}

Measured against the JavaScript backend (Node 22, npm run bench):

Benchmark JS WASM Speedup
sum loop 1e6 107.4 ms 10.9 ms 9.9x
nested loop 1e6 107.5 ms 10.5 ms 10.2x
fib(27) recursive 99.2 ms 16.2 ms 6.1x
float math 300k 80.6 ms 11.3 ms 7.1x
array sum 200k 11.5 ms 4.5 ms 2.5x
string build 20k 2.9 ms 0.9 ms 3.4x
object churn 100k 23.3 ms 16.2 ms 1.4x

Translating a program costs 0.3-3 ms, so the WASM backend is a loss for tiny scripts that run once and a large win for anything long-running or repeated.

There is no external toolchain involved: the module bytes are emitted directly from TypeScript at runtime. docs/wasm-engine.md covers the design (NaN boxing, basic-block dispatch, the host bridge) and the three documented divergences from the JavaScript backend.

Editor Support

A VS Code extension lives in-tree at editors/vscode, so the tooling always matches the compiler. It provides:

  • Syntax highlighting for .ns / .nano files, including template strings and the unicode operators (, )
  • Live diagnostics from the real tokenizer → parser → compiler pipeline, with token-precise squiggles (strict typing on by default, nanoscript.allowAny to relax it)
  • Completion and hover for keywords, : type annotations, and the builtin module's exports
  • Run-in-editor commands for both the JS and WASM backends, plus a Compare both command that flags any divergence between them

See editors/vscode/README.md for settings and build instructions. Example scripts are in editors/vscode/examples.

For building your own tooling, all three pipeline stages throw NSError, an Error subclass carrying the stage, 1-based line, and absolute character offsets — see the Errors section of docs/embedding-api.md.

Documentation

Full reference documentation lives in docs/:

Document Contents
architecture.md The four-stage pipeline and module map
language-reference.md Complete syntax and semantics
tokenizer.md Lexical analysis and token types
parser.md The recursive-descent parser and the AST
compiler.md Scopes, frames, labels, code generation
bytecode-reference.md The full instruction set
type-system.md DType and type operations
executor.md The JavaScript stack machine
embedding-api.md NSEngine, Nenv, modules, security model
wasm-engine.md The WebAssembly backend

Known bugs and unfinished features are tracked in plan.md.

Future Features

  • Custom classes
    • Adding the ability to define classes, like one would in C++
  • TypedArrayBuffer Syntatic Sugar
    • I want to add fancy syntactic sugar for js typed arrays. Something like int[30] in nscript being equal to new Int32Array(30) under the hood (or even being on the stack in the WASM stack machine)
  • Give me more ideas...

License

This project is licensed under the MIT License. See the LICENSE file for details.

About

A server/client javascript-based interpreter for simple custom application specific scripts.

Resources

Stars

Watchers

Forks

Releases

Packages

Used by

Contributors

Languages