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.

TypeTo JavaScriptFrom JavaScript
Int Float Str BoolAs-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 conversionCopied 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:

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.