JavaScript

What comes out

ES modules, one per .lps, with a source map beside them. Inference feeds code generation, so arithmetic on a known type becomes the operator and a send to a known class becomes the method:

static fact$(n) {
  return ((n <= 1) ? 1 : (n * Calc.fact$((n - 1))));
}

$.Http.get$("orders.json");
$.Json.parse$($.send(r, "body", []));

The honest limit: operations on built-in types still go through $.send, because a Str is a JavaScript string and an Array is a JavaScript array — there is no object to hang , or collect: on. On a real screen that is most of the sends. It costs nothing measurable, and what it buys is that one dispatcher, not the emitter, decides what a selector means on a primitive.

Reaching JavaScript

There is no .d.ts to read. You declare what you use, the way OCaml's external and Haskell's FFI do:

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.

A global, a module (from: 'node:path'), a constructor (new:), a property, a setter. That is the whole surface. localStorage, WebSocket, canvas, location and requestAnimationFrame are all reached this way, and none of them is in the language.

The boundary converts, or refuses

The declared type decides the conversion, following the type into its parts:

typeto JavaScriptfrom JavaScript
Int Float Str Boolas the plain value$.int and friends
Date'2026-08-12' — a calendar day carries no zone$.date
Maybe[T]the value or null, converted$.maybe
Dict[Str V]a plain object, values converted$.dict
Array[T]as-is when T iscopy and freeze theirs
Future[T]as-is: a Future is a promiseas-is

What has no honest reading is refused rather than half-converted:

"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

An ActorRef is refused for the same reason: an address means nothing outside this runtime.

It cannot be obfuscated

Running a name mangler (terser, uglify) over the emitted JavaScript breaks it. Two reasons.

A send carries its selector as a string. In $.send(o, "at:put:", [...]), that "at:put:" has to match the method name in the runtime. Renaming properties moves one and not the other.

Html clone: matches the designer's page by CSS selector. Mangle the strings as well and '#lines tr' no longer finds anything.

An exclusion list would work, and would grow with every screen. Do not plan around obfuscation.

What to leave out of a deploy

lpsc build writes a .mjs.map beside each .mjs. The source map carries the .lps verbatim, comments and all. Ship the .mjs only.

Readable JavaScript reaching the browser is fine as it is. Obfuscation raises the cost of reading and defends nothing. Decisions about permission — may this person see the unit price, may this order be advanced — belong on the server. Made there, a readable client costs you nothing; not made there, obfuscation does not hide them either, because the request is on the wire.