Builtin Functions and Constants¶
You can use builtins in your Temper code without import
ing 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:
- To Ints it acts as a bitwise operator.
- To types it produces an intersection type
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 ba <=> b
is0
if a orders with ba <=> b
is1
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:
- To Ints it acts as a bitwise operator.
- To types it produces a union type
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:
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:
- A type name
- Optionally, some type formals
- Optionally, constructor parameters
- Optionally, some super types
- 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 |
---|---|
|
|
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:
- start with the library name
- followed by a separator:
/
- 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 namex
. 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 return
s¶
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:
- If
x
's type tag is compatible withType
then the result isx
, - otherwise
bubble()
.
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.
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!