Public vs closed
Keep export names for an npm-shaped library. Turn
mangle.exports on when the whole consumer graph is
LilScript.
Syntax · behaviors · config
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.
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.
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.
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.
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.
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.
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.
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"
Keep export names for an npm-shaped library. Turn
mangle.exports on when the whole consumer graph is
LilScript.
--mode development and lilpack dev skip
the multi-candidate search so reloads stay usable.
javascript.compression and
javascript.optimizations can name the tactics that
are even legal. Omit them to use the profile defaults.
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.
The language is meant to sit next to ordinary web work, not in a Java-sized batch job.
Vite 8 plugin that shells out to lilscript. Mixed
.lil / JS / TS graphs, HMR, production hashing.
Details on the Lilpack page.
lilscript-lsp plus vscode-extension/:
highlight, diagnostics, completion, hover, symbols, rename,
format, import organize.
lilscript-lint and lilscript-fmt. JSON
and SARIF exist for agents and CI.
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
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.