Skip to content

Releases: sysl-lang/sysl-bootstrap

sysl 0.0.162 — __NAME__, sysl tidy, and a match arm that hands its payload back without copying it

Choose a tag to compare

@edadma edadma released this 03 Oct 23:50

__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 run and sysl test used to key a built binary on the source files alone. The manifest __VERSION__ folds in was not part of the key, so bumping version in an otherwise unchanged tree replayed the old binary and printed the old version. A change to any package's package.hocon name or version now rebuilds. Tests: RunCacheTests.
    • AstCodec.Version 61 → 62, because the manifest a source carries through a .syslib now holds its name. A .syslib written by an older compiler is rebuilt rather than read.

Features

  • __NAME__ is the name in the package.hocon of 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's logcat(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 a const or a module-level val. A file that belongs to no package, or a manifest with no name key, 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 to sysl.sum, so moving a dependency from 0.1.0 to 0.3.0 left the 0.1.0 line recorded for good. tidy resolves the whole graph (every feature, and dev_dependencies too, 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 prints removed <coordinate> <tag> or added <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 no sysl.sum, which is what a build leaves for one. sysl tidy --check writes 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 match arm that hands back a large payload no longer copies it (e4514a4). In val p: &Big = make() match with Ok(s) -> s and Err(_) -> return 1, the box is built and the payload copied straight from the call's result into it. In var p = make() match …, where every other arm leaves, p is 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-byte Big, 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 for unwrap(). musicbox-pico's boot goes from 11,384 B to 5,904 B.

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:6 comes from scala-parser-combinators, Set.scala:62:15 from the Scala.js scaladoc, and the rest are the build-infra -Xplugin and -classpath lines.
  • The darwin tarball was extracted to a scratch prefix and checked there: sysl --version reports 0.0.162 and sysl doc dispatches to sysl-doc. A package naming itself probe-app prints [ probe-app ] started through a tag: string = __NAME__ default, and bumping its version from 1.2.3 to 1.2.4 with nothing else changed makes the next sysl run print 1.2.4.

sysl 0.0.161 — a --lib root stands in for its package, and large values stay where they are

Choose a tag to compare

@edadma edadma released this 03 Oct 20:20

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 --lib source 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 its package.name is the coordinate's repository name: the last path segment, with any /vN suffix 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 to sysl.sum for it. A coordinate that cannot be fetched (unpublished, a bad tag, no network) therefore no longer stops a build that has a --lib copy of it. This works like Cargo's [patch] and Go's replace, 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 --lib root colliding with a coordinate now builds against the root.
  • 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 --lib root's geom, or by the project's own geom/, before the geom its 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 no geom at 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 name and has to declare what it imports.
  • A large by-value self or 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 *self call 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 through self.
    • 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 own vars.
    • A staged snapshot is released at its address (arc.dispose_at.T(ptr)). It used to be loaded whole and passed to arc.dispose.T by 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] = 9 keeps 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 print 9 0 and 0 7.
    • On thumb-freestanding-softfp, musicbox's Synth.render frame goes from 11,128 B to 104 B. A *self method calling a self method over a 4 KB struct goes from 8,224 B to 0.
  • 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), s lives in the payload of the caller's Result from its declaration, so the Ok writes the tag and nothing else. This covers s returned alone or as one argument of a variant or struct (Ok(s), Some(s), Held(s, n)), with any return before s exists returning anything.
    • A copy is kept wherever the difference could be seen: another value returned after s exists; another argument of the result mentioning s; s's address going anywhere but straight into a call; a defer; or, where its address does go into a call, a postcondition or a release that could run a destructor between the return and the end of the function.
    • A ? after s exists keeps the copy (e52c90e). Its failure leaves through the same storage, and would write over s with its counts still owed and its destructors never run.
    • A match or if producing a large value builds each branch's value in place, with no merge slot. An arm V(x) -> x over a local copies the payload straight from the matched value, so Result.unwrap and Option.unwrap no 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's synth frame goes from 7,544 B to 1,976 B, Result[Synth, MusicError].unwrap from 5,568 B to 8 B, and a board program's boot calling them from 22,560 B to 11,464 B. The deepest stack along boot → synth goes from 30,104 B to 13,440 B.

Fixes

  • A build-c archive no longer leaves sysl_wall_us undefined (09c6124). Before, any program reaching sysl.time.now(), which includes every sysl.log call, 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's sysl_wall_us and sysl_monotonic_us. It is taken only on a target whose operating system has posix, and never where the program or a package supplies the same symbol. Tests: LibrarySupplierCliTests.
  • An #if in 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 #if gating and literate tangling hand the parser a new source whenever they change the text. A dependency's file with any #if in it, and any .lsysl file, 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.fs no longer names libc symbols that Android's Bionic lacks (4fc7352, d1b155c). An aarch64-android program reaching write_text_atomic, or any sysl.fs call that reads errno, failed at its link. On Android, errno is now read through Bionic's __errno rather than glibc's __errno_location, and the pending name's token comes from arc4random_buf rather than getentropy, 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 as underrun frames=512. Fields are quoted exactly as text quotes 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). text now 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 in TypeChargingTests and 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:6 comes from scala-parser-combinators, Set.scala:62:15 from the Scala.js scaladoc, and the rest are the build-infra -Xplugin and -classpath lines.
  • check-pointers.py against the site: 2240 pointers, 0 unresolvable.
  • The darwin tarball was extracted to a scratch prefix and checked there: sysl --version reports 0.0.161, sysl run works, and sysl doc dispatches to sysl-doc.

sysl 0.0.160 — __VERSION__, and defaults read at the type each call settles

Choose a tag to compare

@edadma edadma released this 03 Oct 10:53

__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 @tests file is no longer read by its consumer's build (a87760a, d04dbb7). A dependency's @tests files, and its @test functions 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 own sysl test — whether the dependency arrives through --lib or 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). What reference/modules.md § A @tests file states its own capabilities promised now holds.
    • Behaviour change: a consumer's @test that called a helper declared in a dependency's @tests file 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's sysl test and nowhere else.
    • Unchanged: a package's own sysl test compiles and runs its @tests files as before, and a program's own scaffolding is still analyzed by its build and then dropped.
  • An alias of an applied Drop type dies as that type does (45ed551). type M = Option[&Handle], held by a @no_os program, was charged nothing although its value runs Handle's destructor; it is now refused with "a 'sys.M' can die here, and its destructor reaches 'sys'", exactly as Option[&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 a const or a module-level val.
  • 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's stamp(v: string = __VERSION__) answers the version of whichever package the call sits in — which is what lets a library offer a --version helper that reports the program using it.
  • Refused, in the self-hosted compiler's wording, in a file with no package.hocon at 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 no version ("… and declares none").
  • The manifest travels on each source through AstCodec, so an importer re-analyzing a generic body still knows its package. AstCodec.Version 60 → 61: every cached .syslib is 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.
  • TraitCallableTests on 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:6 from scala-parser-combinators, Set.scala:62:15 from the Scala.js scaladoc, and the build-infra -Xplugin lines).
  • Documented on sysl.sh: reference/lexical.md § __VERSION__ is the package's version (the lone-file refusal, as a checked error block) and reference/declarations.md § Default parameters and named arguments (the type-parameter default, as a runnable block printing 2.5 7 440.5).

sysl 0.0.159 — a type costs what it runs, not what names it

Choose a tag to compare

@edadma edadma released this 03 Oct 00:37

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.IoError for its one @needs(os) function is importable by a @no_os program, or one whose target has os = 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 as Option[IoError]; and a @no_os program 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.

  1. Naming. Resolving a type, trait, variant or alias (typeKey, traitKey, variantKey, aliasedKey, and a qualified path through a type in throughModule) records EdgeUse.named with the key, and GatedModules.effective charges a named use nothing. An import whose every use only names a type is charged nothing likewise.
  2. Methods. A method call names no module, so GatedModules.chargeCalls reads 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 into edgeUses only, never the module graph, so they cannot make a cycle; requirements() now walks edgeUses' keys.
  3. Destructors. A named use whose type can die running code — it, or anything it holds through a field, &T, slice or array, has an impl 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

Choose a tag to compare

@edadma edadma released this 02 Oct 21:09

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 @needs function 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_os program and a project whose target has os = false. The call itself is still refused, at the call. In 0.0.157 the body was already covered, but the file-level import sysl.fs the body used, and a signature such as -> Result[unit, sysl.fs.IoError], still made the whole module require os, so the program was refused at its import line.
  • 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:

  1. 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.effective lifts 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.
  2. 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 @needs names. 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 without os
  • a module naming a gated type only in a @needs(os) signature is importable by a program without os
  • the same two through a target with os = false, and through package.hocon and --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 --version prints sysl 0.0.158, and sysl-doc is present. The @no_os program above prints 3, where 0.0.157 refused it at its import. The call to save is refused at the call (this reaches 'store.save', which needs 'os', and this module declared '@no_os'). A module naming sysl.fs.IoError in 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 sysl passes, and the installed sysl --version prints sysl 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 under sysl build . and 5 under sysl build-c <dir>.
    • Four repos fail the bare command by design and were run as their READMEs say. freertos against the FreeRTOS-Kernel POSIX port: 84 passed. libpq against a scratch PostgreSQL: 51 passed. quickjs-ng with its include path: 33 passed.
    • pico: build-lib --target thumbv6m-freestanding gets as far as the pico-sdk's generated cyw43_arch.h, which means the sysl type-check passed.
    • pico2 and zephyr refuse the bare command by design, through their requires { 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-profile and svd.
  • Outside the org, slate: a clean clone of slate-language/slate at dev (c359e6b, 0.1.14). With the tarball's binary, sysl test . passes 2,436 tests with 0 failures, and both sysl build . and sysl build . --features webview (the desktop edition) build.

sysl 0.0.157 — a @needs body is charged to its callers, not its module

Choose a tag to compare

@edadma edadma released this 02 Oct 17:40

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_os program 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 lacks os. 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 the import line as well.
  • An os-less program can link a library whose @tests file uses sysl.fs. A @tests file is dropped by every build except sysl test, and now its imports and bodies are dropped from the module's requirement too. musicbox 0.1.1 hit this: its notation_tests.sysl reads fixtures with sysl.fs, so a board program could not link it.
  • A @no_posix program can use sysl.fs. So can a project that declares os = true, posix = false. Before, sysl.fs imported sysl.posix.rand on every hosted target, so every program that reached the filesystem was made to require posix.
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 os imports the declaration beside a @needs(os) one
  • a closure written inside a @needs body is covered with the body
  • a board program that only renders links a library whose @tests file reads sysl.fs, and the library's sysl test still 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 posix reads the filesystem, and publishes through it, which is the part that draws entropy
  • sysl.fs on 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 to sysl.fs, for os
  • the POSIX module sysl.fs once imported, on a machine without POSIX
  • the same import in a shipping file (not a @tests file), 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 --version prints sysl 0.0.157, and sysl-doc is present. The @no_os program above prints 3. A @no_posix program printing sysl.fs.exists("/") prints true. Under 0.0.156 both programs were refused, the first at its import and the second at its reference to sysl.fs. The call to present is 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 sysl passes, and the installed sysl --version prints sysl 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 under sysl build . and 5 under sysl build-c <dir>.
    • Four repos fail the bare command by design and were run as their READMEs say. freertos against 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.) libpq against a scratch PostgreSQL: 51 passed. quickjs-ng with its include path: 33 passed.
    • pico: build-lib --target thumbv6m-freestanding gets as far as the pico-sdk's generated cyw43_arch.h, which means the sysl type-check passed.
    • pico2 and zephyr refuse the bare command by design, through their requires { 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-profile and svd.
  • Outside the org, slate: a clean clone of slate-language/slate at dev (c359e6b, 0.1.14). With the tarball's binary, sysl test . passes 2,436 tests with 0 failures, and both sysl build . and sysl build . --features webview (the desktop edition) build.

sysl 0.0.156 — a view inside a &sync box is refused

Choose a tag to compare

@edadma edadma released this 02 Oct 14:00

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 &sync box reached through another

Still accepted:

  • walking an array field of a &sync box with for
  • 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 &sync box

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 --version prints sysl 0.0.156, and sysl-doc is present. The example above is refused with the message shown; under 0.0.155 the same program compiled and printed 1. Indexing the field and walking it with for compiles and runs.
  • GitHub Packages: all four artifacts (the pom, the jar, the sources jar and the javadoc jar) answer 302.
  • brew: brew test sysl passes, and the installed sysl --version prints sysl 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.md were 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 to 40 71 made 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 under sysl build . and 5 under sysl build-c <dir>. musicbox, miniaudio and musicbox-miniaudio are green.
    • Four repos fail the bare command by design and were run as their READMEs say. freertos against a freshly built FreeRTOS-Kernel POSIX port: 84 passed. libpq against a scratch PostgreSQL: 51 passed. quickjs-ng with its include path: 33 passed.
    • pico: build-lib --target thumbv6m-freestanding gets as far as the pico-sdk's generated cyw43_arch.h, which means the sysl type-check passed.
    • pico2 and zephyr refuse the bare command by design, through their requires { 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-profile and svd.
  • Outside the org: slate is affected. A clean clone of slate-language/slate at 0.1.12 (f951392), built with sysl build ., is refused at two lines of its test file tests_actor_probe.sysl (180 and 182). Both lines slice box.said[0..<box.said_len] and box.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

Choose a tag to compare

@edadma edadma released this 02 Oct 00:59

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 + 1 was 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 = () -> 1 followed by x + 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 --version prints sysl 0.0.155, and sysl-doc is present. The 0.0.154 reproductions now behave as described above. val g = () -> x + 1 prints 42. x + g() after a closure prints 42. x() in a closure is refused with type 'int' has no method 'call'. The new sysl.sh block prints 11, 21, 2.
  • GitHub Packages: all four artifacts (the pom, the jar, the sources jar and the javadoc jar) answer 302.
  • brew: brew test sysl passes, and the installed sysl --version prints sysl 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.md program 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 under sysl build . and 5 under sysl build-c <dir>.
    • Four repos fail the bare command by design and were run as their READMEs say. freertos against a freshly built FreeRTOS-Kernel POSIX port: 84 passed. libpq against a scratch PostgreSQL: 51 passed. quickjs-ng with its include path: 33 passed.
    • pico: build-lib --target thumbv6m-freestanding gets as far as the pico-sdk's generated cyw43_arch.h, which means the sysl type-check passed.
    • pico2 and zephyr refuse the bare command by design, through their requires { 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-profile and svd.

sysl 0.0.154 — a comparison missing its right operand says 'expression expected'

Choose a tag to compare

@edadma edadma released this 01 Oct 21:58

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 if as 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.
  • 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 for y within it, and the store i32 9 landing 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 '..' expected refusal, 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 --version prints sysl 0.0.154, sysl-doc is present, and the comparison above reports expression expected at 2:13.
  • brew: brew test sysl passes, and the installed sysl --version prints sysl 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 under sysl build . and 5 under sysl build-c <dir>.
    • Three repos fail the bare command by design and were run as their READMEs say. freertos against a freshly built FreeRTOS-Kernel POSIX port: 84 passed. libpq against a scratch PostgreSQL: 51 passed. quickjs-ng with its include path: 33 passed.
    • pico: build-lib --target thumbv6m-freestanding gets as far as the pico-sdk's generated cyw43_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-profile and svd.

sysl 0.0.153 — start now, wait later; value-parameter defaults and trait value parameters

Choose a tag to compare

@edadma edadma released this 01 Oct 02:49

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.
  • opaque was 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.
  • --private showed private types and functions but hid private methods. It shows them.
  • A @param naming no parameter was rendered as though it documented one. It is dropped, and the CLI warns about it.
  • --check passed 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

  • DeferTests grows from 46 to 89: the nested jump refused at each depth and loop shape (a return under an if, break/continue of the surrounding loop, labelled jumps to an outer loop), jumps that stay inside the statement running correctly, and deferred locals — plain, ref and 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 an impl block; ValueGenericsTests changes 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...
Read more