Skip to content
Language reference

Lexical structure#

Programs#

A program is a set of .vlt files. As in Node, it runs its root file's top-level statements in order (Scripts); top-level await works there. Declarations are hoisted: a function can be called above its definition.

ts
console.log(greeting("world"));

function greeting(name: string): string {
  return `hello, ${name}`;
}

A root file may instead declare function main() or async function main() and no top-level statements: then the program runs main, which returns void (exit code 0) or i32 (the exit code). This is where Velt differs from Node. In a file that has top-level statements, main is an ordinary function that runs only when the statements call it, and the compiler warns when nothing does (`main` is never called).

ts
function main(): i32 {
  console.log(greeting("world"));
  return 0;
}

function greeting(name: string): string {
  return `hello, ${name}`;
}

Source files are UTF-8. Each file is a module (Modules).

Comments and semicolons#

  • Comments: // line and /* block */.
  • Doc comments: a JSDoc /** … */ comment, or a block of /// lines, that ends on the line right above a declaration documents it. The text is Markdown, with JSDoc tags (@param, @returns, @throws, @example, @deprecated, @see, {@link name}). A plain // or /* */ comment is not documentation. Plain comments on the lines between a doc comment and its declaration (// eslint-disable-next-line, // @ts-expect-error) are skipped, as in TypeScript, but a blank line between them ends the association (TypeScript allows blank lines). The comment block at the very top of a file, followed by a blank line, documents the module (any comment style). velt doc and the editor show doc comments.
ts
/**
 * The larger of `a` and `b`.
 *
 * @param a - the first number
 * @param b - the second number
 * @returns `a` when the two are equal
 */
export function larger(a: i64, b: i64): i64 {
  return a >= b ? a : b;
}
  • Semicolons are optional: automatic semicolon insertion follows JavaScript's rules, as TypeScript and Node apply them, in .vlt, .ts and .tsx files alike. A statement, a declaration, or a member of a class, an interface or an object type ends with ;, or without one at a line break, before } or at the end of the file, when the next token cannot continue it. No ; is inserted in a for (;;) header; one is after do { … } while (c), even on the same line. velt fmt prints semicolons.
  • Some productions end at a line break whatever follows, as in JavaScript: return and yield (return / a line break / x returns nothing, and x is the next statement), break and continue before a label, and postfix ++ / -- (a / ++b on two lines increments b). A line break after throw, before =>, or after async in an arrow function is an error, as in TypeScript. In a type, type arguments start on the line of their type (a: string / <T>(x: T): T in an interface is a member and a call signature); after new and in extends / implements they may follow on the next line. A modifier followed by a line break is a member name (async / foo() {} in a class is a field async and a method foo), except static, get and set, which apply to the name on the next line; type, interface, declare and namespace followed by a line break do not declare anything, as in TypeScript.
  • The line terminators are line feed, carriage return, U+2028 and U+2029.
  • A line that starts with (, [, +, - or / continues the statement on the line before, as in Node: const a = b / (c).run() calls b. Velt warns about it ("this line continues the expression on the line before; add ; at the end of that line if it is a new statement", ESLint's no-unexpected-multiline), unless the line break is inside parentheses, brackets or braces, where the line before could not have ended the statement. A template literal starting such a line would make a tagged template, which Velt does not have: that is an error. Code formatted without semicolons starts such a line with ;:
ts
const parts: string[] = []
;["a", "b"].forEach((p) => parts.push(p))
console.log(parts.length)

Identifiers#

[A-Za-z_$][A-Za-z0-9_$]*, ASCII only.

Velt code is strict-mode code, as in TypeScript modules: no function, class, enum, variable, parameter or import may be named arguments or eval (properties and methods may).

#name (a # directly followed by an identifier) is an ES private name, used only for class members and in #name in o (Classes).

Literals#

  • Numbers: 123, 1_000_000, 0xff, 0b1010, 0o17, 1.5, 1e21, 2.5e-3, with an optional type suffix: 10u8, 5i32, 1.0f32. There are no BigInt literals (10n); use BigInt from velt/math.

Strings#

"..." or '...', with the escapes \n \r \t \b \v \f \\ \" \' \0 \xHH \uHHHH \u{H…}, cooked as JavaScript does: a backslash before a line break continues the line, a surrogate pair written as two escapes (\uD83D\uDE00) is one character, and a backslash before any other character is dropped ("a\d" is "ad", so new RegExp("\\d") needs the doubled backslash, as in JS). Octal escapes (\1, \01) and \8 \9 are errors, as in TypeScript; a lone surrogate ("\uD800") is an error too, since Velt strings are UTF-8.

  • Template literals: see Template literals below.
  • Regular expressions: /ab+c/gi is new RegExp("ab+c", "gi") from velt/text (a global, like RegExp itself). A / after an operand is division.
  • true, false, null.

Template literals#

`a ${expr} b` may span lines and also escape \` and \$, with the same escapes as strings. There are no tagged templates (tag`…`) or String.raw.

${expr} writes a value as JS's String(x) does: numbers, strings and booleans as JS writes them, an array as its elements joined with , (${[1, 2]} is 1,2), a class instance through its toString() (its own or inherited, a subclass's override also through a base-class value), and an object without one as [object Object] (std's classes as [object Map], [object Headers], …). Errors, unions and other values are written the way console.log prints them (Strings).

  • A toString parameter takes its default value (toString(radix: number = 10)), and a result that is not a string is written with String(result), as JavaScript does.
  • A toString that cannot be called that way is a compile error, never silently left out: a parameter without a default, an async one or a generator, a void result, type parameters of its own, or one that can throw.

Keywords#

Text
as async await break case catch class const constructor continue declare default do else
enum export extends false finally for from function if implements import in instanceof
interface let new null of readonly return shared static struct switch this throw true try
type typeof void while yield

Contextual keywords: abstract, extend, get, set, override, private, public, throws, using.

Not part of the language: var outside the top level of a function body or a module (Variables), protected (as a member modifier; accepted on constructors and constructor parameter properties), delete (except delete r[k] on a Record), export default, mut, match. Writing most of these is an error that names the Velt replacement.

Operators#

From highest to lowest precedence, with JavaScript's associativity:

OperatorsNotes
x++ x--postfix
. ?. () []member access, optional chaining, call, index
! ~ - + ++x --x await typeofprefix
**right-associative
* / %
+ -
<< >> >>>
< <= > >= as instanceof inin: a key test ("k" in o), or a brand check (#x in o); for (k in o) is a for...in loop
== != === !==no coercion; == differs from === only against null / undefined (Types)
&
^
|
&&returns an operand, as in JS (truthiness)
|| ??|| returns an operand; ?? replaces only null and undefined
?:
= += -= *= /= %= **= <<= >>= >>>= &= |= ^= &&= ||= ??=assignment
=>arrow function

The logical assignments &&=, ||= and ??= assign when &&, || or ?? would take their right side: x ??= d is x = x ?? d (Types). As in JavaScript, a compound assignment or ++ / -- evaluates its target's object and indices once, before the right side: rows[next()].out += "a" and f().count++ call next and f once. The write goes to that element where it is after the right side ran: xs[0] += grow(xs) updates xs[0] even when grow made xs longer. If the right side made the array shorter than the index, that write is out of bounds and panics, where JavaScript extends the array: xs[0] += xs.pop()! on [5] panics, on [5, 6] it makes [11] (Velt arrays never grow by assignment, see Types). An assignment to an element or a member is an expression with the value assigned, as in JS: const v = (xs[i] = 7), (c.t[1] += "z") and (get().n = 4) evaluate the object and indices once, before the right side, and give the element written even when the right side changes a variable index ((xs[k] = next()), where next changes k, gives the value written into the element k named before). The target's object may be any expression that gives an object: (c ? a : b).t[0] = 7 and xs.find(f)!.t[1] = "x" write into the object the conditional or the call gave, which every other reference to it sees. A tuple is a value, so a write into an element of a tuple that a call or a conditional gives (m.get(k)![1] = 2) is an error: it would change a copy. A postfix ! after an expression on the same line is the non-null assertion (m.get(k)!).

Edit this page on GitHub