JavaScript
Generated code
Each .lps file produces one ES module and a source map. Arithmetic on known types becomes an operator. A message send to a known class becomes a method call.
static fact$(n) {
return ((n <= 1) ? 1 : (n * Calc.fact$((n - 1))));
}
$.Http.get$("orders.json");
$.Json.parse$($.send(r, "body", []));
Operations on built-in types go through $.send. A Str is a JavaScript string and an Array is a JavaScript array, and neither has methods for , or collect:. LPScript does not add methods to prototypes. On a real screen, most sends are of this kind.
$.send accounts for less than 1% of the time of one render.
Calling browser APIs
There are no .d.ts files. You declare the APIs you use, as with OCaml's external or Haskell's FFI.
Extern subclass: #Session global: 'sessionStorage'.
Session class >> getItem: Str -> Maybe[Str].
Session class >> setItem: Str with: Str -> Unit.
Extern subclass: #Ctx.
Ctx >> fillRect: Int y: Int w: Int h: Int -> Unit.
Ctx >> fillStyle := Str.
You can declare a global, a module (from: 'node:path'), a constructor (new:), a property read, and a property write. localStorage, WebSocket, canvas, location, and requestAnimationFrame are all used this way. None of them is built into the language.
A method call and a property read are distinguished by (). Sock >> close() -> Unit. is a call. Sock >> close -> Unit. is a property read. Writing the second by mistake reads the function object and discards it, and both the type check and the build pass. For this reason, a property read declared to return Unit produces a warning.
"close" is declared as a property read, so it answers the function without
calling it — write `close()` if it is meant to run
Warnings go to standard error. The exit status is unchanged. The editor shows them as warnings.
Type conversion at the boundary
Values are converted according to the declared type. Nested types, such as the elements of Array[Date], are converted too.
| Type | To JavaScript | From JavaScript |
|---|---|---|
Int Float Str Bool | As-is | $.int etc. |
Date | '2026-08-12' | $.date |
Time | '09:30' or '09:30:45' | $.time |
DateTime | '2026-09-05T09:30' | $.dateTime |
Maybe[T] | The value or null, converted | $.maybe |
Dict[Str V] | A plain object, values converted | $.dict |
Array[T] | As-is when T needs no conversion | Copied and frozen |
Future[T] | As-is (a Future is a Promise) | As-is |
Date, Time, and DateTime are passed as strings because converting them to a JavaScript Date would attach a time zone.
Values returning from JavaScript are checked. If a string that is not a valid time arrives where a Time is declared, the boundary raises an error. A value cannot be typed as Time at compile time and be a string at run time.
Types with no single conversion are rejected at the declaration.
"take:" cannot cross to JavaScript: JavaScript has no decimal, and which way to
lose that is the caller's to choose — send `asString` to keep the digits,
`asFloat` to compute with it
ActorRef and Bytes are rejected for the same reason. An ActorRef has no meaning outside the LPScript runtime, and Bytes has no defined JavaScript form.
Minification
Running a name mangler (terser, uglify with mangle) on the generated JavaScript breaks it, for two reasons:
- Message sends carry the selector as a string. In
$.send(o, "at:put:", [...]),"at:put:"must match the method name in the runtime. Html clone:matches the HTML by CSS selector. Mangling the string breaks the match.
The compiler minifies what can be minified safely.
lpsc build orders.lps --minify
Local variables become a, b, and so on, per method. Indentation and comments are removed. Method names, class names, selector strings in $.send, and CSS selectors are unchanged. This is done inside the compiler, so the build requires nothing besides lpsc. Every example in the distribution is tested to produce the same output with --minify. A source map is still produced.
Deployment
lpsc build writes a .mjs.map next to each .mjs. The source map contains the full text of the .lps file, including comments. Deploy only the .mjs files.
The generated JavaScript is readable. Authorization decisions, such as whether a user may see a unit price or advance an order, belong on the server. If they are made there, readable client code is not a problem.