Repository navigation
Releases: sysl-lang/sysl-bootstrap
Release list
sysl 0.0.162 — __NAME__, sysl tidy, and a match arm that hands its payload back without copying it
__NAME__, sysl tidy, and a match arm that hands its payload back without copying it
One behaviour change, two features and one code-generation change. sysl run and sysl test no longer replay a stale binary after a manifest's version moves. __NAME__ reads the name in the code's own package.hocon, as __VERSION__ reads its version. sysl tidy rewrites sysl.sum to exactly the lines the project's graph still reads. And a match arm that hands back a large payload now builds it where it is going, which halves musicbox-pico's boot frame.
Behaviour change
- The run cache now keys on each package's manifest: its path, name and version (3437cfe).
sysl runandsysl testused to key a built binary on the source files alone. The manifest__VERSION__folds in was not part of the key, so bumpingversionin an otherwise unchanged tree replayed the old binary and printed the old version. A change to any package'spackage.hoconname or version now rebuilds. Tests:RunCacheTests.AstCodec.Version61 → 62, because the manifest a source carries through a.syslibnow holds its name. A.syslibwritten by an older compiler is rebuilt rather than read.
Features
__NAME__is thenamein thepackage.hoconof the package a file belongs to (3437cfe). It is the twin of__VERSION__, read from the same manifest under the same rules. A library's__NAME__is the library's own name. Written as a default argument it is the caller's, so a logging library'slogcat(msg: string, tag: string = __NAME__)tags every message with the name of the application that called it. It is a string literal, so it folds into aconstor a module-levelval. A file that belongs to no package, or a manifest with nonamekey, is refused at the use: "'NAME' is the 'name' in package.hocon, and this file is not part of a package", or "… and …/package.hocon declares none".reference/lexical.md § __NAME__ is the package's name. Tests:NameBuiltinTests.sysl tidy [<path>] [--check](8af9806). A build only ever adds a line tosysl.sum, so moving a dependency from 0.1.0 to 0.3.0 left the 0.1.0 line recorded for good.tidyresolves the whole graph (every feature, anddev_dependenciestoo, since pruning by the features one build asked for would drop the others) and keeps exactly the lines that resolution reads. Live lines are kept byte for byte and in place. It printsremoved <coordinate> <tag>oradded <coordinate> <tag>for each line it changes, and nothing when the file was already tidy. A project depending on nothing, or only on paths, ends with nosysl.sum, which is what a build leaves for one.sysl tidy --checkwrites nothing and exits non-zero where the file is not tidy, for CI. Outside a project it is refused. Builds are unchanged and stay append-only. Tests:TidyTests.
Code generation
- A
matcharm that hands back a large payload no longer copies it (e4514a4). Inval p: &Big = make() matchwithOk(s) -> sandErr(_) -> return 1, the box is built and the payload copied straight from the call's result into it. Invar p = make() match …, where every other arm leaves,pis the payload inside the result's own storage, with no copy at all. The call's result is written into one slot and read through its address; only its tag is loaded to pick the arm. It used to be loaded whole, stored again and bound a third time. An arm that does anything else with the binding still binds it, and every count is given back exactly once. Tests:MatchMoveTests.- On
thumb-freestanding-softfp, with a 4,104-byteBig,boot's frame goes from 8,224 B to 4,128 B for the boxed form and from 12,304 B to 4,120 B for the local one, against 4,128 B forunwrap(). musicbox-pico'sbootgoes from 11,384 B to 5,904 B.
- On
Verification
- Full Native gate on
3ff7165e(./run-gate.sh): GATE: GREEN, 12502 succeeded, 0 failed, no retries, nothing timed out. The release is that sha plus the version bump. - Warnings census clean (JVM / JS / Native / syslDocJVM / syslDocNative): after
clean, two / three / three / one / two. Every warning is named, and none is in this repository:Reader.scala:26:6comes from scala-parser-combinators,Set.scala:62:15from the Scala.js scaladoc, and the rest are the build-infra-Xpluginand-classpathlines. - The darwin tarball was extracted to a scratch prefix and checked there:
sysl --versionreports 0.0.162 andsysl docdispatches tosysl-doc. A package naming itselfprobe-appprints[ probe-app ] startedthrough atag: string = __NAME__default, and bumping itsversionfrom 1.2.3 to 1.2.4 with nothing else changed makes the nextsysl runprint 1.2.4.
sysl 0.0.161 — a --lib root stands in for its package, and large values stay where they are
A --lib root stands in for its package, and large values stay where they are
Two packaging changes, two code-generation changes, and three fixes. A --lib root that is a dependency's package now overrides it, so a coordinate that cannot be fetched no longer stops the build. A dependency's own imports are now answered by its own manifest and never by the project's modules. Separately, a large self or by-value parameter is now read where it lies, and a large result is built in the caller's storage. A board program's deepest stack along boot → synth falls from 30,104 B to 13,440 B.
Behaviour changes
Four of the changes alter what an existing build does. Two of them can turn a build that worked into a refused or different one, so they come first.
- A
--libsource root that is a dependency's package now overrides that dependency (a76a3e9). Before, it was refused as a collision. A root stands in for a coordinate when itspackage.nameis the coordinate's repository name: the last path segment, with any/vNsuffix set aside. The coordinate is then dropped before selection wherever the graph names it, in the project's manifest or in any dependency's. Nothing is fetched for it and nothing is written tosysl.sumfor it. A coordinate that cannot be fetched (unpublished, a bad tag, no network) therefore no longer stops a build that has a--libcopy of it. This works like Cargo's[patch]and Go'sreplace, written as one flag. A root that merely holds a module a coordinate also offers, without being that package, is still refused, and the refusal now says what would have made it an override.reference/packages.md § A source root stands in for the package it is.- Behaviour change: a build that used to be refused for a
--libroot colliding with a coordinate now builds against the root.
- Behaviour change: a build that used to be refused for a
- A dependency's own import no longer reaches the project's modules (8b4818c). When a dependency wrote
import geom, the import used to be answered by a--libroot'sgeom, or by the project's owngeom/, before thegeomits own manifest declared. A program could then build and run the wrong package's code with no warning: the regression case printed 42 where the right answer is 28. The dependency now gets the module its manifest bound. A dependency whose manifest reaches nogeomat all is refused the name instead of borrowing the project's. The project's own imports are unchanged.reference/packages.md § Imports are transitive.- Behaviour change: a package that relied on reaching one of its consumer's modules now gets
undefined nameand has to declare what it imports.
- Behaviour change: a package that relied on reaching one of its consumer's modules now gets
- A large by-value
selfor parameter is read in place (a0221b5, a9dee9e). It is no longer copied at entry when the function only reads it: no assignment to it or to a part of it, no&of it, no*selfcall on it, no tail self-call, and nothing outside the program can call the function.reference/declarations.md's "the method gets a copy" still holds. The caller hands over storage that nothing can change while the call runs: a temporary, a local nobody else can name, or a snapshot it stages. A write through an alias in the middle of the call therefore still does not show throughself.- A caller stages no snapshot at all for a callee that can write nothing. That test (
BorrowedParams) now reaches past a leaf: a body may call functions that pass it, recursion included, and may write its ownvars. - A staged snapshot is released at its address (
arc.dispose_at.T(ptr)). It used to be loaded whole and passed toarc.dispose.Tby value, which put a second copy of the struct on the stack. - A view or a slice written through counts as a write to what it views (a9dee9e).
var v = self.table[..]; v[0] = 9keeps the parameter's own copy. A writable view of a local handed to a call counts as letting that local out, so the local is snapshotted. A write through a slice local (var o = out; o[0] = 7) is no longer counted as the function's own storage. Before this fix, each case changed the caller's value (9 9,7 7); they now print9 0and0 7. - On
thumb-freestanding-softfp, musicbox'sSynth.renderframe goes from 11,128 B to 104 B. A*selfmethod calling aselfmethod over a 4 KB struct goes from 8,224 B to 0.
- A caller stages no snapshot at all for a callee that can write nothing. That test (
- A large result is built where it is going (4fc425a, e52c90e). A local returned on every path is built in the caller's storage. In
var s = Synth(…); s.rewind(); Ok(s),slives in the payload of the caller'sResultfrom its declaration, so theOkwrites the tag and nothing else. This coverssreturned alone or as one argument of a variant or struct (Ok(s),Some(s),Held(s, n)), with anyreturnbeforesexists returning anything.- A copy is kept wherever the difference could be seen: another value returned after
sexists; another argument of the result mentionings;s's address going anywhere but straight into a call; adefer; or, where its address does go into a call, a postcondition or a release that could run a destructor between thereturnand the end of the function. - A
?aftersexists keeps the copy (e52c90e). Its failure leaves through the same storage, and would write overswith its counts still owed and its destructors never run. - A
matchorifproducing a large value builds each branch's value in place, with no merge slot. An armV(x) -> xover a local copies the payload straight from the matched value, soResult.unwrapandOption.unwrapno longer stage the whole value twice. - A large call result read through its address, as a receiver is in
synth(…).unwrap(), is written into the slot the read uses and released there. It used to be loaded whole, stored again, and released by value. - On
thumb-freestanding-softfp, musicbox'ssynthframe goes from 7,544 B to 1,976 B,Result[Synth, MusicError].unwrapfrom 5,568 B to 8 B, and a board program'sbootcalling them from 22,560 B to 11,464 B. The deepest stack alongboot → synthgoes from 30,104 B to 13,440 B.
- A copy is kept wherever the difference could be seen: another value returned after
Fixes
- A
build-carchive no longer leavessysl_wall_usundefined (09c6124). Before, any program reachingsysl.time.now(), which includes everysysl.logcall, failed at the consumer's link. Wherever the standard library is compiled from source (build-c,--no-std-lib), the library's own supplier of a seam the program calls is now analyzed and kept:sysl.posix.time'ssysl_wall_usandsysl_monotonic_us. It is taken only on a target whose operating system hasposix, and never where the program or a package supplies the same symbol. Tests:LibrarySupplierCliTests. - An
#ifin a dependency's file no longer drops the module for importers (d290e63, 6144e68). Which package a file belongs to was keyed by the source as collected, but#ifgating and literate tangling hand the parser a new source whenever they change the text. A dependency's file with any#ifin it, and any.lsyslfile, was therefore filed as the program's own, and its module became a second, unprefixed copy that answered imports with none of the package's other files. A prepared source now records what it was prepared from. Tests:DependencyConditionalTests. sysl.fsno longer names libc symbols that Android's Bionic lacks (4fc7352, d1b155c). Anaarch64-androidprogram reachingwrite_text_atomic, or anysysl.fscall that readserrno, failed at its link. On Android,errnois now read through Bionic's__errnorather than glibc's__errno_location, and the pending name's token comes fromarc4random_bufrather thangetentropy, which Bionic has only from API 28 on. macOS (__error) and Linux (__errno_location,getentropy) are unchanged. Tests:LibraryAndroidLibcCliTests.
Library
sysl.log.message_text(r, out)renders a record's message and fields alone, such asunderrun frames=512. Fields are quoted exactly astextquotes them, with no time, no level and no newline. It is meant for a sink whose destination stamps its own time and level (logcat, syslog, journald).textnow shares its field rendering and writes the same bytes as before (b4be242).
Tests: ReceiverInPlaceTests, ReturnSlotTests, AggregateLoweringTests, PackageBuildTests, and library/sysl/log/tests.sysl.
Verification
- Full Native gate on
50dbec81(./run-gate.sh): GATE: GREEN, 12461 succeeded, 0 failed, no retries, nothing timed out. The release is that sha plus two commits: a comment-only pointer fix inTypeChargingTestsand the version bump. - Warnings census clean (JVM / JS / Native / syslDocJVM / syslDocNative): after
clean, two / three / three / one / two. Every warning is named, and none is in this repository:Reader.scala:26:6comes from scala-parser-combinators,Set.scala:62:15from the Scala.js scaladoc, and the rest are the build-infra-Xpluginand-classpathlines. check-pointers.pyagainst the site: 2240 pointers, 0 unresolvable.- The darwin tarball was extracted to a scratch prefix and checked there:
sysl --versionreports 0.0.161,sysl runworks, andsysl docdispatches tosysl-doc.
sysl 0.0.160 — __VERSION__, and defaults read at the type each call settles
__VERSION__, and defaults read at the type each call settles
Five items. A program's version now lives in one place: __VERSION__ reads the version in the package.hocon of the code that uses it. A generic parameter's default no longer steers the call's solve, and a default may now name its declaration's own type parameters. Two capability fixes, from the same freeze exception as 0.0.157–0.0.159, close the last holes in what a board program may link.
Behaviour changes
Two of the five can refuse, or stop compiling, something that compiled before. Both are listed here first.
- A dependency's
@testsfile is no longer read by its consumer's build (a87760a, d04dbb7). A dependency's@testsfiles, and its@testfunctions and hooks, are now taken out before analysis in every build of a consumer —build,run,build-c,emit-llvm,emit-typed,prove, and the consumer's ownsysl test— whether the dependency arrives through--libor as a fetched coordinate. Before, they were name-resolved and type-checked against the consumer's target and dropped only afterwards, so a library whose tests made a scratch directory could not be linked by a board program at all ("'sysl.fs' declares no 'make_temp_dir'", pointing into a file no build of that program keeps). Whatreference/modules.md § A @tests file states its own capabilitiespromised now holds.- Behaviour change: a consumer's
@testthat called a helper declared in a dependency's@testsfile used to compile; the helper is no longer there to call. A test helper meant for other packages belongs in an ordinary file, or in a package of its own. - A mistake in a dependency's tests — a type error, or an import of its own
dev_dependencies— is now reported by that package'ssysl testand nowhere else. - Unchanged: a package's own
sysl testcompiles and runs its@testsfiles as before, and a program's own scaffolding is still analyzed by its build and then dropped.
- Behaviour change: a consumer's
- An alias of an applied Drop type dies as that type does (45ed551).
type M = Option[&Handle], held by a@no_osprogram, was charged nothing although its value runsHandle's destructor; it is now refused with "a 'sys.M' can die here, and its destructor reaches 'sys'", exactly asOption[&Handle]written out is. A bare alias (type H = Handle) was already followed.- Behaviour change: code that compiled only through this hole is refused.
Features
__VERSION__ (47bec55, a92f403, d29ecfe)
__VERSION__ reads the version in the package.hocon of the code that uses it, so a program's version lives in one place; it is refused in a file that is not part of a package.
print("mytool ", __VERSION__) // "mytool 1.4.0" under a manifest saying version = "1.4.0"
- A seventh built-in beside
__FILE__and__LINE__: a string literal, folded where it is written, so it may initialize aconstor a module-levelval. - It is per package as
__FILE__is per file: a library's__VERSION__is the library's own. - As a default argument it takes the CALLER's package version, as
__FILE__does. A library'sstamp(v: string = __VERSION__)answers the version of whichever package the call sits in — which is what lets a library offer a--versionhelper that reports the program using it. - Refused, in the self-hosted compiler's wording, in a file with no
package.hoconat its project root ("'VERSION' is the 'version' in package.hocon, and this file is not part of a package — no package.hocon stands at the root of its tree") and under a manifest with noversion("… and declares none"). - The manifest travels on each source through
AstCodec, so an importer re-analyzing a generic body still knows its package.AstCodec.Version60 → 61: every cached.syslibis rebuilt on first use.
Tests: VersionBuiltinTests.
A default may name its declaration's own type parameters (1742280, cd9342f)
offset[T: Zero + Add](x: T, step: T = T.zero()) -> T = x + step
tuned[F: Float](f: F, a4: F = F(440.0)) -> F = f + a4
print(offset(2.5), offset(7), tuned(0.5)) // 2.5 7 440.5
A type parameter is part of the signature rather than local to it, so a default may name one in value or type position, and it is read at each call at the type that call solved — never the caller's. The default takes no part in the solve: a T only the default could settle is refused as uninferable. At the declaration it is held to the bounds as a generic body is (T.zero() with no Zero bound is refused there); a conversion at T is checked at each instantiation. Other parameters and locals are still undefined in a default.
The same holds on a trait member, including one whose signature names Self through an arrow — over[N: Zero + Add](self, f: Self::Item -> N, base: N = N.zero()) reads N's bounds with Self spelled as the receiver's type, exactly as a call already does. A member with no default never builds the defaults' type parameters at all, so a trait member taking f: Self::Item -> N and no default is untouched by this feature.
Tests: ArgumentTests, TraitCallableTests.
Fixes
A default no longer steers a generic call's solve (f68fedb, fcaf050)
f[T](lo: T, step: T = 1) called with a real lo solves T = real, and the error now says the default cannot be read at the parameter's type at this call. The omitted 1 used to be analyzed bare, as an int, while the call was still solving T — so f(x) with a real x was refused ("'step' of 'f' is real, but int was given"). It now waits like any other literal, is consulted only after everything the call wrote, and is read at the solved type. A default that type cannot hold is refused at the call:
'step' of 'f' was left to its default, which cannot be read at int — the type this call settles it to
Verification
- Full Native gate on
cd9342f6(./run-gate.sh): GATE: GREEN, 12391 succeeded, 0 failed, no retries; full JVM run 12357 / 0. The release is that sha plus the version bump. TraitCallableTestson the release tree: 20 succeeded, 0 failed (syslJVM/testOnly sh.sysl.TraitCallableTests).- Warnings census clean (JVM / JS / Native / syslDocJVM / syslDocNative): after
clean, two / three / three / one / two — every warning named and none in this repository (Reader.scala:26:6from scala-parser-combinators,Set.scala:62:15from the Scala.js scaladoc, and the build-infra-Xpluginlines). - Documented on sysl.sh:
reference/lexical.md § __VERSION__ is the package's version(the lone-file refusal, as a checkederrorblock) andreference/declarations.md § Default parameters and named arguments(the type-parameter default, as a runnable block printing2.5 7 440.5).
sysl 0.0.159 — a type costs what it runs, not what names it
a type costs what it runs, not what names it
One capability fix, released as an exception to the bootstrap freeze. It finishes what 0.0.157 and 0.0.158 began: a module was still charged a gated module's requirement whenever it named one of that module's types — in a field, a variant's payload, a signature or a type argument — though naming a type runs none of its code.
Behaviour changes
Programs that were refused now build. Nothing that built before is refused now, with one exception below.
- Naming a type from a gated module charges nothing. A module whose error enum wraps
sysl.fs.IoErrorfor its one@needs(os)function is importable by a@no_osprogram, or one whose target hasos = false, for everything else it declares. So is a module naming the type in an unannotated signature, in a struct field, or as a type argument such asOption[IoError]; and a@no_osprogram may construct a value of such a type itself. - What a type runs is still charged, where it runs. Calling one of its methods charges its module, exactly as calling a function does; so do taking a method's address and erasing a value into a trait object. A type with a destructor (
impl Drop) charges its module wherever a value of it can die — and so does every type that holds one, through a field, a&T, a slice or an array. The refusal then reads "a 'sys.Handle' can die here, and its destructor reaches 'sys', which requires 'os'", at the place the type is named. - The exception: a method call is now charged by itself. It used to be charged only through the type's name, so a module that called a gated type's method on a value whose type it never wrote down (an inferred one) was not charged for it; it is now.
- Known limit: a method called inside a generic instantiation is not charged to the generic's module, since the type was its caller's choice, made in a module of its own.
module store
import sysl.fs.{IoError, write_bytes}
enum Failure
Io(e: IoError)
@needs(os)
save(p: string) -> Result[unit, Failure] = write_bytes(p, "x".bytes).map_err((e) -> Io(e))
version() -> int = 3
A @no_os program may now import store.version; before this release it was refused at the import, because Failure names IoError. A call to save from it is still refused, at the call.
Fixes
A type is charged for the code it runs, not for being named (140be89)
The rule chosen is the user's option 2: a type is charged only for the code it runs.
- Naming. Resolving a type, trait, variant or alias (
typeKey,traitKey,variantKey,aliasedKey, and a qualified path through a type inthroughModule) recordsEdgeUse.namedwith the key, andGatedModules.effectivecharges a named use nothing. An import whose every use only names a type is charged nothing likewise. - Methods. A method call names no module, so
GatedModules.chargeCallsreads what each non-generic body runs off the typed tree (Reachability.calledBy: calls, addresses taken, tables erased into) and records a running use on the callee's module. These go intoedgeUsesonly, never the module graph, so they cannot make a cycle;requirements()now walksedgeUses' keys. - Destructors. A named use whose type can die running code — it, or anything it holds through a field,
&T, slice or array, has animpl Drop— is charged as an ordinary reference, and reported ahead of the import.
Tests: TypeChargingTests (new) — the IoError-wrapping enum builds and prints 3; signature, field, type-argument, construction and qualified-path naming are free; the @needs call is refused at the call; a method call is refused; a Drop type and a type holding one are refused where held, a plain type beside it is not. DeclCapabilityTests and TargetCapabilityTests: the unannotated-signature case now builds (it asserted the old rule), plus the enum case on an os = false machine.
Verification
- Native gate on
140be899:GATE: GREEN, 12343 succeeded, 0 failed. The release inherits it (gated sha 140be89; dev = 140be89 + version bump). - Warnings census clean (JVM / JS / Native / syslDocJVM / syslDocNative), every warning named and outside this repository.
- Documented on sysl.sh:
reference/modules.md § A type costs what it runs.
sysl 0.0.158 — a @needs declaration's import and signature are charged to its callers
One capability fix, released as an exception to the bootstrap freeze. It completes 0.0.157's @needs scoping: the import a @needs declaration writes through, and the types its signature names, are now charged to the declaration's callers rather than to its module, as its body already was.
Behaviour changes
Programs that were refused now build. Nothing that built before is refused now.
- A program lacking a capability can import a module whose
@needsfunction imports a gated module, or names one of its types in the signature, as long as it never calls that function. That covers a@no_osprogram and a project whose target hasos = false. The call itself is still refused, at the call. In 0.0.157 the body was already covered, but the file-levelimport sysl.fsthe body used, and a signature such as-> Result[unit, sysl.fs.IoError], still made the whole module requireos, so the program was refused at itsimportline. - An import or a signature type that an unannotated declaration uses still charges the module, exactly as before.
module store
import sysl.fs
@needs(os)
save(p: string) -> Result[unit, sysl.fs.IoError] = sysl.fs.write_text(p, "x")
plain() -> int = 3
@no_os
import store.plain
print(plain())
In 0.0.157 this program was refused at the import. In 0.0.158 it builds and prints 3. A call to save from it is still refused, at the call.
Fixes
A @needs declaration's import and signature are charged to its callers (11dd0b4)
0.0.157 (8dd5b9a) made what a @needs(...) body reaches its callers' to answer. Two references the body rule did not reach still charged the module:
- The file-level import the body writes through. An import's edge was recorded with nothing covered, whatever used it. Imports are now recorded as such (
EdgeUse.imported) and charged by their uses:GatedModules.effectivelifts an import's edge to what every shipping reference along that edge covers. One use from an unannotated declaration covers nothing, so the module is still charged at the import. An import nothing references is charged as written. - A gated type named in the declaration's signature. Signatures are resolved by hoisting, generic instantiation, the abstract and opaque-result passes, the signature-visibility check and the drop-return check, all outside the body. Each now runs under
DeclTables.inSignature(needs), which covers what the declaration's@needsnames. An unannotated signature still charges the module.
Tests
Nine new cases: DeclCapabilityTests (5), TargetCapabilityTests (2, an os = false machine) and NeedsScopeCliTests (2, a real package.hocon at thumbv6m-freestanding).
Now accepted:
- a module whose import is used only inside
@needs(os)bodies is importable by a program withoutos - a module naming a gated type only in a
@needs(os)signature is importable by a program withoutos - the same two through a target with
os = false, and throughpackage.hoconand--lib
Still refused:
- calling either declaration, at the call only
- an import an unannotated declaration also uses, at the import
- a gated type in an unannotated declaration's signature
Install
brew install sysl-lang/tap/sysl # or: brew upgrade sysl
The tarballs for macOS arm64, Linux x86_64 and Linux arm64 are attached to this release. The sh.sysl:sysl_3:0.0.158 jars are on GitHub Packages (https://maven.pkg.github.com/sysl-lang/sysl-bootstrap). sysl is not published to Maven Central.
Verification
- Native gate: inherited from the landing gate. 11dd0b4 was gated with the full Native gate: GREEN, 417 suites, 12,331 passed and 0 failed, in 73:44. The tag is 3aadd68, which is 11dd0b4 plus the version bump and nothing else, so the suite was not re-run.
- Warnings census, cleaned: JVM two, JS three, Native three, syslDocJVM one, syslDocNative two. That is the expected count, and every warning belongs to a dependency or is build infrastructure.
- Release tarball: the asset downloaded back from this release, extracted to a scratch prefix and run.
sysl --versionprintssysl 0.0.158, andsysl-docis present. The@no_osprogram above prints3, where 0.0.157 refused it at its import. The call tosaveis refused at the call (this reaches 'store.save', which needs 'os', and this module declared '@no_os'). A module namingsysl.fs.IoErrorin an unannotated signature is still refused at the import. - GitHub Packages: all four artifacts (the pom, the jar, the sources jar and the javadoc jar) were published.
- brew:
brew test syslpasses, and the installedsysl --versionprintssysl 0.0.158. The Linux glibc floor, measured in the release run, is 2.34. - sysl.sh on 0.0.158: 1,418 tests pass with 0 failures. Of those, 1,398 are DocsTests, including the new sentence on
reference/modules.md. - Org sweep with the release tarball's own binary: 79 builds, 73 green under the bare command.
- 57 ran under
sysl test ., 11 undersysl build .and 5 undersysl build-c <dir>. - Four repos fail the bare command by design and were run as their READMEs say.
freertosagainst the FreeRTOS-Kernel POSIX port: 84 passed.libpqagainst a scratch PostgreSQL: 51 passed.quickjs-ngwith its include path: 33 passed. pico:build-lib --target thumbv6m-freestandinggets as far as the pico-sdk's generatedcyw43_arch.h, which means the sysl type-check passed.pico2andzephyrrefuse the bare command by design, through theirrequires { headers }clause.- Not swept, being kernel or board repos that need a toolchain or SDK this machine does not have:
picokit,ogol-pico,ogol-pico2,solder-pico2,zephyr-demo,pico-scratch. Also not swept, being infrastructure:sysl-bootstrap,sysl(the self-hosted compiler),sysl.sh,homebrew-tap,github-profileandsvd.
- 57 ran under
- Outside the org,
slate: a clean clone ofslate-language/slateat dev (c359e6b, 0.1.14). With the tarball's binary,sysl test .passes 2,436 tests with 0 failures, and bothsysl build .andsysl build . --features webview(the desktop edition) build.
sysl 0.0.157 — a @needs body is charged to its callers, not its module
Two capability fixes, released as an exception to the bootstrap freeze. A @needs declaration's body is charged to the functions that call it, not to the whole module holding it. A @tests file no longer counts toward its module's requirement. And sysl.fs requires os alone, which is what library/fs.md has always said.
Behaviour changes
Programs that were refused now build. Nothing that built before is refused now.
- A
@no_osprogram can import a module that holds a@needs(os)function, as long as it never calls that function. The same goes for a program whose target lacksos. The call itself is still refused, at the call. Before, the function's body counted toward the whole module's requirement, so the program was refused at theimportline as well. - An os-less program can link a library whose
@testsfile usessysl.fs. A@testsfile is dropped by every build exceptsysl test, and now its imports and bodies are dropped from the module's requirement too. musicbox 0.1.1 hit this: itsnotation_tests.syslreads fixtures withsysl.fs, so a board program could not link it. - A
@no_posixprogram can usesysl.fs. So can a project that declaresos = true, posix = false. Before,sysl.fsimportedsysl.posix.randon every hosted target, so every program that reached the filesystem was made to requireposix.
module store
@needs(os)
present() -> bool = sysl.fs.exists("/") // touches the filesystem
plain() -> int = 3 // reaches nothing
@no_os
import store.plain
print(plain())
In 0.0.156 this program was refused at the import. In 0.0.157 it builds and prints 3. A call to present from it is still refused, at the call:
error: this reaches 'store.present', which needs 'os', and this module declared '@no_os'
--> main.sysl:5:7
Fixes
A @needs body and a @tests file are not charged to their module (8dd5b9a)
reference/modules.md says that what a @needs declaration reaches "is charged to whoever reaches it rather than to whoever holds it". The analyzer did not do that. The body's reference into sysl.fs was an ordinary edge in the module graph, so the whole module came to require os (and posix).
Scoping.dependsOn now records, for each use, which capabilities the enclosing declaration covers. FunctionBodies sets this from the function's @needs, and a closure written inside the body inherits it. GatedModules leaves those capabilities out of the module's requirement and out of the edge check, and DeclCapabilities does not refuse such a body for what it covers. A body covers only what its own annotation names: a @needs(heap) function that reaches sysl.fs still makes its module require os. An unannotated function that calls a @needs(os) one passes the need on to its own callers, because reaching is transitive.
Uses made in scaffolding (a @tests file, or a test body) no longer go into the module's requirement. testOnlyFiles is now filled before imports are read, so an import in such a file is known to be scaffolding.
sysl.fs requires os, not posix (050a276)
library/sysl/fs/publish.sysl drew its temporary-file entropy from sysl.posix.rand under #if posix, which is true on every hosted target. A module is charged the requirements of everything it imports, so sysl.fs came to require posix. It now declares getentropy(2) as a private extern under #if posix, the way sysl.fs already declares unlink, rename and opendir, and falls back to the clock where there is no POSIX. These POSIX calls are how this host implements the module, and the module's users are not charged for them.
Tests
DeclCapabilityTests, TargetCapabilityTests and a new suite, NeedsScopeCliTests. The new suite drives a real package.hocon (thumbv6m-freestanding with os = false) and --lib, and it runs the library's own sysl test on the host.
Now accepted:
- a module that gave up
osimports the declaration beside a@needs(os)one - a closure written inside a
@needsbody is covered with the body - a board program that only renders links a library whose
@testsfile readssysl.fs, and the library'ssysl teststill runs that file on a host - a program importing the declaration beside a
@needs(os)one builds, including where that body reaches another@needs(os)declaration - a program that gave up
posixreads the filesystem, and publishes through it, which is the part that draws entropy sysl.fson a machine with an os that is not POSIX, publishing included
Still refused:
- calling the
@needs(os)declaration, at the call only and not at the import, naming the machine - an unannotated function calling it, which passes the need on to its own callers
- an unannotated body that reaches
sysl.fs, which still costs the whole module - a body reaching past what its own annotation names
- a program that gave up
os, at its reference tosysl.fs, foros - the POSIX module
sysl.fsonce imported, on a machine without POSIX - the same import in a shipping file (not a
@testsfile), which is still charged to the module
Documentation
reference/modules.md on sysl.sh now says three things. A @needs declaration's body is charged to its callers, not to its module. A function that says nothing passes the need on. A @tests file's imports are not charged to the module. It also says that a target the config says nothing about provides everything, os included, and that capabilities { os = false } is what makes a board refuse sysl.fs.
Install
brew install sysl-lang/tap/sysl # or: brew upgrade sysl
The tarballs for macOS arm64, Linux x86_64 and Linux arm64 are attached to this release. The sh.sysl:sysl_3:0.0.157 jars are on GitHub Packages (https://maven.pkg.github.com/sysl-lang/sysl-bootstrap). sysl is not published to Maven Central.
Verification
- Native gate: inherited from the landing gate. 050a276 was gated with the full Native gate: GREEN, 12,287 succeeded and 0 failed, in 92:08, covering syslNative and syslDocNative. Nothing timed out and nothing was retried. The tag is 99fd46f, which is 050a276 plus the version bump and nothing else, so the suite was not re-run.
- Warnings census, cleaned: JVM two, JS three, Native three, syslDocJVM one, syslDocNative two. That is the expected count, and every warning belongs to a dependency or is build infrastructure.
- Release tarball: extracted to a scratch prefix and run.
sysl --versionprintssysl 0.0.157, andsysl-docis present. The@no_osprogram above prints3. A@no_posixprogram printingsysl.fs.exists("/")printstrue. Under 0.0.156 both programs were refused, the first at its import and the second at its reference tosysl.fs. The call topresentis refused at the call with the message shown. - GitHub Packages: all four artifacts (the pom, the jar, the sources jar and the javadoc jar) answer 302.
- brew:
brew test syslpasses, and the installedsysl --versionprintssysl 0.0.157. The Linux glibc floor, measured in the release run, is 2.34 on both architectures. - sysl.sh on 0.0.157: 1,418 tests pass with 0 failures. Of those, 1,398 are DocsTests, including the new prose on
reference/modules.md. - Org sweep with the release tarball's own binary: 79 builds, 73 green under the bare command.
- 57 ran under
sysl test ., 11 undersysl build .and 5 undersysl build-c <dir>. - Four repos fail the bare command by design and were run as their READMEs say.
freertosagainst the FreeRTOS-Kernel POSIX port: 84 passed. (Its first run, alongside the other README runs on a loaded machine, stopped making progress after 71 tests. Run again alone, it passed all 84.)libpqagainst a scratch PostgreSQL: 51 passed.quickjs-ngwith its include path: 33 passed. pico:build-lib --target thumbv6m-freestandinggets as far as the pico-sdk's generatedcyw43_arch.h, which means the sysl type-check passed.pico2andzephyrrefuse the bare command by design, through theirrequires { headers }clause.- Not swept, being kernel or board repos that need a toolchain or SDK this machine does not have:
picokit,ogol-pico,ogol-pico2,solder-pico2,zephyr-demo,pico-scratch. Also not swept, being infrastructure:sysl-bootstrap,sysl(the self-hosted compiler),sysl.sh,homebrew-tap,github-profileandsvd.
- 57 ran under
- Outside the org,
slate: a clean clone ofslate-language/slateat dev (c359e6b, 0.1.14). With the tarball's binary,sysl test .passes 2,436 tests with 0 failures, and bothsysl build .andsysl build . --features webview(the desktop edition) build.
sysl 0.0.156 — a view inside a &sync box is refused
One fix, released as an exception to the bootstrap freeze: a view of an array inside a &sync box is now refused, because it raced.
Behaviour changes
A view of an array inside a &sync box (a field, a nested field, or one passed to a []T parameter) is now refused. It took the box's share with a count update that was not atomic, which raced with every other domain holding the box. Index it or walk it with for.
struct Ring
samples: [8]f32
first(r: &sync Ring) -> f32
val v = r.samples[0..<4] // 0.0.155: compiled, and raced. 0.0.156: refused.
v[0]
error: a slice does not record whether its owner's count is atomic, so storage inside a '&sync Ring' cannot be sliced — a view of it would take its share of the box with a count update that is not atomic. Read it by index, or walk it with 'for'
r.samples[i] and for s in r.samples take no share and still compile. Slicing the same field through an ordinary &Ring, or slicing a copy taken out of the &sync box, is unchanged.
Fixes
Refuse a view of storage inside a &sync box, not only of a &sync array (9dddcb9)
A view keeps its storage alive by holding a share of the box the storage sits in, and it records nothing about whether that box's count is atomic, so it takes and gives back the share with a plain load-add-store. The analyzer already refused slicing a &sync [N]T for that reason. An array field reached through a &sync box was accepted: r.samples[a..<b], r.samples[..], r.inner.a[..], and r.samples handed to a []T parameter. Each of those updated the box's count non-atomically, racing every other domain that held the box. ThreadSanitizer found it on an audio ring buffer.
checkSliceable (Aliasing.scala) now walks the place through fields and array elements to the box it lies in (syncOwner), and refuses when that box is &sync. Both slice paths, an explicit slice and the coercion of an array to a view, go through it. This is analysis only: no code generation or ABI changes, and every program 0.0.155 accepted that is not one of these shapes compiles to the same thing.
Tests
SharedObjectTests: 9 new cases.
Refused:
- a range of an array field
- the whole of an array field
- an array inside a by-value field of the box
- an array field handed to a parameter that takes a view
- an array inside a
&syncbox reached through another
Still accepted:
- walking an array field of a
&syncbox withfor - reading and writing it by index
- a view of the same field through an ordinary
&box - a view of a copy taken out of the
&syncbox
Documentation
reference/arrays.md on sysl.sh shows the new refusal as an error block, and a runnable block that indexes the field and walks it with for. DocsTests runs both.
Install
brew install sysl-lang/tap/sysl # or: brew upgrade sysl
The tarballs for macOS arm64, Linux x86_64 and Linux arm64 are attached to this release. The sh.sysl:sysl_3:0.0.156 jars are on GitHub Packages (https://maven.pkg.github.com/sysl-lang/sysl-bootstrap). sysl is not published to Maven Central.
Verification
- Native gate on the tagged tree (ce15631, which is 9dddcb9 plus the version bump): GREEN, 12,299 succeeded and 0 failed. That is 416 suites in 46 chunks plus the groups that run alone. Nothing timed out and nothing was retried. The gate covers syslNative and syslDocNative. It took 63:39, after a cleaned warnings census. The fix had landed on a targeted JVM gate (16 suites, 471 passed), so this is its first full run.
- Warnings census, cleaned: JVM two, JS three, Native three, syslDocJVM one, syslDocNative two. That is the expected count, and every warning belongs to a dependency or is build infrastructure.
- Release tarball: extracted to a scratch prefix and run.
sysl --versionprintssysl 0.0.156, andsysl-docis present. The example above is refused with the message shown; under 0.0.155 the same program compiled and printed1. Indexing the field and walking it withforcompiles and runs. - GitHub Packages: all four artifacts (the pom, the jar, the sources jar and the javadoc jar) answer 302.
- brew:
brew test syslpasses, and the installedsysl --versionprintssysl 0.0.156. The Linux glibc floor, measured in the release run, is 2.34 on both architectures. - sysl.sh on 0.0.156: 1,418 tests pass with 0 failures. Of those, 1,398 are DocsTests. Both new blocks on
reference/arrays.mdwere checked by mutation. Changing the error block's last clause to "walk it with a loop" made program 38 fail, and changing the expected output to40 71made program 39 fail. Nothing else failed, and restoring them turned the suite green again. - Org sweep with the release tarball's own binary: 79 builds, 73 green under the bare command.
- 57 ran under
sysl test ., 11 undersysl build .and 5 undersysl build-c <dir>.musicbox,miniaudioandmusicbox-miniaudioare green. - Four repos fail the bare command by design and were run as their READMEs say.
freertosagainst a freshly built FreeRTOS-Kernel POSIX port: 84 passed.libpqagainst a scratch PostgreSQL: 51 passed.quickjs-ngwith its include path: 33 passed. pico:build-lib --target thumbv6m-freestandinggets as far as the pico-sdk's generatedcyw43_arch.h, which means the sysl type-check passed.pico2andzephyrrefuse the bare command by design, through theirrequires { headers }clause.- Not swept, being kernel or board repos that need a toolchain or SDK this machine does not have:
picokit,ogol-pico,ogol-pico2,solder-pico2,zephyr-demo,pico-scratch. Also not swept, being infrastructure:sysl-bootstrap,sysl(the self-hosted compiler),sysl.sh,homebrew-tap,github-profileandsvd.
- 57 ran under
- Outside the org:
slateis affected. A clean clone ofslate-language/slateat 0.1.12 (f951392), built withsysl build ., is refused at two lines of its test filetests_actor_probe.sysl(180 and 182). Both lines slicebox.said[0..<box.said_len]andbox.got[0..<box.got_len]through a&sync ProbeBox, which is exactly the shape this release refuses. The fix belongs in slate: copy the bytes out by index, or slice a copy of the array. It is not made here.
sysl 0.0.155 — a by-name parameter is a call inside a closure too
A parameter passed by name is now read the same way everywhere in its function: inside a closure, inside a nested function, and in the body after either one. Each read evaluates the argument. One spelling that 0.0.154 accepted is now refused (below).
Behaviour changes
Calling a by-name parameter inside a closure or nested function is now refused. In 0.0.154 that was the only way to read one there. Write the bare name instead, as the body always required.
f(x: -> int) -> int
val g = () -> x() + 1 // 0.0.154: compiled. 0.0.155: refused.
g()
error: type 'int' has no method 'call'
The fix is to drop the parentheses: val g = () -> x + 1. In the function's own body, x() was always refused with this message. A closure is now treated the same way.
What 0.0.154 refused and now compiles:
- A bare read inside a closure or nested function.
val g = () -> x + 1was refused with'+' needs '$F0: sysl.Add'and then'+' needs matching types, got a closure and int. - A bare read in the function's own body, when it comes after any closure or nested function.
val g = () -> 1followed byx + g()was refused with'+' needs '$F0: sysl.Add[int]'. With the closure removed, the same line compiled.
Every other program compiles to the same thing as on 0.0.154. Nothing in the org wrote x() inside a closure. A grep of every repo under sysl-lang, and of slate-language, found no by-name parameter in any package or program. The only matches are parse-test strings in the self-hosted compiler. So nothing needed fixing for this release.
Fixes
A by-name parameter read in a closure or nested function is a call (020a727)
reference/types.md § A parameter passed by name says: "Each use is an evaluation, because each use is a call." Inside a closure or nested function in the body, the compiler did not follow that rule. A read there gave the thunk itself, not its value.
There were two causes, both in one mechanism. analyzeNested calls resetFunction, which cleared byNameLocals. Nothing marked the captured names as by-name again, so the closure's body read the thunk. Nothing restored the set afterwards either, so the function's own reads after a closure stopped being calls as well. Now the by-name mark follows a captured name into the closure's scope, and is restored when the closure ends (FunctionBodies.scala).
Building the closure's environment has to capture the callable itself, not call it. captureRead in Closures.scala does this for a closure's fields and for a nested group's addresses. So the argument is evaluated at each read inside the closure, and never at capture.
Tests
ByNameTests: 8 new cases. All of them failed before the fix.
- a closure of the body calls it
- a nested function of the body calls it
- a closure evaluates the argument at every call of it
- a nested function evaluates the argument at every read
- a closure that outlives the call evaluates the argument when it is called
- a closure inside a nested function reaches it through both
- the body still calls it after a closure is written
- calling it inside a closure is refused, as it is in the body
Documentation
reference/types.md § A parameter passed by name on sysl.sh has a new runnable block. In it, a closure reads a by-name parameter and is called twice, and the argument runs once per call, printing 11 and then 21. One sentence was also added: the rule holds inside a closure or nested function too. DocsTests runs the block, so the fixed behaviour stays pinned.
Install
brew install sysl-lang/tap/sysl # or: brew upgrade sysl
The tarballs for macOS arm64, Linux x86_64 and Linux arm64 are attached to this release. The sh.sysl:sysl_3:0.0.155 jars are on GitHub Packages (https://maven.pkg.github.com/sysl-lang/sysl-bootstrap). sysl is not published to Maven Central.
Verification
- Native gate on the tagged tree (a34479b, which is 020a727 plus the version bump): GREEN, 12,290 succeeded and 0 failed. That is 416 suites in 46 chunks plus the groups that run alone. Nothing timed out and nothing was retried. The gate covers syslNative and syslDocNative. It took 57:04, after a cleaned warnings census. The fix had landed on a targeted gate, so this is its first full run.
- Warnings census, cleaned: JVM two, JS three, Native three, syslDocJVM one, syslDocNative two. That is the expected count, and every warning belongs to a dependency or is build infrastructure.
- Release tarball: extracted to a scratch prefix and run.
sysl --versionprintssysl 0.0.155, andsysl-docis present. The 0.0.154 reproductions now behave as described above.val g = () -> x + 1prints42.x + g()after a closure prints42.x()in a closure is refused withtype 'int' has no method 'call'. The new sysl.sh block prints11,21,2. - GitHub Packages: all four artifacts (the pom, the jar, the sources jar and the javadoc jar) answer 302.
- brew:
brew test syslpasses, and the installedsysl --versionprintssysl 0.0.155. The Linux glibc floor, measured in the release run, is 2.34 on both architectures. - sysl.sh on 0.0.155: 1,416 tests pass with 0 failures. Of those, 1,396 are DocsTests. The new block was checked by mutation: changing its expected output to
11 11 2, which is what evaluating the argument once at capture would print, turned DocsTests red on exactly that block (types.mdprogram 26). Restoring it turned the suite green again. - Org sweep with the release tarball's own binary: 77 builds, 71 green under the bare command.
- 54 ran under
sysl test ., 12 undersysl build .and 5 undersysl build-c <dir>. - Four repos fail the bare command by design and were run as their READMEs say.
freertosagainst a freshly built FreeRTOS-Kernel POSIX port: 84 passed.libpqagainst a scratch PostgreSQL: 51 passed.quickjs-ngwith its include path: 33 passed. pico:build-lib --target thumbv6m-freestandinggets as far as the pico-sdk's generatedcyw43_arch.h, which means the sysl type-check passed.pico2andzephyrrefuse the bare command by design, through theirrequires { headers }clause.- Not swept, being kernel or board repos that need a toolchain or SDK this machine does not have:
zephyr,picokit,ogol-pico,ogol-pico2,solder-pico2,zephyr-demo,pico-scratch. Also not swept, being infrastructure:sysl-bootstrap,sysl(the self-hosted compiler),sysl.sh,homebrew-tap,github-profileandsvd.
- 54 ran under
sysl 0.0.154 — a comparison missing its right operand says 'expression expected'
A comparison with nothing usable on its right now says expression expected instead of telling you about a range you never wrote. Nothing else changes in how programs compile.
Behaviour changes
None. Every program that compiled on 0.0.153 compiles to the same thing on 0.0.154, and every program it refused is still refused. The one change is the wording and the reason of one parse error (below); its position is the same as before.
Fixes
A comparison missing its right operand says expression expected, not '..' expected
Before: when the right side of a comparison was missing, or was something that cannot be an operand, the parser reported the range operator:
val c = true
var z = 1 < if c then 1 else 2
error: '..' expected
--> main.sysl:2:13
print(1 < ) got the same message, as did every other comparison operator (==, !=, <=, >=, >) and the second link of a chain (print(1 < 2 < )). No range was involved. A comparison's operand is parsed as a range expression, and the last thing that rule tries is a bare ... Every alternative failed at the same token, the last failure won the tie, and so the message named the range.
Now the same position gets error: expression expected. The range rule is labelled as an expression, the same way the unary rule already was, so a failure at its first token names what was missing. Parse results do not change. The range's own refusal, '..=' is not a range — inclusive is 'a..b' and exclusive is 'a..<b', is still reported as before. (99e0306)
Tests
ParseDiagnosticTests § a comparison with no value on its right wants a value, not a range: 4 new cases.- Nothing after the operator:
print(1 < )at 1:11. - An
ifas the right operand at 2:13. - All six comparison operators, plus the second link of a chain at 1:15.
- The
..=refusal still naming the range.
- Nothing after the operator:
ArcCodegenTests"a field through a reference reaches past the header" used to match the weak-count store at header slot 2. It now asserts the real field access: a GEP into slot 3 (the payload), the GEP forywithin it, and thestore i32 9landing on that address. (0acf71a)
Documentation
- The README now says sysl is not published to Maven Central and points to GitHub Packages, where the jars are. (ef803a9)
- No page on sysl.sh quoted the old
'..' expectedrefusal, so the site needed only the version pin.
Install
brew install sysl-lang/tap/sysl # or: brew upgrade sysl
The tarballs for macOS arm64, Linux x86_64 and Linux arm64 are attached to this release. The sh.sysl:sysl_3:0.0.154 jars are on GitHub Packages (https://maven.pkg.github.com/sysl-lang/sysl-bootstrap). As of 0.0.153, sysl is not published to Maven Central.
Verification
- Native gate on the tagged tree (80c8695): GREEN, 12,282 succeeded and 0 failed. That is 416 suites: 6 ran alone and 410 in 46 chunks. Nothing timed out and nothing was retried. The gate covers syslNative and syslDocNative. It took 51:58, after a cleaned warnings census.
- Warnings census, cleaned: JVM two, JS three, Native three, syslDocJVM one, syslDocNative two. That is the expected count, and every warning belongs to a dependency or is build infrastructure.
- Release tarball: extracted to a scratch prefix and run.
sysl --versionprintssysl 0.0.154,sysl-docis present, and the comparison above reportsexpression expectedat 2:13. - brew:
brew test syslpasses, and the installedsysl --versionprintssysl 0.0.154. The Linux glibc floor, measured in the release run, is 2.34. - sysl.sh on 0.0.154: 1,415 tests pass with 0 failures. Of those, 1,395 are DocsTests.
- Org sweep with the release tarball's own binary: 74 builds, 74 green.
- 54 ran under
sysl test ., 12 undersysl build .and 5 undersysl build-c <dir>. - Three repos fail the bare command by design and were run as their READMEs say.
freertosagainst a freshly built FreeRTOS-Kernel POSIX port: 84 passed.libpqagainst a scratch PostgreSQL: 51 passed.quickjs-ngwith its include path: 33 passed. pico:build-lib --target thumbv6m-freestandinggets as far as the pico-sdk's generatedcyw43_arch.h, which means the sysl type-check passed.- Not swept, being kernel or board repos that need a toolchain or SDK this machine does not have:
pico2,zephyr,picokit,ogol-pico,ogol-pico2,solder-pico2,zephyr-demo,pico-scratch. Also not swept, being infrastructure:sysl-bootstrap,sysl(the self-hosted compiler),sysl.sh,homebrew-tap,github-profileandsvd.
- 54 ran under
sysl 0.0.153 — start now, wait later; value-parameter defaults and trait value parameters
sysl.process.start runs several programs at once, sysl.cpu_count() says how many to run, a value parameter's default now means something, a trait may take value parameters, sysl doc gets ten page fixes, and three programs that used to crash the compiler or fail inside clang now get a refusal or a working build.
Behaviour changes
= N on a value parameter now means something — and on a function it is refused
Before: struct Ring[T, const N: usize = 4] parsed the default and ignored it, so Ring[int] was refused as "takes 2 type arguments"; on a function, total[const N: usize = 3](xs: [N]int) was silently accepted and the default did nothing.
Now: a value default on a struct or enum works exactly as a type default does — it fills the gap a use leaves, counts toward the same arity, may share a list with type defaults, and may be a constant expression over the value parameters before it ([const M: usize, const N: usize = M * 2]). Ring[int] is Ring[int, 4], one instantiation. The default has to fit the parameter's type ([const N: u8 = 300] is refused: "the default for 'N' does not fit byte: 300").
On a function, a method or an impl block a value default is refused, since those parameters are solved from what the call is given:
error: 'N' is a value parameter of the function 'total', whose type parameters are solved from what it is given rather than written where it is used — so '= 3' has nothing to stand in for
A program that wrote = N on a function's value parameter compiled on 0.0.152 and is refused on 0.0.153 — delete the default; it never did anything. (cee9a94)
A written value argument that does not fit its parameter's type is refused
Before: a value argument was not checked against its parameter's type where it was written — -1 for a const N: usize surfaced only later, as "an array cannot have -1 elements", and only where it sized an array. Now it is refused at the argument itself: this argument does not fit usize: -1, and this argument does not fit byte: 300 for a u8. A program that passed an out-of-range value argument that nothing downstream happened to reject is refused. (cee9a94, 7dcd6f6)
const N on a trait now declares a value parameter, where 0.0.152 read it as a type parameter
Before: trait Bytes[const N: usize] parsed, but the analyzer silently read N as a type parameter, so a member signature [N]u8 and an impl Bytes[4] did not mean what they said. Now a trait takes value parameters exactly as a struct does: members read N in their signatures and default bodies, impl Bytes[4] for W fixes it, a generic block abstracts over it (impl[const M: usize] Bytes[M] for Bits[M]), bounds and trait objects name it at a value (f[T: Bytes[4]], &Bytes[4]), two values are two implementations side by side, and a default (trait Hash[const N: usize = 32]) fills a use that leaves it out. The value is checked against the parameter's type wherever it is written. A program that wrote trait T[const N: …] and relied on N being a type now gets a value parameter. (0dd0eae)
Otherwise none for a program that compiled on 0.0.152. Every fix below turns a compiler crash or a clang failure into either a refusal with a source location or a program that builds.
New API
sysl.process.start and Child.wait — start a program now, wait for it later
start(program: string, args: []const string = [], dir: string = "", env: []const Var = [],
stderr: bool = false, timeout: int = 0) -> Result[&Child, IoError]
struct Child
wait(*self) -> Result[Output, IoError]
start is capture with the wait taken out: same arguments, same meanings, and wait answers exactly what capture would have — the same Output, the same TimedOut. capture is now written as start followed by wait, so the two cannot disagree. Start several children and wait for them in any order; they run at the same time, and because output still goes through temporary files rather than pipes, no child can block on a full pipe while its parent waits for another. A program that is not there is refused at start (NotFound), not at the wait. timeout is measured from start and kept by wait; waiting twice answers the first answer again. A Child owns its child: dropping one that was never waited for stops it (the same two steps a timeout takes) and reaps it, so no zombie is left, and its output files are removed.
sysl.cpu_count() -> usize
In the root module beside os() and cpu(): the number of logical processors online on the machine the program is running on — the number to size a pool of workers by. Never less than one. It is @needs(os), so a module that has given up os is refused at the call; where there is no POSIX sysconf (a freestanding image, WASI) it answers one.
9 new library tests cover both (StdSelfTests.floor 875 → 884). (06b8b97)
Fixes
A jump nested inside a deferred statement is refused instead of overflowing the stack
Before: the direct forms defer return 1 and defer break were refused, but the same jump one level down — defer if c then return 1, defer if i == 1 then break — was scheduled. Laying the jump down runs the block's deferred statements, of which it is one, so the emitter recursed until the compiler died with a StackOverflowError.
Now: a deferred statement may not contain a jump that leaves it — a return, or a break/continue of a loop outside it — at any depth. It is refused during analysis with the direct form's message, anchored at the jump, once per deferred statement:
error: a deferred statement runs while its block is being left, so it cannot 'return' — there is no exit left to take. Compute what the function returns before the block ends
A loop written inside the deferred statement is its own, so its break and continue stay inside and are fine; so is a jump inside a closure or a nested function written there. (91fe162)
A local declared by a deferred statement no longer trips clang in a block with several exits
Before: a deferred statement is laid down once per edge that leaves its block. One that declared a local, in a block with more than one exit, emitted %j.addr once per copy, and clang refused the module: multiple definition of local value named 'j.addr'. After that was fixed, a deferred ref binding and a deferred array whose view escapes still failed the same way, because their address is a register computed where the declaration stands rather than a stack slot.
Now: a stack slot is shared by every copy (91fe162), and a register-valued address takes a name of its own in each further copy — %r.addr, then %r.d1.addr, %r.d2.addr — the names the self-hosted compiler gives (d528eee).
An extern followed by a sysl function of the same name reports the refusal instead of crashing at the call
Before: declaring an extern, then a sysl function with the same name, then calling that name took the compiler down with NoSuchElementException: key not found: <name>.2 out of CallCore.callOverloaded, and the refusal the second declaration had earned was never printed. The declaration had been counted into the name's overload set before the check that refused it, so the set held a key naming nothing.
Now: the refusal is made after the tables are filled, like the other checks in that chain, so the declaration's refusal is what is printed and nothing is said at the call. (8ba86fa)
sysl doc — ten page defects fixed
sysl-doc printed pages that were wrong in ten ways; every one is fixed, and regenerating a package's pages with 0.0.153 changes them. (b0f65d6)
- A summary wrapped over two lines was printed twice — once as the summary and again as the body. It is printed once.
opaquewas dropped from an opaque struct's signature, and a private field was unmarked, so it read as public. Both are now shown as declared.- Aliases, externs and module
vals were missing altogether. They get groups of their own —## Values,## Externs,## Aliases— in the order the self-hosted writer prints them; an extern shows its sysl name, not its link symbol. --privateshowed private types and functions but hid private methods. It shows them.- A
@paramnaming no parameter was rendered as though it documented one. It is dropped, and the CLI warns about it. --checkpassed with an orphaned page — a generated page whose module no longer exists. It now fails on one, and a regenerate deletes it; a hand-written page beside the generated ones is left alone.- Signatures misprinted three things: a string default came out unescaped, a by-name parameter was printed wrongly, and the parentheses that group a default expression were dropped. All three print as the source spells them.
Tests
DeferTestsgrows from 46 to 89: the nested jump refused at each depth and loop shape (areturnunder anif,break/continueof the surrounding loop, labelled jumps to an outer loop), jumps that stay inside the statement running correctly, and deferred locals — plain,refand escaping arrays — in blocks with two, three and more exits building and running.DefaultTypeParamTests § a value parameter's default: 13 new cases — a struct and an enum taking the default, type and value defaults filling together, a default naming an earlier value parameter, the filled and written spellings being one instantiation, ordering (a defaulted parameter before an undefaulted one; a default naming a later one), a default and a written argument that do not fit their type, a default that is not a value, and the refusal on a function, a method and animplblock;ValueGenericsTestschanges one expected wording to the new "does not fit" refusal, and gains the block "a trait's value parameter" — members reading it, impls at a value, generic imp...