Status: active clean-break design Compiler: current VelarScript compiler Runtime: existing JavaScript engine; no separate VM
The Core library combines the most useful everyday parts of Python and modern JavaScript behind a small explicit VelarScript surface. It is not a copy of either standard library.
velar/* is a closed vocabulary owned by the language. A module belongs here
only when it provides universal computation that any program may need, or a
minimal orthogonal capability primitive for interacting with the outside world.
Domain functionality such as editor, game, or chart tooling is always an
installable library, even when it is implemented entirely in VelarScript.
- Core ownership, permanent vocabulary, and purity are separate contracts.
The permanent namespaces provide familiar, widely useful operations; their
names do not promise purity.
Math.randomobserves nondeterminism,Promise.sleepschedules time, and the preludeprintproduces diagnostic output. Files, networks, processes, environment, and UI resources require an explicitly imported capability. Deterministic toolboxes that only some programs need also keep an import. Caching, reordering, or repeating a call requires proof from that operation's contract. - Everyday value operations live on checked string, number, and collection members. Nothing patches JavaScript prototypes or creates new global names.
- Implementation language does not determine membership. Reusable domain
modules written in VelarScript distribute an ordinary installable package with
a mandatory root
velar.entryand any needed exactvelar.entriessubpaths, then are imported by those package specifiers after installation. Official non-standard experiments use the dedicated public@velarscript-labs/*npm scope;@velarscript/*stays reserved for Standard owners, official targets, and the language toolchain. - Collection transforms return new lists and maps unless their name explicitly
describes another result. JavaScript reference identity and
numbersemantics remain unchanged. - Missing collection and parsing results use
null, never JavaScriptundefined. - The compiler preserves element, callback-result, map, optional, runtime data, and Promise result types through built-ins. User-defined generic functions, records, and classes use the same checked type relationships and call-site inference as the Core vocabulary.
- Runtime validation is explicit. Argument mistakes that can be proven
statically are diagnostics; dynamic misuse throws
TypeErrororRangeError. - Fixed official signatures expose the same
name=valuecalls as user-defined functions. Their documented parameter names are part of the checked API and editor signature help; source expressions still evaluate left to right. - A positional overload or pure rest call has no invented keyword surface.
randomInt, numericmin/max, and URLjointherefore stay positional, matching the familiar operation they represent. - Every API that requires
List<T>enforces the same dense, field-free, mutable data-element List boundary used by the language runtime. Sparse or frozen JavaScript arrays, arrays carrying hidden/extra fields, and accessor-backed elements do not become valid Lists through a library call; validation never invokes an element getter. - Map and Set boundaries use native internal-slot checks and prototype
operations. Cross-realm native collections are accepted, while subclass
overrides cannot replace size, iteration, membership, or lookup semantics.
Collection brands and runtime
Typetraversal capture their Array/Map/Set, reflection, size, iterator-factory, and iterator-step operations when the generated module initializes, so later global or prototype replacement cannot change a checkedis List/Set/Map/Recordresult. Direct List construction, indexing, iteration, snapshots, transforms, ordering, and mutation likewise use an initialization-captured List host ABI. Dense reflection and numeric bounds use explicit wrappers; value traversal is indexed and native join/sort/reverse operations are captured once. Set/Map/ Record operation hosts are separate runtime layers rather than aliases of this List contract. Set and Map construction and receiver operations also have their own captured host layer: native size, membership, lookup, mutation, keys/values/entries, iterator steps, copy, and clear are fixed when the generated module starts. Subclass overrides and later prototype or iterator replacement therefore cannot redirect ordinary Set/Map behavior. Record operations remain a separate data-object boundary. Record validation and receiver operations capture their own field discovery, descriptors, definition, deletion, identity, freezing, Reflect, and errors. Bracket access, one-slot/pair iteration, copy, mutation, and field snapshots use explicit indexed loops, independently of Array iterator or.mapbehavior. Record literal/spread and Record/List binding remain compiler lowering rather than receiver-method shortcuts, but reuse the same captured reflection, definition, allocation, and Error operations. Their generated part, field, and rest traversal is explicitly indexed, so later ambient replacement cannot redirect construction or destructuring. - Compiler-known runtime
Typevalues use two adjacent owned runtimes. One immutable registry proves that aType<T>value was compiler-created; the other captures WeakMap/Set recursion state, descriptor reads, Array/Promise/class identity, freezing, Reflect, and ValidationError behavior.Type.isandType.parsetherefore remain cycle-bounded, getter-free, and stable after ambient constructor/prototype or classSymbol.hasInstancereplacement without conflating validation execution with registry identity. - Core conversion is deliberately asymmetric and small:
str(value)performs explicit display conversion, whilenumber(text) -> number?parses one complete finite decimal after trimming surrounding whitespace and answersnullfor everything else — a partial parse, a radix form, a digit separator,"Infinity", or an overflow. The charter's section 7 states the exact grammar. JavaScriptBoolean,Number, andStringglobals are not source bindings, so truthiness, empty-string-to-zero, partial parsing, andNaNdo not re-enter through ambient coercion. - Core Node builds copy only imported official modules beside the generated
output. Portable modules also bundle and tree-shake in Web builds. Local
platform modules (
velar/serve,velar/fs,velar/env,velar/host,velar/terminal,velar/path,velar/process) are compile-time rejected for Web targets asVEL6008, positioned on the import, with platform-specific guidance: each names the Web module that does that job (velar/httpand the dev server,velar/files,velar/config,velar/url) or says plainly that the Web has no equivalent. - Resource-producing APIs are bounded contracts, not best-effort host calls.
A List contains at most 1,000,000 items; text and encoded JSON are limited to
16 MiB; JSON data contains at most 1,000,000 values and 128 nested
collections. List spread,
Set/Mapconstruction, and collection mutation preserve the 1,000,000-item invariant; an update to an existing Set value or Map key remains valid at the ceiling, while growth fails withRangeError. Dynamic misuse fails before a native capability is invoked.
The spelling of an operation, its package owner, and its effects are separate parts of its contract. Permanent vocabulary is immediately in scope; named modules are chosen explicitly; external-resource capabilities declare their host boundary. An import does not itself mean an operation is effectful, and a permanent namespace does not itself mean an operation is pure.
Json., Promise., Math., and Text., alongside the prelude names print,
str, number, equals, and range.
This set is short and closed because it carries familiar, broadly useful
computation and target-neutral runtime control. Purity belongs to individual
operations: Math.random, Promise.sleep, and print expose nondeterminism,
scheduling, and diagnostic output respectively. A new permanent name must
justify its universal responsibility and discoverability; it cannot silently
introduce access to external resources.
Both spellings that reach a permanent member are retired — the named import and
the namespace import — and each earns a diagnostic that teaches the namespace
spelling and a velar fix rewrite that performs it.
velar/binary, velar/hash, velar/validation, velar/random, velar/task, velar/url,
velar/test, and, on Web, velar/look.
These modules provide tools a program chooses deliberately. Each operation states its effects; deterministic computation, task scheduling, test execution, and visual declarations do not become one effect category just because they share the named-import spelling.
A computation already expressed by a collection operation belongs on the
collection. Those operations are checked List members; range is a prelude.
Clock reading, identifiers, and application logging remain explicit imports
through velar/time, velar/id, and velar/log.
velar/fs, velar/path, velar/process, velar/env, velar/host,
velar/serve, velar/terminal, velar/http, velar/worker,
velar/websocket, and the Web modules documented in web-api.md.
For a capability the import line is the audit signal — it is how a reader sees what a module touches — so no capability becomes permanent for convenience, however often it is used.
range(end), range(start, end), and range(start, end, step) produce a
stop-exclusive, checked List<number> of at most 1,000,000 items; negative
steps count down, and a zero or non-advancing step fails. The bounds are also
named — range(start=1, end=4, step=2). Importing the name is an error that
teaches the bare spelling.
The compiler recognizes only the direct one-slot loop head
for value in range(...):: it evaluates the arguments once, performs the same
complete range validation, and emits a native counter loop without
materializing the List. Aliased, saved, nested, or two-slot iteration keeps the
ordinary List contract, so there is no second public iterable type.
Every other collection operation is a checked List member — unique,
compact, flatten, chunk, partition, groupBy, keyBy, countBy,
zip, repeat, min(by=), max(by=), and sorted(by=, descending=) among
them. Charter section 8 is their reference. zip answers
List<Pair<T, U>>: Pair<A, B> is the Core record {first: A, second: B},
needs no import, and is written in an annotation exactly like List<T> is.
velar/binary is the common Node/Web binary boundary. Bytes is an immutable
snapshot with size and read-only integer indexing. UInt8Buffer,
UInt16Buffer, UInt32Buffer, and Float32Buffer are fixed-size mutable
working memory for compact state and numeric datasets. They provide checked
indexing, independent copy()/slice(start=0, end=size) values,
values() -> List<number>, and toBytes;
multi-byte buffers require an explicit ByteOrder. Matching *FromBytes
functions restore them. Every value, size, slice bound, and index is checked, so
the API never inherits typed-array truncation, non-finite floats, or
out-of-bounds no-ops. Constructors and every runtime Type.is/Type.parse
boundary enforce one 64 MiB byte ceiling before scanning or copying a typed
array. The compiler emits direct specialized index operations rather than
routing these types through reactive Lists or ordinary methods.
values() snapshots positions 0 through size - 1 into one fresh mutable
List; later writes to either value do not reach the other. It is available on
mutable and readonly fixed buffers and enforces the universal 1,000,000-item
List ceiling before allocating. Bytes remains the immutable byte snapshot and
does not gain this numeric-working-memory conversion.
UInt32Builder and Float32Builder grow only up to their required
maxElements; push(value) rejects overflow and finish() returns one exact
fixed buffer and seals the builder. Safe JavaScript declarations map
Uint8Array and Node Buffer results to read-only Bytes, Uint16Array to
UInt16Buffer, Uint32Array to UInt32Buffer, and Float32Array to
Float32Buffer. A JavaScript Uint8Array parameter accepts either Bytes or
UInt8Buffer; no Buffer-specific API enters source.
The constructors come in one shape per element width, and all of them are named
imports from velar/binary. uint8Buffer(size), uint16Buffer(size),
uint32Buffer(size) and float32Buffer(size) allocate a zeroed fixed buffer of
that many elements. uint16FromBytes(snapshot, order),
uint32FromBytes(snapshot, order) and float32FromBytes(snapshot, order)
rebuild one from a Bytes snapshot under the given ByteOrder;
uint8FromBytes(snapshot) takes no order, because a single byte has none to
read. uint32Builder(maxElements) and float32Builder(maxElements) open the
two growing builders above — there is no 8- or 16-bit builder, so a buffer of
either width is allocated at its final size.
A standard type carries its members wherever it is handed to you. velar/fs's
readBytes returns a Bytes; you can read .size and index it without
importing velar/binary. Import the declaring module only when you need to
write the type's name — in an annotation, a parameter type, or a Type
validator.
import {ByteOrder, Bytes, uint16Buffer, uint16FromBytes} from "velar/binary"
const values = uint16Buffer(4096)
values[0] = 7
const snapshot: Bytes = values.toBytes(ByteOrder.little)
const restored = uint16FromBytes(snapshot, ByteOrder.little)
assert restored[0] == 7
const numbers: List<number> = restored.values()
velar/random creates a deterministic Random from a string or safe-integer
seed. Its number(), int(start, end?), bool(probability=0.5), pick(values),
shuffle(values), and fork(label) operations have identical Node/browser
results. A fork derives an independent stream from the original seed and label;
it does not consume or couple itself to the parent's current position. The seed
is absorbed into the generator's full internal state, so distinct string or
safe-integer seeds — and distinct fork labels — give distinct streams rather
than colliding onto a smaller space. int(start, end?) accepts the same
increasing safe-integer range as Math.randomInt. number() returns a value in
[0, 1) carrying 32 bits of entropy; code that needs a full-mantissa double
should compose two draws itself.
velar/task owns structured asynchronous work. task(work, parent?) passes a
Cancellation into work, propagates a parent cancellation, and returns an
owned Task<T>. cancel(reason?) requests cancellation and waits for the work
to finish; CPU-heavy work cooperates with await cancellation.checkpoint().
A checkpoint reached after cancellation, and a send or next waiting on a
channel when its cancellation fires, reject with CancellationError — a named
export of velar/task, so if error is CancellationError: tells a cancelled
call apart from a failed one. cancel() and close() wait for cleanup without
re-raising that cancellation; result() still rejects with the task's error.
withTimeout(task, duration) cancels the underlying task before it rejects with
the Core built-in TimeoutError, which needs no import and is the same class
Promise.timeout raises — one concept, one identity (charter section 11). The
retired TaskTimeoutError spelling is refused at the import with that rewrite.
A using Task cancels and joins automatically on every exit.
channel(MessageType, capacity=64) creates a typed, bounded FIFO
Channel<MessageType> for many producers and one consumer. The Runtime Type is
passed explicitly because capacity alone cannot determine MessageType, and
every value is validated before entering the queue. send(value, cancellation?) waits while the buffer is full, but waiting senders are also
bounded by capacity; exceeding that second bound rejects with
ChannelBackpressureError. trySend(value) never waits and returns false
when the buffer is full — that is the only answer it gives instead of sending.
next(cancellation?) returns the next value or null after a closed channel
has drained, and therefore also supports async for.
Only one next call may wait at a time. close() is idempotent: buffered values
remain readable and no new value is accepted. After it, every sender fails with
ChannelClosedError — the senders already waiting, send, and trySend alike:
a closed channel is not backpressure, so there is no false that would mean
"try again later".
A channel is also how a caller parks until its own answer arrives, which is what
request/response multiplexing over a single connection needs: one reader owns
the connection's next() and forwards each answer into a capacity-1 channel the
waiting call is already reading, so nothing polls.
packages/desktop/README.md writes the pattern
out over a ServiceConnection; it is the same over a WebSocketConnection or
any other bounded pull source.
import {channel} from "velar/task"
type Message:
kind: string
value: string
const messages = channel(Message, capacity=64)
await messages.send({kind: "changed", value: "0,0,0"})
messages.close()
async for message in messages: print(message.value)
velar/worker uses entries declared in velar.json:
{
"formatVersion": 2,
"entry": "src/main.vel",
"workers": {"processor": "src/data-worker.vel"}
}worker(name, RequestType, ResponseType, capacity=64) starts one worker;
workerPool(name, RequestType, ResponseType, size, capacity=64) starts a bounded
pool. Both expose call(request, cancellation?, timeout?) and close(). A pool
also exposes broadcast(request, cancellation?, timeout?), which preflights
aggregate and per-member capacity, sends the request once to every live member,
and returns replies in member-creation order. A worker entry calls
serveWorker(RequestType, ResponseType, handler, capacity=64). Runtime Types validate both directions. Full-storage Bytes and
fixed numeric buffers are found inside bounded, cycle-safe List/Map/record
graphs. postMessage copies small caller-owned backing buffers directly;
larger shared buffers use a transferable snapshot. The caller's values are
never detached implicitly. Queue capacity supplies backpressure, and a crash
rejects every pending call with one stable worker error identity. The four
identities are named exports of velar/worker: WorkerBackpressureError when
the queue — a worker's own, or a pool's aggregate — is already full,
WorkerClosedError for a call on a closed worker or pool and for the calls
close() rejects, WorkerCallError for a request the handler itself failed
(the remote name, message, and stack travel with it), and WorkerCrashedError
when the worker dies, sends an invalid response, fails to acknowledge a
cancelled request within its grace period, or a pool has no live member left.
A call's configured timeout rejects with Core TimeoutError. The
implementation selects browser Worker or Node worker_threads; source never
handles native URLs or ports.
velar/websocket exposes connect(url, options?) on Node and Web, plus Node
listen(options). A WebSocketConnection sends string | Bytes, and its
next() -> Promise<(string | Bytes)?> is consumed directly or with async for.
closeInfo() -> Promise<WebSocketClose> returns the terminal numeric code and
reason after the handshake completes.
send resolves only after the bounded pending-byte budget drains. Connection,
message, send, accept, unread-message count, and aggregate unread-byte limits
fail explicitly, each as its own class (charter section 11):
WebSocketBackpressureError when a budget is full, WebSocketProtocolError
when the peer breaks the protocol or the handshake, WebSocketTimeoutError when
a bounded wait expires, and WebSocketClosedError when the connection has
already ended. Node additionally enforces process-wide connection, queued
message, pending connection, pending send, and 128 MiB byte budgets, so empty
messages cannot bypass accounting. maxQueuedBytes defaults to 16 MiB on Node
and Web. listen may receive a ServeApp or the same typed low-level handler
as velar/serve, so HTTP and upgrade traffic share one port and a composed app
keeps one startup/shutdown lifecycle. A ServeApp may declare multiple
@websocket routes; each route owns its checked pattern, pre-upgrade inputs,
one WebSocketConnection, and complete handler lifetime. In that declarative
mode WebSocketServer.next() and the listener's legacy single path option are
unavailable. Its maxBodyBytes option bounds HTTP
bodies on that shared listener. host is non-empty text of at most 255 code
units without NUL. origins accepts exact canonical HTTP/HTTPS
origins; by default upgrades carrying Origin receive 403 before admission,
while non-browser requests without Origin remain allowed. ["*"] is the
explicit unrestricted policy. Normal EOF preserves accepted messages
until next() drains them. Protocol/limit failure and server stop release
unread queues immediately; an abandoned connection is terminated and releases
its reservations when its owner is collected.
Connections and servers are owned resources for using.
Codecs, compression, procedural algorithms, database models, concrete drivers, and deployment-provider integrations are not part of the Standard API or this repository. The consuming project defines or installs the smallest checked VelarScript surface it needs and owns the implementation, dependency versions, resource ceilings, concurrency behavior, and tests.
Core has no database keyword, schema lowering, SQL parser, privileged adapter
package name, or provider-specific deployment surface. Reuse across projects is
handled through the public velar.entry package contract, never by expanding
the reserved velar/* namespace or adding hidden CLI dependencies.
Common string operations are checked members: size, trim, upper, lower,
slice, char, has, index, count, startsWith, endsWith, isBlank,
split, replace,
replaceAll, repeat, padStart, and padEnd. The compiler lowers them to
bounded helpers rather than trusting JavaScript prototype methods. They support
named arguments and first-class binding exactly like collection methods.
size, char(index), slice(start=0, end=size), and the positions returned by
index(text, start=0) use Unicode code points,
matching string iteration rather than JavaScript UTF-16 units. slice and
index count negative positions from the end and clamp; char does not — its
index domain is 0 ≤ index < size, so an index at or past the end reads as the
absent null its string? result exists to report, while a negative index is
out of range and throws IndexError, naming the index and the size. A
non-integer position throws the same IndexError in every one of them: charter
section 11 files an out-of-range or non-integer position under that one name,
whether the position is a List's or a string's, and try never turns one into
null. A count is not a position — repeat, padStart and padEnd reject a
size outside 0 through 16777216 as a range failure. index also clamps its
start and returns null when no match exists. text.has(part) and part in text are the method and operator forms of
the same substring test. Direct string indexing stays absent.
The Text. namespace is the extension toolbox beside those core methods: it
needs no import, and nothing moves between the two, so the member list a reader
must hold in mind never grows. It carries the transformations that are not
simple receiver operations: Text.trimStart, Text.trimEnd, Text.capitalize,
Text.title, Text.lines, Text.lineStarts, Text.chunks, Text.words,
Text.slug, Text.normalize, Text.truncate, Text.indent, Text.dedent,
Text.normalizeWhitespace, Text.utf8Size, Text.escapeHtml,
Text.codePoint, and Text.fromCodePoint. Blank text is tested with the
text.isBlank() member — true for empty or whitespace-only text.
Text.utf8Size(text) returns the exact byte count used
by official UTF-8 transport, JSON, and filesystem budgets, including stable
three-byte treatment of an unpaired surrogate. Text.lineStarts(text) performs one
bounded scan and returns [0, ...] Unicode code-point offsets immediately after
each line-feed character, including the final text size when the text ends in a
line feed. This keeps large-file line indexes out of repeated .char(index)
lookups without exposing JavaScript UTF-16 units. Text.chunks(text, size) performs
the same single bounded code-point scan and returns non-empty pieces of at most
size code points; it never splits a surrogate pair, and an empty input returns
an empty List. Stateless pattern operations are Text.matches, Text.findMatch,
Text.findMatches, Text.replaceMatches, and Text.splitPattern.
Text.title treats separators as word boundaries. Text.truncate reserves room
for its suffix. Text.slug lowercases Unicode text, folds Latin accents, removes
punctuation, and joins word runs with -. The fold is a compatibility fold: the
text is decomposed as "NFKD" before the marks are dropped, so every character
that carries a compatibility decomposition is replaced by that decomposition —
½ becomes 1-2, the fi ligature becomes fi, an Arabic presentation form
becomes the plain letters it stands for, and Thai ำ becomes the two signs it
decomposes to. Text.slug does not transliterate: a script that has no Latin
spelling keeps its own letters, and combining marks that carry meaning in their
own script — Devanagari and Bengali dependent vowels, Thai vowel and tone marks,
Hebrew niqqud, Arabic harakat — are kept, so distinct titles do not collapse onto
one slug. A run of marks with no letter, digit, or mark in front of it is dropped
rather than kept, because it has nothing to sit on: a variation selector left
behind by a removed emoji is invisible in a URL, and two slugs that read alike
would otherwise name different pages. The result is recomposed to NFC, so a slug
is not a decomposed near-miss of the text it renders as — a Hangul slug is found
again as a route key, a Map key, and a Set key, and its size is the number of
characters it renders as. Recomposition does not undo the compatibility fold, so
text a compatibility mapping separates from its own slug does not equal it.
Text.normalize(text, form="NFC") applies one of the four Unicode
normalization forms — "NFC", "NFD", "NFKC", or "NFKD"; any other form
throws RangeError. Text equality is code-point-sequence identity, so
canonically equivalent text is not equal: a precomposed accented character and
the letter plus its combining accent render identically but compare unequal,
report different size, and miss each other as Map and Set keys. Normalize
where such text enters the program — macOS filenames arrive decomposed while
typed text is usually composed.
Text.escapeHtml escapes text for HTML content and attribute contexts but does
not mark it as trusted HTML. Text.codePoint(character) answers the code point
of exactly one character and null for anything else — empty text, several
characters, or a lone surrogate half; Text.fromCodePoint(value) is its inverse
and throws RangeError for a value outside 0–1114111 or inside the
surrogate range, so no call can build text that is not a sequence of characters.
Pattern expressions use JavaScript pattern syntax in Unicode mode through a
captured intrinsic implementation, not a replaceable ambient RegExp global.
Each operation creates a fresh pattern; source code never receives RegExp or
its mutable lastIndex. Options are copied from one typed data record containing only optional
ignoreCase, multiline, and dotAll booleans. findMatch returns
{value, index, groups} or null; index is a Unicode code-point position, so
it can be passed directly to char or slice without leaking JavaScript UTF-16
offsets. Text.findMatches returns all such records and
normalizes an unmatched capture to null. Text.replaceMatches replaces every
match with one literal string, and Text.splitPattern omits capture groups from
the result. Invalid patterns throw TypeError at the VelarScript boundary,
carrying the engine's reason for the rejection. Patterns compile in Unicode
mode, so an identity escape that is tolerated elsewhere in JavaScript — \@,
\- outside a character class — is an error here and says so.
Pattern source is limited to 4,096 code units, pattern input/output and returned
match text to 16 MiB, and list-producing pattern operations to 1,000,000 results.
Matches are copied from checked data fields, empty Unicode matches always make
code-point progress, and replacement size is checked before the final string is
allocated. Those are size bounds; matching time carries its own. Every operation
that runs a caller-supplied pattern is given 250 milliseconds, checked after
each match, and exceeds it with a RangeError rather than running for as long
as a backtracking pattern likes. The engine is not interruptible, so one
catastrophic match is caught when it returns rather than pre-empted; what the
budget denies is the amplification a per-match loop would give that match.
Text.slug, Text.title, and Text.normalizeWhitespace are deliberately not
charged against it — they run this module's own linear patterns, so a large but
legal text must not succeed or fail by how busy the machine is. Patterns
are application code, not a sandbox for executing arbitrary user-supplied
regular expressions; applications that accept search text should use the
literal .has()/.startsWith()/.endsWith() operations unless they deliberately
own a pattern grammar.
Text counts used by .repeat, .padStart, .padEnd, and Text.truncate are
non-negative safe integers; native string-to-number coercion is not exposed.
Dynamic pattern options must be plain enumerable data fields, so getters,
symbols, and class instances are rejected without hidden evaluation. Text
composition such as .replace, .replaceAll, Text.escapeHtml, and
Text.indent check
its complete output budget before allocating the final string.
The compiler-owned String runtime and the Text. runtime capture their string, array,
numeric, reflection, RegExp, iterator-independent Unicode, and Error operations
when the module initializes. Pattern replacement and splitting are driven by the
captured native exec operation instead of mutable RegExp symbol hooks. Later
changes to JavaScript globals, prototypes, or string/array iterators therefore
cannot redirect a checked text operation.
const valid = Text.matches("VelarScript 42", "^velar [0-9]+$", {ignoreCase: true})
const ticket = Text.findMatch("ticket-42", "[0-9]+")
const fields = Text.splitPattern("one, two; three", " *[,;] *")
const initial = "Ada".char(0)
const short = "VelarScript".slice(0, 5)
print(f"{initial ?? "?"}:{short.size}")
The namespace exposes JavaScript Number mathematics without claiming Python
integer or decimal behavior. Every operation nevertheless requires an actual
number at runtime; the JavaScript Math global's coercion cannot turn "2",
[], or another dynamic JavaScript value into a VelarScript number.
What belongs on a number is a number method, so what is left here is exactly what cannot be one: the constants, the multi-argument functions, and the transcendentals.
| Group | Members |
|---|---|
| Constants | pi, e, tau, infinity |
| Bounds | min, max, clamp |
| Powers and logarithms | sqrt, cbrt, pow, exp, log, log2, log10 |
| Trigonometry | sin, cos, tan, asin, acos, atan, atan2, degrees, radians |
| Numeric helpers | hypot, random, randomInt, gcd, lcm |
The receiver-shaped operations are number members: .abs(), .round(),
.floor(), .ceil(), .sign(), .trunc(), .toFixed(digits), and the
predicates .isInteger(), .isSafeInteger(), .isNaN(), and .isFinite(). Writing a receiver operation
through the namespace — Math.abs(value) — is answered with the receiver
spelling to write instead. round returns a number at the
nearest integer; toFixed returns decimal text with 0 through 100 digits;
isInteger follows Number.isInteger, so Infinity and NaN are not
integers. .isSafeInteger() also requires the inclusive JavaScript safe-integer
range ±9,007,199,254,740,991.
These members use a compiler-owned Number runtime that captures their Math,
Number, reflection, and Error operations when the generated module initializes;
later replacement of JavaScript globals or prototypes cannot redirect them.
randomInt uses an inclusive lower and exclusive upper safe-integer
bound; with one argument its lower bound is zero. gcd and lcm likewise own
safe integers, and lcm rejects an inexact result. Randomness is the host
JavaScript runtime's Math.random and is not cryptographically secure. Its
result is checked as a finite number in the native [0, 1) range before either
random API returns a value. The numeric constants, mathematical operations,
random source, numeric predicates, Reflect invocation, and error constructors
are captured when velar/math initializes. Replacing Math, Number, their
methods, or the ambient error constructors afterward cannot redirect an
already initialized module; a host that supplies a missing/accessor-backed
operation or an invalid random result fails explicitly.
| Export | Behavior |
|---|---|
parse |
Parses JSON; an optional VelarScript type validates and types the result. |
tryParse |
Returns a validated value, an explicit fallback, or null. |
stringify |
Strictly serializes JSON data with an optional compact/indent setting. |
stableStringify |
Strictly serializes while recursively ordering record keys. |
clone |
Strictly JSON-clones a value and optionally validates the result with a VelarScript type. |
isSerializable |
Reports whether a value is losslessly representable as JSON data. |
Content comparison is one concept with one spelling: the prelude equals(a, b)
(charter section 4). It needs no import, requires its operands to intersect
statically, rejects class instances, functions, Promises, and unvalidated
unknown at compile time, compares Set members and Map keys structurally
rather than by reference, agrees with == on NaN, and throws on a cycle
rather than answering a quiet false.
Strict JSON is a compiler-owned runtime shared by Core and platform consumers.
It captures parsing/serialization, Array/Set traversal, reflection and data
descriptors, numeric/text/path operations, allocation, Reflect invocation, and
error constructors when the generated module initializes. stableStringify
uses that same captured sort operation. Replacing ambient globals or prototypes afterward
cannot redirect validation, snapshots, cloning, ordering, or equality. The
versioned reactive registry remains the one explicit dynamic seam used to
unwrap tracked values before JSON inspection.
Every member above is a permanent Core namespace member: JSON handling is pure
computation, so no Json.* call needs an import and named imports from
velar/json are retired with a diagnostic that teaches the namespace spelling.
type User:
id: string
name: string
const user = Json.parse(source, User)
const unchanged = equals(user, previousUser)
The second argument is the existing runtime form of a record type, a runtime
transparent alias, or an enum; there is no parallel schema declaration. The
compiler registers those frozen Type values under one shared runtime identity,
including when they cross module boundaries. parse, tryParse, and clone
check that identity before parsing or cloning and never inspect an arbitrary
object's is or parse fields as permission to skip validation.
JSON data is deliberately narrower than arbitrary JavaScript values: it
contains finite numbers, strings, booleans, null, dense Lists, fixed records,
and Record<T> dynamic string-key records recursively. Map, Set, class
instances, functions, sparse Lists,
accessor/symbol fields, non-finite numbers, and cyclic graphs are rejected
instead of inheriting JSON.stringify's silent field omission or {} / null
substitution. Known unsupported VelarScript types fail checking; unknown and unsafe
JavaScript values are validated at runtime. Repeated references are allowed
when the graph is acyclic. Indentation is false, true (two spaces), or an
integer from 0 through 10.
Parsing and encoded output are limited to 16 MiB. Validation stops beyond 1,000,000 values or 128 nested collections, checks array data descriptors without invoking getters, and estimates pretty-print expansion before calling the host serializer. Serialization and cloning operate on the data-descriptor snapshot created during that same validation pass, so a Proxy or List subclass cannot present one value for validation and a different value to the serializer. Parsed host results are copied through the same boundary, and the serializer's return value must be actual text.
| Member | Behavior |
|---|---|
sleep |
Resolves after a non-negative Duration (250ms, 1s). |
all |
Awaits a List of Promises to a List of results, or a record of Promises to a record of the same shape. A List whose elements resolve to different types is rejected in favour of the record form, so every result keeps a name. |
race |
Settles with the first Promise in a List to settle. A runtime-empty race List throws RangeError("race requires at least one Promise") — an empty race would never settle. |
timeout |
Rejects with TimeoutError if a Promise does not settle before a Duration; accepts an optional message, which becomes the error's message. TimeoutError is a Core built-in that needs no import, so catch error: if error is TimeoutError: tells "too slow" from "the task failed". |
retry |
Runs a zero-argument sync/async task again after failure, up to the retry count, waiting an optional Duration between attempts. |
map |
Maps a list with a sync/async worker and optional positive concurrency limit while preserving result order. |
series |
Runs a list of zero-argument sync/async tasks sequentially. |
Promise. is always in scope. Both spellings that reach these members through
velar/async — the named import and the namespace import — are retired and
receive a diagnostic that teaches the namespace spelling.
The table is the whole namespace, and the three JavaScript statics it does not
carry are absent for one reason each. Promise.resolve(value) has no Vel
spelling because an async def already produces a settled Promise from its
result: pass the value, and the awaiting side sees it. Promise.reject(error)
has none because the rejection is the throw inside that async def.
Promise.allSettled(list) has none because failure is already a value here:
try await task() answers null instead of rejecting, so mapping each task
through it and awaiting Promise.all gives the same list of settled outcomes
with no second result vocabulary. Writing any of the three earns a diagnostic
that names the replacement above.
These helpers use the host Promise queue. They do not create threads, cancel a
Promise, or replace the JavaScript event loop. Their List arguments use the
same dense List validation as collection helpers; concurrency counts are
positive safe integers. all, race, and timeout require actual Promises at
runtime, including across JavaScript realms; arbitrary thenables are rejected
without probing a then accessor. The module snapshots the Promise, timer,
numeric-validation, reflection, and dense-List operations it needs when the
module initializes. Later ambient replacement and List subclass overrides
cannot redirect its work. Retry attempts are positive safe integers and timeout
messages remain real strings at dynamic boundaries. all and race may start
at most 10,000 operations at once, map concurrency is at most 1,024, retry is
at most 10,000 attempts, and timer durations stay within the signed 32-bit host
timer range.
Short asynchronous workers use the language's expression arrow directly, for
example await Promise.map(urls, async url => await load(url)). Its inferred result is
Promise<T>; ordinary synchronous callbacks and named async def workflows
remain available for expression and block bodies respectively. All async forms
share native Promise adoption: a named async worker declared -> T may return
either T or another Promise<T> without adding return await merely to
satisfy the checker. JavaScript reserves a resolved value's top-level then
member, so retry cannot return a record/class with callable then or a class
with a then getter; the compiler rejects known cases and the runtime fails
closed at a dynamic callback. map and series may still collect those values
as ordinary List elements because their Promise resolves to the containing
List, not to an individual element, and their sync callback path never probes
the element's then member.
| Export | Behavior |
|---|---|
parse |
Returns href, protocol/host fields, path, query Map, hash, and origin; accepts an optional base. |
join |
Joins one or more string URL/path segments without duplicate separators while preserving a leading scheme://. |
query, parseQuery |
Convert an object/Map to a query string or a query string to Map<string, string>. |
withQuery, withHash |
Return a URL/path with a replaced query or hash. |
isExternal |
Compares a URL with an optional base/current location origin. |
encode, decode |
Encode or decode one URL component. |
normalize |
Resolve and normalize a URL/path with an optional base. |
Query names are strings. Values are explicit string/number/bool scalars,
null, or dense Lists of those scalars; records/classes/functions are rejected
instead of becoming "[object Object]". null values are omitted, List values
produce repeated keys, and duplicate parsed keys keep their last value in the
returned Map. Query numbers must be finite. URL text operations validate
actual strings at dynamic boundaries rather than inheriting JavaScript
String(...) coercion; this includes the browser's current base URL and every
field copied from a native URL result. URL text and encoded query output are
limited to 2 MiB; component encoding and multi-part joining check the resulting
length before allocation. Normalization and query/hash replacement likewise
budget every native URL fragment before concatenating the returned relative or
protocol-relative text. Parsed names/values share the same text budget, and
query maps contain at most 100,000 fields.
The URL and URLSearchParams constructors and prototype operations, Map
brand/iterator operations, component codecs, numeric/text/reflection helpers,
error constructors, and browser location object plus its href reader are
captured when velar/url initializes. Replacing globals or prototypes afterward
cannot redirect an initialized module. A real captured browser Location still
reports later navigation changes; the contract fixes the capability source, not
the page URL value. Hostile URL text and query iterator results continue to fail
closed without invoking conversion hooks.
Time values are Unix epoch milliseconds, matching the host JavaScript runtime
without exposing mutable Date objects.
| Export | Behavior |
|---|---|
now |
Current Unix epoch milliseconds. |
monotonic |
Monotonic milliseconds for elapsed-time measurement. |
parse |
Parse deterministic ISO YYYY-MM-DD or a T datetime with Z/numeric offset to epoch milliseconds, accepting RFC 3339's lowercase t/z and truncating fractional seconds to milliseconds; return null for invalid text. |
iso |
Format epoch milliseconds as an ISO 8601 UTC string; defaults to now(). |
format |
Locale-format a time, or a chosen part of one, with optional locale, time-zone, and date/time style strings. |
date |
Construct local epoch milliseconds from strict year, month, day, and optional time fields. |
utc |
Construct UTC epoch milliseconds from the same strict fields. |
parts |
Return numeric year through millisecond parts in local time or an explicit time zone. |
date and utc reject out-of-range or nonexistent calendar values instead of
using JavaScript Date rollover (2024-02-31 never becomes March). Years are
0 through 9999 and do not receive JavaScript's special 1900 offset for 0–99.
One case is deliberately not a rejection: date(year, month, day) with no time
fields names a calendar day, not a wall clock, so in a zone whose
daylight-saving transition skips local midnight it resolves forward to the first
instant that exists on that day rather than declaring a real day
unrepresentable. date with explicit time fields still rejects a wall clock
that does not exist. Where a local wall clock occurs twice — the hour a
daylight-saving fall-back repeats — date returns the earlier, pre-transition
instant; the later occurrence is not reachable through this API, so a program
that must distinguish them should hold the instant it was given rather than
rebuilding it from parts.
parse accepts date-only ISO as UTC midnight; a datetime must include Z or a
numeric offset, which keeps results identical across JavaScript engines and
host time zones. The offset is written +HH:MM, +HHMM, or +HH, and RFC
3339's lowercase t and z are accepted beside the uppercase spellings.
Fractional seconds are read as a decimal fraction and truncated to
milliseconds; the number of digits is not bounded separately, because the
64-character input limit below already bounds it. :60 is accepted where RFC
3339 writes an inserted leap second — at the end of a UTC day, whatever local
hour the offset makes of it — and names the instant that follows, which is what
a reader of that timestamp means; :60 anywhere else is a typo rather than a
timestamp and returns null. Non-ISO/native locale text is deliberately
unsupported.
Locale and named-time-zone arguments must be actual strings.
format takes a dateStyle and a timeStyle, both defaulting to medium, and
both drawn from the host's own vocabulary: full, long, medium, short,
and none for the half a call leaves out. format(value, locale="en-US", dateStyle="none", timeStyle="short") is the time of day a message list shows,
and it is the locale's time of day — 3:45 PM where the reader expects a
twelve-hour clock and 15:45 where they do not. Rendering that from parts()
by padding an hour and a minute by hand gives every locale a twenty-four-hour
clock, which is why the styles are here rather than left to the caller. Both
styles none is refused: that names no format at all.
Returned timestamps are checked as finite values inside JavaScript's supported
date range. Host clock and internationalization results are validated before
they cross back into VelarScript: invalid clocks, non-string formatting output,
missing/duplicate time-zone parts, accessors, and impossible calendar fields
fail explicitly rather than leaking NaN or guessed values through the typed
API. A time-zone parts List has one bounded length snapshot, so a changing host
collection cannot extend validation midway. Formatted output is limited to
65,536 characters.
The wall and monotonic clocks, Date constructor and prototype operations,
internationalization constructor and formatting operations, numeric/text
helpers, result freezing, Reflect invocation, and error constructors are
captured when velar/time initializes. Replacing those globals or prototypes
afterward cannot redirect an initialized module. Invalid host clock and Intl
results are still checked at the operation boundary; locale and time-zone data
continue to come from the host rather than being snapshotted or reimplemented.
import {iso, now, parts, utc} from "velar/time"
const launched = utc(2026, 8, 1)
print(iso(launched))
print(parts(now(), "UTC").year)
Months are one-based. ISO parsing is owned and deterministic; locale formatting
and named-time-zone projection still use the host internationalization data.
VelarScript does not invent a second timezone database or datetime object model.
ISO input longer than 64 characters is invalid without entering the matching
engine. Locale and time-zone names are limited to 1,024 characters before the
host internationalization API is called. Explicit time-zone projection also
keeps astronomical year numbering (1 BC is year 0), matching JavaScript
Date and the local parts result.
uuid() returns a cryptographically secure host UUID. It delegates to the
existing JavaScript host's crypto.randomUUID() and fails explicitly when that
capability is unavailable or returns a non-canonical result; it never falls
back to timestamps or Math.random. The capability must be a data method rather
than an accessor, and non-Error host failures are wrapped without invoking
conversion hooks. The crypto object, data method, matching operation, and error
identity are captured when velar/id initializes; replacing globals or
prototypes afterward cannot redirect generation or validation. isUuid(value)
checks canonical UUID text without changing it and rejects non-36-character
input before pattern matching. It answers the textual question only — 36
characters, hyphenated 8-4-4-4-12, hexadecimal in either case — so the nil and
max UUIDs of RFC 9562 and GUIDs from variants other than RFC 4122 are accepted
as the canonical text they are. It is not a claim about which version generated
the value.
import {isUuid, uuid} from "velar/id"
const taskId = uuid()
print(isUuid(taskId))
Structured logging replaces direct source-level access to console while
remaining on the existing JavaScript runtime.
import {LogRecord, logger, setLevel, useSink} from "velar/log"
def sendRecord(record: LogRecord): postToCollector(record.scope, record.level, record.message)
component BuildStatus:
const buildLog = logger("build", Map({target: "web"}))
const stopCapture = useSink(sendRecord)
@mounted:
setLevel("debug")
buildLog.info("Compilation ready")
@cleanup:
stopCapture()
return <p>Ready</p>
logis the unscoped logger;logger(scope, fields=Map())creates a scoped logger with optional base fields. The fields are written as an ordinary literal —Map({target: "web"})— because aMap(...)in a position that already says what the Map holds takes that type, exactly as[]does. Scope/message/field names remain actual strings at dynamic boundaries; logging never calls ambientString(...)on invalid input.- Loggers provide
debug,info,warn, anderror. Fields areMap<string, unknown>values;erroroptionally receives anErrorand additional fields. setLevelacceptsdebug,info,warn,error, orsilent;levelreturns the current threshold. The threshold is read on every call, sosetLevelreaches loggers that already exist.- A call below the active level returns before it builds anything: the logger's
context is not merged, the message and fields are not validated, and no bound
is checked. Turning a level off is therefore free and side-effect-free, and a
suppressed call cannot raise. The message and field bounds below apply to
records that are actually emitted, and the runtime check that
error(...)received an actualErrorsits behind the same gate — the compiler still requires one at that position and a boundary value arrives asunknown, so only anexternJavaScript contract that declared anErrorcan hand it something else. useSink(sink)redirects records while at least one custom sink exists and returns an explicit cleanup function. The record it hands a sink isLogRecord, a published type name:timestamp,level,scope,message,fields, and an optionalerror. A sink is therefore either a nameddefwith an annotated parameter, as above, or an arrow thatuseSinktypes contextually — the same choicevelar/servegives a handler throughServeRequest.LogRecordis a runtime value as well as a type name, soLogRecord.is(value)answers whether a value is a record andLogRecord.parse(value)validates one at a dynamic boundary. That is the surfacevelar/fspublishes forFileWatchBatch.- Each sink receives its own fields snapshot, so mutation inside one callback
cannot rewrite what another sink observes. The optional error is either an
actual
Errorornull. Rejections from an actual Promise returned by a sink are reported internally; arbitrary objects are not treated as thenables and theirthenaccessors are never probed. - One logger accepts at most 1,000 merged fields, field names are limited to 1,024 characters, messages to 64 KiB, scopes to 1,024 characters, and an application may install at most 1,000 sinks. These limits are checked before dispatch; native Map iteration is used without invoking subclass overrides.
- Record timestamps are accepted only when the host clock returns a finite value inside JavaScript's date range. The fallback console must expose data methods; logging will not execute accessor properties to discover a writer.
- Without a custom sink, the runtime writes through the host console internally.
VelarScript source still receives no
consoleglobal. Sink failures fall back to the internal host logger and cannot recursively invoke the failing sink. - The clock, collection operations, Promise rejection observer, string
normalization, error identity, and fallback console writers are captured when
velar/loginitializes. Ambient replacement afterward cannot redirect log delivery;setLevelanduseSinkare the explicit customization points. - VelarScript never uploads logs or telemetry automatically.
This chapter is the reference for the Node, Server and Desktop-shared platform
modules: there is no separate node-api.md or server-api.md, by design. Web
has its own web-api.md because its vocabulary is a second language surface —
components, reactivity, Look — while these modules are ordinary functions and
types over one capability each, and splitting them out would separate them from
the Core rules they are read against.
The Desktop API reference covers Desktop-owned windows, system integration, supervised services, notifications, secure storage, and the browser-test host seam. The shared scoped platform APIs stay in this chapter.
Standard API 0.5 adds a small first-party surface for local applications and
servers. Node is the current internal engine, but Node classes, callbacks,
events, buffers, and overloads are not part of the VelarScript contract. These
modules work under velar run, Core tests, and Core builds. A Web project that
imports a Node-only module fails during project compilation; velar/serve
points Web code to the application dev server and the browser target of
velar/http. Their contracts and implementations are owned by the
independently reusable @velarscript/node package; the CLI only composes that
extension for local programs.
Errors these modules raise. Only velar/fs, velar/serve, and
velar/http name their own error classes, because only their failures have
more than one recovery. Every failure of velar/path, velar/process,
velar/env, velar/host, and velar/terminal — a wrong argument, an exceeded
budget, an unusable host — has exactly one recovery, which is to change the
code, so each arrives as an ordinary Error. A class exists only where a
caller would write different recovery for it.
The Node extension's native serving capability declares an immutable route table instead of a controller class or a group of decorated functions:
import {HttpProblem, Request, created} from "velar/serve"
type CreateArticle:
title: string
export server app:
@get health(p"/health") => {ok: true}
@get readArticle(p"/articles/{id:number}?{details:bool?}"):
if id < 1:
throw HttpProblem({status: 404, reason: "article.not_found", title: "Article not found"})
return {id, details: details ?? false}
@post createArticle(p"/articles", input: CreateArticle) => created({id: 1, title: input.title})
@notFound(request: Request) => {error: "route_not_found", path: request.path}
server name: and the Node-only p"..." path pattern are enabled by
@velarscript/node. @get, @post, @put, @patch, and @delete are the
route roles; @notFound is the one unmatched-path fallback on the final
application; @response is its one semantic-result policy. They are compiler
declarations, not decorators or runtime functions. A {name:type}
An inline pattern projects path captures and query fields as immutable handler
locals. A referenced pattern uses @get(pattern as route) and receives a
RouteMatch with route.pattern, route.pathname, route.params, and
route.query. A query type ending in ? is optional, while every other field
is required and rejected before the handler when absent. The type is string,
number, bool, or a named enum. str(route) and str(route.pattern) return the complete
route declaration. ?wire={field:type} maps distinct names; writing
?field={field:type} is accepted but advisory A11 offers the mechanical
?{field:type} shorthand. One concrete Data record on
POST, PUT, or PATCH is the JSON body. Request asks for the complete
low-level request explicitly. Route bodies may await directly and use either
=> expression or an indented block.
An optional identifier after the role gives the route a stable operation
identity, for example @get readArticle(...) or @websocket worldRealtime(...). The identity is unique across the final composed app and
is preserved by prefix. OpenAPI emits it verbatim. Anonymous routes remain
valid and receive a method/path-derived identity for documentation only.
@notFound accepts zero parameters or one explicitly typed Request.
Ordinary Data becomes a 404 JSON response; an explicit ServeResponse keeps
its selected status. A matched route's HttpProblem and framework 405 response
remain outside this fallback. It participates in application middleware and
lifecycle handling, but cannot be carried through a non-root prefix; declare
it after composing prefixed route tables. A catch-all route such as
staticFiles("/", ...) is already a matched route and retains ownership of its
own file fallback.
Returning ordinary Data produces a negotiated semantic response.
respond(value, status=200, headers=null), created(value, headers=null), and
noContent(completion=null, headers=null) select semantic status.
json(value, status=200, headers=null), redirect(location, status=302, headers=null), text(value, status=200, contentType="text/plain; charset=utf-8", headers=null),
stream(producer, status=200, headers=null), and file(path, root=".", fallback=null) express the response cases whose status or transport should be
visible. HttpProblem(options) exits a route with a checked 4xx/5xx problem;
the default encoder uses application/problem+json. Its semantic code is
reason: charter section 11 owns code on every checked Error as that
instance's class name, so HttpProblem.code is "HttpProblem" and reading it
on an HttpProblem is refused with reason named as the successor. The wire
problem document is unchanged — it publishes that reason under its JSON field
name code, which is what clients and openapi() already read.
stream(producer, status=200, headers=null) sends exactly the headers it is
given and nothing else: it does not guess a media type, so a streaming route
sets its own content-type — the framework will not, and a client left to sniff
one is a client that will eventually sniff wrong.
Returning null is returning Data: it is a negotiated 200 whose body is the
JSON value null. The way to answer "there is nothing to send" is
noContent(), which is a bodyless 204. One application-wide
@response(outcome: HttpOutcome, request: Request) policy may map every
semantic success and failure to shared Data or one final response. A final
response owns its status and headers; a policy that uses json or text only
to select an encoder therefore passes through outcome.status and either
outcome.headers or, for a problem, outcome.problem.headers. The policy is
global and cannot be scoped with prefix; compose it on the final application.
prefix(path, app) and server-body ...app entries
compose route tables, and a composed table is refereed before it can answer
anything. The compiler judges whatever the composition resolves to inside one
module — a spread naming a server declared there, an alias of one, and prefix
with a literal path, use, bodyLimit, docs, or lifecycle around one — and
refuses two routes of one method that collapse to the same path shape, or that
overlap with neither more specific. An imported server or a computed prefix path
resolves to nothing there and is let through, because a false conflict would
block a correct program; assembling the ServeApp then applies the shape test
to the finished table, and two routes of one method that collapse to the same
shape refuse to build, naming both routes and where each came from.
staticFiles(path, root, fallback=null) adds a bounded, root-contained
streaming route with HEAD, validators, and single byte-range support. A
relative root, here and in file(path, root, fallback=null) and
fileResponse(root, path, fallback=null), resolves against the project root the
build knew — the directory holding velar.json, which is where the author wrote
"public" — so the same build serves the same files whatever directory it is
started from, and velar run, which compiles into <project>/.velar/, serves
the same files a directory build does. An output that does not sit inside that
project — copied away after the build, or written to an --out-dir outside it
— falls back to the emitted entry's own directory, so a deployment carrying its
assets beside the entry keeps serving them. An absolute root is used as given.
Which of the two it is, is decided once when the program starts and by identity
rather than by whether a directory happens to be there: the build bakes the
project's identity into the emitted module, and the project root answers only
when the velar.json standing at that place is this project's. That identity is
the manifest's name when it declares one, and the SHA-256 of the manifest's own
text when it does not — a digest nothing but that file produces, which is why a
project with no name is as safe from a stranger's directory as one with a name.
Every other outcome falls back to the entry's own directory and says so once on
stderr, naming both identities: a velar.json that is there and belongs to
another project, which is what a dist/ copied next to somebody else's public/
gets, and — because a nameless project is identified by its manifest's bytes — a
velar.json that was edited, deleted or made unreadable after the build. Each of
those reports is a notice about how the output is deployed, not an error the
program raised: rebuilding the output beside its project makes it stop.
Declaring a name is what makes a manifest editable without a rebuild.
One consequence is worth stating, because it is invisible locally: while the
output still sits inside its project, the project root always wins, so a dist/
in the tree never reads its own dist/public/. velar build copies public/
into the output for the deployment to carry, and any post-build processing of
dist/public takes effect only once that directory has been moved away from the
project.
A relative root names a directory inside the project. One carrying ..
escapes it, and is refused: by the build when it is written as a literal, and by
velar/serve otherwise — when the route is declared for staticFiles, and when
a route reaches it for file and fileResponse. Name a directory outside the
project with an absolute root. A root that names nothing at all is not refused:
when the build recorded where the project root is, every root the application
declared before the server starts is checked once then, and one that does not
name a directory is reported on stderr with the path it resolved to, so a
misspelled "pubic" is distinguishable from a file that is missing. The
requests themselves answer the 404 they always did, and a root first named
inside a handler is checked no earlier than that handler runs.
bodyLimit(app, maxBytes) narrows inferred JSON input for that route group,
and use(app, middleware) wraps only that app's routes after composition. A
middleware next() continuation is single-use.
Every ending a route can have is a response by the time it leaves that route, so
middleware shapes all of them: a thrown HttpProblem, a framework rejection
such as 405, 415, or 422, and an unexpected error narrowed to the opaque 500
carry the same securityHeaders, cors, requestId, and access-log treatment
a 200 does — which is what makes an error response readable across origins. The
framework's own 404 for a path no route claims leaves through the middleware
use attached to the application, whether or not the application declares a
@notFound fallback. middleware.errors(handle) keeps its recovery role: it is
offered the error a route ended with, wherever it sits in the chain, and its
result becomes the response.
openapi(app, title=null, version="1.0.0") derives an OpenAPI 3.1 document with
checked parameter, request-body, and response schemas. It also documents the
framework-generated 400, 401, 413, 415, and 422 responses that apply to each
route; explicit RouteDocumentation.errors entries override their
descriptions. Typed path syntax such as p"/articles/{id:number}" is rendered
as /articles/{id} in that document. Every route publishes the success response
its declared status and return type describe, and no judgement is made about
whether a handler can reach it: a route whose body only throws still publishes
its success response — 200 with the null schema its return type gives — and
the statuses it throws appear only if the route documents them through
RouteDocumentation.errors.
Declarative WebSocket routes appear in the same paths table as GET upgrades,
with response 101 and x-velar-transport: websocket; an HTTP GET and WebSocket
upgrade cannot share one documented path.
The separate @velarscript/server application extension composes this Node
capability. It adds velar/server, explicit server.configuration loading,
application(app), typed configuration(Type), request-scoped
authenticate(credential, verify), and an abstract database(connect, disconnect) provider lifecycle. Authentication accepts only Node-owned
security descriptors; its nullable async verifier produces a typed identity
Provider, and null produces the descriptor's opaque 401 challenge. Token,
session, password, identity-store, and authorization policy implementations are
installed library or application responsibilities. Its selected entry starts
and awaits the transport inside @main; no exported startup binding or hidden
launcher is part of the application contract.
velar dev watches and restarts the last-good build, velar serve runs checked
source with production behavior, and velar build emits a standalone Node
directory with the same declared configuration. Each of them bakes the same
project root and project identity a Node build bakes, so a relative static
root in a Server application resolves by the rule stated above — against the
directory holding velar.json — under velar run, velar dev, velar test
and velar build alike; this is the one emitted velar/serve, not a second
one. Direct serve(...) is the lower-level operation for tests, embedded
servers, and handler adapters.
Explicit route defaults cover the inputs that cannot be inferred:
input.header, input.cookie, input.form, input.upload, and
input.dependency. Security descriptors include API key, Basic, Bearer,
OAuth2, and OpenID. provide resolves request- or application-scoped
dependencies once, detects cycles, optionally initializes app providers
eagerly, and invokes release callbacks before clearing the ended scope’s cache.
supply(app, provider, value) attaches an already constructed value to an
app-scoped provider without process-global state; the compiler checks the
provider's concrete result type and the provider's release callback retains
ownership at shutdown.
lifecycle, background, cookie helpers, sse, and
velar/websocket.listen({http: app, ...}) cover owned service lifetime and
realtime transport without adding compiler-owned names.
velar/server also publishes applicationConfigurationPath, the
server.configuration path this build was compiled with, as the program reads
it: the same text velar.json declares, so a program can name its own
configuration file in a log or a health response without restating it.
Its velar/realtime half is the server side of the typed session the Web module
documents: realtimeSession(...) runs it, RealtimePeer<T> is one connected
peer, and RealtimeCodec<Incoming, Outgoing> is the pair of decode(message)
and encode(message) functions that turn wire frames into the two message types
a session declares. RealtimePeerState — open, closing, closed — is what
peer.state() answers, and RealtimeBackpressureError is what a send raises
when the peer's pending-byte budget is full; trySend answers false in that
same situation instead of raising.
Lifecycle hooks are paired: successful startups unwind in reverse order, while
a hook whose startup failed never receives a shutdown call. Multiple cookies
remain multiple Set-Cookie fields rather than being comma-joined.
Built-in middleware covers CORS, trusted hosts, request IDs, access logs,
security headers, compression, error recovery, timeouts, and concurrency.
docs composes a bundled offline UI and OpenAPI endpoint; its optional ordinary
Map adds summary, tags, success/error statuses, or visibility without changing
the single role of @. The test-only velar/server-test module runs routing,
lifecycle, provider overrides, cookies, multipart uploads, streams, and files
in process: await client(app, overrides?) answers a TestClient whose get,
post, put, patch, delete and request return a TestResponse —
status, headers, text() and json() — and whose close(grace?) ends the
application's lifecycle. No port is bound and no socket is opened.
GET routes answer HEAD automatically without writing the response body.
OPTIONS and 405 responses publish GET, HEAD, and OPTIONS consistently,
and 204 and 304 responses are always bodyless at the transport boundary. Public
response and documentation statuses are final HTTP statuses from 200 through
599; an application cannot emit an informational response as its final result.
Path and query number inputs use finite decimal syntax rather than JavaScript
coercion, so whitespace and hexadecimal text are rejected with 422. An enum
capture whose wire values are all integers — the form D102 introduced, and the
one its OpenAPI schema states as {"type":"integer","enum":[1,2]} — decodes by
that same rule before its membership is checked, because the URL segment is text
and the members it must match are numbers. It is exactly the number rule and
no more: what a {n:number} capture accepts is what such a capture accepts, a
decoded value that is not a member is the same 422 an undecodable one is, and a
capture whose enum is all strings, or whose wire values mix strings and
integers, matches the raw text as it always has.
An inferred JSON body accepts a missing media type, application/json, or an
application/*+json type; another declared media type returns 415 before body
decoding.
serve(app, port, host="127.0.0.1", maxBodyBytes=16777216) binds a declared
ServeApp and resolves to a Server record containing the actual port and an
idempotent async stop(grace=30000). Stop closes admission, requests cooperative
cancellation, drains admitted work until the bounded deadline, and only then
releases application providers and lifecycle hooks.
For low-level protocol adapters, serve continues to accept an async handler.
The handler receives a ServeRequest with method, a once-decoded canonical URL
path, first-value query and repeated-value queryAll Maps, normalized headers,
and a request cancellation. Invalid percent-encoded UTF-8, encoded path
separators, NULs, and dot segments are rejected before routing, and a path
parameter matches one non-empty segment: /n/ does not match
/n/{id:number} at all, so it is a 404 rather than a 422 about an id the
request never supplied. It also provides cached async
text(maxBytes=16777216) and json(maxBytes=16777216) body readers. The first
read enforces its byte budget while the request arrives, before accumulating a
larger body; a later read may impose a smaller budget on the cached body.
parse(Type, maxBytes=16777216) applies json() and then validates the result
with a compiler-known VelarScript runtime Type, returning Promise<T> with the
same inference and callable-then rejection as velar/http. The Type identity
is checked before any body read, so an invalid or forged Type cannot consume the
request stream.
Errors it raises. serve reports a port already bound as
AddressInUseError (bind another port, or 0 for any free one) and a port the
host refuses as PermissionError; both are nameable Core classes that need no
import. Budgets are positive integers up to the 16 MiB hard ceiling. Exceeding the
application budget throws RequestBodyTooLargeError, whose read-only
maxBytes field lets a handler return 413 without matching error text. Request
text must be valid UTF-8. json() then applies the same finite, bounded,
accessor-free JSON contract as velar/json and every velar/http target;
native JSON.parse values such as an overflowed 1e400 never enter Vel as
Infinity. No byte that decodes is removed: a leading U+FEFF reaches text()
as the character it is, and json() and parse() therefore refuse a
BOM-prefixed body exactly as Json.parse does, with the framework's own 400
request.invalid.json problem.
import {RequestBodyTooLargeError, ServeRequest, ServeResponse, fileResponse, serve} from "velar/serve"
type MessageInput:
text: string
async def handle(request: ServeRequest) -> ServeResponse:
if request.path == "/api/health": return {status: 200, json: {ok: true}}
if request.path == "/api/message":
try:
const input = await request.parse(MessageInput, maxBytes=65536)
return {status: 200, text: input.text}
catch error:
if error is RequestBodyTooLargeError: return {status: 413, json: {error: "Request body is too large"}}
throw error
return fileResponse(root="dist", path=request.path, fallback="index.html")
const server = await serve(handle, port=8787)
A response is one checked plain record body:
{status, json, headers?}serializes bounded, finite, acyclic JSON data without invoking getters ortoJSONhooks;{status, text, contentType?, headers?}sends at most 16 MiB of UTF-8 text;{status, stream, headers?}runs an async producer whose awaitedwriteaccepts at most 1 MiB per chunk and 64 MiB in total.writefollows transport backpressure and rejects if the client connection closes, so a producer can release its upstream request infinallyinstead of remaining suspended.
The exported ServeRequest, ServeResponse, and Server runtime types inspect
only enumerable own data fields. Type checks and response dispatch never invoke
getters, symbol hooks, toJSON, overridden collection methods, or sparse List
entries. A Server JSON response takes one compiler-owned strict JSON snapshot
when the response is created; finalization, middleware copies, test transport,
and native transport carry that same encoded snapshot instead of walking and
serializing the application value again.
The application-facing module never owns a Node HTTP server or request stream.
It exchanges bounded request/response commands with the same private isolated
Node host used by velar/fs; only that Worker imports node:http,
node:fs/promises, and Node stream machinery. Its port is unreferenced while
idle and referenced while an operation is pending or a server is active. Live
server and request identities are bounded and collision-free across handle
wraparound. Per-value limits do not multiply without bound under concurrency:
the Worker has one 128 MiB aggregate budget for cached request bodies, static
files, buffered text/JSON responses, and in-flight stream chunks. Completion
and disconnect return stable request ownership only after concurrent host
operations settle; a stream write owns a separate temporary reservation until
its flush or failure. This private compiler dependency is materialized
transitively and cannot be imported as a Standard API module.
The count side is bounded too: at most 4,096 inbound requests, routes, lifecycle pairs, and middleware concurrency slots, plus 256 request providers, 512 app providers, and 256 unfinished timed-out requests. OpenAPI JSON cannot exceed 16 MiB. Request caches and upload views are cleared after response/background completion; app provider caches are cleared after release and shutdown. WebSocket has its own 128 MiB aggregate budget plus global active connection, pending connection, queued message, and pending-send counts, so zero-byte messages cannot evade backpressure by consuming objects instead of bytes.
A producer that fails after its first chunk cannot retract what the client has already read, so the server ends the connection with a FIN and no terminating chunk. A conforming client sees a truncated chunked body and reports it as the transport failure it is; the framework does not append a trailer that would make a partial response look complete.
Every refusal the framework makes once a request exists is one wire form: a
problem document with application/problem+json, matching what openapi()
publishes. That covers 413 for a body over budget, 415, 400 for a path that is
not a path, and the static routes' 404 and 416. Only a transport preflight
failure — a request line or header block that never became a request, such as
414 or 431 — answers with one line of text/plain.
Transport-owned headers cannot be overridden. Handler failures are reported to
stderr and return an opaque 500 Internal server error; no development stack
is disclosed. A client that goes away before its response completes is not a
handler failure and does not use that wording: it is reported on the same
channel as its own line, Client closed the connection before the response completed <method> <path>, so a stopped download and a bug in a route are
distinguishable in a log. fileResponse(root, path, fallback=null) resolves a
relative root against the one application root base settled at startup — the
project root the build knew, or the emitted entry's own directory when the
project standing there is not this one — then resolves the real root and target,
rejects decoded traversal/backslashes/symlink escape, reads only regular files
up to 64 MiB, and owns the static content-type table. The optional fallback goes
through the identical containment and size checks. Upload.save(path, root)
reads a relative root by that same rule, and an upload still cannot land
outside the directory that resolves to. An Upload also reads without saving:
name is the form field it arrived under, filename what the client called it,
contentType what the client declared, size its byte count, and text() and
bytes() its content — each bounded by the request's body limit.
RoutePattern is the type a p"/orders/{id:number}" literal has, so a route
table can name one; it is written as that literal, never constructed.
setCookie(response, name, value, path="/", httpOnly=true, secure=true, sameSite="lax", maxAge=null) and clearCookie(response, name, path="/") add and
expire one Set-Cookie field on a response the route is already returning; each
answers a new response rather than mutating the one it was given, and multiple
cookies stay multiple fields.
| Export | Behavior |
|---|---|
readText(path, maxBytes=16777216) |
Reads one valid UTF-8 regular file under an explicit byte budget. |
readBytes(path, maxBytes=16777216) |
Reads one regular file as Bytes under an explicit byte budget, for content that is not text. |
createText(path, text) |
Atomically creates one new UTF-8 file and refuses every existing entry, including symbolic links. |
replaceTextIfMatches(path, expected, replacement) |
Replaces a matching UTF-8 file as one complete directory-entry commit and reports a detected conflict as false. |
createBytes(path, bytes) |
The createText rule for binary content: creates one new file, refuses every existing entry. |
writeText(path, text) |
Writes at most 16 MiB of UTF-8 text. |
writeBytes(path, bytes) |
Writes at most 16 MiB of binary content. |
appendText(path, text) |
Appends at most 16 MiB of UTF-8 text. |
exists(path) |
Resolves to false only for a missing path; permission and host failures remain errors. |
list(path, maxItems=100000) |
Returns a sorted, caller-bounded List with at most 2 MiB of name text. |
info(path) |
Returns bounded {name, kind, size, modifiedAt} metadata, or null when absent. |
canonical(path) |
Resolves the host real path for containment and identity checks. |
makeDirectory(path) |
Creates the requested directory and missing parents; an existing directory succeeds unchanged. |
copyFile(source, target, replace=false) |
Copies one regular file; replacement is explicit. |
move(source, target, replace=false) |
Moves one path; replacement is explicit. |
removeFile(path) |
Removes one file and never recursively removes a directory. |
watchFiles(path, recursive=false) |
Creates a bounded, resource-owned invalidation watcher for one existing file or directory. |
FileWatcher.next() |
Pulls one sorted/deduplicated FileWatchBatch, permits only one active pull, and returns null after release. |
FileWatcher.close() |
Idempotently releases the watcher and settles a pending pull with null. |
Errors it raises. A failure a caller recovers from differently arrives as
its own class (charter section 11): FileNotFoundError when the path is
absent, PermissionError when the host denies access, NotADirectoryError
when a path component or a list target is a file, and FileExistsError when
createText, copyFile, or move would overwrite without replace. Each
carries the failing path: string?. Every other failure — a wrong argument
type, an exceeded budget, an unusable host — stays an ordinary Error.
readText returns the file's text as the file spells it. A leading U+FEFF is
text like any other character: it is kept, counted, and hashed, so
sha256Text(await readText(path)) is the digest of the file. makeDirectory is
idempotent for a directory that already exists and raises FileExistsError when
the path is an existing file.
Paths are non-empty, NUL-free strings of at most 4,096 code units. The module
has no synchronous forms, recursive deletion, byte inspection, callback event
surface, or public filesystem streams. FileWatchBatch contains an ordinary
List<string> named paths and a rescan flag. It is deliberately an
invalidation stream rather than a lossless mutation log: paths are bounded,
absolute, sorted, and deduplicated; an unknown native filename or exhaustion of
the 4,096-path/2 MiB queue becomes {paths: [], rescan: true}. Each host owns
at most 128 watchers. Native failure is terminal, and callers explicitly close
the watcher when its consumer stops.
A watcher reports only the changes that happen after it is armed.
watchFiles resolves when the request is accepted, not when the host has
finished arming its native stream, and the stream begins at the moment arming
completes. A change written between those two points can therefore never
arrive — measured on macOS FSEvents under load, 4 of 40 notifications were
lost, one with a 25-second window. This is the real semantics of every
filesystem watcher, so the module states it rather than pretending to erase it.
If you need to observe a change you are about to make, write first and then
start watching, or query the state on both sides of the write. A watcher is
for changes another actor makes; it is not a delivery receipt for your own.
A watcher reports your own writes too, and has no self-invalidation cap. A
file the program itself writes into a watched directory triggers the watcher
exactly as any other actor's write does, for as many rounds as the program keeps
writing. This is deliberate: a build tool writing its output into a directory it
also watches is a legitimate program, and a cap would break it. A watcher that
must not observe its own effects is one the program stops, or filters by path,
before writing.
createText is the no-clobber primitive for generated files, approvals, and
other check-then-create workflows. Its exclusive-create decision and file
creation are one host operation; callers must not emulate it with
exists/info followed by writeText, because another actor can occupy the
entry between those calls.
replaceTextIfMatches is the optimistic edit primitive. File mutations that
address the same canonical target through one runtime host participate in one
coordination queue. After an exact byte match, a same-directory temporary file
is renamed over the target so readers observe the old or new complete contents,
never a partial replacement. A mismatch returns false without writing. This
contract does not pretend to be a portable operating-system CAS: an unrelated
process that bypasses this API can still write between comparison and rename,
and applications that coordinate such processes need their own shared lock or
repository transaction.
Node initializes the module's path, number, UTF-8 encoder/decoder, typed-byte,
Promise, reflection, and immutable-result operations once. It validates and
assembles only Velar values in the application Realm, then delegates filesystem
effects to the private isolated Node host shared with velar/serve. Only that
Worker imports node:fs/promises and callback node:fs watcher machinery;
later replacement of application-Realm
filesystem modules, Promise.prototype.then, typed-array sizing, or captured
validation operations cannot redirect the public API.
Desktop applies two distinct symlink rules. Content operations such as read,
write, append, copy, list, and canonicalization follow the target only after
its real path remains inside a granted root. Entry operations such as info,
move, and removeFile operate on the final directory entry itself, so
removing or moving an in-root symlink never mutates its target. A dangling
symlink cannot be reclassified as an absent write target. Recursive
makeDirectory authorizes the nearest existing ancestor, permits only
in-root intermediate links, and refuses a final symlink target.
sha256Text(text) returns the SHA-256 digest of the text's UTF-8 bytes as
exactly 64 lowercase hexadecimal characters. Input is limited to 16 MiB of
UTF-8 text. The module is a synchronous Core operation shared unchanged by
Node, Web, and Desktop; it exposes neither an algorithm string nor a mutable
host Hash object. Identifiers continue to use velar/id, and
reproducible non-cryptographic streams continue to use velar/random.
The runtime implements SHA-256 over Core's bounded binary buffers and captures the string/reflection operations used for UTF-8 encoding when the module initializes. Later replacement of ambient reflection or string prototype operations cannot redirect a digest. A missing intrinsic, wrong argument type, or exceeded input budget fails explicitly.
velar/validation composes target-neutral semantic rules after structural
parsing. Runtime Type<T> values remain the single source of truth for record,
collection, enum, optional, and primitive shape; this module does not introduce
a second schema language or perform coercion, defaults, transforms, I/O, or
environment lookup.
integer(minimum=null, maximum=null, message=null),
safeInteger(minimum=null, maximum=null, message=null), finite(message=null),
and nonBlank(maximum=null, message=null) create primitive rules. refine
creates an application predicate rule; field, each, optional, and all
compose rules while preserving field and List-index paths. inspect(value, rule) returns every issue, and validate(value, rule) returns the same typed
value or throws ValidationError for the first issue.
integer uses the same predicate as number.isInteger; safeInteger uses
number.isSafeInteger. Their optional minimum and maximum are finite inclusive
bounds and may have fractional values; minimum cannot exceed maximum. Bounds
are an additional constraint, so the interval [0.1, 1.9] accepts integer 1.
Use safeInteger when exact integer arithmetic within the safe range is part of
the domain contract; collection APIs separately enforce their own index/size
bounds.
parse(value, Type, rule=null) first calls Type.parse and then applies the
semantic rule. safeParse performs the same work without turning invalid input
into an exception: it returns {success, value, issues}. validator(Type, rule=null) binds those four operations as parse, safeParse, validate, and
inspect for reuse at an owned boundary.
import {all, field, integer, nonBlank, validator} from "velar/validation"
type ServerOptions:
host: string
port: number
const ServerOptionsValidator = validator(ServerOptions, all([
field("host", (value: ServerOptions) => value.host, nonBlank(maximum=255)),
field("port", (value: ServerOptions) => value.port, integer(minimum=0, maximum=65535)),
]))
const input: unknown = {host: "127.0.0.1", port: 3000}
const options = ServerOptionsValidator.parse(input)
ValidationError.path and each issue's path use the shared readonly
ValidationPath. ValidationPathKind discriminates readonly path segments:
field carries a name; listIndex, mapKey, mapValue, setElement, and
recordEntry carry a nonnegative index in the relevant collection order.
truncated is a final marker for exhausted diagnostic effort. These types are
exported as ValidationPathKind, ValidationPathSegment, and ValidationPath.
Structure and semantic rules retain the deepest determinable failure path. A
nested field can therefore be reported as field(items), listIndex(0),
field(name), and displayed as value.items[listIndex:0].name. The type expected
at that location belongs in the message. The optional field convenience
property derives from the final field segment; it does not store another path.
Manual ValidationErrors without a location have the empty root path.
Paths contain at most 64 segments; messages at most 4096 UTF-16 code units; additional diagnostic traversal visits at most 4096 nodes. A truncated result keeps the known prefix and explicitly reports its limit. These limits do not change which successful values the validator accepts. Field descriptors and collection checks do not execute getters or call user value-conversion methods. An ambiguous union failure stays at the union position; a discriminator that selects one branch allows the path to continue into it. Opaque class and Promise checks remain at their own position.
field(name, select, rule) takes a label and a selector, and they are two
separate things: name is the path the issue is reported under, and select is
what the rule is applied to. They are normally the same field, and nothing checks
that they are, so field("port", (value: ServerOptions) => value.host, ...)
reports a failure of host under the path port.
Rule lists, paths, messages, and issue aggregation are bounded. The runtime captures the host operations it uses when the module initializes, and the module is shared unchanged by Node, Web, and Desktop.
resolve(parts=[]), join(parts=[]), normalize, relative, dirname,
basename, extension, and isAbsolute expose checked host-path operations;
multi-part operations take a dense List<string> of enumerable data values.
Every input path and every public path result is limited to 4,096 code units;
many individually valid parts cannot manufacture an oversized result.
On the current macOS Desktop target these operations use the same POSIX lexical
semantics as the Node target on macOS. In particular, dirname, basename, and
extension inspect the supplied path rather than normalizing . or .. away
first, while normalize, join, resolve, and relative perform their named
normalization steps. The two targets are kept aligned by a differential corpus
covering duplicate separators, dot segments, hidden names, trailing separators,
absolute paths, and relative pairs.
contains(root, target) performs a
lexical containment check suitable for the first gate of a workspace policy;
code that crosses symbolic-link authority also checks velar/fs.canonical
before granting access.
toFileUrl(path) -> string resolves a path and returns its encoded local file:
URL. fromFileUrl(url) -> string performs the inverse operation for a local file
URL and rejects other schemes; Desktop additionally rejects credentials, ports,
queries, fragments, non-local hosts, and encoded path separators. These functions
are the public boundary between filesystem paths and LSP/document identifiers, so
applications do not reproduce percent encoding or platform URL rules.
start(command, args=[], options={}) -> Promise<Process> and
run(command, args=[], options={}) -> Promise<ProcessResult> execute one
program directly, without a shell. Options may set cwd, an explicit env,
UTF-8 stdin, timeout, and maxOutputBytes. A child receives only the
runtime's small safe environment baseline plus the explicit map; the parent
environment is not copied wholesale, so unrelated API keys cannot leak by
default. Process exposes read-only pid, pull-based next(), terminally
idempotent wait(), and stop(). Each successful pull returns
{channel: ProcessOutputChannel, text: string}; the official enum has
stdout and stderr members. A process therefore uses the language's normal
asynchronous iteration protocol rather than a callback or JavaScript iterator:
import {ProcessOutputChannel, start} from "velar/process"
const child = await start("git", ["status", "--short"])
async for output in child:
if output.channel == ProcessOutputChannel.stdout:
print(output.text)
const result = await child.wait()
Chunks preserve the order in which stdout and stderr data is observed by the
host. Each channel has its own incremental UTF-8 decoder, so a code point split
across native chunks is never exposed as replacement fragments. Only one
next() pull may be active. Incremental output must be drained before
wait() starts; calling wait() directly remains the explicit aggregate-only
path. After stop(), callers obtain the terminal capture through wait()
rather than starting a new output read.
stop() is idempotent after confirmation and retryable after rejection. A stop
request permanently closes incremental output for that handle, but a transport
failure or an unconfirmed termination does not discard the handle or cache one
rejected stop Promise. The Node host sends SIGTERM, escalates to SIGKILL after
two seconds, and rejects after a five-second confirmation deadline if inherited
pipes or an escaped descendant still prevent close; a later stop() retries
forced termination against the same owned handle. Desktop applies the same
retry contract across its bounded capability bridge.
wait() follows the same ownership distinction. A confirmed result or process
failure is terminal, releases the host handle, and is cached for later calls.
A bridge failure or a five-second failure to confirm termination leaves the
handle owned, clears only that in-flight wait() Promise, and permits a later
wait() or stop() to retry. A later wait after a retained outcome reissues
forced termination before starting a new confirmation window. Concurrent waits still coalesce into one host
request. The host uses a private {result,error,retained} envelope to preserve
that distinction; it is not a VelarScript value. run() cannot return its
temporary Process owner to application code, so a non-terminal wait failure
transfers that owner to a runtime cleanup task which keeps retrying stop()
until the host confirms release. A convenience call therefore cannot orphan a
child merely because its bounded error was already reported to the caller.
If wait() and stop() race, a confirmed Stop outcome replaces the stale
in-flight wait cache. The original wait caller still observes its own Promise,
while every later wait receives the confirmed terminal result instead of an
unknown-handle transport artifact.
If the root process exits without an explicit stop() but inherited output
pipes remain open, the host owns a separate bounded drain phase. It terminates
the original process group, escalates after two seconds, and allows five
seconds for stdout/stderr to close. A pipe still held by an escaped descendant
is then closed at the host read end and becomes one terminal process error;
next(), wait(), and the run() convenience therefore cannot hang forever
after the root has already exited. An explicit stop() cancels that automatic
pipe-abandonment path and retains the stronger retryable-owner contract above.
Node and Desktop preserve the same terminal process error through stop() and
the following wait() instead of treating a failed process as successful.
Process acquisition is asynchronous on every target so the same source
contract works for an in-process Node host and a capability-isolated Desktop
host.
Process option records are snapshotted only from enumerable own data fields;
accessors, symbols, inherited state, and unknown fields are rejected before a
child can start. Arguments and explicit environment data each have a 1 MiB
aggregate text ceiling in addition to their item-count limits.
The Node and Desktop targets initialize one shared process host ABI before
accepting application or bridge values. It captures the validation, reflection,
Map iteration, Promise, timer, and immutable-result operations used by this
contract, while both targets compose the compiler-owned captured UTF-8 sizing
runtime. Replacing those JavaScript globals or prototypes after module
initialization therefore cannot change process validation or result assembly.
Node child-process events and the Desktop worker transport remain target-owned
implementation boundaries rather than public VelarScript values.
The Node target initializes a dedicated process Worker before the official
module becomes available. That Worker loads only compiler-owned source and
Node built-ins, so application dependencies never share the Realm in which
child_process, streams, decoders, or Buffers execute. Its captured
MessagePort proxy is referenced only while a request is pending or a child is
running: importing velar/process alone does not keep a CLI alive, while an
unobserved active child still owns its lifecycle until it settles. At most 128
unreleased process handles may exist; callers release a settled handle through
wait() or stop().
A spawn the operating system refuses is an application failure, not a host
failure. A missing executable, a target that is not executable, a target reached
through something that is not a directory, and a cwd that does not exist all
settle that one call with an error naming the executable and the errno family —
ENOENT, EACCES, or ENOTDIR — and leave velar/process working: the next
run() or start() behaves as though the refused one had never happened, and a
try: / catch failure: around the call is the only place it lands. Permanent
poisoning below is the answer for the Worker itself failing, which is a
different event.
timeout: 0 means no timeout, exactly as it does in velar/http. The default is
120,000 milliseconds; the accepted range is 0 through 600,000.
The Worker transfers each successful child handle and PID to its captured
application-side proxy before resolving start(). If the Worker hits an
uncaught internal failure, it stops accepting requests, force-drains every
owned child through the still-live ChildProcess handles, and exits only after
they settle or the fatal-drain deadline expires. The proxy records the first
host failure permanently, rejects both pending and later calls with that same
failure, and performs a bounded process-group kill fallback for already
transferred owners. It never posts a retry to a dead MessagePort.
The private filesystem/serve/HTTP Worker and terminal Worker use the same
permanent fail-closed rule. A clean or non-zero unexpected Worker exit is a host
failure; terminal failure is not disguised as ordinary end-of-input. These
Workers are not restarted inside the current application process: old process,
server, request, and terminal handles have no safe identity in a fresh Worker
generation. Restarting the application is the explicit authority and identity
reset.
All three Worker proxies capture the Worker and MessagePort operations they
need when they are initialized, so replacing one of those prototype methods
afterwards changes nothing they do. A replacement installed before the module
is loaded — which is how a profiler or tracing agent patches worker_threads —
is captured along with everything real, and a captured operation that refuses is
then treated as any dead host operation is: a named module failure that rejects
every call, stops the thread, and lets the process exit rather than leaving it
running with nothing left to do. A replacement that answers without doing the
work it stands for is indistinguishable from the operation itself, and a
released reference that was never released can still leave a finished program
unable to exit. Replacing MessagePort.prototype.ref or unref before loading
is refused earlier still, inside Node's own worker pipeline while the Worker is
being constructed; the report is Node's rather than VelarScript's, and the
process exits rather than hanging. None of this is a supported way to instrument
a program.
Desktop navigation is a narrower boundary than application restart. Every
main-document bridge instance generates an unguessable private generation and
combines it with its page-local request number. The native shell translates
that pair to a host-global Worker request identity, so a response from a
destroyed document cannot settle a request whose counter restarted after
reload. Navigation retirement discards old responses and transfers no Process
or HTTP handles: the capability Worker stops the old document's processes,
aborts its HTTP bodies, and rejects cross-generation handle use. A filesystem
operation already committed by the operating system is not rolled back, but
its obsolete result is never delivered into the new document. The completion
hooks exposed for native response injection also require the private
generation, preventing application code from completing an arbitrary pending
bridge Promise by calling the hook directly.
The page bridge and native request ledger each enforce a 128 MiB aggregate
budget across pending serialized requests; response chunk assembly has the same
aggregate ceiling in addition to its per-response bound. A finite bridge
timeout sends a private cancellation for the exact generation/request pair
before rejecting the page Promise. Native code retains the Worker identity
until its terminal response, so a late response is discarded without becoming
an unknown-protocol failure. The Worker tracks active dispatches: cancellation
aborts an in-flight HTTP request and stops a hidden process owned by run() or
an as-yet-unpublished start(). Filesystem syscalls are not falsely described
as cancellable; their eventual result only releases the retained capacity and
is discarded. Closing the host input applies the same cancellation path before
any request that was awaiting process launch can publish a new child.
Standard output and error share a bounded capture budget, and no command-string
parsing or shell expansion is performed. Timeouts and output-bound failures
request termination and independently bound confirmation at five seconds even
if the operating system never emits exit or close; an unconfirmed handle is
retained rather than misreported as released. A started process controls its
ordinary descendant process group by default. A deliberately detached process
can escape that group, so an executable grant is not an OS sandbox; retained
ownership and bounded host-pipe closure make that limitation explicit instead
of silently losing the lifecycle.
Local programs use the same http.request/get/post/put/patch/delete/head
vocabulary as Web code. Requests expose response, json, text, bytes,
streamText(consume), parse(Type), and cancel; responses expose checked
metadata and the same body readers including parse(Type). Node's response
carries the same members Web's does except the browser-only blob() and, like
Web's, has no ok: a non-2xx status throws HttpResponseError from
response() before the value exists. Typed parsing uses
the compiler-known runtime Type registry and returns Promise<T> without a
second schema system. A result Type with a callable top-level then is rejected
statically because native Promise resolution would assimilate it. Options
include headers, body, timeout, and maxBytes.
secretHeaders accepts up to 16 descriptors created by
secretHeader(name, environment, prefix=""). The descriptor contains only the
environment-variable name; the official Node runtime or Desktop capability host
resolves the value when the request starts and never returns it to application
code. The Node value is sent only across the private host transport. Creating
the lazy request validates and snapshots descriptors but does not read their
environment variables. A rotation made before the first response/body reader is
therefore observed, while a missing value rejects that first effect before any
network operation begins.
The Web module captures its in-process HTTP host operations when it initializes.
Node does not treat a captured Fetch or Headers wrapper as isolation, because
Node's transport can consult public application-Realm prototypes internally.
Its application-facing module captures validation, collection, URL, timer, and
result operations, while the private velar/node-host-v1 dependency owns
node:http, node:https, redirects, sockets, response streams, fatal
incremental UTF-8 decoding, and cancellation in an isolated Worker. Desktop
performs the corresponding privileged effects in its capability worker.
Buffered response readers have one cross-target lifecycle: concurrent
text(), json(), and parse(Type) calls share the same pending body read and
successful text is cached for later readers. Web blob() shares its byte cache
with those readers. streamText() remains deliberately exclusive while it is
active, because replaying or duplicating an incremental stream would hide
buffering and backpressure. Before the first buffered or streaming body read,
all targets reject and cancel a present body whose valid Content-Length
already exceeds maxBytes; the running byte bound still protects absent or
incorrect declarations. A HEAD or other bodyless response does not fail merely
because its representation metadata carries a larger length. Decimal length
parsing uses compiler-captured transport intrinsics rather than mutable
application regex, number, or string methods; Node inbound request preflight
uses the same parser before ServeRequest starts accumulating a body.
import {http, secretHeader} from "velar/http"
const request = http.post("https://api.example.com/v1/run", {
headers: Map([["content-type", "application/json"]]),
secretHeaders: [secretHeader("authorization", "PROVIDER_API_KEY", prefix="Bearer ")],
body: {input: "hello"},
})
Secret header names cannot claim transport-controlled or ambient credential
headers, cannot conflict with ordinary headers, and their combined header
budget remains 64 KiB. Node and Desktop both follow at most 20 redirects and
remove every secret-derived header when a redirect crosses origins.
Only HTTP(S) URLs without embedded credentials are accepted. Header, request,
and response sizes are bounded, JSON stays on the lossless VelarScript data
boundary, and cancellation or timeout remains active until a streamed body has
finished rather than ending when response headers arrive. Desktop additionally
rechecks the manifest's exact-origin grant before every hop, so a redirect
cannot escape the declared network capability. Node and Desktop validate and
normalize the method and absolute URL when the request object is created; the
Desktop worker independently repeats that validation before any network effect.
Every target defaults HTTP timeout to 120,000 milliseconds, accepts only integer
values from 0 through 600,000, and reserves 0 as the explicit no-timeout mode.
Cancellation clears the owned deadline immediately, while every success or
failure path finalizes it before propagating a result; an aborted request cannot
leave a process alive through a forgotten default timer.
HTTP failures have three stable categories on every target. A non-2xx response
throws HttpResponseError; explicit cancellation or deadline expiry throws
HttpAbortError; DNS, connection, socket, or native body-stream failure throws
HttpTransportError. Its phase is the typed
HttpTransportPhase.request or HttpTransportPhase.response value. A response
phase failure may occur after streamText has already delivered text, so a
caller must not blindly replay it. Protocol validation, malformed UTF-8, body
bounds, and consumer callback failures keep their own errors and are never
misclassified as network transport. Retry count, delay, status classification,
idempotency, and whether already-visible output may be replayed remain
application/provider policy rather than hidden behavior in velar/http.
Node and Desktop also share the exact strict JSON validator used by Web.
Request option records are snapshotted from enumerable data descriptors before
any network effect; accessors and unknown fields are rejected without being
invoked. Non-text bodies are validated and serialized before dispatch, so Map,
Set, class values, sparse Lists, cycles, non-finite numbers, and oversized data
cannot be silently converted by host JSON.stringify. Response json() reads
reject the same lossy values, including a JSON exponent that JavaScript would
otherwise parse as Infinity. Desktop sends the already validated body text
across its bridge, and the isolated worker refuses structural bodies or unknown
wire options instead of becoming a second permissive serializer.
Across Web, Node, and Desktop, the 16 MiB request-body ceiling measures encoded
UTF-8 transport bytes rather than JavaScript code units. The compiler owns one
platform-neutral byte counter, so multibyte text, surrogate pairs, and unpaired
surrogates cannot receive different budgets from TextEncoder and Buffer.
JSON bodies are serialized and snapshotted when the lazy request is created;
later mutation cannot change what is dispatched. An automatically generated
content-type: application/json header is counted inside the same combined
100-field/64-KiB header ceiling, never appended after validation.
The compiler-owned JSON runtime captures the host parser and serializer when a
standard module initializes. velar/json, Web/Node/Desktop HTTP, Node serve,
browser storage, and IndexedDB all call those same captured intrinsics and the
same strict snapshot. Later JavaScript mutation of ambient JSON.parse or
JSON.stringify therefore cannot silently change official-module semantics.
Browser storage and IndexedDB additionally accept a caller-selected positive
UTF-8 byte budget up to 16 MiB. Oversized reads return the typed fallback before
JSON parsing, while oversized writes fail before the host mutation or database
transaction; applications can therefore enforce a smaller durable-state budget
without reimplementing serialization or encoding rules.
Response metadata is also snapshotted once at the host boundary. Node accepts
only native Response, Headers, byte-stream, and byte-chunk values; Desktop
validates the worker's exact response and chunk records again in the renderer.
Every target requires an integer status from 100 through 599 and requires ok
to equal the 200-through-299 classification. Status zero and contradictory
metadata are rejected at the host boundary before HttpResponseError
construction or body processing. Headers are limited to 100 fields and 64 KiB,
status text to 64 KiB, and the response URL to 2 MiB. Every target uses that
final response URL for a non-2xx HttpResponseError, so diagnostics identify
the endpoint that actually failed after redirects. Only a synthetic host
response with an empty URL falls back to the initial request URL. In addition
to maxBytes, all targets
stop after at most 1,000,000 source chunks, so an infinite stream of empty
chunks cannot keep a request alive without consuming its byte budget. A
response with no body releases its Node or Desktop request lifecycle as soon
as metadata arrives.
All text and JSON readers, including Web text() after a buffered bytes()
read, require valid UTF-8; malformed bytes are never repaired with replacement
characters, and no byte that decodes is removed. A leading U+FEFF is text like
any other character: text() returns it, bytes() and text() describe the
same content, and json() therefore refuses a BOM-prefixed body for the same
reason Json.parse does — a byte-order mark is not JSON. Metadata rejection, malformed chunks, byte/chunk overflow, decoder
failure, transport failure, cancellation, and timeout all release or cancel the owned response
stream. Request header names must be HTTP tokens and values must be single-line
text on every target, with validation occurring before browser or host fetch.
get(name) -> string? reads one explicit environment variable. require(name) -> string throws a VelarScript error naming an absent variable. Names use the
portable [A-Za-z_][A-Za-z0-9_]* shape and at most 256 characters; there is no
process-wide environment dump.
A variable that is set to the empty string is present: get returns "" and
require returns it without complaint. Only an absent name is missing. .env
files are not loaded — the environment the process was started with is the whole
input — and every read is text: there is no typed or defaulted form here, so a
number or a flag is parsed by the application, or declared in
@velarscript/server's configuration file instead.
On Node, the official module captures the original process.env object and its
name-validation/descriptor operations when the module initializes. Replacing
the global environment object or RegExp/Object/Reflect operations afterwards
cannot redirect a read, and accessor-backed environment values are rejected
without execution.
In Desktop, the manifest must grant readable values as individual uppercase
environment names. Host-only secrets are a separate permission list and
cannot overlap the readable list. A secret may be consumed only through a
velar/http.secretHeader descriptor; velar/env and the WebView bridge never
receive its value.
The native host captures only those values once at application startup, with
at most 64 variables, 64 KiB per value, and 1 MiB across names plus values.
The renderer independently snapshots only enumerable own string data fields
without invoking getters. Desktop home, app-data, and project APIs accept only
host-returned absolute paths of at most 4,096 code units. Desktop filesystem
calls are confined to declared app-data or
project roots, process launches to exact executable names, and HTTP to exact
HTTPS or loopback origins. A process-only application does not need to grant
velar/fs: its omitted cwd is the application launch directory. An explicit
Desktop process cwd must be an existing directory inside a granted file root,
and Desktop does not allow an option map to replace PATH.
An executable grant is authority to run that native program and its descendant
tree as the current operating-system user. It prevents shell parsing and
accidental command substitution; it is not an operating-system sandbox for the
program's own filesystem or network effects. Approval policy, argument policy,
and stronger sandboxing for untrusted tools belong to the consuming product,
not to @velarscript/desktop or the language standard library.
Desktop HTTP bodies are pulled across the bridge in bounded chunks rather than buffered into one transport message. The versioned native bridge also chunks and reassembles large requests and results under explicit aggregate bounds, so the documented 16 MiB filesystem, process-input, and HTTP request contracts do not silently collapse to a smaller transport limit.
exit(code=0) accepts integer exit codes from 0 through 255.
onShutdown(cleanup) registers an async () -> Promise<null> cleanup for
SIGINT/SIGTERM, with at most 1,024 registered callbacks. Cleanups run in
registration order under one 30-second graceful-shutdown deadline. Successful
shutdown exits with conventional status 130 or 143; a rejection, invalid
result, or expired deadline is reported and selects exit 1 instead of leaving
the process alive forever. A second signal force-quits immediately, and exits
with status 1: a shutdown cut short is a shutdown that did not complete, so it
reports as a failed one rather than as 130 or 143.
exit and onShutdown are the whole module. There is no platform name, CPU
count, hostname, uptime, or user: the Standard API deliberately publishes no
host values, because a value read from the host is a portability decision the
application should make in its own words.
Signal registration, exit, clocks, timers, Promise observation, cleanup-list
mutation, and synchronous diagnostics are captured during module
initialization. Async cleanup results are observed through the captured native
Promise operation rather than assimilated through a later replacement of
Promise.prototype.then.
import {terminal} from "velar/terminal"
await terminal.write(terminal.args().join(" ") + "\n")
const answer = await terminal.readLine("Continue? [y/N] ")
await terminal.writeError(answer == "y" ? "continuing\n" : "stopped\n")
terminal.close()
terminal.args() returns a fresh List<string> containing only program
arguments after velar run ... --. isInteractive() reports whether both
input and output are attached to a terminal. readLine(prompt="") resolves to
one line without its newline, or null at EOF. write and writeError await
stdout/stderr backpressure instead of silently accumulating streamed output.
Each argument list, line, prompt, write, and queued-input window has an explicit
bound. Input that arrives between readLine calls is paused and delivered
through the next Promise; an oversized line rejects that Promise rather than
escaping from a Node event callback. close() is permanent and idempotent even
before the first read, so a later readLine() returns null instead of opening
stdin again. Output closes with it: write and writeError after close()
throw Error("Terminal is closed"), so a CLI that closes stdin after its last
prompt must print everything it still owes before closing. args() and
isInteractive() keep answering. Node streams, readline events, raw-mode state, and process globals
are not part of the language API.
Node does not implement this contract by constructing readline in the
application Realm. An eagerly initialized compiler-owned Worker owns stdin
decoding and fd 1/2 writes in an isolated Realm, while a captured MessagePort proxy
revalidates every line, completion, and error. On POSIX, stdin is read through
an owned duplicate descriptor so a pending read can be cancelled without
closing the process-wide fd 0. Idle imports are unreferenced; pending operations
retain the process, and close() waits for the host to release its read before
terminating the Worker. The Worker's stdin stream itself is initialized only by
the first readLine; importing velar/terminal or writing output does not
leave an open reader that can keep a CLI alive.
velar/test provides the small assertion surface used by velar test without
introducing a second testing language.
import {expect} from "velar/test"
test "a profile keeps its declared name and tags":
const profile = {name: "Ada", tags: ["compiler", "web"]}
expect(profile.name).toBe("Ada")
expect(profile.tags).toContain("web")
expect(profile).toEqual({name: "Ada", tags: ["compiler", "web"]})
- The two comparison matchers do the language's two comparison jobs, and each
is the operation it names rather than a second implementation of it. An
assertion that disagreed with the language's own equality would be the worst
kind of trap, so neither one restates a comparison.
toBeis value equality: the same SameValueZero comparisona == blowers to. Withconst missing = 0 / 0,expect(missing).toBe(missing)passes for exactly the reasonmissing == missingistrue, and0and-0compare equal.toEqualis content equality: the preludeequals(a, b), the same implementation and not a matching one. It therefore inherits everythingequalsdoes, including structural Set members and Map keys,NaNequal to itself, and a refusal — rather than a quietfalse— for cyclic or over-deep data.
toBeTruthyandtoBeFalsyrequire actualtrueandfalse, rather than JavaScript truthiness.toContainaccepts text or a dense List, andtoHaveLengthaccepts text or a dense List.toMatchaccepts a Unicode string pattern of at most 4,096 code units. It uses the native regular-expression intrinsic captured by the module and does not trust a replaced globalRegExpconstructor. An invalidtoMatchpattern throwsTypeErrorcarrying the engine's reason, the same wayText.matchesdoes;toMatchcompiles in Unicode mode too.toThrowrequires a synchronous function.toRejectrequires an actual Promise or a function returning one; arbitrary thenables are rejected without reading athenaccessor.- Matchers and failure display capture their collection, text, JSON quoting,
numeric, RegExp, Promise, reflection, and Error operations when
velar/testinitializes. Replacing JavaScript globals or prototypes later cannot change a pass/fail result or redirect diagnostic formatting. - Failure display is deliberately bounded to 1,000 visited nodes, 16 levels, 50 collection items, and 256 string code units. It reads only accepted dense List and data-record fields; assertion diagnostics never become an unbounded object inspector.
Standard API 0.5 deliberately keeps shells, sockets below the checked server
abstraction, filesystem streams, recursive deletion, byte inspection,
reflection, pickle, dynamic import machinery, and JavaScript's legacy prototype
surface out of Core. Process execution and filesystem mutation live in
explicit Node or Desktop target extensions rather than ambient Core globals.
Browser capabilities remain in independently versioned Web modules. Canvas and game development
remain a later velar/game package built on the Web platform, not part of the
language runtime.