Syntax · behaviors · config

The language the compiler can prove.

LilScript is its own typed language. It is not TypeScript with extra comments. Types are the compilation model: they decide representation, what may be mangled, and what must keep a JavaScript name. This page is the readable tour. The full contract is in the docs.

Syntax

Declarations put the type first. Locals need an initializer. auto infers from that initializer. Methods and functions look familiar; the difference is that the compiler still knows the types after parse.

int count = 5;
float ratio = 3.14;
string title = "LilScript";
bool enabled = true;
int[] scores = [10, 20, 30];
auto doubled = scores.map((int value) => value * 2);

pure int square(int value) {
  return value * value;
}

Statements end with ; except blocks. Strings are double quotes; templates use backticks. Comments are // and /* */. Source is UTF-8. The file extension is .lil.

Types

Each type has a defined JavaScript (and, where it is portable, native) representation. There is no erased layer that a later minifier has to reverse-engineer.

Type Meaning JavaScript
int Signed 32-bit, operator-defined overflow Number with i32 operations
float / number IEEE-754 binary64 Number
bool, string Exact values, immutable text Boolean, String
T? T or null, narrowed by checks Value or null
struct / class Fixed fields, field indexes in IR Scalars, array slots, or a boundary object
Record<T> Open string keys — keys are data Null-prototype object
extern class Host object, exact member names Existing host object
JsValue Narrow dynamic hatch Unchanged host value
Task<T> Typed async result Promise-compatible

int widens to number. Bitwise operators stay int. Enums are integer discriminants with no metadata object. Generics are checked, then erased or boxed at a boundary.

Structs, classes, objects

This is the main reason a rewrite can be both smaller and a bit faster. The language does not pretend every value is a string-keyed object.

Kind Job What the compiler may do
struct Positional value Scalar-replace if it never escapes; otherwise array slots
class Nominal reference, init, methods Devirtualize; dissolve locals; flatten single inheritance
object Closed public singleton Keep ABI keys; nest and mangle private bodies
Record<T> Open map Never mangle keys

Overriding is rejected on purpose. Silent static dispatch would be unsound; vtables would add size. Construction is Point{10, 20} for structs and new Vector(3, 4) for classes.

Two independent knobs sit on top of that. javascript.aggregate_layout chooses array backing versus named objects for instances. javascript.public_aggregate_abi chooses named fields versus opaque array handles at a reusable JavaScript boundary.

Mangling

Most minifiers mangle locals. LilScript also mangles owned properties and the method names that would live on a prototype — when the owner is LilScript. document.createElement does not get renamed. Record keys are data. Export names are a separate opt-in for closed apps.

[mangle]
# identifiers = true
# properties = true   # size-first default
# exports = false     # keep public ESM names unless the whole app is LilScript
# pool_strings = true

Identifier spelling is searched, not guessed. The compiler re-ranks short names against the selected codec: gzip cares about a 32 KiB window, Brotli about a much larger one and a context model. Similar functions can reserve the same local letters so back-references hit.

Codec objective

Served bytes are the product. javascript.cost_model is the score:

cost_model What is measured
raw Emitted UTF-8 length
gzip Stock zlib 1.3.1, level 9, deterministic mtime
brotli Official Google Brotli 1.1.0, quality 11 — the usual default

The three disagree. A raw win can be a Brotli loss. Candidate search emits legal alternatives and keeps the winner for the configured objective only. That is why a gzip-tuned build is allowed to look worse on Brotli.

Performance is an objective too

Compression is the reason the language exists. It is not the only ranking key. javascript.priority trades transfer size against a static model of parse, compile, allocation, and indirect calls:

priority Ranks Typical effect
size-first Transfer first Broadest search, property mangling, packing, layout search
balanced Mix of size and shape Fewer size-only tactics
performance-first Shape first Keeps |0, less packing, hotter lowering

Array-backed structs and scalar replacement are also runtime choices: fewer hidden classes, less pointer chasing, sometimes less heap. That is why the same rewrite can be smaller on the wire and a little cheaper in memory.

Configuration

The compiler discovers lilscript.toml from the entry file. Unknown keys are errors. There are a lot of knobs — that is intentional. A reusable package and a closed app are not the same product.

[javascript]
priority = "size-first"
cost_model = "brotli"
optimization_level = 15
candidate_search = "production"

[optimization]
preset = "maximum"
inlining = true
scalar_replacement = true
dead_code_elimination = true

[mangle]
properties = true
exports = false

[bundle]
mode = "split"

Public vs closed

Keep export names for an npm-shaped library. Turn mangle.exports on when the whole consumer graph is LilScript.

Development

--mode development and lilpack dev skip the multi-candidate search so reloads stay usable.

Exact allowlists

javascript.compression and javascript.optimizations can name the tactics that are even legal. Omit them to use the profile defaults.

Native

The same IR can emit C11 or a native binary. Host-only features are rejected there rather than approximated.

The schema dump lives in the configuration docs.

Toolchain

The language is meant to sit next to ordinary web work, not in a Java-sized batch job.

Lilpack

Vite 8 plugin that shells out to lilscript. Mixed .lil / JS / TS graphs, HMR, production hashing. Details on the Lilpack page.

VS Code and LSP

lilscript-lsp plus vscode-extension/: highlight, diagnostics, completion, hover, symbols, rename, format, import organize.

Lint and format

lilscript-lint and lilscript-fmt. JSON and SARIF exist for agents and CI.

Playground

Compile a snippet in the browser on the playground.

cargo build --release --bins
target/release/lilscript app.lil -o app.js
target/release/lilpack dev
target/release/lilscript-lsp --help

Closed world

The entry file and every static import are one compilation unit. Crossing into the host takes an explicit extern or extern class. Values that escape keep a boundary ABI. Dynamic import("./feature") is a typed lazy chunk, not an untyped Promise of anything.

That closed world is the Closure Advanced idea, restated as a language. The difference is that proofs exist before JavaScript is spelled, and the compile loop is built for everyday web work.