Skip to content

Sprig v0.7.1-beta.1

Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 07 Oct 03:52
· 110 commits to main since this release
b7fe68e

Sprig v0.7.1-beta.1

Sprig v0.7.1-beta.1 is the second v0.7 Beta. It fixes what an evaluation
of v0.7.0-beta.1 found with real programs and Java baselines: for over
range no longer builds a list, top-level code with several hot loops runs as
fast as the same Java, a value that may be null no longer slips into text as
null, an Error shows its message wherever it becomes text, Strings gain
compareTo, and help and sprig capabilities now match what the compiler
does. Java calls also get easier: an Int goes into an int parameter with a
run-time range check, and wildcard types in Java signatures are read at their
bounds instead of being rejected. sprig api now shows the comment written
above each declaration. It is an
experimental Beta, not a promise of production stability or a frozen API. The
compiler is 0.7.1-beta.1; the language version remains 0.8-dev. JDK 17+
is required. Linux and macOS are release-supported; Windows remains an
experimental, non-blocking preview.

This is the first release under the release and versioning
policy
, which says what each of the two version numbers
means and when it changes. Under that policy this is a PATCH release, and the
language stays 0.8-dev: the new Java interop accepts programs that were
rejected before, the changes that reject code accepted before are soundness
fixes, and the one change to what a compiling program prints is a behavior
fix. The upgrade section lists all of them.

Fixes

for over range counts instead of building a list

for i in range(n) used to build the whole list before the first iteration:
30,000,000 iterations held about 1 GB, and a larger bound ran out of memory. A
for loop over range(...) is now a counted loop with constant memory. The
bounds are evaluated once, from left to right, a zero step fails with the same
Error as before, and each iteration has its own variable, so a lambda still
captures one value per iteration. range(...) used as a value is still a
MutableList[Int].

Top-level hot loops as fast as Java

A script that ran several hot loops in its top-level code got slower every
round: the same 200-million-iteration loop took 100, 210 and then 465 ms, while
the equivalent Java stayed near 100 ms. Every operand of a string join went
through a general formatter that the JIT inlines, and in one long top-level
block those copies used up HotSpot's inlining budget, so a later loop's checked
arithmetic stayed a real call. Strings, Int, Int32, Float, Float32 and
Bool now join with Java's own concatenation. The same benchmark runs in 100,
113 and 115 ms, and every kind of value still joins to the same text.

A value that may be null is not joined into text

let count = "x".toIntOrNull()
print("count " + count)   # v0.7.0 printed "count null"

Joining a T? into a String, with + or +=, is now SPR-TYPE-NULLABLE.
The hint shows the repair for the operand you wrote: check it with
if count != null:, copy a var into a let first, bind an expression such as
prices["tea"] to a let (only a let narrows), or give a fallback with
or_else from @std/nulls.spr. A toString() result from Java is now a
non-null String, as Object's contract says, so "at " + date.toString()
keeps compiling.

An Error shows its message everywhere

try:
    throw Error("disk full")
catch problem: Error:
    print(problem.toString())   # v0.7.0 printed "sprig.runtime.SprigError: disk full"

print(problem) and "failed: " + problem already showed the message, but
problem.toString() returned Java's text with the runtime's class name. It now
gives the message too, as toString() gives the printed text of every other
value. The same holds for the errors built on Error (BadRequest,
SqliteError) and for an Error caught as RuntimeException. A Java
exception keeps Java's text, class name first, whichever way it becomes text.

A Java exception's message is now a String?, because Java's getMessage()
may return null; "cannot read: " + problem.message printed cannot read: null for such an exception. An Error's message stays a String. Reading
message from the type itself (Error.message) passed sprig check and then
failed in javac; it is now SPR-JVM-MEMBER, and that message names the type
Error rather than SprigError.

String.compareTo for Java comparators

a.compareTo(b) returns -1, 0 or 1 in the order < uses for Strings, as
an Int32, which is the int a Java Comparator returns:

names.sort(fn(a: String, b: String) => a.compareTo(b))

A comparator lambda that returns an Int works as well; see Java interop
below.

== on classes is written down

== compares numbers, Bool, String, enums, variants, lists and maps by
value, and two class objects by identity: two objects whose fields match are
still two objects. That was already the behavior, but help language said
"== and != compare values" and nothing mentioned classes. The rule is now in
help language, help classes, help variants, the quick reference and the
language tour, and a runtime test keeps it. Classes keep identity on purpose:
a class can have var fields, so two objects equal now can differ after one
changes, a field-by-field == would walk whole object graphs (and loop on
cycles), and a map or set key compared by contents breaks when the key
changes. When you want a value compared by its contents, use a variant, or
compare the field that identifies the object, such as a.id == b.id.

Help and capabilities

  • sprig capabilities listed varargs and "arbitrary Java SAM" as unsupported
    while its JSON said jvmVarargs and jvmFunctionalInterfaces are true.
    The lists now name what works (Java varargs calls, Sprig function values as
    functional interfaces with up to three parameters, Comparable, rethrows)
    and the exact remaining limits, and a test keeps them consistent with the
    flags.
  • A nullable operand that isn't a plain name, such as m["k"], used to get
    the hint "check it first with 'if the value != null:'", which isn't code you
    can write. It now says to bind the expression to a let and shows the line.
  • String? + String? reports SPR-TYPE-NULLABLE instead of a numeric-mixing
    error.
  • m[key] += 1 on a map typed with a type parameter (MutableMap[K, Int])
    inside generic code passed sprig check and then failed in javac; it now
    runs.
  • toString() works on a Java value typed by an interface, such as the List
    that subList returns.

Java interop

Int into int

An Int now goes into an int or Integer Java parameter, constructor
argument, varargs element or field with a run-time range check, so computed
values no longer need .toInt32Exact(). A value outside the Int32 range
fails as the usual numeric error. A Sprig function passed as a Java callback
may return an Int where Java expects an int:

names.sort(fn(a: String, b: String) => a.length() - b.length())
let index = 2
print(names.get(index))

An exact long overload still wins (Math.max(a, b) keeps the long
version). Int into short or byte (literals only), Float into float
(precision, not range) and compound assignment to an int field stay
explicit.

Wildcards

Wildcard types in Java signatures are no longer rejected. A result reads at
its upper bound (a List<? extends Entity> gives Entity? elements), a
parameter accepts any value that fits the bound
(addAll(Collection<? extends E>), Class<?>), and a write through
? extends is rejected with SPR-JVM-MEMBER naming the capture. A
? super X slot takes X. Sprig itself still has no wildcard syntax: a
wildcard-typed value can be held and passed on, not written in a declaration.
A public Java field with a generic type is now read with its type arguments,
as sprig api already reported.

Tooling

sprig api module.spr --json includes doc, the # comment written directly
above each declaration and member, the same text editor hover shows. The text
output prints it under the signature as # ....

Upgrade from v0.7.0-beta.1

On a managed Linux/macOS installation, run sprig upgrade --check and then
sprig upgrade. The installer verifies the published checksum and archive
before switching SDKs; older installed versions remain available. A manual
installation can use the ZIP and .sha256 file from the
release page.

Then run sprig resolve in each project root. A lock records the compiler
that wrote it, so a lock from v0.7.0-beta.1 is reported as
SPR-PROJECT-LOCK-STALE.

Two changes can reject code that compiled before. Both are soundness fixes,
reported as SPR-TYPE-NULLABLE:

  • A T? value cannot be joined into a String. Each place is reported at
    the +. Check the value first, or give it a fallback:

    import "@std/nulls.spr" as nulls
    
    let count = "x".toIntOrNull()
    print("count " + nulls.or_else[Int](count, 0))
    

    A value returned by a Java method counts as possibly null, so
    "year " + LocalDate.of(2024, 1, 2) is rejected too: bind the result to a
    let, check it, then join it. A toString() result is a String and joins
    as before.

  • A Java exception's message is a String?. Join the exception itself
    ("IO error: " + problem, which shows Java's text with the class name), or
    check the message first. An Error's message is still a String.

One change makes a program that still compiles print something else. It is a
behavior fix, and no diagnostic reports it:

  • toString() on an Error returns its message, without the
    sprig.runtime.SprigError: prefix; likewise for BadRequest, SqliteError
    and an Error caught as RuntimeException. Search for .toString() on a
    caught error, such as "ERR:" + problem.toString(). Code that stripped the
    prefix can drop that step. Java code that turns an Error into text still
    sees the prefix.

A public Java field with a generic type is now read with its type arguments
instead of its raw class, which can change the type of such a read. Apart from
these, programs that compiled before and still compile behave as before.

Known limits

The v0.7.0-beta.1 limits still apply,
except that wildcards in Java signatures are now read at their bounds.
Recursive and intersection bounds (such as Collections.max), type-variable
varargs and generic arrays remain unsupported. Top-level statements run once,
as one JVM method that HotSpot compiles while it runs; a long script with
several hot loops shares that one compilation, so keep hot loops in
functions.

See docs/language/known-limitations.md, docs/language/feature-status.md,
and the versioned output of sprig capabilities --json before relying on a
particular feature. Every change since v0.7.0-beta.1 is listed in the
comparison.