Version 0.4.0#
- 🚨Breaking change: Compile-time checks for RTTI operators
- 🆕Quoted character syntax
char'a' - 🚨Breaking change: backed
classproperties now appear in parenthesized constructor - Control flow revamped in frontend
- Extension functions allow extensions to method call syntax
breakandcontinuework infor (… of …)loop bodies- 🚨Breaking change: StringSlice removed in favor of indexed Strings
- @json type declaration decorator
- Null-aware handling
- Default arguments requested by null
- Preview: Rust backend
- Sorting
- Support src dir
- 🆕std/strings's StringBuilder allows composing strings
- Preview: Name selection and "temper.keep/"
- 🚨Breaking change: visibility notations required on class members
- 🚨Breaking change: Use
whenfor pattern matching
🚨Breaking change: Compile-time checks for RTTI operators#
Previously, Temper had no checks as to whether a runtime-type information (RTTI) check could be subject to false positives.
Consider the following:
inertlet a = when (x) {
is String -> "String";
is Int -> "Int";
is Float64 -> "Float64";
else -> "Other";
};
let b = when (x) {
is Float64 -> "Float64";
is Int -> "Int";
is String -> "String";
else -> "Other";
};
a and b should be the same, per Temper semantics, because there is
no Temper value that is more than one of (Float64, Int, String).
But in many programming languages' runtimes, there isn't enough information to distinguish these values reliably:
- in JavaScript, the number type spans floating and integer types.
- in Perl and PHP, the same scalar type is used for all three; there are approximate predicates for making distinctions but these are not reliable, especially for values that enter from external target language code.
In Perl, for example, given my $x = 1; we might get
$a == "String" && $b == "Float64" though 1 looks integer-esque.
Now, Temper performs long planned restrictions on type casting and
checking.
Backends only need to be able to distinguish between and unconnected
types (those translated from Temper types) and those explicitly
marked with @mayDowncastTo(true).
Now, the above code leads to compile-time errors, but we preserve some RTTI distinctions.
Temper's long-term goal is for type matches (as above) and the
.is<Type>() RTTI predicate and .as<Type>() checked cast to allow
distinguishing variants of sealed types, and to work with unconnected,
Temper defined types, and to allow distinguishing (T | Null) from (T)
for all non-null T.
🆕Quoted character syntax char'a'#
The char string tag can be followed by exactly one quoted code-point
and resolves to that code-point's integer value.
It interoperates with code-point related: String.fromCodePoint, String.fromCodePoints, and StringBuilder.appendCodePoint, and String.codeAt.
console.log("A: ${ char'A'.toString() }");
// Logs "A: 65"
🚨Breaking change: backed class properties now appear in parenthesized constructor#
Previously, any backed properties declared in a class would become
constructor parameters. And a property with an = expression
corresponds to an optional constructor parameter with a default
expression.
class C {
public let a: Int;
public let b: Int;
public let sum: Int = a + b;
}
let c = {
a: 1,
b: 2,
sum: 4,
};
console.log(c.sum.toString());
This made it hard to specify properties that were not overridable by explicit constructor parameters.
Now, a class declaration can include parentheses and formal
parameters there specify both constructor parameters and backed
properties.
class C(
public let a: Int, // comma separates
public let b: Int,
) {
public let sum: Int = a + b;
}
let c = {
a: 1,
b: 2,
// sum not allowed here
};
console.log(c.sum.toString());
A new @noProperty decorator allows specifying a constructor parameter
that does not have a backed property auto-created.
// Accepts a list or a mutable list builder but stores a list.
class SomeStrings(
@noProperty strings: Listed<String>
) {
public strings: List<String> = strings.toList();
}
let myListBuilder = new ListBuilder<String>();
myListBuilder.add("Hello");
let ss = { strings: myListBuilder };
console.log(ss.strings.is<List<String>>().toString()); //!outputs true
Control flow revamped in frontend#
A large scale reorganization of the Temper frontend changes the intermediate representation of control flow. This change is aimed at enabling feature work but there are a number of user visible changes.
Capture of local variables in loop bodies#
let lb = new ListBuilder<fn (): Int>();
for (var i = 0; i < 10; ++i) {
// Locals declared in loop bodies are captured separately per iteration.
let j = i + 1;
lb.add(fn (): Int { j });
}
let ls = lb.toList();
ls.forEach { (element: fn (): Int);;
console.log(element().toString(10));
}
Previously, running that in temper repl output 10 10 10 10 ... 10 but
now, that outputs 1 2 3 4 ... 10.
Explicit return bubble() is equivalent to implicit return of bubble()#
Previously, these two functions translated subtly differently.
// Explicit return
let f1(): Bubble { return bubble() }
// Implicit return
let f2(): Bubble { bubble() }
The bubble function produces no usable value and, when it appears inside the left of an orelse, jumps to the right.
Now, bubble is consistently recognized as ending a branch of control flow without a need to capture a result.
This makes it easier to write backends for languages with strict
"no dead code after throw" policies, and leads to more idiomatic
code in existing backends.
Fewered labeled break statements in generated code#
Previously, our intermediate representation required decompiling
a control-flow graph to recover structured flow control
(if, while, break, continue).
The decompiler introduced some labeled blocks in situations where they were not strictly necessary: nested loops with complex interactions.
This should lead to more idiomatic translation on many backends and more performant translation on un-optimized Python, a language which does not support labeled blocks.
Extension functions allow extensions to method call syntax#
Previously subject.verb(object) syntax worked only when subject's
type declared a method named verb.
Now, regular functions can be declared with the @extension decorator
and participate in the same syntax.
class C {
public isExtension(): Boolean { false }
}
@extension("isExtension")
let otherIsExtension(
s: String // The first argument is the subject
): Boolean { true }
!(new C().isExtension()) && // Regular method call
"Hello".isExtension() // Invoking a regular function with dot syntax
Similarly, @staticExtension allows extending
SubjectType.verb(object) syntax. The @staticExtension decorator
takes a subject type and the verb text.
@staticExtension(Int, "three")
let intThree(): Int { 3 }
Int.three() == 3
See the documentation for @extension and @staticExtension
for more details.
break and continue work in for (… of …) loop bodies#
for (let x of subject) { body } is syntactic sugar for
subject.forEach { (x);; body }.
Normally, break and continue statements have to appear directly
within the same function as the loop.
Since .forEach is a method, it's body is defined on subject's type.
Now, .forEach method implementations can opt into inlining.
When they do not require any private implementation details of the
type, their bodies can be inlined allowing break and continue to
appear to Temper backends as part of the same function body.
Previously the below would have been an error, but now it logs the even numbers from the list.
for (let i of [0, 1, 2, 3, 4, 5, 6]) {
if ((i & 1) == 1) {
continue
}
console.log(i.toString())
}
🚨Breaking change: StringSlice removed in favor of indexed Strings#
Previously, Temper provided multiple StringSlices types for string processing tasks.
Those have been removed, and StringIndex allows for string processing.
StringIndex connects to Int or size_t types in target languages, but the precise relationship between the integral value depends on the target language's "native" string encoding.
let s = "Blah blah Hello, World!";
// The below logs "Hello, World!"
var idx = String.begin; // An index at the start
while (idx < s.end) { // Still in the string
if (s[idx] == char'H') {
console.log(s.slice(idx, s.end));
break;
}
idx = s.next(idx); // Step forward one code-point
}
@json type declaration decorator#
Applying @json to a type declaration adds instructions for encoding/decoding instances of the type to/from JSON.
// JSON definitions are from std/json
let {
InterchangeContext,
NullInterchangeContext,
JsonTextProducer,
parseJson,
} = import("std/json");
// @json applies to class and interface declarations
@json class Point {
public x: Int;
public y: Int;
}
let myPoint = new Point(1, 2);
// JSON adapters allow encoding
let myJsonTextProducer = new JsonTextProducer();
Point.jsonAdapter().encodeToJson(
myPoint,
jsonTextProducer
);
console.log(myJsonTextProducer.toJsonString());
//!outputs "{\"x\":1,\"y\":2}"
// They also allow decoding
let anotherPoint = Point.jsonAdapter().decodeFromJson(
"{\"x\":3,\"y\":4}",
NullInterchangeContext.instance
);
Null-aware handling#
Temper now has limited flow typing for null-checked branches. For example, this is now legal:
let maybeReportNegative(a: Int | Null): Void {
// So far, support is limited to a simple check against a single name.
if (a != null) {
// We know *a* isn't null here, so we can use it as type Int.
console.log((-a).toString());
} else {
console.log("not an int");
}
}
Further, similar to operators in JS and other languages, Temper now has operators for explicitly managing null values. Here are examples:
let maybeLength(a: String | Null): Int | Null {
// Because of non-null inference on simple names, `a.end` is ok here.
a?.countBetween(String.begin, a.end)
}
let chainMore(a: String | Null): String {
(maybeLength(a)?.max(0) ?? -1).toString()
}
Specifically, these operators have been added:
?.null chaining, where the right-hand side is evaluated only when the left side is non-null. If the left is null, the resulting value is also null.??null coalescing, where the right-hand side is the alternate value provided when the left side is null. Some languages use the token?:rather than??for similar semantics.
Default arguments requested by null#
To simplify optional arguments, Temper now indicates excluded arguments by passing in a null value. This is carried through to all backends. Optional parameters are always nullable, and if a null argument is supplied, the default value is provided internally. For example, for this function:
let intName(i: Int, name: String = i.toString()): Pair<Int, String> {
new Pair(i, name)
}
Here are examples from the repl:
$ intName(1, "one")
interactive#10: {class: Pair__32, key: 1, value: "one"}
$ intName(1)
interactive#11: {class: Pair__32, key: 1, value: "1"}
$ intName(1, null)
interactive#12: {class: Pair__32, key: 1, value: "1"}
And here's an example translation to Java, including a generated overload:
public static Entry<Integer, String> intName(int i__1, @Nullable String name__4) {
String t_19;
String name__2;
if (name__4 == null) {
t_19 = Integer.toString(i__1);
name__2 = t_19;
} else {
name__2 = name__4;
}
return new SimpleImmutableEntry<>(i__1, name__2);
}
public static Entry<Integer, String> intName(int i__1) {
return intName(i__1, null);
}
Or to Python:
def int_name(i: 'int', name: 'Union[str, None]' = None) -> 'Pair[int, str]':
_name: 'Union[str, None]' = name
t: 'str'
name: 'str'
if _name is None:
t = int_to_string(i)
name = t
else:
name = _name
return pair(i, name)
Prior to this change, unset arguments had specialized semantics within the interpreter and inconsistent handling across backends.
Preview: Rust backend#
An incomplete Rust backend is available. You can manually build to Rust with
temper build -b rust, but it's not included in default build targets and still
has a number of bugs and unfinished features.
Sorting#
Both in-place and non-destructive sorting are now built in. Sorting is also stable. That is, order for values considered equal by the comparer remain unchanged. Here is an example:
class Entry(
public id: Int,
public group: Int,
) {
public toString(): String {
"{ id: ${id.toString()}, group: ${group.toString()} }"
}
}
let entries = [
{ id: 0, group: 0 },
{ id: 1, group: 1 },
{ id: 2, group: 2 },
{ id: 3, group: 0 },
{ id: 4, group: 1 },
{ id: 5, group: 2 },
].toListBuilder();
let sortedEntries = entries.sorted { (a, b);; a.group - b.group };
console.log(sortedEntries.join("\n") { (a);; a.toString() })
This is guaranteed to log the following message on all backends:
{ id: 0, group: 0 }
{ id: 3, group: 0 }
{ id: 1, group: 1 }
{ id: 4, group: 1 }
{ id: 2, group: 2 }
{ id: 5, group: 2 }
Support src dir#
Temper now recognizes a subdir called src in project structure if no config
is already in the current dir or higher. Creating a src dir is also now the
default behavior of temper init. For example:
~/whatever/blah$ temper init
Initialized new Temper project "blah" in /home/me/whatever/blah
~/whatever/blah$ find -type f
./src/blah.temper.md
./src/config.temper.md
~/whatever/blah$ temper build
~/whatever/blah$ ls temper.out/js/blah
index.js index.js.map package.json
🆕std/strings's StringBuilder allows composing strings#
The StringBuilder type connects to the standard library type of the same name in Java and C#, and connects to efficient left-to-right string composition idioms in dynamic language backends.
let { StringBuilder } = import("std/strings");
let sb = new StringBuilder();
sb.append("Hello, ");
sb.append("World");
sb.appendCodePoint(char'!');
// Logs "Hello, World!"
console.log(sb.toString());
Preview: Name selection and "temper.keep/"#
Each Temper backend has to handle name style conversion and also choosing names to address conflict with backend language rules. For example, switch is a keyword in some languages but not others. Name choices also might vary from build to build.
To stabilize name choices and provide user customization over name selection, we have begun saving name choices at build time. Some items of note:
- Names are saved in directory
temper.keep/as a placeholder for build-generated files that should be kept in source control in the future. - However, the data stored here is currently in preview, so it should be ignored until some future release.
- The
temper.keep/name, as well astemper.out/, might change its name or structure in future releases. - Temper doesn't yet load or use saved name choices at this time.
In other words, this is an experiment toward features in future releases.
🚨Breaking change: visibility notations required on class members#
Previously, if a class definition member was missing a notation, the
member defaulted to private.
class C {
f(): Int { 42 }
public g(): Int { f() }
}
Now, all class members require a visibility notation: one of public,
private, or protected.
class C {
- f(): Int { 42 }
+ private f(): Int { 42 }
public g(): Int { f() }
}
🚨Breaking change: Use when for pattern matching#
The builtin match has changed to when to reduce risk of collision with local
declarations called match, such as for instances of regex Match. Another
potential collision is for methods or functions named match. It was already
possible to work around collisions using builtins.match, but when seems much
less likely to collide with useful names in other code.
Old example with match:
inertlet description = match (2) {
0 -> "none";
1, 2, 3 -> "little";
else -> "lots or negative";
};
New example with when:
inertlet description = when (2) {
0 -> "none";
1, 2, 3 -> "little";
else -> "lots or negative";
};
Some options considered:
case- Used to lead pattern matching blocks in some languages but carries different expectations in C-based syntax.match- The existing name is consistent with the TC39 proposal and several existing languages, but it has the collision risk previously discussed. The TC39 proposal useswhenin place of common switchcase, but it's also just a proposal rather than an accepted feature.when- Already used in Kotlin in similar ways. Used in other languages for match guards, which we eventually want in Temper. The corresponding placeholder keyword for match guards in Temper is nowgiven.switch- Already a keyword in several common languages but possibly carries mental semantic baggage that are incompatible with Temper usage.
Given these options, we decide when is the most practical name to lea pattern
matching blocks.