Skip to content

Builtin Functions and Constants

You can use builtins in your Temper code without importing anything.

Some definitions are marked Infix, Prefix, or Postfix. These are operators: they are used like mathematical symbols:

  • Infix + appears between arguments: a + b
  • Prefix - appears before its sole argument: -b
  • Postfix ++ appears afters its argument: i++

Other functions are called with parentheses: f(x). See also Call Syntax.

(The full set of operator definitions is Operator.kt which also determines when you need parentheses. Temper's operator precedence matches JavaScript's & TypeScript's as close as possible so doing what you would in those languages is a good guide.)

Constants

console

The global default Console instance. It is designed to integrate with standard backend logging services. Frontend processing replaces references to the global console with a call to getConsole for the current module.

false

The value false of type Boolean.

Infinity

The Float64 value for ∞.

NaN

The Float64 value for [unrepresentable "Not a Number"][https://en.wikipedia.org/wiki/NaN] values.

null

The value null of type Null.

true

The value true of type Boolean.

void

The value void is the sole value in type Void.

WARNING: To interface better across idiomatic void behavior in backends, current plans are to make Void disjoint from AnyValue such that no void value is usable. See also issue#38.

Special functions

async { ... }

The async builtin function takes a safe generator block and runs it out of band.

TODO: example

await(promise)

TODO: document me

cat

Short for "concatenate", combines multiple strings into one string.

""       == cat()             &&
"foo"    == cat("foo")        &&
"foobar" == cat("foo", "bar")
// ✅

+ does not concatenate strings; it's reserved for math.

cat is an implementation detail. Prefer String interpolation to compose strings.

let a = "foo";
let b = "bar";

"foo-bar" == "${ a }-${ b }"
// ✅

new

The new operator allows constructing new instances of types.

yield()

Called from the body of a generator function, to pause the body and cause Generator.next to return.

The block lambda below has an extends GeneratorFn.

A generator function may use the no-argument version of yield in which case its wrapper generator's Generator.next returns null.

If the generator function wishes to convey a value to its scheduler, it may use yield(x) where x is the value returned by Generator.next.

Note: yield is syntactically like return and throw in that parentheses around its argument are optional.

// Two co-operating functions each have print
// calls which interleave because the generator
// function uses yield() to return control to
// its caller temporarily.

//!outputs "S: Before first call"
//!outputs "C: In coroutine, before yield"
//!outputs "S: Between calls"
//!outputs "C: Coroutine resumed"
//!outputs "S: After calls"

// scheduleTwice manually schedules execution of the generator/coroutine.
let scheduleTwice(generatorFactory: fn (): SafeGenerator<Null>): Void {
  let generator: SafeGenerator<Null> = generatorFactory();
  console.log("S: Before first call");
  generator.next();
  console.log("S: Between calls");
  generator.next();
  console.log("S: After calls");
}

// The block lambda declares a coroutine constructor
// which yields between two print statements causing
// them to interleave with the statement above.
scheduleTwice { (): GeneratorResult<Null> extends GeneratorFn;;
  console.log("C: In coroutine, before yield");
  yield; // Return control to the caller
  console.log("C: Coroutine resumed");
}
// ✅

Above, the generator function has type (): Null extends GeneratorFn which specifies what the block lambda does. But generator functions are received by their receiver function as a factory for generator instances: For example, scheduleTwice receives a (fn (): Generator<Null, Nothing>) & GeneratorFn.

assignment (=) builtin

The assignment operator allows assigning to local variables, and properties of objects.

var s = "before";
console.log(s); //!outputs "before"
s = "after"; // <-- assignment
console.log(s); //!outputs "after"
// ✅

The result of assignment is the result of the right-hand, assigned expression.

let a;
let b;

// The result of the nested `b = 42` is `42`
a = b = 42;

a == 42
// ✅

Assignment to a property with a setter is really a method call, under the hood.

class C {
  public set p(newValue: String) {
    console.log("Assigned ${newValue}.");
  }
}
(new C()).p = "a value" //!outputs "Assigned a value."
// ✅ "a value"

Functions

bubble()

A function that immediately bubbles. Typically, this will result in execution of the right argument to the closest containing orelse.

Failure

Expressions either evaluate to a value or fail to produce a usable result. See also:

  • Bubble which provides type system support for tracking failure.
  • orelse which allows recovering from failure.

char

A string tag that requires a single code-point string and returns that code-point as an Int.

char'a' == 97
// ✅

getConsole

Access or make a console for the given name. If name is unspecified, use the idiomatic backend library or module name for logging purposes. The default of empty string is merely a placeholder.

ignore

Ignore any value.

panic()

A function that immediately panics, which is unrecoverable inside Temper. It might be recoverable in backend code.

!

The prefix ! operator performs Boolean inverse.

!false is true and vice versa.

!=

a != b is the Boolean inverse of ==

Remainder %

Given two Ints it produces an Int and given two Float64s it produces a Float64.

13   % 3   == 1    &&
13.0 % 3.0 == 1.0
// ✅

Division by Zero bubbles

(1 % 0) orelse console.log("mod by zero");
//!outputs "mod by zero"
// ✅
(1.0 % 0.0) orelse console.log("mod by zero");
//!outputs "mod by zero"
// ✅

Operator &

The & operator can be applied in two ways:

Int &

Takes two Ints and returns the Int that has any bit set that is set in both input.

// Using binary number syntax
(0b0010101 &
 0b1011011) ==
 0b0010001
// ✅

Intersection type &

When the & operator is applied to types instead of numbers, it constructs an intersection type.

An intersection type is a sub-type of each of its members. So an expression with type I & J can be assigned to a declaration with type I and a declaration with type J. See also snippet/type/relationships.

Multiplication *

Infix * allows multiplying numbers.

Given two Ints it produces an Int and given two Float64s it produces a Float64.

3   * 4   == 12   &&
3.0 * 4.0 == 12.0
// ✅

Exponentiation **

Infix ** allows raising one number to the power of another.

Given two Float64s it produces a Float64.

3.0 **  2.0 == 9.0 &&
4.0 ** -0.5 == 0.5
// ✅

+

The builtin + operator has four variants: - Infix with two Ints: signed addition - Prefix with one Int: numeric identity - Infix with two Float64s: signed addition - Prefix with one Float64: numeric identity

1   + 2   == 3   &&
1.0 + 2.0 == 3.0 &&
+1        == 1   &&
+1.0      == 1.0
// ✅

As explained above, you cannot mix Int and Float64 inputs:

1 + 1.0
// ❌ No applicable variants in (fn (Int, Int): Int & fn (Int): Int & fn (Float64, Float64): Float64 & fn (Float64): Float64) for inputs (Int, Float64)!

+ does not work on Strings. Use cat instead.

"foo" + "bar"
// ❌ No applicable variants in (fn (Int, Int): Int & fn (Int): Int & fn (Float64, Float64): Float64 & fn (Float64): Float64) for inputs (String, String)!

-

The builtin - operator has four variants like +.

3   - 1   == 2   &&
3.0 - 1.0 == 2.0 &&
-3        <  0   &&
-3.0      <  0.0
// ✅

As with +, you cannot mix Int and Float64 inputs:

1 + 1.0
// ❌ No applicable variants in (fn (Int, Int): Int & fn (Int): Int & fn (Float64, Float64): Float64 & fn (Float64): Float64) for inputs (Int, Float64)!

The - operator is left-associative:

1 - 1 - 1 == (1 - 1) - 1 &&
1 - 1 - 1 == -1
// ✅

Since there is a -- operator operator, --x is not a negation of a negation.

var x = 1;
+x == -(-x) &&  // double negation is identity
--x == 0        // but two adjacent `-` means pre-decrement
// ✅

Division /

Infix / allows dividing numbers.

Given two Ints it produces an Int and given two Float64s it produces a Float64.

12   / 3   == 4    &&
12.0 / 3.0 == 4.0
// ✅

Integer division rounds towards zero.

 7   / 2   ==  3   &&
-7   / 2   == -3   &&
 7.0 / 2.0 ==  3.5 &&
-7.0 / 2.0 == -3.5
// ✅

Division by zero has Bubble.

(1 / 0) orelse console.log("div by zero");
//!outputs "div by zero"
// ✅

Float64 division by zero is a Bubble too.

console.log("${ (0.0 /  0.0).toString() orelse "Bubble" }"); //!outputs "Bubble"
console.log("${ (1.0 /  0.0).toString() orelse "Bubble" }"); //!outputs "Bubble"
console.log("${ (1.0 / -0.0).toString() orelse "Bubble" }"); //!outputs "Bubble"
// ✅

<

a < b is true when a orders before b, and is a compile-time error if the two are not mutually comparable.

See the General comparison algorithm for details of how they are compiled and especially the General Comparison Caveats.

<=

a <= b is true when a orders with or before b, and is a compile-time error if the two are not mutually comparable.

See the General comparison algorithm for details of how they are compiled and especially the General Comparison Caveats.

<=>

a <=> b results in an Int based on whether a orders before, after, or with b, and is a compile-time error if the two are not mutually comparable.

  • a <=> b is -1 if a orders before b
  • a <=> b is 0 if a orders with b
  • a <=> b is 1 if a orders after b
(   42 <=>   123) == -1 &&  //    42 orders before   123
(  1.0 <=>   1.0) == 0  &&  //   1.0 orders with     1.0
("foo" <=> "bar") == 1      // "foo" orders after  "bar"
// ✅

General comparison algorithm

The general comparison algorithm is designed to allow for easy structural comparison of data values that work the same regardless of target language.

Ints are compared based on their position on the number line. No surprises here.

-1 < 0 && 0 < 1 && 1 < 2
// ✅

Float64s are also compared numerically.

-1.0 < 0.0 && 0.0 < 1.0 && 1.0 < 2.0
// ✅

But the default comparison operators are meant to support structural comparison of records so see also caveats for how Float64 ordering differs from other languages.

Strings are compared lexicographically based on their code-points.

"foo" > "bar"
// ✅

See also caveats for String related ordering.

Custom comparison for classes

issue#37: custom comparison for classes.

General Comparison Caveats

String Ordering Caveats

String ordering based on code-points means that supplementary code-points (code-points greater than U+10000) sort higher than all basic plane code-points,

"\u{10000}" > "\u{FFFF}" // Hex code-point escapes
// ✅

Developers used to lexicographic UTF-16 might be surprised since UTF-16 ordering treats each supplementary code-point as two surrogates in the range [0xD800, 0xDFFF]. The first string above would be "\uD800\uDC00" if Temper string literals supported surrogate pairs. In some languages, that might compare as less than "\u{FFFF}", but Temper views all strings in terms of full code-points, or more precisely, in terms of Unicode scalar values, which exclude surrogate codes.

Float64 Ordering Caveats

Float64s are compared on a modified number line where -0.0 precedes +0.0 and all NaN values sort above +∞. This differs from the IEEE-754 comparison predicate which treats NaN as incomparable.

-Infinity < -1.0     &&
     -1.0 <  0.0     &&
     -0.0 < +0.0     &&  // Positive and negative zero order separately
      0.0 <  1.0     &&
      1.0 < Infinity &&
 Infinity < NaN          // NaN is ordered high
// ✅

==

a == b is the default equivalence operation.

Two values are equivalent if they have the same type-tag and the same content.

issue#36: custom equivalence and default equivalence for record classes

>

a > b is true when a orders after b, and is a compile-time error if the two are not mutually comparable.

See the General comparison algorithm for details of how they are compiled and especially the General Comparison Caveats.

>=

a >= b is true when a orders after or with b, and is a compile-time error if the two are not mutually comparable.

See the General comparison algorithm for details of how they are compiled and especially the General Comparison Caveats.

Operator |

The | operator can be applied in two ways:

Int |

Takes two Ints and returns the Int that has any bit set that is set in either input.

// Using binary number syntax
(0b0010101 |
 0b1011011) ==
 0b1011111
// ✅

Union type |

When the | operator is applied to types instead of numbers, it constructs a union type.

A union type is a super-type of each of its members. So for a declaration with type Boolean | Null, both Booleans and the value null can be assigned to it. See also snippet/type/relationships.

let x: Boolean | Null;
x = true
// ✅
let x: Boolean | Null;
x = null
// ✅ null
let x: Boolean | Null;
x = "neither Boolean nor Null"
// ❌ Cannot assign to Boolean | Null from String!, Type Boolean | Null rejected value "neither Boolean nor Null"!

Types

Boolean

The name Boolean refers to the builtin type, Boolean.

It has only two values:

BooleanLiteral := "false" | "true" false true

Bubble

A name that may be used to reference the special Bubble type.

Float64

The name Float64 refers to the builtin type, Float64.

Function

The name Function refers to the builtin type, Function, which is the super-type for all Function Types.

Int

The name Int refers to the builtin type, Int.

List

The name List refers to the builtin type, List.

Listed

The name Listed refers to the builtin type, Listed.

Never

A name that may be used to reference the special Never type.

Null

The name Null refers to the builtin type, Null.

There is only one value of this type: null.

Problem

The name Problem refers to the builtin type, Problem, an abstraction that allows running some tests despite failures during compilation.

TODO: StageRange

String

The name String refers to the builtin type, String.

Symbol

Symbol is the type for values that are used, during compilation, to represent parameter names, member names, and metadata keys.

They're relevant to the Temper language, and symbol values are not meant to translate to values in other languages.

Top

A name that may be used to reference the special Top type.

Type

Type values represent a Temper type, allowing them to be inputs to macros.

Void

The name Void refers to the builtin type Void.

Void is an appropriate return type for functions that are called for their effect, not to produce a value.

Macros

assert

A macro for convenience in test cases. Calls assert on the current test instance.

test("something important") {
  // Provide the given message if the assertion value is false.
  assert(1 + 1 == 2) { "the sky is falling" }
}
// ⏸️

break statement

break jumps to the end of a block.

Without a label, break jumps to the narrowest surrounding for loop, while loop, or do loop.

for (var i = 0; i < 10; ++i) {
  if (i == 3) { break; }
  console.log(i.toString());
}
//!outputs "0"
//!outputs "1"
//!outputs "2"
// ✅

break may also be followed by a label, in which case, it jumps to just after the statement with that label.

label: do {
  console.log("foo"); //!outputs "foo"
  break label;
  console.log("bar"); // never executed
}
// ✅

A break that matches neither label nor loop within the same function or module body is a compile-time error.

break;
// ❌ break/continue not within a matching block!
for (var i = 0; i < 10; ++i) {
  break label;
}
// ❌ break/continue not within a matching block!

class

Defines a concrete class type. Classes may extend interface types but may not themselves be extended.

Class declarations consist of several parts:

  1. A type name
  2. Optionally, some type formals
  3. Optionally, constructor parameters
  4. Optionally, some super types
  5. A block containing type members
//    ①          ②      ②
class MyTypeName<TYPE, FORMAL>(

  // ③
  public propertyName: Int,

  //      ④       ⑤
) extends AnyValue {

  public anotherProperty: Int = propertyName + 1;

  public method(): Int { anotherProperty * 2 }
}

// Given such a declaration, we can create values of type MyTypeName.
// Properties that appear in the parenthetical are both constructor parameters.
let value = { propertyName: 11 };
// But parenthetical declarations also declare a property.
console.log(value.propertyName.toString()); //!outputs "11"
// Public members (the default) declared in the body are also available.
console.log(value.anotherProperty.toString()); //!outputs "12"
console.log(value.method().toString()); //!outputs "24"

// For unconnected classes, you can check whether a value is of that type
// at runtime.
console.log(value.is<MyTypeName<AnyValue, AnyValue>>().toString()); //!outputs "true"
// ✅

A minimal class declaration may omit many of those elements along with the brackets and keyword (like # extends keyword):

class Minimal {}

let m = new Minimal();

console.log(m.is<Minimal>().toString()); //!outputs "true"
// ✅

Source: TypeDefinitionMacro.kt

Re parenthetical declarations, see also: @noProperty decorator

compilelog

TODO: Migrate the logging APIs to console style objects APIs per issue #1180 Name the logger that runs during early stages compile so that compile.log(...) aids in debugging macro calls.

continue statement

A continue statement jumps back to the beginning of a loop. In a for loop loop, it jumps back to the increment part.

A continue without a label jumps back to the innermost containing loop.

for (var i: Int = 0; i < 4; ++i) {
  if (i == 1) { continue; }
  console.log(i.toString());
}
//!outputs "0"
// 1 is skipped
//!outputs "2"
//!outputs "3"
// ✅

A continue with a label jumps back to the loop with that label.

outer:
for (var i: Int = 0; i < 2; ++i) {
  for (var j: Int = 0; j < 10; ++j) {
    if (j == 1) { continue outer; }
    // Reached once for each value of i, with j == 0
    console.log("(${i.toString()}, ${j.toString()})");
  }
  // Continue skips over this so it never runs
  console.log("Not reached");
}
//!outputs "(0, 0)"
//!outputs "(1, 0)"

console.log("done"); //!outputs "done"
// ✅

do loops and do blocks

do loops are like for loop and while loop except that the body runs once before the condition is checked the first time. This is the same as how these loops operate in many other languages.

do differs in Temper that it can be used without a condition to do something once. This can be useful when you need a block in the middle of a larger expression.

do while
stateDiagram-v2
  [*] --> Body
  Body --> Condition
  Condition --> Body : true
  Condition --> [*] : false
stateDiagram-v2
  [*] --> Condition
  Condition --> Body : true
  Body --> Condition
  Condition --> [*] : false
var i = 1;
// Prints "Done" initially, and goes back to do it again when `i` is (1, 0)
do {
  console.log("Done");
} while (i-- >= 0);

//!outputs "Done"
//!outputs "Done"
//!outputs "Done"
// ✅

versus

var i = 1;
// Prints "Done" when `i` is (1, 0)
while (i-- >= 0) {
  console.log("Done");
}

//!outputs "Done"
//!outputs "Done"
// ✅

You can see that when the condition is initially false, the body still executes once.

do {
  console.log("Done once"); //!outputs "Done once"
} while (false);
// ✅

continue jumps to just before the condition, not over the condition to the top of the loop.

do {
  console.log("Done once"); //!outputs "Done once"
  continue;                 // jumps ━━━┓
  console.log("Not done");  //          ┃
  //     ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
  //     ┃
} while (false);
// ✅

If you just want to do something once, you can omit the while clause.

console.log( //!outputs "Done once"
  do { "Done once" }
);
// ✅

This allows you to group expressions, some for their side effects, followed by another to compute a result.

// These related statements are now grouped together, and
// the `do` block can be nested in a complex expression.
do {
  var i = 0;  // Scoped
  i += 4;     // For side-effect
  i * 2       // Result
}
// ✅ 8

When do is followed by a curly bracket, the content between the brackets are treated as statements, not object properties.

Blocks vs bags

Putting do before curly brackets, do {⋯} specifies a block of statements, not a bag of key/value properties à la JSON.

// A bag of properties
{
   key: value,
   key: value
}

// A series of statements
do {
  statement;
  label: statement
}
// ⏸️

Temper uses a simple rule to decide whether {⋯} surrounds properties or statements because there can be confusion otherwise.

{ labelOrKey: valueOrStatement } // ?

{ /* are empty brackets a group of zero properties or zero statements? */ }

// This is complicated with "punning": when a key and value have the same text.
{ x } // may be equivalent to `{ x: x }` in a property context.
// Within small or heavily punned brackets, there are fewer cues like colons,
// commas, or semicolons.
// ⏸️

Whether a { is followed by properties or statements is made based on the preceding non-comment & non-space token.

Preceding Token Text Contains Classification
let, var, const Properties declaration
new Properties new value context
no preceding token Properties the start of a module
], ), Angle >, => Statements block or block lambda
word Statements block or block lambda
anything else Properties default is bag

enum

TODO(issue #859): Fix translation of enum definitions before documenting.

enum declarations are like class declarations

enum C { A, B, C; rest }
// ⏸️

desugars to something like

class C(public let ordinal: Int, public name: String) {
  public static @enumMember let A = new C(0, "A");
  public static @enumMember let B = new C(1, "B");
  public static @enumMember let C = new C(2, "C");

  private constructor(ordinal: Int, name: String) {
    this.ordinal = ordinal;
    this.name = name;
  }

  rest
}
// ⏸️

Backends should make a best effort to translate classes with enum members to their language's idiomatic equivalent.

TODO: Document how Temper enum types relate to types per backend.

SubType extends SuperType

The extends keyword expresses that the thing to the left is a subtype of the thing to the right.

It's used in two different contexts.

First, when defining types.

interface SuperType {}
class SubType extends SuperType {}

// An instance of a subtype may be assigned to a variable
// whose type is the supertype.
let x: SuperType = new SubType();
// ✅ null

Second, when declaring a type variable. The below defines an upper bound: the type variable may only bind to types that are subtypes of the upper bound.

let f<T extends Listed<String>>(x: T): T { x }
// ✅ null

When more than one super-type is extended, use type intersection syntax (&).

interface I {}
interface J {}

class C extends I & J {}

let f<T extends I & J>(x: T): T { x }
// T extends I and T extends J
// ✅ null

fn

The fn keyword is used to define function values and Function Types. Contrast this with let which is used to declare functions

TODO: more explanation needed.

for loop

for (initializer; condition; increment) { body } is a simple for loops. for (let one of many) { body } is a for...of loop.

simple for loops

First it executes the initializer, once only.

Then it repeatedly executes the condition which must be a Boolean expression. The first time the condition evaluates to false, the loop exits. After each time the condition evaluates to true, the increment is evaluated and control transfers back to the body.

An unlabelled break statement in the body exits the loop. An unlabelled continue statement in the body jumps back to the increment and then to the condition.

for (var i = 0; i < 3; ++i) {
  console.log(i.toString());
}
//!outputs "0"
//!outputs "1"
//!outputs "2"
// ✅

Unlike in some other languages, curly brackets ({...}) are required around the body.

for (var i = 0; i < 3; ++i)
  console.log(i.toString()) // Curly brackets missing
// ❌ Expected function type, but got Invalid!, Expected function type, but got Invalid!, No declaration for i!

So a for loop is roughly equivalent to

{ <-- // Block establishes scope for variables defined in initializer
  initializer;
  while (condition) {
    body;
    // <-- `continue` in body goes here
    increment;
  }
} // <-- `break` in body goes here
// ⏸️

except that, as noted, continue statements in body do not skip over the increment.

The initializer may declare variables that are scoped to the loop.

let s = "declared-before-loop";
for (var s = "declared-in-loop-initializer"; true;) {
  console.log(s); //!outputs "declared-in-loop-initializer"
  break;
}
// The use of `s` here refers to the top-level `s`,not the loop's `s`;
// the loop variable is only visible within the loop.
console.log(s); //!outputs "declared-before-loop"
// ✅

for...of loop

For...of loops allow iterating over each value in some group of values.

for (let x of ["a", "b", "c"]) {
  console.log(x);
}
//!outputs "a"
//!outputs "b"
//!outputs "c"
// ✅

The parts of a for...of loop are:

  • The declaration. let x in the example above.
  • The source of elements. The list ["a", "b", "c"] above.
  • The body. { console.log(x) } above.

A for...of loop is syntactic sugar for a call to the forEach method. (for...of simply turns one construct into another). The example above is equivalent to the .forEach call below.

["a", "b", "c"].forEach { (x);;
  console.log(x);
}
//!outputs "a"
//!outputs "b"
//!outputs "c"
// ✅

In both cases, jumps like break statement, continue statement, and return may appear in the body when the body is a lexical block provided directly to forEach and the forEach implementation uses @inlineUnrealizedGoal decorator as List.forEach does.

for (let x of ["a", "b", "c", "d", "e"]) {
  if (x == "b") { continue }
  console.log(x);
  if (x == "d") { break }
}
//!outputs "a"
//!outputs "c"
//!outputs "d"
// ⏸️

Unlike some other languages, a bare name is not allowed instead of a declaration in a for...of loop.

var x;
for (x of ["a", "b", "c") {
  console.log(x)
}
// ❌ Expected a TopLevel here!

if

if allows branching on a Boolean predicate.

if (true) {
  console.log("Runs");
}
if (false) {
  console.log("Does not run");
}
//!outputs "Runs"
// ✅

An if may include an else block which is run when the predicate is false.

if (false) {
  console.log("Does not run")
} else {
  console.log("Runs")
}
if (true) {
  console.log("Runs")
} else {
  console.log("Does not run")
}
//!outputs ["Runs", "Runs"]
// ✅

An else can change to another if statement.

let twoPlusTwo = 2 + 2;
if (twoPlusTwo == 3) {
  console.log("Does not run")
} else if (twoPlusTwo == 4) {
  console.log("Runs")
} else {
  console.log("Does not run")
}
//!outputs "Runs"
// ✅

Temper is an expression language, so if may be used outside an expression context.

let x = (if (true) { 42 } else { 0 });
x == 42
// ✅

In some syntactically similar languages, you can skip the brackets ({...}) around if and else bodies. You can't do that in Temper; always put {...} around the bodies.

if (true) console.log("Runs"); else console.log("Does not run");
// ❌ Expected a TopLevel here!

Note

The reason for this is that in Temper, control-flow operators like if are not special syntactic forms. They're macros that take blocks as arguments and phrases like else and else if are named parameters to that macro which also must be passed blocks.

Unlike C, conditions must be Boolean or a sub-type.

if (0) { console.log("truthy") } else { console.log("falsey") }
// ❌ Expected value of type Boolean not Int!

import

The import function, in conjunction with export, allows connecting multiple Temper modules together.

One module may export a symbol.

export let world = "Earth";
// ⏸️

and another may import it

let { world } = import("./earth");
console.log("Hello, ${ world }!"); //!outputs "Hello, Earth!"
// ⏸️

As shown above, when importing from files under the same work root, use a relative file path starting with one of "./" or "../". This allows multiple co-compiled libraries to link to one another.

Import specifiers always use the URL file separator (/), regardless of whether compilation happens on an operating-system like Linux which uses the same separator or on Windows which uses \\.

To link to separately compiled libraries:

  1. start with the library name
  2. followed by a separator: /
  3. followed by the path to the source file relative to the library root using the URL file separator
// Temper's standard library uses the name `std`.
// The temporal module includes `class Date` and other time-related
// types and functions.
let { Date } = import("std/temporal");

let d: Date = { year: 2023, month: 12, day: 13 };
console.log(d.toString()); //!outputs "2023-12-13"
// ✅

interface

Defines an abstract interface type. Interface types may define abstract properties but may not define constructors or backed properties. Interface's properties may be overridden by backed properties in a class sub-type.

See also class for details and examples of type declaration syntax.

Source: TypeDefinitionMacro.kt

let

The word let is used to define names for both variables and functions.

Defining variables

let, var, and const are used to define variables.

  • let x defines a variable within the innermost containing block with the name x. It may be assigned once.
let x = "initial-value";
x == "initial-value"
// ✅

but it may not be re-assigned.

let x = "initial-value";
x = "new-value";
// ❌ x is reassigned after build-user-docs/build/snippet/scoping/examples/snippet.md+386-401 but is not declared `var` at build-user-docs/build/snippet/scoping/examples/snippet.md+378-401!
  • const x is equivalent to @const let x.
const x = "initial-value";
x == "initial-value"
// ✅
  • var x is equivalent to @var let x and defines a variable that may be re-assigned after being initialized.
var x = "initial-value";
x = "new-value"; // Re-assigned
x == "new-value"
// ✅

Unlike in JavaScript, var is scoped the same way as let.

var i = "outer-var-i";
do {
  var i = "inner-var-i";
  console.log("inside  block ${i}"); //!outputs "inside  block inner-var-i"
}
console.log("outside block ${i}"); //!outputs "outside block outer-var-i"
// ✅

Names can be re-used. A declared name does not mask a name declared in a containing scope that appear before it, but do mask uses that appear after it.

let i = "outer-i";
do {
  let j = i; // The `i` in the initializer refers to the innermost earlier declaration.
  let i = "inner-i";
  console.log("Inside : j=${j}, i=${i}");
  //!outputs "Inside : j=outer-i, i=inner-i"
}
console.log("Outside: i=${i}");
//!outputs "Outside: i=outer-i"
// ✅

Defining functions

To define a function, use let, but with an argument list and body.

// Declare a function named `f`
let f(): String { "foo" }
// and call it
console.log(f()); //!outputs "foo"
// ✅

The syntax for function values is similar, but uses fn instead of let. A function value may have a name, so that it may recursively call itself, but that name is not visible outside the function expression.

// A function "declaration"
let f(): String { "function-declaration" }
// A function expression's name does not affect uses of that name outside itself
fn f(): String { "function-expression" }
console.log(f()); //!outputs "function-declaration"
// ✅

orelse

The orelse infix operator allows recovering from failure to produce a result.

The left is executed and if it does not produce a result, then the right is executed and its result is used instead.

let f(n: Int, d: Int): Int { n / d orelse -1 }
console.log("f(3, 0)=${f(3, 0).toString()}"); //!outputs "f(3, 0)=-1"
console.log("f(6, 2)=${f(6, 2).toString()}"); //!outputs "f(6, 2)=3"
// ✅

When you find that you're on a track that cannot produce a usable result, use orelse to switch to a track that may yet succeed or allow a client to try something else, or recover gracefully.

raw

A tag for raw strings; escape sequences are not expanded.

// Since this is raw, the \b is treated as two characters
// '\' and 'b', not an ASCII bell.
raw"foo\bar"
// ✅ "foo\\bar"

You can have a raw string with quotes by putting it in a multi-quoted string.

raw"""
   "quo\ted"
   """
// ✅ "\u0022quo\\ted\u0022"

return

The return keyword ends execution of the innermost enclosing function.

let f(returnEarly: Boolean): Void {
  if (returnEarly) { return } // Exit f without performing the next statement
  console.log("Did not return early");
}
console.log("Return early");        //!outputs "Return early"
f(true);                      // outputs nothing
console.log("Do not return early"); //!outputs "Do not return early"
f(false);                     //!outputs "Did not return early"
// ✅

return may be followed by an expression to specify the result value.

let answer(): Int { return 42; }
answer() == 42
// ✅

Implied returns

You can leave out a return if the type is Void or the terminal statements in a function body are not in a loop and should be the output.

let yesOrNo(b: Boolean): String {
  if (b) {
    "yes" // Implicitly returned
  } else {
    "no"  // me too
  }
}

console.log(yesOrNo(true));  //!outputs "yes"
console.log(yesOrNo(false)); //!outputs "no"
// ✅

Terminal statements are those statements that may be executed just before control leaves the function body.

rgx

Constructs a compiled regex object from a regex literal.

// These all do the same thing.
let regex1 = rgx"[ab]c";
let regex2 = /[ab]c/;
let regex3 = new Sequence([
  new CodeSet([new CodePoints("ab")]),
  new CodePoints("c"),
]).compiled();
// ⏸️

test

Define a test case. Any module that defines tests is considered to be a test module rather than a production module.

test("something important") {
  // Provide the given message if the assertion value is false.
  assert(1 + 1 == 2) { "the sky is falling" }
}
// ⏸️

when

You can check when an expression matches types or values.

interface I {}
class A(public message: String) extends I {}
class B extends I {}
class C extends I {}
class D extends I {}

let nameOfI(x: I): String {
  when (x) {
    is A -> "A ${x.message}"; // Auto cast for single `is` type.
    is B, is C -> "B or C";
    else -> do {
      let a = nameOfI(new A("at all"));
      let b = nameOfI(new B());
      "not ${a} or ${b}"
    }
  }
}

console.log(nameOfI(new A("here"))); //!outputs "A here"
console.log(nameOfI(new B())); //!outputs "B or C"
console.log(nameOfI(new D())); //!outputs "not A at all or B or C"

console.log(
  when (2) {
    0 -> "none";
    1, 2, 3 -> "little";
    else -> "lots or negative";
  }
); //!outputs "little"
// ✅

while loop

while (condition) { body } repeatedly executes the condition until it yields false, and after each true result of the condition, executes the body.

An unlabelled break statement in the body exits the loop. An unlabelled continue statement in the body jumps back to the condition.

var i = 0;
while (i < 3) {
  console.log((i++).toString());
}
//!outputs "0"
//!outputs "1"
//!outputs "2"
// ✅

Unlike in some other languages, curly brackets ({...}) are required around the body.

var i = 0;
while (i < 3)
  console.log((i++).toString()) // Curly brackets missing
// ❌

&&

a && b performs a short-circuiting logical-and of its arguments as in C. By short-circuiting, we mean that if a is false, then b is never evaluated, so you may assume that a is true when writing b.

a b a && b
false false false
false true false
true false false
true true true
let isTwiceIsh(numerator: Int, denominator: Int): Void {
  // Here, panic asserts that the division will never fail.
  if (denominator != 0 && numerator / denominator == 2 orelse panic()) {
    console.log("yes");
  } else {
    console.log("no");
  }
}
isTwiceIsh(4, 2); //!outputs "yes"
isTwiceIsh(3, 1); //!outputs "no"
isTwiceIsh(1, 0); //!outputs "no"
// -ish might be doing a lot of work.
// ✅

++ operator

++x is equivalent to x += 1. x++ has the same effect as ++x, but produces the value of x before incrementing.

var x: Int = 0;
// when `x` comes after  `++`, produces value after  increment
console.log((++x).toString()); //!outputs "1"
// when `x` comes before `++`, produces value before increment
console.log((x++).toString()); //!outputs "1"
x == 2
// ✅

The effects of ++x and x++ differ from x = x + 1, in that if x is a complex expression, its parts are only evaluated once. For example, in ++array[f()], the function call, f(), which computes the index, only happens once.

-- operator

--x is equivalent to x -= 1. x-- has the same effect as --x, but produces the value of x before incrementing.

var x: Int = 0;
// when `x` comes after  `--`, produces value after  increment
console.log((--x).toString()); //!outputs "-1"
// when `x` comes before `--`, produces value before increment
console.log((x--).toString()); //!outputs "-1"
x == -2
// ✅

The effects of --x and x-- differ from x = x - 1, in that if x is a complex expression, its parts are only evaluated once. For example, in --array[f()], the function call, f(), which computes the index, only happens once.

=> definition

TODO: Do we need this shorthand for function value/type construction? TODO: Does it need more testing? Issue #1549

Hook operator ?:

The hook operator works like in JavaScript.

condition ? consequent : alternate first evaluates condition, which must have type Boolean. When the result is true, consequent is evaluated and its result used. When the result is false, alternate is evaluated and its result used.

let f(b: Boolean): String { b ? "yes" : "no" }

console.log("f(true)  = ${f(true)}");  //!outputs "f(true)  = yes"
console.log("f(false) = ${f(false)}"); //!outputs "f(false) = no"
// ✅

if can be used instead. ?: is provided for familiarity with JS/TS and if should be preferred.

let same(b: Boolean): Boolean {
  let usingHook =     b  ? "yes"        : "no";    // DEPRECATED
  let usingIf   = if (b) { "yes" } else { "no" };  // PREFERRED
  usingHook == usingIf
}
same(true) && same(false)
// ✅

Null-coalescing ??

Infix ?? allows choosing a default value when the subject is null.

let prod(i: Int, j: Int | Null): Int { i * (j ?? 1) }
prod(2, 3)    == 6 &&
prod(2, null) == 2
// ✅

@

Decorators like @Foo allow adapting or adjusting the behavior of definitions.

TODO: write me

Legacy decorators

To make Temper more readable for people familiar with other languages, some decorators don't need an @ character before them.

The following modifying words are converted to decorations when followed by an identifier or keyword token:

Additionally, some decorators imply the word let: var and const, when not followed by let imply let.

do {
  @var let i = 1;
  i += 10;
  console.log(i.toString()); //!outputs "11"
}
do {
  var i = 1;
  i += 10;
  console.log(i.toString()); //!outputs "11"
}
// ✅

@const decorator

@const may decorate a let declaration. It is a legacy decorator.

By default, names declared by let may be assigned once per entry into their containing scope. @const is provided for familiarity with readers of other languages.

Contrast @const with @var.

@export

The @export decorator keyword makes the declaration available outside the module. Exported declarations may be linked to via import.

It is a legacy decorator, so the export keyword is shorthand for the @export decorator.

Exported symbols are also visible within the module following normal scoping rules.

export let x = 3;
x == 3
// ✅

@extension decorator

The \@extension decorator applies to a function declaration that should be callable as if it were an instance member of a separately defined type.

(See also @staticExtension decorator which allows calling a function as if it were a static member of a separately defined type)

For example, the String type does not have an isPalindrome method, but languages like C# and Kotlin allow you to define extension methods: functions that you can import and call as if they were methods of the extended type.

@extension("isPalindrome")
let stringIsPalindrome(s: String): Boolean {
  var i = String.begin;
  var j = s.end;
  while (i < j) {
    j = s.prev(j);
    if (s[i] != s[j]) { return false }
    i = s.next(i);
  } orelse panic();
  return true
}

// Equivalent calls
"step on no pets".isPalindrome() &&
stringIsPalindrome("step on no pets")
// ✅

The quoted string "isPalindrome" above specifies that the name for the function when used via method call syntax, but an extension function may still be called via its regular name: stringIsPalindrome above.

Note that the extension function must have at least one required, positional parameter and that parameter is the this argument, the subject when called via subject.methodName(other, args) syntax.

When translated to target languages that allow for extensions, these functions are presented as extensions using the member name.

In other target languages, they translate as if the \@extension decorator were ignored.

@inlineUnrealizedGoal decorator

@inlineUnrealizedGoal may decorate a function parameter declaration.

An unrealized goal is a jump like a break statement, continue statement, or return that crosses from a block lambda into the containing function.

let f(ls: List<Int>): Boolean {
  ls.forEach { (x);; // Here's a block lambda
    if (x == 2) {
      console.log("Found 2!");
      // This `return` wants to exit `f`
      // but is in a different function.
      return true; // UNREALIZED
    }
  };
  false
}
f([2]) //!outputs "Found 2!"
// ✅

Inlining a call is the act of taking the called function's body and ensuring that names and parameters have the same meaning as if it was called.

Inlining the call to .forEach above while also inlining uses of the block lambda into f's body allows connecting unrealized goals to f's body.

It does come with limitations:

  • @inlineUnrealizedGoal applies to parameters with function type.
  • The containing method or function, hereafter the "callee", must not use any @private visibility APIs so that uses of them can be moved.
  • The callee must not be an overridable method.
  • The callee must call any decorated parameter at one lexical call site. Inlining a function multiple time can lead to explosions in code size.
  • The callee must not use any decorated parameter as an r-value; it may not delegate calling the block lambda.
  • For a decorated parameter to be inlined, it must be a block lambda

@json decorator

The @json decorator applies to a type definition. It auto-generates JsonAdapter (see std/json) implementations to make it easy to encode instances of the type to and from JSON.

There are a number of strategies used to generate encoders and decoders outlined below.

The concrete class strategy

A class type's default encoded form is just a JSON object with a JSON property per backed field. IntPoint below encode to JSON like {"x":1,"y":2} because its x and y properties' values encode to JSON numbers.

let {
  InterchangeContext,
  NullInterchangeContext,
  JsonTextProducer,
  parseJson,
} = import("std/json");

// Define a simple class with JSON interop via `@json`
@json class IntPoint(
  public x: Int,
  public y: Int,
) {}

// A builder for a JSON string.
let jsonTextProducer = new JsonTextProducer();
// Encode a point
IntPoint.jsonAdapter().encodeToJson(
  new IntPoint(1, 2),
  jsonTextProducer
);
//!outputs "{\"x\":1,\"y\":2}"
console.log(jsonTextProducer.toJsonString());

// Decode a point
let jsonSyntaxTree = parseJson("{\"x\":3,\"y\":4}");
let p = IntPoint.jsonAdapter()
    .decodeFromJson(jsonSyntaxTree, NullInterchangeContext.instance);

console.log("x is ${p.x.toString()}, y is ${p.y.toString()}");
//!outputs "x is 3, y is 4"
// ✅

The custom JSON adapter strategy

If a type, whether it's a class or interface type, already has encoding and decoding functions then none are auto-generated.

let {
  InterchangeContext,
  JsonInt,
  JsonNumeric,
  JsonProducer,
  JsonSyntaxTree,
  JsonTextProducer,
} = import("std/json");

@json class IntWrapper(
  public i: Int,
) {
  public encodeToJson(p: JsonProducer): Void {
    p.intValue(i);
  }

  public static decodeFromJson(t: JsonSyntaxTree, ic: InterchangeContext): IntWrapper | Bubble {
    new IntWrapper(t.as<JsonNumeric>().asInt())
  }
}

let p = new JsonTextProducer();
// IntWrapper got a static jsonAdapter() method but not encoders and decoders.
IntWrapper.jsonAdapter().encodeToJson(new IntWrapper(123), p);

"123" == p.toJsonString()
// ✅

Sealed interface strategy

For sealed interfaces, we generate adapters that delegate to adapters for the appropriate sub-type.

let {
  JsonTextProducer,
  listJsonAdapter,
} = import("std/json");

@json sealed interface Animal {}

@json class Cat(
  public meowCount: Int,
) extends Animal {}

@json class Dog(
  public hydrantsSniffed: Int,
) extends Animal {}

let ls: List<Animal> = [new Cat(11), new Dog(111)];

let p = new JsonTextProducer();
List.jsonAdapter(Animal.jsonAdapter()).encodeToJson(ls, p);
p.toJsonString() == "[{\"meowCount\":11},{\"hydrantsSniffed\":111}]"
// ✅

Adapting JSON for generic types

If a type is generic, its jsonAdapter static method might require an adapter for its type arguments.

let {
  JsonAdapter,
  JsonTextProducer,
  listJsonAdapter,
  intJsonAdapter,
} = import("std/json");

let intListAdapter: JsonAdapter<List<Int>> =
    List.jsonAdapter(Int.jsonAdapter());

let p = new JsonTextProducer();
intListAdapter.encodeToJson([123], p);

"[123]" == p.toJsonString()
// ✅

@mayDowncastTo(true) on a type definition indicates that the type's values must be distinguishable via runtime type information (RTTI) from other types' values, as when casting down from a less specific type to a more specific type.

It is safe to use is or as with distinguishable types.

@mayDowncastTo(false) indicates the opposite.

If a type declaration has neither, then the following rules are used to decide whether a type is safe to use.

  • If a type is a direct subtype of a sealed type, and there is a path from the static type of the expression to that type via sealed types, then it is distinguishable.
  • Otherwise, if the type is connected, it is assumed not distinguishable. Backends may connect multiple Temper types to the same backend type. For example, JavaScript and Lua backends connect both Int and Float64 to their builtin number type. Perl and PHP blur the distinction between numeric, boolean, and string types.
  • Otherwise, it is assumed distinguishable.

@noProperty decorator

The @noProperty decorator may apply to constructor inputs that appear inside the parenthetical part of a [builtin/class] declaration to indicate that the constructor input does not correspond to a backed property.

class C(
  // constructor arguments here
  @noProperty let x: Int,
) {
  // Non constructor properties here must be initialized.
  // Their initializers may reference any constructor input including @noProperty inputs.
  public y: Int = x + 1;
}

// This works.
let c = { x: 1 };
console.log(c.y.toString()); //!outputs "2"
// ✅

@private visibility

@private decorates type declaration members. It indicates that the member is an implementation detail, meant for internal use. Private members entail no backwards compatibility commitments.

@private is a legacy decorator so the @ is optional.

A private member may be used from within its class definition via this..

class C {
  private s: String = "Hello, World!";
  public say(): Void { console.log(this.s); }
}

new C().say() //!outputs "Hello, World!"
// ✅

But a private member may not be read from outside the class.

class C {
  private s: String = "Hello, World!";
}
console.log(new C().s); // ERROR
// ❌ Expected function type, but got Invalid!, Member s defined in C not publicly accessible!

See also snippet/builtin/@public

TODO: write me. Visibility of type members

TODO: write me. Visibility of type members

See also @private visibility

sealed type modifier

Marks an interface type as sealed; only types declared in the same source file may extend it.

Sealed types are important because the Temper translator can assume there are no direct subtypes that it does not know about.

For example, when matching a sealed type value, if there is an is clause for each known subtype, there is no need for an else clause.

Backwards compatibility note

If you add a sub-type to an existing sealed type, this may break code that uses the old version of the sealed type with a when or which otherwise embeds an "is none of those subtypes, therefore is this subtype" assumption.

Backends should translate user-defined sealed types to sum types where available, and may insert tagged union tag field where needed.

@static decorator

@static may decorate a type members, and indicates that the member is associated with the containing type, not with any instance of that type.

It is a legacy decorator, so the static keyword is shorthand for the @static decorator.

Static members are accessed via the type name, dot, member name.

class C {
  public static let foo = "foo";
}
C.foo == "foo"
// ✅

Unlike in Java, static members are not accessible from an instance.

class C {
  public static let foo = "foo";
}
(new C()).foo == "foo"  // .foo is not declared on an instance of C
// ❌ Expected function type, but got Invalid!, Member foo defined in C incompatible with usage!

@staticExtension decorator

The \@staticExtension decorator applies to a function declaration and allows calling it as if it were a static method of a separately defined type.

Class and interface types may be extended with @static decorator methods, by passing a type expression as the second argument to \@extension.

Float64 contains no static member tau, but when the declaration of float64Tau below is in scope, it can be accessed using Type.member syntax.

@staticExtension(Float64, "tau")
let float64Tau(): Float64 { Float64.pi * 2.0 }

// That function can be called with static method syntax and
// regular function call syntax.
Float64.tau() == float64Tau()
// ✅

Types may only be extended with static methods in this way, not properties. Note that parentheses are required after Float64.tau() but not after Float64.pi.

As with instance extensions, the Type.method() syntax only works in scopes that include an import of the extension function or its original definition.

@test decorator

@test may decorate a function, indicating the function is a test. The intention is that this not be used directly but through a test macro that builds an appropriate function.

@var decorator

@var may decorate a let declaration. It is a legacy decorator.

By default, names declared by let may be assigned once per entry into their containing scope, but @var allows re-assigning values after initialization.

Contrast @var with @const. See examples of how var contrasts with let and const.

Square brackets are used to access an element of a list, map, or other container.

Abbreviated syntax container[key] is shorthand for container.get[key].

let ls = ["zero", "one"];
console.log("[0] -> ${ls[0]}, [1] -> ${ls[1]}");         //!outputs "[0] -> zero, [1] -> one"
console.log("[0] -> ${ls.get(0)}, [1] -> ${ls.get(1)}"); //!outputs "[0] -> zero, [1] -> one"
// ✅

This syntax may also appear to the left of =, as in container[key] = newValue. That's shorthand for container.set(key, newValue).

let ls = ["zero", "one"].toListBuilder();
console.log("[0] -> ${ls[0]}, [1] -> ${ls[1]}");         //!outputs "[0] -> zero, [1] -> one"
ls[1] = "ONE";
console.log("[0] -> ${ls[0]}, [1] -> ${ls[1]}");         //!outputs "[0] -> zero, [1] -> ONE"
ls.set(0, "Zero");
console.log("[0] -> ${ls[0]}, [1] -> ${ls[1]}");         //!outputs "[0] -> Zero, [1] -> ONE"
// ✅

||

a || b performs a short-circuiting logical-or of its arguments as in C. By short-circuiting, we mean that if a is true, then b is never evaluated, so you may assume that a is false when writing b.

a b a || b
false false false
false true true
true false true
true true true
let isTwiceIsh(numerator: Int, denominator: Int): Void {
  // Here, panic asserts that the division will never fail.
  if (denominator == 0 || numerator / denominator == 2 orelse panic()) {
    console.log("yes");
  } else {
    console.log("no");
  }
}
isTwiceIsh(4, 2); //!outputs "yes"
isTwiceIsh(0, 0); //!outputs "yes"
isTwiceIsh(3, 1); //!outputs "no"
// -ish might be doing a lot of work.
// ✅

Null-chaining ?.

Infix ?. considers the right-hand side only when the left side is non-null. It otherwise evaluates to null.

let maybeLength(a: String | Null): Int | Null {
  // Because of non-null inference, `a.end` is ok here.
  // Inferred non-null on the right side works only for simple name references,
  // not for nested expressions on the left side of `?.`.
  a?.countBetween(String.begin, a.end)
}
maybeLength("ab") == 2 &&
maybeLength(null) == null
// ✅

as

The generic as method allows safe type-casting.

x.as<Type>() means:

class StringBox(public s: String) {}

let castToString(x: StringBox | Null): StringBox | Bubble {
  x.as<StringBox>()
}
for (let x: StringBox | Null of [new StringBox("a string"), null]) {
  console.log(x.as<StringBox>().s orelse "cast failed");
}
//!outputs "a string"
//!outputs "cast failed"
// ✅

is

The generic is method allows type-checking.

x.is<Type>() evaluates to true when x's type tag is compatible with Type.

class Foo {}
class Bar {}

let isAFoo(x: AnyValue): Boolean {
  x.is<Foo>()
}

console.log(isAFoo(new Foo()).toString());  //!outputs "true"
console.log(isAFoo(new Bar()).toString());  //!outputs "false"
// ✅

Keywords

Some words have special syntactic meanings in Temper, and so may not be used as identifiers.

ReservedWord := "nym" | "builtins" | "this" | "super" nym builtins this super

There are fewer of these than in many other languages, as many constructs are provided via Builtin Functions and Constants. The Temper language will probably evolve to disallow masking certain builtins, effectively reserving those words from use as declaration names.

builtins keyword

It's a direct reference to a name in the module's context environment record.

This bypasses any in-module declaration of the same name.

class MyConsole extends Console {
  public log(str: String): Void {
    builtins.console.log("[ ${str} ]");
  }
}
let console = new MyConsole();

builtins.console.log("builtins.console"); //!outputs "builtins.console"
console.log("console");                   //!outputs "[ console ]"
// ✅

builtins must be followed by a .. Unlike globalThis in JavaScript, builtins is not an object; it's a special syntactic form.

let a = builtins;
// ❌ Expected a TopLevel here!

nym`...` quoted name

Used to escape names.

nym`...`

parses to the name with the unescaped text between the quotes so

nym`nym`

can be used to get the name with text "nym" through the parser.

This is meant to allow names in Temper thar directly correspond to names in other systems, including dash-case names that are widely used in databases and the web platform.

super keyword

super. is reserved syntax. We have yet to figure out what our goals are w.r.t. super calls and whether it makes sense to do a super call without specifying which interface's implementation to use given we have no plans to allow subtyping of class types.

this keyword

this is a way to refer to the enclosing instance. So inside a method in a class or interface definition, you can use this to refer to the instance on which the method was called.

class C {
  public isSame(x: C): Boolean { this == x }
}
let c = new C();
c.isSame(c)
// ✅

this cannot be used outside a type definition.

class C { /* Inside the type definition */ }
// Outside
this
// ❌ `this` may only appear inside a type definition!