Skip to content

Type System

NSL employs a strong, static type system. Every variable, parameter, and return value has a type known at compile time. Type mismatches are caught during semantic validation, before any bytecode is generated or executed.

┌─────────────┐
│ Types │
└──────┬──────┘
┌────────────┼────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│Primitives│ │ Complex │ │ Special │
│(by value)│ │(by ref) │ │ │
└────┬─────┘ └────┬─────┘ └────┬─────┘
┌──┼──┬──┐ ─┼──────┐ ┌─┼────┐
▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼
int dbl bool str struct arr json void

Primitives are immutable and passed to functions by creating a copy of their value. They are stored directly in the primitive register bank (pMem) as 64-bit long values.

A 64-bit signed integer.

int count = 42;
int negative = -100;
int max = 9223372036854775807; // Long.MAX_VALUE

Internal representation: Stored directly as long in pMem.

A 64-bit IEEE 754 floating-point number.

double pi = 3.14159;
double rate = 0.05;
double large = 1.7e308;

Internal representation: Stored as Double.doubleToRawLongBits(value) in pMem. Read back with Double.longBitsToDouble(pMem[i]).

A logical true or false.

boolean active = true;
boolean deleted = false;

Internal representation: Stored as 1L (true) or 0L (false) in pMem.

An immutable sequence of UTF-8 characters.

string name = "Alice";
string greeting = `Hello, ${name}!`; // Interpolation
string empty = "";

Internal representation: Stored as a JVM String object in the reference bank (rMem).

Note: Despite being conceptually “primitive” in NSL, strings are stored in rMem because they are JVM objects. However, they are immutable and exhibit value-like semantics, modifying a string always creates a new one.

Complex types are passed by reference. Modifying a complex type inside a function affects the original object in the caller’s scope.

Structs are pure data schemas. They define the expected shape of a JSON object. They contain no methods or logic, only field declarations.

type ApiConfig {
string endpoint;
int timeout_seconds;
boolean enable_retries;
}

Structs are instantiated using object literal syntax:

ApiConfig config = {
endpoint: "https://api.example.com",
timeout_seconds: 30,
enable_retries: true
};

The compiler validates struct instantiations at compile time:

// SemanticError: Missing field 'enable_retries' for type 'ApiConfig'
ApiConfig bad = {
endpoint: "https://api.example.com",
timeout_seconds: 30
};
// SemanticError: Unknown field 'extra' for type 'ApiConfig'
ApiConfig also_bad = {
endpoint: "https://api.example.com",
timeout_seconds: 30,
enable_retries: true,
extra: "oops"
};
string url = config.endpoint;
config.timeout_seconds = 60; // Modifies the original (pass-by-reference)

Internal representation: A NoxObject (backed by HashMap<String, Object>). Field access compiles to HACC/HMOD super-instructions.

Structs can contain fields of other struct types, including their own type:

type Address {
string city;
string zip;
}
type User {
string name;
Address address; // Nested struct
}
type TreeNode {
string value;
TreeNode left; // Recursive, nullable (null = leaf)
TreeNode right;
}

Recursive struct fields are always nullable. A null value indicates the absence of the nested object (e.g., a leaf node in a tree).

Homogeneous, ordered lists of a single type.

int[] numbers = [1, 2, 3, 4, 5];
string[] names = ["Alice", "Bob", "Charlie"];
ApiConfig[] configs = []; // Empty typed array
int len = numbers.length(); // Method call
int first = numbers[0]; // Index access
numbers[0] = 99; // Index assignment
numbers.push(6); // Append element

Internal representation: A Kotlin ArrayList (or NoxArray for JSON-derived data).

The json type is a flexible, dynamic type that represents an arbitrary JSON object. It is the bridge between the typed NSL world and the unstructured data that comes from external sources.

json data = Http.getJson("/api/data");
json config = { key: "value", count: 42 };

JSON literals can be nested arbitrarily — field values may themselves be JSON objects, arrays, or arrays of objects:

json payload = {
user: { id: 1, name: "Alice" },
tags: ["admin", "owner"],
items: [{ sku: "A", qty: 2 }, { sku: "B", qty: 1 }],
};

Inside a JSON literal, every value is treated as a JSON value, so nested { ... } does not need an explicit struct type.

Any user-defined struct can be implicitly cast to json:

ApiConfig config = { endpoint: "url", timeout_seconds: 30, enable_retries: true };
json generic = config; // Implicit upcast (always safe)

Going the other direction requires an explicit cast using as. The VM (CAST_STRUCT opcode) performs deep structural validation during the cast, checking all primitive fields, nested structs, and typed arrays against a compile-time TypeDescriptor:

json rawConfig = Http.getJson("/config");
// Cast will fail at runtime if rawConfig is missing required fields
ApiConfig config = rawConfig as ApiConfig;

If the cast fails (missing or mistyped fields), a CastError is thrown that can be caught with try-catch.

The json type provides safety-first methods for accessing data without knowing the schema. Every method takes a default value that is returned if the key is missing or the wrong type:

MethodReturn TypeDescription
json.getString(key, default)stringGet string value, or default
json.getInt(key, default)intGet integer value, or default
json.getBool(key, default)booleanGet boolean value, or default
json.getDouble(key, default)doubleGet double value, or default
json.getJSON(key, default)jsonGet nested JSON object, or default
json.getObject(key, default)typeGet and typecheck against default’s type
json.has(key)booleanCheck if key exists
json.keys()string[]Get all top-level keys
json.size()intNumber of keys (object) or elements (array)
json.getIntArray(key, default)int[]Extract a typed integer array, or default
json.getStringArray(key, default)string[]Extract a typed string array, or default
json.getDoubleArray(key, default)double[]Extract a typed double array, or default
json.getArray(key, StructType, default)StructType[]Extract and cast each element to a struct type
json user = Http.getJson("/api/user/123");
string name = user.getString("name", "Unknown");
int age = user.getInt("age", 0);
boolean active = user.getBool("is_active", false);
if (user.has("preferences")) {
json prefs = user.getJSON("preferences", {});
string theme = prefs.getString("theme", "dark");
}

NSL enforces strict rules for string manipulation to prevent common type-related bugs.

Template literals using backticks are the preferred way to build strings. Expressions inside ${...} are evaluated and safely converted to strings:

int count = 42;
double rate = 3.14;
string msg = `Found ${count} items at rate ${rate}.`;
// Result: "Found 42 items at rate 3.14."

Any expression is valid inside ${...}:

string result = `Sum is ${a + b}, product is ${a * b}.`;

The + operator is only defined for string + string. Concatenating a string with any other type is a SemanticError:

string a = "hello";
string b = " world";
string c = a + b; // OK since string + string
int num = 10;
string d = a + num; // SemanticError: Operator '+' not defined for (string, int)
// Use interpolation instead:
string e = `${a}${num}`; // OK gives "hello10"

This strictness prevents subtle bugs where automatic coercion produces unexpected results (e.g., "Count: " + 1 + 2 producing "Count: 12" instead of "Count: 3").

Reference types in NSL are nullable. Primitive types are not.

TypeNullable?Default Value
intNo0
doubleNo0.0
booleanNofalse
stringYes-
jsonYes-
StructsYes-
ArraysYes-
string name = null; // Valid
json data = null; // Valid
int[] items = null; // Valid
int count = null; // SemanticError: Cannot assign null to primitive 'int'
if (name == null) { ... } // Check for null
if (name != null) { ... } // Check for non-null

Accessing a property or calling a method on a null reference throws a NullAccessError:

string name = null;
int len = name.length(); // NullAccessError: Cannot access 'length()' on null

This is a runtime exception and can be caught with try-catch.

NSL uses type-bound methods for explicit conversions between types. There are no implicit narrowing conversions, only int to double is implicit (widening).

int x = 42;
double d = x.toDouble(); // 42.0
double pi = 3.14;
int truncated = pi.toInt(); // 3 (truncation toward zero)
int x = 42;
string s = x.toString(); // "42"
double rate = 3.14;
string r = rate.toString(); // "3.14"
string numStr = "123";
int parsed = numStr.toInt(0); // 123 (0 is the default if parsing fails)
double val = numStr.toDouble(0.0); // 123.0
MethodOn TypeReturnsNotes
.toDouble()intdoubleWidening, lossless
.toInt()doubleintTruncation toward zero
.toString()int, double, booleanstringString representation
.toInt(default)stringintParses string; returns default on failure
.toDouble(default)stringdoubleParses string; returns default on failure
From → TointdoublebooleanstringjsonStructArray
intImplicit.toString()
double.toInt().toString()
boolean.toString()
string.toInt(d).toDouble(d)
jsonExplicit cast
StructImplicitSame type
ArraySame element type