Version 0.5.0#
- 🚨Breaking change: Infix
asandisoperators - C# backend uses Optional type for nullable type parameters
- Modules named using directory paths
- Empty type
- 🚨Breaking change: Numeric indexing operations panic
- be-java: Doc comments turned into javadoc.
- @jsonExtra decorator
- 🚨Breaking change: Named arguments removed
- Removed hook operator:
(condition ? consequent : alternate)⚠️ - 🚨Breaking change: Require Node 18 and Python 3.11
- Abbreviated nullable type syntax: Type?
- Rust backend enabled by default
- 🚨Breaking change: StringBuilder moved to core
- Autodoc comments extracted from Markdown
🚨Breaking change: Infix as and is operators#
Previously, casting and runtime type checks looked like this in Temper:
inertlet something = a.as<Something>();
if (another.is<Something>()) {
// do other things
}
Now, they look like this:
inertlet something = a as Something;
if (another is Something) {
// do other things
}
Note that infix as also still means rename in destructuring contexts:
inertlet { exportedName as localName } = import("...");
let { a as b } = c();
C# backend uses Optional type for nullable type parameters#
The C# backend (-b csharp) had a bug where the translated output
produced compiler errors when a type parameter was used in a nullable
context.
class C<T>(
public x: T?,
) {
public f(y: T?): T? {
let z: T? = null;
if (y != null) {
z = y;
}
if (x != null) {
z = x;
}
z
}
}
do {
console.log(new C<Int>(null).f(123).toString()); //!outputs 123
}
That class uses a type parameter <T>.
Unfortunately, Temper T? (formerly T | Null) cannot be translated to C#
T? because the suffix ? was added late to the language to express the idea
that a type might be null.
- When the modified type is a reference type like
object, the underlying type can store a null value so it has no semantic impact outside of additional type checks. - When the modified type is a value type like
intit translates toNullable<int>, a builtin value type that allows distinguishing0fromnull. - When the modified type is
T?onlywhere Tconstraints could guide the compiler to one of those behaviours but our type varaibles need to allow unbounded Temper type variables to bind to both C# value types (as TemperIntdoes) and C# reference types (as TemperStringdoes).
Without these changes, we generated C# like the following:
T? z__0 = null;
That fails to compile.
Compilation error (...): Cannot convert null to type parameter 'T' because it could be a non-nullable value type. Consider using 'default(T)' instead.
Now, our C# temper-core support library contains an Optional type that is API compatible with DotNext's version which hopefully we can transition to as supported versions allow.
For example, instead of T? z__0 = null; the translated output includes:
Optional<T> z__0 = Optional.None<T>();
Visible changes include:
- No changes for non-generic classes or interfaces
- When a property has a type that is a type parameter or null, like
T?, the translated C# property will store anOptional<T>. - When a method takes an input with such a type or returns such a type, the
translated method's input or output will have type
Optional<T>. - When a method overrides a method that does, the override will too.
With this change the above Temper code translates to C# like the below.
Note the use of Optional<T> instead of T? and the inserted calls to wrap values as optionals and to unwrap them back into, in the case where T binds to int, a Nullable<int>.
class C<T>
{
public Optional<T> X;
// Auto-generated constructor
public C(Optional<T> x)
{
this.X = x;
}
public Optional<T> F(Optional<T> y)
{
Optional<T> z = Optional.None<T>();
if (y.HasValue)
{
z = y;
}
if (X.HasValue)
{
z = X;
}
return z;
}
}
// In the generated globals class
Console.WriteLine(
Optional.ToNullable<int>( // Unpack output
// Pack inputs
new C<int>(Optional.None<int>()).F(Optional.Of<int>(123))
)
.ToString()
)
Modules named using directory paths#
Release 0.2.0 introduced directory modules. This is now the only way to define modules which affects existing import statements, output file layout, and diagnostic messages
Now, temper build creates a module for each directory that contains
one or more *.temper or *.temper.md files. That module is named
based on the directory only both within the compiler and in the
metadata provided to backend translators.
Diagnostic methods change#
This affects module names in compiler messages. Previously you might have seen:
[my-lib//path/dir/file.temper]: Error message guff
Now, you'll see:
[my-lib//path/dir/]: Error message guff
Line numbered code snippets still include file paths.
Backend file paths change#
Backends have changed how they allocate output file names. Now, a Temper module with a single Temper source file does not have that file's base-name as part of the output file path.
Updating existing code#
Most Temper code should continue to compile without changes since
directory modules were enabled for temper build. There are a
few breaking changes.
Previously, import("std/regex.temper.md") worked. You could
import via a specifier string with a Temper extension on it.
That no longer works. Below is an example of the kind of changes needed:
let { StringBuilder } = import(
- "std/strings.temper.md"
+ "std/strings"
);
Valid module identifiers are now either relative paths starting with one of (., ..) or a fully-qualified specifier as below:
- A library name, like
std - Zero or more
/s followed by directory names specifying the relative path of the directory within the library. - No trailing slash.
The "no trailing slash" caveat applies to relative paths too.
Empty type#
The Void type may not bind to type parameters making it awkward to use with generic functions.
Many functional languages have a type, often called Unit, that is subtly different. Instead of meaning "no need for space on the stack for the result," it means "the result allows for no further operations."
Empty is such a type. It should translate to Unit on typed, functional language backends.
Void returning functions often translate the same as null returning functions in dynamic language backends, but empty()
Here's the problem with Void: the block's return type, Void cannot bind to <T> in Fn (): T.
inertlet callThenLog<T>(f: fn (): T): T {
let result = f();
console.log("I called f!");
result
}
callThenLog { (): Void;; // ERROR: Void cannot bind to T
console.log("Someone called me!");
}
With Empty instead, this works.
let callThenLog<T>(f: fn (): T): T {
let result = f();
console.log("I called f!");
result
}
callThenLog { (): Empty;; // OK
console.log("Someone called me!");
empty()
}
//!outputs "Someone called me!"
//!outputs "I called f!"
🚨Breaking change: Numeric indexing operations panic#
Bubbles require declaration or handling. They're best used in cases where they are either hard to predict or possibly expensive to check for in advance. For lists and other ordered collections, it's easy to know or to check when an item isn't available. For that reason, the follow methods now panic instead of bubble:
- Deque::removeFirst
- List::get
- ListBuilder::add
- ListBuilder::addAll
- ListBuilder::removeLast
- ListBuilder::set
- Listed::get
- Listed::reduce
- String::get
Allowing these to panic simplifies their usage. For example, the default
interpreter implementation of List::toListBuilder previously said this:
@connected("List::toListBuilder")
public toListBuilder(): ListBuilder<T> {
let result: ListBuilder<T> = new ListBuilder<T>();
result.addAll(this) orelse panic();
result
}
But orelse panic() is no longer needed for the addAll call:
@connected("List::toListBuilder")
public toListBuilder(): ListBuilder<T> {
let result: ListBuilder<T> = new ListBuilder<T>();
result.addAll(this);
result
}
Casts, operations on maps, and other similar operations still bubble.
be-java: Doc comments turned into javadoc.#
Previously the Java and Java8 backends did not include Javadoc
comments, so running javadoc would just describe the structure.
Now, Temper documenation strings are combined into Javadoc.
/** Says "Hello!" */
export let sayHello(/** to whom to say hello */name: String): Void {
console.log("Hello, ${name}!");
}
That will correspond to a Java method with javadoc like the below:
/**
* Says "Hello!"
*
* @param name
* to whom to say hello
*/
public static void sayHello(String name__0) {
...
}
Temper doc strings are markdown. This change does not include a
proper translation of markdown meta-characters into Javadoc concepts.
For example, backticked sections are not yet converted to javadoc
{@code ...} sections.
A proper structured translation of docs will have to wait for a future
release.
@jsonExtra decorator#
The new @jsonExtra decorator allows for decoding of sealed interfaces from JSON that would otherwise be ambiguous because two or more subtypes have the same expected JSON structure.
With the new decorator, a subtype can specify that it expects a JSON property to have a specific value. This property does not need to correspond to a constructor input or field of the Temper type.
let {
JsonTextProducer,
} = import("std/json");
@json @jsonExtra("jsonFormIsEmpty", false)
class Empty {}
let jsonTextProducer = new JsonTextProducer();
Empty.jsonAdapter().encodeToJson(new Empty(), jsonTextProducer);
jsonTextProducer.toJsonString() == """
{"jsonFormIsEmpty":false}
"""
And this comes in handy when disambiguating sealed interface subtypes.
let {
InterchangeContext,
NullInterchangeContext,
parseJson,
} = import("std/json");
// A sealed interface with two subtypes: Foo and Bar.
@json
sealed interface Sup {}
@json @jsonExtra("type", "Foo")
class Foo(public i: Int) extends Sup {}
@json @jsonExtra("type", "Bar")
class Bar(public i: Int) extends Sup {}
// Decoding from JSON with the "type" parameter
// determines the variant.
let fooTree = parseJson("""
{"type": "Foo", "i": 1}
""");
let barTree = parseJson("""
{"type": "Bar", "i": -1}
""");
// We can ask Sup's adapter to decode the JSON above.
Sup.jsonAdapter()
.decodeFromJson(fooTree, NullInterchangeContext.instance)
.is<Foo>()
&&
Sup.jsonAdapter()
.decodeFromJson(barTree, NullInterchangeContext.instance)
.is<Bar>()
🚨Breaking change: Named arguments removed#
Because Temper needs to translate to usable libraries in other languages
without named arguments, having named args in Temper is deceptive. For example,
this looks nice in Temper (based on Temper's builtin Float64.near method):
// Given this function ...
let near(
x: Float64, y: Float64, relTol: Float64 = 1e-9, absTol: Float64 = 0.0
): builtins.Boolean { ... }
// ... here are some example calls that might be in other contexts.
// But such named args are now *illegal* in Temper.
near(x, y, absTol = 0.01)
near(x, y, relTol = 0.01)
But in C, JS, Java, and many other languages, named args are unavailable. This could be especially bad for functions or constructors with many parameters. We discussed a variety of elaborate translations to address these situations, but the simplest thing is just to remove named args entirely.
// You now instead have to provide positional arguments.
near(x, y, null, 0.01)
near(x, y, 0.01)
However, we do still have JS-style object literal syntax with named properties for constructing class instances. And we've started adding builders to target languages to enable proper user experience. For example, the above example could be refactored as follows:
// Given these definitions ...
class Tolerances(
public relTol: Float64 = 1e-9,
public absTol: Float64 = 0.0,
) {}
let near(x: Float64, y: Float64, tols: Tolerances) { ... }
// ... here are some example calls that might be in other contexts.
// Such calls are *still legal* in Temper.
near(x, y, { absTol: 0.01 })
near(x, y, { relTol: 0.01 })
Because Temper generates named-property builders in targets lacking builtin naming features, the expected user experience is still available. This pattern for semi-named-args also is already commonly used in languages such as JS. The cost of object instantiation exists, but the usability is sufficient, even for long lists of properties.
Removed hook operator: (condition ? consequent : alternate) ⚠️#
The C-style hook operator, also known as the ternary operator, has been removed from Temper.
Previously, condition ? consequent : alternate was equivalent to
if (condition) {
consequent
} else {
alternate
}
It had been deprecated in documentation.
Now, it is unrecognized syntax. Since if statements can appear as expressions, the
hook operator was unnecessary.
🚨Breaking change: Require Node 18 and Python 3.11#
To keep up with changing ecosystems, we have updated minimum required versions for some backends and related internal dependencies:
- Node updated from 16 to 18
- Python updated from 3.8 to 3.11
Abbreviated nullable type syntax: Type?#
A question mark, ?, after a type now marks the type nullable.
// The input type is just String.
let acceptsStringOnly(s: String): Void {
console.log("Got string: ${s}.");
}
acceptsStringOnly("a string"); //!outputs "Got string: a string."
// Can't pass the special null value to a non-nullable parameter
//// acceptsStringOnly(null);
// The string "null" is not the special value null.
acceptsStringOnly("null"); //!outputs "Got string: null."
// The empty string is not null either.
acceptsStringOnly(""); //!outputs "Got string: ."
// The input type has a postifx `?`.
let acceptsStringOrNull(s: String?): Void {
console.log("Got string or null: ${s}.");
}
acceptsStringOrNull("a string"); //!outputs "Got string or null: a string."
acceptsStringOrNull(null); //!outputs "Got string or null: null."
Rust backend enabled by default#
Temper 0.4.0 included a preview release of the Rust language backend. This
backend is now enabled by default when running temper build.
🚨Breaking change: StringBuilder moved to core#
You used to have to import StringBuilder before using it:
let { StringBuilder } = import("std/strings");
let builder = new StringBuilder();
But the above is no longer needed and is now illegal because StringBuilder is now part of the implicitly available core library. So now, that's just this:
let builder = new StringBuilder();
Autodoc comments extracted from Markdown#
Previously, Temper in Markdown allowed describing Temper declarations but that information was not stored as documentation-string metadata.
This is markdown prose. Explaining why this code is the way it is.
*fib* computes the n-th fibonacci number.
export let fib(
n should be >= 1.
n: Int
): Int {
...
}
There, markdown paragraphs are interleaved with indented, unfenced code sections.
There are two declarations there, fib, and its parameter, n.
Now, when parsing in markdown mode, Temper stores enough information
about Markdown paragraphs so that it can find that the second paragraph
starts with "fib" and lexically precedes the definition of fib
and similarly for the third paragraph and n.
Both of those declarations get metadata, available to backends, with the paragraphs following the one that starts with their name.
The specific rules are:
- If a declaration has no associated
/** ... */comment preceding it inside the fenced code, then Temper looks for paragraphs of markdown. - It only considers paragraphs that precede the declaration and which are not lexically separated by another declaration.
- The first eligible paragraph is the first considered that starts with the declared name, case insensitively, and after stripping markdown metacharacters: backtick (`) for code formatting and asterisk (*) for bold/italic styling.
- The first eligible and following paragraphs are joined together to make the doc strings with blank lines separating paragraphs.
Because of the name matching rule, the first paragraph is not included in fib's documentation.