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" 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 the Empty value.
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<Empty>): Void {
let generator: SafeGenerator<Empty> = 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<Empty> 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 (): Empty 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<Empty, 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¶
as¶
The as operator allows safe type-casting.
x as Type means:
- If
x's type tag is compatible withTypethen the result isx, - otherwise
bubble().
class StringBox(public s: String) {}
let castToString(x: StringBox?): StringBox throws Bubble {
x as StringBox
}
for (let x: StringBox? of [new StringBox("a string"), null]) {
console.log((x as StringBox).s orelse "cast failed");
}
//!outputs "a string"
//!outputs "cast failed"
// ✅
Note that as renames rather than casts in deconstructing
assignment context such as in
let { exportedName as localName } = import("...");.
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.
orelsewhich allows recovering from failure.
char¶
A string tag that requires a single code-point string and returns that code-point as an Int32.
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.
is¶
The is operator 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"
// ✅
panic()¶
A function that immediately panics, which is unrecoverable inside Temper. It might be recoverable in backend code.
Type operator throws¶
The infix throws operator can be used on a return type to indicate that a function
can fail in one of several ways.
A call that throws may be used within orelse.
let parseZero(s: String): Int throws Bubble {
if (s != "0") { bubble() }
return 0
}
//!outputs "parseZero(\"0\") = 0"
console.log("parseZero(\"0\") = ${ parseZero("0") }");
//!outputs "parseZero(\"hi\") orelse -1 = -1"
console.log("parseZero(\"hi\") orelse -1 = ${ parseZero("hi") orelse -1 }");
// ✅
!¶
The prefix ! operator performs Boolean inverse.
!false is true and vice versa.
!=¶
a != b is the Boolean inverse of ==
Remainder %¶
Given two Int32s 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 Int32s it acts as a bitwise operator.
- To types it produces an intersection type
Int &¶
Takes two Int32s and returns the Int that has any bit set that is set in both input.
// Using binary number syntax
(0b0010101 &
0b1011011) ==
0b0010001
// ✅
Intersection type bound &¶
When the & operator is applied to types instead of numbers, it constructs
an intersection type bound.
An intersection type bound specifies that the bounded type is a sub-type of each of its members.
So a value of a type that extends I & J declares a type can be assigned to a type I and can be assigned a declaration with type J.
See also snippet/type/relationships.
interface A {
a(): Void {}
}
interface B {
b(): Void {}
}
class C extends A & B {}
let c: C = new C();
let a: A = c;
let b: B = c;
let f<T extends A & B>(t: T): Void {
let a: A = t;
let b: B = t;
a.a();
b.b();
}
f<C>(c);
// ✅ null
Multiplication *¶
Infix * allows multiplying numbers.
Given two Int32s 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 Int32s: signed addition
- Prefix with one Int32: 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 Int32 and Float64 inputs:
1 + 1.0
// ❌ Actual arguments do not match signature: (Int32, Int32) -> Int32 expected [Int32, Int32], but got [Int32, Float64]!
+ does not work on Strings. Use cat instead.
"foo" + "bar"
// ❌ Actual arguments do not match signature: (Int32, Int32) -> Int32 expected [Int32, Int32], but got [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 Int32 and Float64 inputs:
1 + 1.0
// ❌ Actual arguments do not match signature: (Int32, Int32) -> Int32 expected [Int32, Int32], but got [Int32, 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 Int32s 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 <=> bis-1if a orders before ba <=> bis0if a orders with ba <=> bis1if 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.
Int32s 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.
Postfix ?¶
A question mark (?) after a type indicates that the type accepts the value null.
var firstEven: Int = null; // ILLEGAL
let ints = [1, 2, 3, 4];
for (let i of ints) {
if (0 == (i % 2)) {
firstEven = i;
break;
}
}
console.log(firstEven?.toString() ?? "so odd");
// ⏸️
But with a postfix question mark, that works.
// ⬇️
var firstEven: Int? = null; // OK
let ints = [1, 2, 3, 4];
for (let i of ints) {
if (0 == (i % 2)) {
firstEven = i;
break;
}
}
console.log(firstEven?.toString() ?? "so odd"); //!outputs "2"
// ✅
Operator |¶
The | operator performs bitwise union.
It takes two Int32s and returns the Int32 that has any bit set that is set in either input.
// Using binary number syntax
(0b0010101 |
0b1011011) ==
0b1011111
// ✅
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 is an alias for Int32, for convenience in indicating the
default int type.
Int32¶
The name Int32 refers to the builtin type, Int32.
Int64¶
The name Int64 refers to the builtin type, Int64.
List¶
The name List refers to the builtin type, List.
Listed¶
The name Listed refers to the builtin type, Listed.
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
// ❌ No member toString in MissingType!, 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 xin 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 Int32!
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 xdefines 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 xis equivalent to@const let x.
const x = "initial-value";
x == "initial-value"
// ✅
var xis equivalent to@var let xand 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
Null-coalescing ??¶
Infix ?? allows choosing a default value when the subject is null.
let prod(i: Int, j: Int?): 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);
}
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.
@fun decorator¶
Marks an interface declaration as a Functional interface.
For example:
@fun interface Predicate<T>(x: T): Boolean;
// ⏸️
@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:
@inlineUnrealizedGoalapplies to parameters with function type.- The containing method or function, hereafter the "callee", must not use any
@privatevisibility 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.int32Value(i);
}
public static decodeFromJson(t: JsonSyntaxTree, ic: InterchangeContext): IntWrapper throws Bubble {
new IntWrapper((t as JsonNumeric).asInt32())
}
}
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,
int32JsonAdapter,
} = import("std/json");
let intListAdapter: JsonAdapter<List<Int>> =
List.jsonAdapter(Int.jsonAdapter());
let p = new JsonTextProducer();
intListAdapter.encodeToJson([123], p);
"[123]" == p.toJsonString()
// ✅
Solving Ambiguity with extra properties¶
Sometimes, ambiguity is unavoidable in the JSON structure of two related types, like two subtypes of the same sealed interface.
@jsonExtra decorator allows adding extra JSON properties
with known values that do not correspond to a class constructor
parameter but which the sealed interface strategy can use.
@jsonExtra decorator¶
Allows customizing code generation for @json decorator support.
This allows avoiding ambiguity when two variants of a sealed interface have otherwise similar JSON structure in their JSON wire formats.
let {
InterchangeContext,
NullInterchangeContext,
parseJson,
} = import("std/json");
@json
sealed interface FooRequest {}
// Without any disambiguation both variants of FooRequest
// would have a wire form like
// { "name": "..." }
// Foo requests are tagged with a version: `"v": 2.0`.
@json @jsonExtra("v", 2.0)
class FooRequestVersion2(
public name: String,
) extends FooRequest {}
// But servers still need to support a deprecated, legacy format
// tagged with `"v": 1.0`
@json @jsonExtra("v", 1.0)
class FooRequestVersion1(
public name: String,
) extends FooRequest {}
// Neither class has a constructor input named "v", but it's
// required in the JSON because of the @jsonExtra decorations.
let jsonSyntaxTreeV1 = parseJson(
"""
"{"v": 1.0, "name": "a"}
);
let jsonSyntaxTreeV2 = parseJson(
"""
"{"v": 2.0, "name": "a"}
);
FooRequest.jsonAdapter()
.decodeFromJson(jsonSyntaxTreeV1, NullInterchangeContext.instance)
is FooRequestVersion1
&&
FooRequest.jsonAdapter()
.decodeFromJson(jsonSyntaxTreeV2, NullInterchangeContext.instance)
is FooRequestVersion2
// ✅
@jsonName decorator¶
Specifies the JSON property name for a Temper property name.
Without the @jsonName decoration below, the JSON form would be
{"messageText":"Hello, World!"} but as seen below, the JSON
output has dash case.
@json
class Message(
@jsonName("message-text")
public messageText: String
) {}
let { JsonTextProducer } = import("std/json");
let jsonOut = new JsonTextProducer();
Message.jsonAdapter()
.encodeToJson(new Message("Hello, World!"), jsonOut);
//!outputs "{\"message-text\":\"Hello, World!\"}"
console.log(jsonOut.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 Int32 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
// ❌ 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
// ❌ 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?): Int? {
// 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
// ✅
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!