Chuks v0.2.0-rc.1: The Release Candidate

Chuks v0.2.0-rc.1 is the build we believe is 0.2.0. Everything in it went through the same gate as a release: the golden suite on both backends, the fuzzed differential between the bytecode VM and the native binary, twenty-four differential suites, cross-compilation for five targets, and two consumer test suites that must print byte-identical output both ways. What that gate cannot supply is a program it has never seen. Most of what is fixed below was found not by a test but by building real programs in Chuks; the rest of the way to 0.2.0 is other people’s programs, which is what a release candidate is for.

chuks --version prints 0.2.0-rc.1. A fix found during the candidate ships as rc.2, rc.3 and so on, each picked up by chuks upgrade --prerelease; the build that survives is republished as plain v0.2.0, which chuks upgrade then offers everyone.

Build something with it, or point your existing program at it, in both execution modes:

The two must agree, byte for byte, on everything a program prints. Where they do not, or

where either of them is wrong, that is a bug we want. Report it at

https://github.com/chuks-programming-language/releases/issues/new?template=bug_report.yml

with the output of chuks --version and the smallest file that shows it. A message that

begins “Internal compiler error” is always ours; report it as it stands.

Programs written for v0.1.2 may need these edits. Each is a correction to something that was either undefined or different between the two execution modes; the checker reports the first three at compile time.

- A loop binds anew on every pass, and an ifbranch is a scope. Aconstorvardeclared inside a loop body used to be one slot for the whole function, so every closure made in the loop read the last iteration’s value, on the VM and the native binary alike. Now each pass gets its own binding: loop bodies, theforcounter,for...ofandfor...inelements,whilebodies andcatchvariables. A variable declared outside the loop is still one variable, shared with its closures. Code that relied on the old behaviour, typically a helper parameter used to freeze a loop value, can drop the helper.

- An async function is a function that returns a task. async function f(): Task<T>is assignable whereverfunction(...): Task<T>is expected and the other way round; theasynckeyword is no longer part of the function type. ATask<void>body no longer warns about a missing return, and reading a member of the awaited value through theTaskitself (f().xwithoutawait) is now an error.

- A call or spawnstanding alone as a statement drops its task, and a dropped task’s failure is reported. Before, a task nobody awaited could fail silently. A command-line program reports it at exit; an embedding host is told the moment it happens. If a background task is meant to be left alone, catch inside it.

- awaitand- spawnbind to the call, not to the rest of the expression.- "[" + await f() + "]"awaits- f()and joins the brackets; it used to parse as- await (f() + "]"), which the VM printed as “Task(pending)” and the native binary as “0”. The checker also rejects a- Taskused where a value is concatenated.

- String positions are bytes, and no accessor cuts a character. length,indexOf,slice,at,charAtandsubstringall count the same unit now, the byte, on both backends (the VM used to count characters for some of them). A position inside a multi-byte character snaps to the character:at,charAtands[i]return the whole character containing the byte,sliceandsubstringreturn the whole characters inside the range, andpadStart/padEndcount characters because padding is about width. Usefor (var c of s)ors.split("")to walk characters andruneCount()to count them. The optional second argument ofindexOf,lastIndexOf,includes,startsWith,endsWithandsplitis honoured on both backends (the native binary dropped it), andmatch/matchAllreturn the matches as a[]string.std/stringsis the string methods under module names and can no longer answer differently from them;strings.replacedeliberately replaces every occurrence wheres.replacereplaces the first.

- A caught error is its message. string(e)in acatchis the bare message on both backends; the file, line and trace belong to the report of an error nobody caught. Code that parsed a location out of a caught error’s text should stop.

- varis not how a- dataTypefield is declared.- dataType R { var x: string }was silently accepted by dropping the- var; it is now an error naming the fix.

- Assigning to an imported name has been an error since v0.1.1 and stays one.

- A reserved word is an ordinary name wherever a name cannot be an expression: a

dataTypefield, a map key, an interface or class member.dataType Rec { from: string, in: string, class: string }parses, because a payload’s fields are not the language’s to rename. A reserved word is still not a binding.

- A type parameter may not be named after a builtin type, or listed twice.

- A function or class may be named Task,ChuksError,add,eq,Divor anything else the native runtime uses internally; the runtime keeps out of the way.

- Two modules may each declare a private variable, type, function or class under one name; a name means what the module that uses it says it means, on every layer.

- catch,- finally,- extends,- implements,- as,- instanceof,- inand- ofmay open a line; the statement they continue is the one above.

- concaton arrays.- a.concat(b)returns a new array, on both backends.

- A literal takes its type from where it lands. A record literal nested in another

({ state: { checked: true } }against adataTypefield), a literal initialising an annotated class field (private session: Session = { ... }), a typed map value, a typed list element, an assignment, an argument and a return are all checked against the declared type, with unknown keys, wrong values and missing fields reported inside.

- Generics across modules. A generic base class imported from another module can be

extended (class FeedStore extends Store<Feed>), a generic function called from a specialised class method gets its own typed copy, and a base-class method that reflects overthis(json.stringify(this),this[k] = v) sees the whole object, subclass fields included, in the native binary as on the VM. Nothing is widened toany.

- An imported base class links to the module it came from, under the name that module

exported it as. Two packages exporting a Storeno longer cross, andimport { Base as B }; class X extends Bworks on both backends.

- Error messages name values as the program knows them. “string index must be an integer, got string”, never a runtime type name. A value that refuses an index, a lock or a slice reads the same on both backends.

- An awaited task’s failure reports from the throw through every await, on both backends: the trace reads from where it was thrown down through each coroutine that awaited it. A runaway recursion is a runtime error with a trace that elides the middle of the stack, not a crash.

- The native runtime, embedded. A host that embeds a Chuks program (chuks build --c-archive) can hand it its time zone (chuks_set_timezone), is told of a dropped task’s failure the moment it happens, and survives a call into the engine that fails: the engine recovers and unwinds instead of taking the process down.

- chuks serve: linked packages (a symlink in- chuks_packages/) reload like the app’s own files; a restarted server resyncs a device that saw an earlier run; two devices never share a pack in flight.- chuks installkeeps a package’s- tests/and- examples/out of the consumer’s tree.

- chuks upgradeunderstands pre-release versions and never moves you to an older build unless you name it.

Found by building real programs rather than fixtures; each has a golden fixture now and, where it fits, a differential cell:

- A method called through anythat returns a slice or map came back empty in every program that importedstd/strings.

- this.x = nullon a nullable class field compiled to nothing.

- A type the runtime references lost its methods when hoisted between generated files.

- A failed HTTP request returned nothing instead of throwing its cause.

- A module whose only runtime use was ??failed to build (undefined: Nullish).

- A dynamic field set through a generic or anyholder dropped a slice; a map assigned where a record was declared did not become the record;!=between two functions crashed.

Performance: a literal passed to a Record | []T parameter is built as the record (no

hydration per call), and a dynamic field read is a switch with a cached plan instead of a

reflection scan.

Since v0.1.2: 30 commits, 160 files, +8,612 / -1,616. Every change ran the twelve-stage release preflight: the golden suite on both backends, the VM-to-native differential fuzz, 24 differential suites, cross-compilation for five targets, the installed-package consumer build, and two consumer test suites byte-identical on both backends.