Repository navigation
Sprig v0.8.0-beta.1
Pre-releaseSprig v0.8.0-beta.1
The release body is this file as of the tag. The latest published SDK is
v0.7.1-beta.1; the version number follows the
release and versioning policy: a new MINOR, because the
release requires JDK 21 and ships a large feature set.
Sprig v0.8.0-beta.1 is the release in which the language grows for the
first time since v0.7: if as an expression, type arguments worked out from a
call's arguments, contract classes for open polymorphism without a new keyword,
one-line classes, function references, error classes, and read-only views of
mutable collections. Java interop can extend a Java class, reads nullability
annotations, and infers a Java method's own type parameters. Concurrency moves
into the standard library as structured scopes over JDK 21 virtual threads, and
packages are published to the repository's registry through pull requests
validated by CI. The compiler is 0.8.0-beta.1; the language version remains
0.8-dev. JDK 21+ is required. Linux and macOS are release-supported;
Windows remains an experimental, non-blocking preview. It is an experimental
Beta, not a promise of production stability or a frozen API.
Language
if as an expression
if is an expression wherever an expression match is accepted: let and var
initializers, assignments, return, throw, lambda bodies and the branches of
other if or match expressions. Each branch is one expression on its own
indented line and else is required. At the start of a statement, if is
still the statement.
let size = if count > 9:
"big"
elif count > 0:
"some"
else:
"none"
Typing and narrowing are the expression match's and the if statement's: a
contextual expected type when there is one, a null branch makes the result
nullable, conditions are Bool, only the chosen branch is evaluated. A missing
else is one SPR-SYNTAX-ERROR that no longer stops the rest of the file from
being checked, so the editor keeps hover, definition and the outline while the
else is written. The a if c else b and c ? a : b spellings get targeted
messages.
Every elif and the else know the earlier conditions
In if statements and if expressions alike, each elif (its condition included)
and the else see every earlier condition false, and the statements after a
chain whose earlier branches all exit see them false too. A name declared T?
may be compared with null while it is narrowed. See the upgrade section:
two patterns that compiled before take a narrower type.
Type arguments inferred from a call's arguments
Calls of generic Sprig functions, constructors of generic classes and
constructors of generic variant cases with a payload infer the type arguments
they leave out, from their arguments only:
import "@std/lists.spr" as lists
let names: List[String] = ["Ada", "Grace", "Barbara"]
print(lists.sorted(names)) # T = String
print(lists.group_by(names, fn(n: String) => n.length())) # T = String, K = Int
Written [Type] arguments still win and are all or none. Nothing is inferred
from the expected type, the assignment target or the result: lists.first([]),
a payloadless case such as Option[Int].None and a parameter that no argument
mentions still write their arguments, and the SPR-TYPE-GENERIC-ARGS-REQUIRED
hint writes the call with the inferred arguments filled in. requires X: Comparable is checked on the inferred argument.
Contract classes
A class whose methods all have no body, and no fields, is a contract. A class
declares that it fulfils one with conform, and its objects then go wherever
the contract type is expected. There is no interface keyword and no
inheritance between Sprig classes.
class Sink:
func write(line: String) -> Unit
func flush() -> Int
class Console:
var written: Int = 0
func write(line: String) -> Unit:
written += 1
print(line)
func flush() -> Int:
return written
conform Console to Sink
func drain(lines: List[String], sink: Sink) -> Int:
lines.forEach(sink.write)
return sink.flush()
Every method of the contract is matched exactly (SPR-CONFORM-MEMBER), the
class throws no more than the contract declares (SPR-CONFORM-EFFECTS), a
contract in an imported module is named alias.Contract, and a contract cannot
be constructed, declare fields or mix methods with and without bodies
(SPR-CLASS-ABSTRACT). A contract value is never converted back to the class.
A generated contract is a Java interface, so Java code sees the conforming
class as implementing it.
Three rules are settled for the 0.8 language, each with a targeted
diagnostic that names the alternative: a contract is never generic
(SPR-CLASS-ABSTRACT; one contract per element type, or a generic class
holding the operation as a fn field), a contract is a type and never a
requires bound (SPR-GENERIC-CONSTRAINT; take the contract as the parameter
type), and a contract has no default methods (shared behavior is a module
function that takes the contract). There is no downcast and no type test on
class or contract values, conform is written in the module that declares
the class, and conformance is always declared. The decision guide: a closed
set of types is a variant; an open set is a contract.
One-line classes
class Pair(first: Int, second: Int) declares a class of immutable fields and
builds the same class as the block form: named construction, the same printed
text and identity ==. var fields, defaults, methods and empty parentheses
are not accepted in this form and get a targeted SPR-SYNTAX-ERROR pointing at
the block form. This is the answer to tuples: featureGuidance.tuples names
it.
Function references
A named function, a module function or a method used without a call is a
function value with the type of the lambda that forwards to it, throws clause
included:
func shout(word: String) -> String:
return word.toUpperCase()
print(words.map(shout))
print(lists.sum(numbers.map(parse)))
numbers.forEach(counter.add)
words.forEach(print)
A method reference evaluates its receiver once, when the value is created. A
generic function writes its type arguments (identity[Int],
SPR-TYPE-GENERIC-ARGS-REQUIRED otherwise). print is a value only where a
fn(T) -> Unit is expected, such as a forEach argument; let p = print is
SPR-TYPE-NOT-CALLABLE with the lambda to write. Java and built-in methods
(list.size, text.trim) stay calls: write the lambda. Function values are not
compared with == or != (SPR-TYPE-OPERAND).
Error classes
A class with a message: String field that conforms to Error is an error
type: it is thrown, declared in throws, caught by its own name or as Error,
and shows its message wherever it becomes text.
class ParseError:
let message: String
let line: Int
conform ParseError to Error(message)
func parse(text: String) -> Int throws ParseError:
...
A catch of such a class after a catch of Error is unreachable; a lambda that
throws one has throws Error, because only Error crosses a function value.
Read-only views of mutable collections
A MutableList[T] is accepted where a List[T] is expected, and a
MutableMap[K, V] where a Map[K, V] is, as the same collection seen
read-only: no copy, and later changes through the mutable name are visible
through the read-only one. The reverse is never accepted, and element types
stay invariant (MutableList[MutableList[Int]] is not a List[List[Int]]).
Small additions
compareToonInt,Int32,Float,Float32,DecimalandBigInt:
the argument has the receiver's type and the result is anInt32, theinta
JavaComparatorreturns.String.lastIndexOf.- An f-string (
f"total {n}") gets a hint that spells the+join;format,
linesand the other common Python, Java and JavaScript String method names
get a hint that names the Sprig method. - A list or map literal compared with
==/!=takes the other operand's
type (#166):xs == []needs no annotation, andxs == [1, 2]with
xs: List[Int32]comparesInt32elements. Both were type errors. When
both operands are literals, the one that has a type of its own gives it to
the other, on either side:[[]] == [[1]]compiles like[[1]] == [[]]
(#212). - Two cases of one variant can be compared (#166):
Shape.Circle(radius=1.0) == Shape.Square(side=1.0)isfalse; it was
SPR-TYPE-MISMATCH.
Java interop
- Extending a Java class.
conform C to J(field, ...) as NAMEmakes the
generated class extend a public, non-final, non-generic Java class: the named
fields select the superclass constructor and are passed assuper(...),
methods with the same shape override, andNAME.m(...)calls the inherited
implementation. The Fabric starter's item is written this way
(conform Wand to Item(properties) as parent). - Nullability annotations are read. A result or field annotated
NotNull/NonNull/Nonnullis a plainT; a parameter annotated
Nullable/CheckForNullaccepts aT?;NullMarked,NonNullApiand
MethodsReturnNonnullByDefaultmake unannotated results non-null, and
NullUnmarkedcancels the default; both are read on a method as well as on
a class, a package or a module, and the innermost one wins (a
@NullUnmarkedmethod inside a@NullMarkedclass keeps itsT?, so its
nullis checked instead of failing at run time). Annotations kept only in class files
(org.jetbrains.annotations, Android, Eclipse) are read from the class file.
Parameters are never made nullable by a default. - Java method type parameters are inferred when the arguments fix every one
of them exactly:Collections.sort(names)takesTfrom an
ArrayList[String]argument andJList.of(1, 2, 3)from its elements.
Through a lambda,null, a Sprig collection or the result they are not, and
Collectors.toList()still writes its argument. - A functional-interface formal bound by a type argument takes a
fn. A
parameter written as a type variable (AtomicReference[V].set(V), Fabric's
Event[T].register(T)) names the interface its type argument bound it to, so
let slot = AtomicReference[Runnable](); slot.set(fn() => print("ran"))
compiles and adapts like any other Java callback; the erasedObjectformal
hid it before. The same fix adapts the callback at code generation. --classpath-file FILEoncheck,build,run,api,wrap,
doctor,testandlsp; the Gradle plugin uses it, so a Loom classpath of
several hundred JARs no longer exceeds the Windows command-line limit.Fn0..Fn3slots whose type arguments are a Java method's own type
parameters are typed once those parameters are bound (start[Int](work)).
Standard library and libraries
@std/concurrent.spr: structured scopes over virtual threads
Concurrency is a bundled module, not a separate library. Every task runs inside
a scope, on a JDK 21 virtual thread:
import "@std/concurrent.spr" as concurrent
func fetch_all(urls: List[String]) -> List[String] throws Error:
return concurrent.scope(fn(tasks: concurrent.Scope) =>
concurrent.await_all(urls.map(fn(url: String) => concurrent.spawn(tasks, fn() => fetch(url)))))
scope returns when its body and every task spawned in it have finished; the
first failure cancels the sibling tasks and is rethrown from scope, so no task
outlives the function that started it. spawn_on(scope, pool, work) runs a task
on a fixed pool(threads) of platform threads for CPU-bound work;
parallel_map, map_in, await_all, channel[T](capacity), counter,
lock/locked, latch and sleep complete the module. There is no unscoped
spawn, no async/await syntax (featureGuidance.asyncAwait explains why),
no then on a task and no scope deadline. examples/parallel_words uses it.
More of @std, and faster JSON
regex.find_groups(pattern, text)returns the capture groups 1..n of the
first match, ornullwhen nothing matches; a group that took no part is
null, so the type isList[String?]?.regex.find_all_groupsreturns the
groups of every match, in order.lists.max/lists.min(Comparable items) andlists.max_by/lists.min_by
(Comparable key) returnnullfor an empty list. They use the order of
sorted, so a Float NaN is the largest value; of equal items or keys the
first wins.text.escape_htmlreplaces&,<,>,"and'with entities.json.parseno longer copies the rest of the document for every token
(#186), and parsing a string,quoteandstringifybuild their text in one
buffer instead of concatenating. An array of 100,000true/nullvalues
parses in about 4 ms instead of 430 ms (5 ms instead of 6.5 s when the text
holds a character outside Latin-1);stringifyof 100,000 numbers takes
23 ms instead of 2.3 s.sprig help modules,std/json.sprand the sprig-web README point from
@std/jsonto@std/json_codecfor reading fields with paths and kind
checks, andsprig help testing, the testing guide and tutorial §12.3 show
how to keepsrc/main.sprto argument handling so tests import the work
without running the program (#150).- Every declaration of
@stdand the bundled packages has a doc that
sprig apishows; helpers that callers should not use say so with a doc
that starts withInternal:(#210).
Web, HTTP, SQLite
sprig-web gains app.run_on(host, port) for binding an address other than
127.0.0.1, and PUT/PATCH routes; CORS covers both methods.
sprig-sqlite's Migrations.apply() checks the whole directory before it runs
anything: names, sequence numbers, order against the ledger, and that every
pending file can be read. A problem is an Error and no migration runs, and
the order error names both files (#208). A migration file may end with a
comment.
Package registry
Packages are published by pull request to registry/ in this repository. The
index is strict: names are [a-z][a-z0-9-]* (std and sprig reserved),
versions are SemVer, every release pins a tag and its commit, an entry carries
license and owners, a release is withdrawn with yanked/reason rather
than deleted, and a renamed package points at its successor with moved_to.
sprig publish writes the entry and names the pull request step; a version
that is already listed is an error. The Registry index workflow validates
every change to registry/ (schema, immutability of published releases, that
the tag's commit holds the manifest, and that the author is an owner), and
CODEOWNERS routes the review. sprig search and sprig add NAME read the index, pinned under ~/.sprig/registry for --offline.
There is still no hosted registry, no authentication and no upload.
Tooling
sprig build --bundlewrites a directory that runs the program on a
machine without a JDK: a POSIX sh and a Windows launcher,lib/with the
program's JAR, the Sprig runtime and every locked JAR, andruntime/, a
jlink image of the modules jdeps finds in those JARs, with the JDK's legal
notices kept.--archiveadds a ZIP. The launchers keep a class-data-sharing
archive of the program's classes under the user's cache directory, so a warm
start is the JVM's own start plus the program. The image runs only on the
platform that built it;SPR-BUNDLE-TOOLS,SPR-BUNDLE-JDEPSand
SPR-BUNDLE-LAYOUTname what a bundle needs. Seedocs/projects/bundle.md.- The test gate runs in parallel.
scripts/test.py --jobs N(default CPU
count capped at 8) andscripts/test-affected.py, which runs the gates a
change touches, fromtests/test-map.json. - Quick fixes in
sprig lsp. The suggested edits thatsprig check --json
carries (else iftoelif, positional constructor arguments to named ones,
a missing@stdimport, an undeclaredthrows,Int.toFloat()to
toFloatExact()) are offered asquickfixcode actions; the VS Code
extension shows them in the lightbulb. - Editor assistance from the language server.
textDocument/signatureHelp
describes the call being typed — Sprig functions and methods, named
constructors, callable values and Java overloads, including incomplete calls
and nested or multiline arguments — andtextDocument/semanticTokens/full
classifies parameters, locals, fields, types, functions, methods and Java
members. Find References now warns when its 200-file analysis limit leaves
matching project files out instead of stopping silently. The VS Code
extension 0.3.1 ships with this release and registers both providers. - Formatter. A continued line starting with a binary
+or-keeps its
spaces (sprig fmtwrote+"y"for\n + "y"). - Output is UTF-8 on Linux and macOS whatever the locale says; a process
without a locale, as in a container, printed?for non-ASCII text. runandtestcache javac output under~/.sprig/cache/javac, keyed by
the program's sources and classpath, so a program that ran before starts
without a second javac run;--no-cachecompiles anyway.- Diagnostics. An error in an unannotated top-level initializer no longer
stops the module's check, so the other errors are reported in the same round,
and identical duplicate diagnostics are dropped. Chains of more than 64
elifconditions in an if expression are emitted as a flat switch block
(javac overflowed near 1,500 nested conditionals). New stable codes:
SPR-CLASS-ABSTRACT,SPR-CONFORM-PARENT,SPR-DEP-REGISTRYand the three
SPR-BUNDLE-*codes;SPR-CONFORM-MEMBER,SPR-CONFORM-TARGETand
SPR-TYPE-GENERIC-ARGS-REQUIREDcover more cases than before. The stable set
dropsSPR-NAME-NOT-A-TYPEandSPR-NAME-MODULE, which no check ever
emitted:let t: v = 2isSPR-NAME-UNRESOLVEDand a duplicate import alias
isSPR-NAME-DUPLICATE. - A type argument that a generic declaration rejects is reported at your
use (#212).sets.of([0.5]),sets.Set[Float32](),lists.distinct([0.5])
and asets.Set[Float]annotation reportedSPR-NUM-CONVERSIONinside
std/sets.sprorstd/lists.spr, once for all such uses. The error is now
at each call, construction or annotation that gives the type argument, with
the same code and message, and the hint names the place inside
(Rejected inside 'of' with T = Float, at @std/sets.spr:7:33). - A runtime failure inside
@stdpoints at your line.sprig runand
sprig testreported an uncaught failure in a standard-library function,
such as an overflowingconcurrent.Counter.addorlists.sum, at a line of
std/.... They now report the innermost line in the program's own modules,
imported modules and dependency packages included;--stacktracestill
prints the whole stack. A program run outsidesprig runprints one
sprig-runtime-frame:line per distinct frame of generated code after its
sprig-runtime-failure:line. - Help and capabilities describe each addition:
help classes(contracts,
one-line form),help conform,help functions(references),help concurrency,help dependencies(registry rules);supportedSyntax,
unsupportedSyntaxandfeatureGuidance(interfaces,tuples,
namedFunctionReferences,asyncAwait,packageRegistry) follow the
compiler. - Website. The tutorial is a 24-chapter beginner book in Chinese and
English (/tutorial/,/en/tutorial/): every complete program is a checked
snippet with its real output, each chapter ends with exercises and folded
answers, and the old chapter URLs redirect to their new homes. The book
requires the 0.8 compiler frommainuntil this release is installed. - Documentation audit.
docs/, the website guides,sprig help,
sprig explainand the READMEs were read claim by claim against the
compiler; the statements that had drifted were corrected, never-emitted
diagnostic codes were removed, andsprig explainof an unknown code now
exits 2.
Upgrade from v0.7.1-beta.1
Run sprig upgrade, then sprig resolve once in each project: a lock records
the compiler that wrote it.
Incompatible: JDK 21 or newer
The compiler, the generated code, the Gradle plugin and the installer require
JDK 21. sprig doctor and install-sprig.sh report an older JDK with the
version it found. v0.7.1-beta.1 accepted JDK 17 and remains available for
projects that cannot move yet. CI runs JDK 21 and 26; the Windows preview runs
the same.
Incompatible: one spelling per numeric conversion
Four conversions had two equivalent spellings. Each now has one:
| Removed | Write instead |
|---|---|
n.toFloat() on an Int |
n.toFloatExact() (same behaviour), or n.toFloatLossy() to round |
x.toInt() on a Float |
x.toIntExact() (same behaviour), or x.toIntTrunc() to drop the fraction |
Decimal.fromInt(n) |
n.toDecimal() |
Int.parse(text) |
text.toInt() |
A conversion between number types without a suffix never fails
(Int32.toInt(), Int32.toFloat(), Float32.toFloat(), toDecimal()). One
that can lose information says what happens: Exact fails, Lossy rounds
and Trunc drops the fraction. The checker reports each removed spelling as
SPR-NAME-UNRESOLVED with its replacement. The language server's quick fix
rewrites toFloat() and toInt().
Soundness fixes (code that compiled before is rejected)
- Narrowing in
elifandelse(#125). Avarinitialized from a
narrowed name in anelseafter aneliftakes the narrowed type, where it
took the declared type before; declare it (var node: Node? = head). A
lambda capturing such a name gets the narrower function type; declare the
function type (let again: fn() -> String? = fn() => name). Both already
happened in a then branch. The diagnostic isSPR-TYPE-ASSIGNor
SPR-TYPE-MISMATCHat the later assignment. - Library calls that can throw say so (#209).
sqliteRows.is_null,
webRequest.json()andweb.json_responsedeclarethrows Error;
json_responsethrows anError("cannot write the response as JSON: ...")
instead ofIllegalStateException. Route handlers are
fn(Request) -> Response throws Error, so a handler may let these errors
escape: the server still answers 400 for a malformed JSON body and 500
otherwise. A named function that calls them outsidetrymust now declare
throws Error; handlers withoutthrowscompile as before. - A Float map key through generic code is rejected (#212). The map key
rule was checked only whereMap[Float, V]is written. A generic function or
class whose body keyed a map literal by its type parameter
(let seen = {value: true}), or passed it on to another generic such as
sets.of(values), compiled withT = Float, andvalue in seenthen found
a NaN. Each use of generic code is now checked with its type arguments
against what its body infers as well as what it writes, all the way down, so
such a call isSPR-NUM-CONVERSIONlike a writtenMap[Float, Bool].
Inferred inner uses also follow the nullability rule: a nullable type
argument that reaches a declaration writingT?is
SPR-TYPE-GENERIC-NULLABLEat the outer call, as it already was when the
inner use wrote[T]. - A contract never conforms to a Java interface or class.
conform Runner to RunnablewithRunnera contract class checked clean and did nothing:
the generated interface did not extendRunnable, so a class conforming to
Runnerwas still rejected where aRunnablewas expected. It is now
SPR-CONFORM-SOURCE, as conforming a contract to another contract already
was; conform each implementing class to the Java interface as well.
Behavior fixes (code that still compiles does something else)
- Function values are rejected for
Equatableand as Map keys (#222).
requires T: Equatableaccepted an instantiation with a function type, so a
generic function could compare two function values althoughf == gis
SPR-TYPE-OPERANDwhere written; the capability is now checked at the call
(SPR-GENERIC-CONSTRAINT, likeComparable), through a nullable type
argument too (U?is Equatable exactly whereUis). A function-valued Map
key, written in the annotation, inferred from a literal or reached through
generic code such assets.of, isSPR-TYPE-OPERANDtoo: a Map key needs
equality. - A qualified name as an index is resolved like any expression (#222). A
bracket payload that parsed as a name (by_day[Day.Mon]) and whose leftmost
name was a type or an imported module never got its symbols: the checker
typed it as an error without reporting one, and code generation stopped with
SPR-JVM-INTERNAL. It now resolves as a value, a type or a module, so the
enum case indexes the Map and a genuine mistake is a normal diagnostic
(by_name[math.PI]isSPR-NAME-UNRESOLVED,by_int[Box]is
SPR-NAME-NOT-A-VALUE). - A match expression does not unbox a nullable branch beside a primitive
branch (#219). A branch yielding anInt?/Int32?/Bool?/Float?/
Float32?name, field or call beside a primitive literal or local was
compiled into a Java switch expression whose inferred type was the primitive,
so selecting the nullable branch calledlongValue()(and the like) on a
null and the program stopped withSPR-RUNTIME-EXCEPTION. Every branch now
yields the Java type of the match result, as if-expression branches already
did;nullstays null and the other branch's value is unchanged. - A function value passed to a Java functional interface is evaluated
once.words.forEach(printer())calledprinter()for every element;
it is now called once, before the Java call, as every other argument is. Only
a lambda written in place is created per call as before. Find the code
affected by searching for Java callback arguments that are calls rather than
lambdas or names. - A number literal takes the other operand's width on either side (#212).
Only a literal on the right did:0.1 == f32wasfalsewhile
f32 == 0.1wastrue,1.0 + f32printed1.1000000014901161while
f32 + 1.0printed1.1, a negated literal (f32 + -0.5) wasFloat, and
1 + smallwithsmall: Int32wasIntarithmetic that did not overflow
wheresmall + 1did. Now the order of the operands never changes a result;
3000000000 + smallisSPR-NUM-RANGElikesmall + 3000000000, and
0.5 in xswithxs: List[Float32]compiles likexs.contains(0.5). - List membership uses
==for floats (#165).x in xs,contains,
indexOfandMutableList.removefind the first element==the value: a
Float or Float32 NaN is never found, and-0.0finds0.0and the reverse.
v0.7.1 usedDouble.equals, which found a NaN and told the zeros apart. To
find a NaN, testx.isNaN(). - A scope reports the failure, not the cancellation it caused (#205). When
a body awaited a task that a sibling's failure had cancelled (as
await_all,parallel_mapandmap_indo),scope()threw "task was
cancelled"; it now throws that first failure. @std/json_codecreader failures name the path (#206). A field of a
hand-built object with a duplicate key throws$: duplicate object key: a;
a number whose exponent is out ofDecimal's range throws
$.n: expected decimal in range, found number 1e9999999999where the
program used to stop withSPR-RUNTIME-EXCEPTION;*_number_textreaders
reject a hand-built number whose text is not a JSON number;
reject_unknown_fieldsreports a duplicate key in the same words, where it
said$: duplicate field 'a'.- SQL text holds one statement (#208).
Database.execute,
Database.queryand each batchStatementthrow anErrorfor a second
statement and change nothing; before, only the first ran, silently. A
trailing semicolon or comment is still fine. - A SQLite trigger body ends where SQLite ends it.
sprig-sqliteended a
CREATE TRIGGERbody at the firstENDthat noCASEhad opened, so a
column namedendin the body (UPDATE events SET end = 5;) cut the trigger
short:executecounted several statements and a migration failed with
"incomplete input". The body now ends as SQLite'ssqlite3_completeends it,
at anENDthat is the first token after one of its semicolons. concurrent.Counterfails on overflow (#209).add,incrementand
decrementfail past theIntrange with the same numeric error asInt
arithmetic and leave the value unchanged; they wrapped around silently.files.atomic_write_utf8keeps permissions and follows links (#209). It
keeps the permissions of the file it replaces, a new file gets what
write_utf8gives it (the process umask) instead ofrw-------, and a
symbolic link at the path is written through, as withwrite_utf8.files.atomic_write_utf8refuses a file it may not write. A rename
needs only the directory's permission, so it replaced a read-only file that
write_utf8refuses. It now fails the same way,cannot write PATH: access denied, and writes nothing, also through a symbolic link.Request.querycannot fail.sprig-webdecodes the query once, before
routing, into the newRequest.query_values(each name's first decoded
value), andquery(name)looks the name up there; a malformed query is still
a 400. ARequestbuilt by hand, as in a handler test, reads only the
query_valuesit is given (empty by default). Before,querydecoded
raw's query on every call and could throwBadRequestalthough it does not
declarethrows.Pool.shutdown_now()cancels running tasks (#209). A running pooled
task used to be interrupted without being cancelled and failed its scope
with "sleep was interrupted"; it now counts as cancelled, which is not a
scope failure.files.file_nameof a root is anError(#210).files.file_name("/")
stopped the program with a JVMNullPointerException.- A terminated
sprig runwaits for its program (#199). It used to exit
while the program was still shutting down, so a server's port could still be
open; it now waits up to 5 s, then stops the program and its children. - A Git dependency is checked out as committed. The cache compares every
checked-out file with the commit, byte for byte, but let the dependency's own
.gitattributesconvert files during checkout. The Sprig repository asks for
CRLF in.batfiles, so a fresh cache could not resolve any dependency from
it:--gitdependencies on the first-party libraries, andsprig searchand
sprig add NAMEwith the default registry, failed withSPR-DEP-GIT Git checkout verification failed. Line-ending conversion, keyword expansion,
filters and re-encoding are now off for dependency checkouts.
Diagnostics that replace javac errors
==or!=on two function values isSPR-TYPE-OPERANDat check time; it
reached javac before. Compare the result of calling them, or compare the
values they close over.
New meaning for syntax that was rejected before
- A method header without a body inside a class makes the class a contract.
v0.7.1 reported a syntax error, so no existing program changes meaning. A
function without a body outside a class staysSPR-SYNTAX-ERROR. class Name(fields)on one line is a class; it was a syntax error.- An
ifafter=,return,throwor=>is an if expression; it was a
syntax error.
Libraries
libraries/sprig-concurrent, which existed only onmainbetween two
releases and was never published, is removed in favor of@std/concurrent.spr.
Itsspawn(work)becomesspawn(scope, work)insidescope(...);
await()on a task is unchanged;pool(threads)plusspawn_onkeep their
names;cancel()on a task iscancel_all()on the scope.sprig publishrefuses a version that the registry already lists; publish
the next version or--yankthe listed one with a reason.
Known limits
Contracts are non-generic and have no default bodies; function references take
at most three parameters and never reference Java or built-in methods; type
arguments are never inferred from the expected type; a scope has no deadline
and a task no then; the registry is the repository index without hosting or
authentication; the compiler is not self-hosted. See
known-limitations.md.