Para Lang
An optional .pts / .pjs syntax over Para Lib. Adds reactive
bindings (signal / effect / ~> / ->), pipelines
(|>), error chaining (..! / ..&), integer ranges,
pure / memo declarators, and defer / arena blocks. Compiles
to standard JavaScript at parse time.
The reactive forms desugar to imports from @para/signals; the rest desugar to plain JS. The
.pts compiler today lives inside ParaBun; a standalone
@para/transpile for non-ParaBun build hosts is in development.
Syntax (Para Lang)
Signals and effects
A signal declaration creates a reactive cell. Bare reads inside a tracked context (an
effect, a derived, a when block, or another signal's RHS) compile to
.get(). Bare writes compile to .set(). A signal whose initializer reads other signals
is auto-promoted to a derived value.
signal count = 0;
signal doubled = count * 2; // derived; recomputes when count changes
effect { console.log(doubled); } // runs once now, again on each change
count++; // count.set(count.get() + 1)
Edge-triggered handlers
A when block fires its body once on each false→true transition of the predicate.
when not fires on the true→false transition. The predicate is tracked the same way an effect
body is.
signal score = 0;
when score >= 100 { unlockAchievement("century"); }
when not online { showOfflineBanner(); }
Reactive bindings
A ~> B desugars to effect(() => { B = A; }), an assignment that stays in sync.
A -> fn desugars to effect(() => { fn(A); }), a call binding. Both are shorthand
for the common single-statement effect.
signal name = "world";
name ~> document.title; // title tracks name
name -> console.log; // logs on every change
Ranges and pipelines
a..b is an exclusive integer range; a..=b is inclusive. The pipeline operator
|> threads a value through a sequence of unary calls.
for (const i of 0..n) work(i);
const evens = 0..=20 |> filter(i => i % 2 === 0);
const out = pixels |> map(p => p * 1.2) |> clamp(0, 255);
Promise operators
Three sibling operators cover the whole Promise.prototype chain symmetrically.
..> is .then, ..! is .catch, ..& is
.finally — same precedence, same handler shape, composable in any order. Bare arrow handlers
compose without parens (each arrow body terminates at the next chain op), and a leading . in
..> / ..! position is sugar for "method/property on the resolved value":
..> .json() means ..> (_) => _.json(). (No leading-dot sugar for
..& — finally callbacks have no value to bind.) await binds tighter than the
dotted operators, so wrap the chain to await it.
const data = await (
fetch(url)
..> .json() // .then — call .json() on the response
..! .message // .catch — extract error message
..& () => spinner.hide() // .finally — runs always
);
Parallel awaits
parallel is the answer to const [a,b,c,d,e] = await Promise.all([f,g,h,i,j]) — the
positional-array shape where reordering one side without the other is a silent bug, and where every long name
appears twice. Two forms: a statement form that hoists names directly (each appears once), and
an expression form that returns the bag (chainable with ..!).
// statement — names appear exactly once, hoisted into scope
parallel let user = fetchUser(id),
posts = fetchPosts(id),
comments = fetchComments(id);
// each binding can have its own ..! for per-item error handling:
parallel let user = fetchUser(id) ..! defaultUser,
posts = fetchPosts(id) ..! [],
comments = fetchComments(id) ..! [];
// expression — returns a Promise of the resolved object, chainable
const bundle = await parallel { user: fetchUser(id), posts: fetchPosts(id) };
const data = await parallel { user: …, posts: … } ..! err => fallbackBundle;
Decimal literals
0.1 + 0.2 !== 0.3 keeps biting people. The Nd literal suffix produces a
Decimal with exact arithmetic — BigInt-backed coef × 10^exp internally, no
floating-point roundoff. JS doesn't allow operator overloading so arithmetic is explicit method calls
(.plus, .minus, .times, .dividedBy); division takes
{ precision, roundingMode }.
0.1d.plus(0.2d).eq(0.3d); // true (the headline)
1d.dividedBy(3d, { precision: 20 }).toString(); // "0.33333333333333333333"
100d.dividedBy(8d).toString(); // "12.5" — exact
const tax = price.times(0.0825d);
const total = price.plus(tax);
Compilation
Para files (.pts) are parsed by ParaBun's transpiler. (Mainline Bun doesn't
recognize the syntax — the parser additions are part of the ParaBun fork.) The output is standard JavaScript
with a handful of imports from para:* module specifiers.
On ParaBun, those specifiers resolve to built-in modules. On any other host (browser, Node, Bun, Deno,
Cloudflare Workers, …) alias them to the matching @para/* npm packages in your bundler.
// vite.config.ts
import { defineConfig } from "vite";
export default defineConfig({
resolve: {
alias: [{ find: /^para:(.*)$/, replacement: "@para/$1" }],
},
});
The same one-line alias works for esbuild, webpack, and rollup. See the install guide for the variants.
ParaBun is required for the build step today (it owns the .pts parser). A standalone
npm-installable transpiler (@para/transpile) is on the roadmap.
Examples
Three worked projects, one per host environment. Each is a complete project with file layout, source, and build commands.