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.
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).
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:
// lineand/* 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 docand the editor show doc comments.
/**
* 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,.tsand.tsxfiles 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 afor (;;)header; one is afterdo { … } while (c), even on the same line.velt fmtprints semicolons. - Some productions end at a line break whatever follows, as in JavaScript:
returnandyield(return/ a line break /xreturns nothing, andxis the next statement),breakandcontinuebefore a label, and postfix++/--(a/++bon two lines incrementsb). A line break afterthrow, before=>, or afterasyncin 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): Tin an interface is a member and a call signature); afternewand inextends/implementsthey may follow on the next line. A modifier followed by a line break is a member name (async/foo() {}in a class is a fieldasyncand a methodfoo), exceptstatic,getandset, which apply to the name on the next line;type,interface,declareandnamespacefollowed 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()callsb. 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'sno-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;:
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 noBigIntliterals (10n); useBigIntfromvelt/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/giisnew RegExp("ab+c", "gi")fromvelt/text(a global, likeRegExpitself). 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
toStringparameter takes its default value (toString(radix: number = 10)), and a result that is not astringis written withString(result), as JavaScript does. - A
toStringthat cannot be called that way is a compile error, never silently left out: a parameter without a default, anasyncone or a generator, avoidresult, type parameters of its own, or one that can throw.
Keywords
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 yieldContextual 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:
| Operators | Notes |
|---|---|
x++ x-- | postfix |
. ?. () [] | member access, optional chaining, call, index |
! ~ - + ++x --x await typeof | prefix |
** | right-associative |
* / % | |
+ - | |
<< >> >>> | |
< <= > >= as instanceof in | in: 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)!).