Version 0.4.0#

🚨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 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:

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:

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:

Given these options, we decide when is the most practical name to lea pattern matching blocks.