Repository navigation
Visual X# 0.5.0: lazy evaluation, match, enums, console output, method references and native executables - #42
Conversation
Deep nesting ended the compiler with a stack overflow and no diagnostic. On the one-megabyte stack a Windows process starts with, a sum of 100 operands, 100 nested parentheses, an `if` nested 100 levels deep and an `else if` chain of about 1500 links each overflowed. Two causes, found with stack traces: - The native stages after Core recurse once per level of nesting, and the frames of the Core wire reader are several kilobytes: about 80 levels fit in one megabyte. - The Core-to-CorePrep adapter copied every function before preparing it. Copying nested statements recurses once per level, and an `else if` chain is one level per link. Changes: - Add Visual::XSharp::Support::RunOnCompilerStack and run `vxs`, `vxsi`, the source fuzz targets and the smoke program on a thread with 256 MiB of reserved stack. The stack is reserved, not committed: on Windows the thread is created with STACK_SIZE_PARAM_IS_A_RESERVATION, because the default would commit the whole size on every run. - Add Visual.XSharp.NestingLimits to the frontend. A function body that nests statements more than 256 levels deep reports VXP0039 and one that nests expressions more than 1024 levels deep reports VXP0040, each once per function at the first node that is too deep. An `else if` chain is not nesting. The body of a match arm counts one level per arm of its match, up to seventeen, which is how deep its lowering nests. - Bound the nesting of statement bodies in the native Core wire reader and writer at 4096 levels, as expression depth already was. The links of a chain share a level and an empty body costs none. - Stop copying functions in the adapter: the work queue refers to the functions of the module, and lifted closures live in a deque. Measured on the compiler stack, the native stages pass 4000 levels of statements and of expressions. The limits leave room for sanitizer builds and for the levels a lowering adds. Verification - NestingLimitTests.hs nests every construct to the limit and one level beyond, checks the position of the diagnostic and runs the accepted programs on both forms of Core. - NestingLimitTests.cpp pins the wire limit and walks 1500 levels and a chain of 3000 links through the native Core stages on the compiler stack. - BranchingExecutionCases.cpp runs 255 nested `if` statements and a sum of 1024 operands through both pipeline modes. To keep the smoke program within its watchdog under sanitizers, the wide match and the long chain now run on two subjects each. - The source corpus gains seeds for deep nesting, for both limits and one level beyond each, and for long chains. Compile time still grows faster than the program for long `else if` chains, long statement sequences and nested loops; the changelog records the measurements as known limitations. The specification is unchanged.
Native stack use. The Core wire reader, the Core verifier and the Core-to-CorePrep adapter held statements, instructions and diagnostic strings in the frames of the functions that recurse once per level of nesting. A statement is now read into its place, every kind of statement and expression has a function of its own, and instructions and diagnostics are built by functions that return before the next level is entered. Chains of operators, of conditional expressions and of let bindings are walked in a loop. Measured with the new stack_probe program, the native pipeline uses 0.67 KiB of stack per statement level instead of 14.9 and 0.42 KiB per expression level instead of 10.6; a sanitizer build uses 1.67 and 0.67. Wire.cpp is split into Wire/Decode.cpp and Wire/Encode.cpp. The stack size and the nesting limits are unchanged. Compile time. The integer analysis repeated the fixed point of an inner loop on every pass over the loop around it, so each level of nested loops multiplied its time; it now iterates only loops that hold at most one further level of loops. The Haskell Core wire encoder, the CorePrep lowering and the CorePrep verifier no longer copy their output once per level or per block. Fifty nested loops compile in 0.3 seconds instead of not finishing, and an else-if chain of 2048 links is lowered in 20 ms instead of 2.2 s. The Criterion suite gains benchmarks for nested loops, chains and sequences, and Benchmarks/2026-10-04-Nesting-And-Chains.md records all measurements before and after. Smoke program. The execution tables compile every distinct body once and check all of its runs in that program, with up to eight small bodies per program, instead of compiling a program per run. All 237 runs are kept, and the large programs are run on the arguments that had been removed to save time. The watchdog is unchanged. Language. The comma after a match arm is optional, as in the grammar. A pattern binding is an ordinary local and may be assigned. An if with two value blocks and a match are the value of a value block they end. The arms of a match lower to one else-if-shaped chain, so the body of an arm is one level below its match however many arms there are. Documents/BRANCHING.md describes the implementation and no longer lists choices as if they were language decisions. Also fixes the three clang-tidy findings of the previous commit.
…e limits Language, following the decisions recorded in Spec/Language/Decls.vxs examples 295 to 309 and in the grammar: - A numeric constant pattern may be negative. The minus sign belongs to the numeric literal that follows it; no other expression is a pattern. The constant is checked against the type of its subject, so the most negative value of a signed type is a pattern and a negative constant for an unsigned subject is rejected. - A block used as a value may leave with return, break or continue under the rules those statements have anywhere else. A block that cannot complete has no value and is lowered as its statements alone; the expression takes its type from the blocks that complete, and VXT0062 reports an expression none of whose blocks does. - The else block of a guard is checked by its control flow: a loop that cannot end and a statement match that always selects an arm and whose arms all leave are accepted, as is leaving before the last statement. - An if with an else whose other block leaves is still the value of a value block it ends. Method bodies are unchanged. Three ways of leaving a value block are recognized and reported as not implemented: break or continue in the condition or update clause of a loop, a break that carries a value, and a return where the enclosing return type is inferred or inside a loop used as an expression. The binding form of guard still needs optional values. Chains. A chain of a binary operator no longer counts against the expression nesting limit: the left operand of a binary operator is at the level of the operator in the frontend limit and in both Core wire codecs. The native wire writer walks such chains in a loop like the reader, the verifier and the adapter, and the destructors of the native Core expression and statement release operands and nested statements from a list, so no native stage uses stack per link of an operator chain or of an else-if chain. A sum of 50000 operands and a chain of 20000 links are handled on a one-megabyte stack. The native wire reader reads every expression into its place. Nesting in a second operand cost 7.1 KiB of stack per level and costs 0.37. Compile time. Constant propagation asked the integer facts about every node of an expression and the CorePrep lowering collected symbol identities by appending lists; both were quadratic in the length of an operator chain. The facts are now consulted for Boolean results only and only while the condition has at most 256 nodes. A sum of 50000 operands compiles in 9 seconds instead of 198, and 200 comparisons joined by && in 1.1 seconds instead of 47. The stack size and the limits of 256 and 1024 levels are unchanged.
An if or match expression none of whose branches completes normally is valid again; VXT0062 is gone. Whether an expression completes is kept apart from types in a new module, Visual.XSharp.Completion, which the type checker and the desugarer share. The place that would have received the value is not held to a type, because it is never reached, and it is not lowered: the statement becomes the statements that run until control leaves, with no result slot and no placeholder, and the statements after it are checked but not lowered. Operands evaluated before the one that never completes keep their effects. Where some branch completes, the expected type and the assignment of the result slot follow the completing paths, as before. Spec example 310 records the rule. The parts of the specified language that the compiler recognizes and rejects as not implemented keep their diagnostics and are listed as pending work, with what each needs, in Documents/BRANCHING.md. The binding form of guard, if and while is among them. None is written into the specification as a restriction. Stack. CommittedStackBytes reports what a thread's stack has committed, and two measuring programs print it: stack_probe for one native stage and the new source_stack_probe for a whole compilation. The benchmark record gains the peak stack of whole compilations at and beyond the frontend limits in an ordinary and a sanitizer build, the Core nesting the lowering produces per source level, the reserve and commit of compiler processes alone and eight at once, and what was not measured. The largest commit at the limits is 2.5 MiB, 4.6 MiB under the sanitizers; the reservation costs address space only. The stack size and the limits are unchanged, and the limits are documented as limits of this implementation, not of the language.
Five uses of a block used as a value were recognized and rejected as not implemented. They are implemented now, through the type checker, the completion analysis, the lowering and the verifiers, and run through LLVM with and without optimization: - `break` in the condition of a loop. The condition belongs to its loop, so the transfer leaves that loop, with the effects of the condition up to it. - `continue` in the update clause of a `for`. It ends the update and the condition is tested next. An update clause that holds one is lowered to a loop that runs once, which the transfer leaves. - a `break` that carries a value to the loop expression around the block. - `return` inside a loop used as an expression, directly and from a block. A loop expression that returns and never breaks yields no value and is valid; one that neither breaks nor returns is still VXT0042. - `return` in a callable whose result type is inferred. Returns are collected through expressions and stop at nested callables. Returns of different types are VXT0062, which no longer has its former meaning. VXT0045 and VXT0047 are retired. VXT0059 remains for the two header cases that are still pending: `continue` in a loop condition, whose target the specification does not state, and `break` in an update clause. The Core verifiers, Haskell and native, accept a body that ends in a loop that cannot be left: a literal true condition and no `break` of that loop. Such a loop is what a method that only returns from inside an endless loop lowers to, and it was reported as a missing return. Completion analysis: a loop does not complete only when nothing breaks out of it and its condition is the literal true or never completes; a transfer inside an expression does not make the expression non-completing unless every path takes it. Tests cover short-circuit operators, operand order, match guards, nested loop targets and the condition of a do/while, which runs after the body and after a `continue` and is skipped by a `break`. The execution tables leave `source_fuzz_smoke` for a program of their own, `source_execution_smoke`. As one program they ran 221 seconds in the fuzzing configuration under a 240 second watchdog, and a second attempt that passed was not a fix. No case was removed and the watchdog is unchanged. The new program also compiles and verifies programs that own closures while control leaves through a value block. Documentation: the figure of `source_stack_probe` is named for what it is, committed stack, an upper bound to the page that excludes frames a sanitizer keeps on a heap-allocated fake stack. The pending list moves from BRANCHING.md to IMPLEMENTATION.md, and no longer suggests that a `never` type was decided. It gains two gaps found here that `main` has as well: calls of methods with an `auto` return type and callables created inside callables fail in the Core verifier.
…erate tables Loop headers. The condition and the update clause of a loop belong to that loop; Spec/Language/Iteration.vxs states the rules in examples 79 to 83: - a `break` in the condition or in the update clause leaves that loop; - a `continue` in the condition abandons the rest of the condition and evaluates the condition of the same loop again, without the body and, in a `for`, without the update; a condition that always continues is an endless loop; - a `continue` in the update clause skips the rest of the update and goes on to the condition; - a callable is not inside the loops around the place that creates it. The two transfers that were still rejected are lowered. A `for` condition that holds a `continue` is evaluated in a loop of its own, which the `continue` repeats, because the Core `continue` of the `for` would run the update. A `break` in an update clause is the Core `break` there; in an update clause that is wrapped for a `continue` it sets a flag bound before the loop. VXT0059 is retired. Inferred return types of methods. A call of a method declared with `auto` passed the frontend with an unresolved type and failed in the Core verifier (VXC1018), on `main` as well. Return types are now inferred before any caller is checked, in rounds over the whole catalog: a call of a method that is not inferred yet takes no part, so recursion is inferred from its base case, chains from their end, across classes and against declaration order. A method with no result independent of itself is VXT0063, as example 140 of Decls.vxs requires, and one whose returns disagree is VXT0062. No verifier check changed. Callables inside callables. The free names of a callable counted the parameters, captures and locals of a callable nested in it, so the outer one captured names that exist only inside the inner one and the Core verifier rejected the module (VXC1020), on `main` as well. A nested callable now contributes only what it reads from further out. Tests. The reference Core evaluator runs closures and leaves a loop from its update clause. InferredReturnTests.hs runs inferred methods, nested callables, captures and callables alive across the new transfers on the unoptimized and the optimized Core. The native smoke program runs the new transfers and a hand-written table of inferred methods through LLVM, and compiles nested callables through every stage and its ownership verifier; its JIT still does not link closures. Generated tables. The branching and leaving cases are written once, in Compiler/Fuzzing/Cases. The new Go helper `execution-cases` generates the C++ rows and the Haskell module from them; its `check` command, its tests and CI fail on a stale table. This replaces a script outside the repository. One source keeps the tables equal and is not an independent oracle; the tables and tests that do not come from these files remain. Also: the clang-tidy finding of the previous commit, a move inside an assertion macro in a new test; IMPLEMENTATION.md no longer lists implemented forms as missing; the nesting limits and the stack reservation are documented as the limits this version ships with, with the note that stack use is measured on Windows only; the literal rules of the type checker move to a module of their own to keep the file within its limit.
No test ran a closure through LLVM: a program that creates one calls the AARC runtime, the JIT resolves a runtime symbol in the process that hosts it, and the smoke programs did not link the runtime, so ORC failed on `vxs_aarc_allocate`. `source_execution_smoke` now links the runtime and exports its entry points, with `/EXPORT` on Windows and `-rdynamic` elsewhere, and checks the ABI version of what it linked. It runs a hand-written table of closures in both pipeline modes: created and called, nested two and three levels deep with implicit and explicit captures, returned from the call that created them and kept, a capture initializer evaluated once, return types inferred through a value block and a loop expression, and closures alive across a `break` in a loop condition and in an update clause. The expected values are worked out by hand from the capture rules. Neither the JIT session nor any product target changes; the other fuzz programs still do not link the runtime.
Running closures through LLVM in the previous commit made LeakSanitizer fail the Linux jobs: every closure object leaked. That was not a fault of the test. No stage of the compiler wrote a retain or a release; the vocabulary and its verifiers existed, the placement did not, and nothing had executed an AARC object before. `Visual::XSharp::Xpp::PlaceOwnership` runs once on the Xpp form lowered from CorePrep, after the Xpp optimizer when that is enabled, and gives every AARC value an owner: - a parameter is borrowed, a result is owned; - a local is released after the instruction that uses it last, directly after its definition when nothing uses it, and on the edge on which it dies when it lives on another edge of the same branch, in a block of its own when the target has other predecessors; - a copy of a value that is used again retains; a copy of a value that is not used again takes over the reference; - a returned parameter is retained for the caller; a discarded owned result is given a symbol and released; an instruction that reads the symbol it writes reads the old value from a symbol of its own; a parameter the body assigns is copied into a local at a new entry block. Liveness is computed for the owned symbols with the analysis the optimizer uses. A value that is live at a point is defined on every path to it, so no release tests whether there is anything to release. A method used as a value, not called, was the address of its code, and calling it read an invoke pointer out of that code. The pass makes it a closure without captures where it is used. Tests: - OwnershipPlacementTests.cpp runs each placed function on a reference-count model that knows nothing of the pass, along every path and twice around every loop: each reference released exactly once, nothing used after its last release. The model is first shown to reject a leak, a double release and a use after release. The output must also satisfy the Xpp ownership verifier. - The AARC runtime counts its live allocations, a C++ entry point outside the C ABI. source_execution_smoke runs 26 closure programs one at a time and fails the program that leaves an allocation behind, in both pipeline modes and on every platform. With the pass switched off that check fails on the first program. - The new closure programs pass closures to methods, return them, choose between them, reassign them in loops and from themselves, create them on one branch only, discard them, and name methods as values. The frontend tests run the same programs in the reference evaluator, which now treats a method value as a closure.
`CommittedStackBytes` returned zero outside Windows, so the stack use at the nesting limits was measured on one platform. On Linux and macOS it now counts the resident pages of the thread's stack mapping, which are the pages the thread has touched, with `mincore`. The initial thread of a process, whose stack is not one mapping, still reports nothing. `source_execution_smoke` compiles a program of 1023 calls nested in each other's arguments, the shape that costs most stack for each level, and ends by printing the stack its compiler thread committed. On Windows that is 2584 KiB in an ordinary build and 4736 KiB under ASan and UBSan, in line with the figures recorded earlier for that shape. The line is how the figure will be read on the other platforms from their CI logs. The Ubuntu clang-tidy job failed on the previous commit: four assertions of a new test moved a vector inside the assertion macro. The values are bound to locals first.
The CI jobs of a0b435a printed the stack the compiler thread committed for a program of 1023 nested calls, the worst measured shape, on every platform: Windows Server x86-64 2580 KiB 4736 KiB ASan+UBSan Ubuntu 26.04 x86-64 2588 KiB 3060 KiB ASan+UBSan 2880 KiB TSan Fedora 43 x86-64 2588 KiB 3060 KiB ASan+UBSan macOS 15 arm64 2480 KiB 5536 KiB ASan+UBSan 2752 KiB TSan macOS 26 arm64 2480 KiB 5536 KiB ASan+UBSan 2768 KiB TSan The ordinary builds are within 5 percent of each other. The largest figure is 2.1 percent of the 256 MiB reservation. The Linux sanitizer figure leaves out the frames the runtime keeps on its fake stack, and the benchmark record says so. The documents no longer say that stack use is measured on Windows only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`enum Name { A, B = 2, C }` declares a value type whose members are named
integers, with an optional underlying integer type written
`enum Name = byte { ... }`. Members are numbered from zero, a member without
a value follows the one before it, and two members may share a value, as
examples 37 to 43 of Spec/Language/Decls.vxs say.
Three points the specification left open are decided and written into it as
examples 311 to 315:
- values of an enum are compared with `==` and `\=`, with values of the
same enum, and take part in no other operation: an enum has no order and
no arithmetic;
- there is no conversion between an enum and an integer in either
direction;
- the target-typed `.Member` spelling is the same for a classic enum as for
an enum class.
`Name.Member` is a value of the enum. `match` over an enum uses `.Member`
patterns; a pattern accepts the value of its member, so two members with one
value are one case and the second arm for it is unreachable. A match that
names every value needs no catch-all arm, alone or in combination with other
enum and `bool` subjects, up to 256 combinations.
An enum type carries its underlying type and its distinct values. That is
what lets the lowering store it as an integer and the coverage rule count
its values without a table of declarations. An enum never reaches Core as a
named type: named types are references there.
Not implemented, and listed as pending rather than forbidden: `.Member` in
an expression, enums declared inside a class, constant expressions as member
values, and `enum class`.
Tests: EnumTests.hs pins the declaration, the numbering, every accepted and
rejected operation, and runs 14 programs in the reference evaluator on both
Core forms. source_execution_smoke runs the same programs through LLVM in
both pipeline modes; its harness takes declarations that stand before the
class. `enum` is a reserved word.
The type checker's catalog, template context and type resolution move to a
module of their own to keep the file within its limit.
Visual X# is a lazy language with call-by-need evaluation, thunks that the source cannot see and an effect system that the source cannot see either. The specification did not say so: it used the word only for generators, BLINQ queries and template members, and the compiler evaluated everything where it was written. Spec/Language/Evaluation.vxs now states it. A value is computed when it is first needed and at most once; a value that is never needed is never computed, so it neither fails nor runs without end; an argument is passed by need; a value that cannot be computed fails where it is needed. Effects are not lazy: a store happens where it is written, and a value means what its variables held where it was bound. `.Strict` and `.AllStrict` belong to BLINQ and make nothing else strict. The compiler implements the first part. A local binding of bool, numeric or enum type that is never assigned again, that no closure captures, and whose initializer has no effect is computed by the first read that is reached: - the binding declares the local, a flag, and a copy of each variable its initializer reads that is assigned anywhere in the function; - each read tests the flag and, the first time, evaluates the initializer over those copies; - a binding nothing reads computes nothing. No new form of Core is involved, so every later stage is unchanged. The body of a method is lowered twice, the first time only to learn which locals are assigned and which are captured. A value is computed in place, without a flag, where no program can tell: its initializer cannot fail and reads no deferred value, or the next statement is certain to read it. The second case assumes that a call needs its arguments, which is true of the compiler and not of the language; the documentation says that it must be narrowed when arguments are passed by need. Still computed where they are written, and listed as pending in Documents/EVALUATION.md: arguments, results, values of other types, variables that are assigned again, captured bindings, bindings in callable bodies and discarded values. Tests. LazyEvaluationTests.hs observes laziness through a division by zero, a call that never returns and the step budget of the reference evaluator, which shows that a value needed fifty times is computed once. source_execution_smoke runs the same programs through LLVM in both pipeline modes, where a computed division by zero would end the process. Seven optimizer tests used an unused division as the thing that must not be deleted; an unused value is no longer computed at all, so they now read the value in a condition, which keeps their question the same.
Evaluation by need. The body of a callable now defers its bindings as the body of a method does, from its own assigned and captured locals, also when the callable stands inside another. An expression written as a statement, and a value assigned to the discard, are evaluated: the statement is the need. Example 12 of Spec/Language/Evaluation.vxs states that rule. Enums. The target-typed `.Member` spelling is accepted in an expression wherever an enum type is expected: a declared type, a parameter, a return type, the right operand of a comparison, the variable assigned to. The parser keeps the missing receiver as the unit literal, which no source can write, so no new expression form is needed. Where no enum type is expected the spelling is VXT0069. A conditional and a loop expression may yield an enum; they were restricted to bool and numeric results, and an enum is its underlying integer below the type checker. One parser test said that a dot without a receiver is rejected. It now pins the shape the parser gives it, and the type checker's rejection of a call through it is pinned in EnumTests.hs. Tests run the new programs in the reference evaluator on both Core forms and through LLVM in both pipeline modes. Sharing a value needed twice. Every read of a value by need carries the computation of that value behind its flag. An expression that read one value twice carried it twice, so the Core of a chain of bindings that each read the one before it twice doubled with every link: twelve links were 64 times the Core of six. An expression that is certain to read such a value more than once now computes it ahead of itself and reads it as an ordinary local; a chain of 40 links is a test. Reads in different branches of one expression are not certain and still carry the computation each; that is recorded as pending in Documents/EVALUATION.md and ends with thunks as values. Smoke programs. With the tables for evaluation by need and enums, source_execution_smoke ran past its 240 second watchdog in the fuzzing configuration. The tables written by hand for single features, with the closure programs and the runtime they link, are a third program, source_feature_smoke. The watchdog is unchanged and no case was removed. Apart they take 91 and 45 seconds in that configuration on the same machine.
Leitwolf11
left a comment
There was a problem hiding this comment.
Review notes
Scope. This pass read the lowering for evaluation by need (Desugarer.hs, Desugarer/Laziness.hs) and the target-typed .Member path (parser and type checker) line by line. Ownership placement and the rest of the diff were not read in this pass, so these notes say nothing about them. This is not an approval.
Fixed in 56e170b
Core grew exponentially along a chain of values by need. Every read of a value by need carries the computation of that value behind its flag. An expression that read one such value twice carried it twice, so a chain of bindings that each read the one before twice doubled with every link: the Core of twelve links was 64 times the Core of six. An expression that is certain to read such a value more than once now computes it once ahead of itself (neededTwice). LazyEvaluationTests.hs holds the size comparison, which failed before the change, and a chain of 40 links; source_feature_smoke runs a chain of 24 through LLVM.
Open
The same growth remains for reads in different branches. In int b = (c ? a : 0) + (d ? a : 1); neither read of a is certain, so each still carries the computation, and a chain of such bindings still doubles with every link. The result of the program is right; the cost is compile time and code size. Hoisting the computation would evaluate a value that may not be needed, so the fix without changing meaning is a thunk that a read can call. Documents/EVALUATION.md lists it as pending. Until then a short pathological source can make the compiler run very long, which the nesting limits do not catch.
Notes, not blocking
.Memberis encoded with a sentinel. The parser keeps the missing receiver as a unit literal insideMemberAccessExpression. It works because no source can write that literal, but every pass that walks the AST has to know the convention. A constructor of its own would make a forgotten case a compile error instead of a silent misreading.- Calls are treated as free of effects.
deferrableExpressionlets a call be deferred. That is right today, since the implemented subset has no input, output or shared state, and the document says so. The first built-in with an effect needs an effect analysis before it lands, or the effect moves to the first read. - Order of failure. Computing a value ahead of the expression that needs it, here and for a binding the next statement is certain to read, can change which of two failing computations fails first. The comments state this; the specification should state whether that order is observable behaviour a program may rely on.
Evaluation by need gives the values that one statement needs no order among themselves. Example 13 of Spec/Language/Evaluation.vxs now says what a program may rely on: whether it fails is determined, and which of several failures one statement meets is not. A value that runs without end counts as one that cannot be computed, so a statement that needs it and a value that fails may fail or may never end. Statements are carried out in the order they are written, and so are effects. The compiler already depends on this in two places: it computes a value in place when the next statement is certain to read it, and it computes a value ahead of an expression that is certain to read it more than once. Documents/EVALUATION.md refers both to the rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`vxs check` on one class with 1000 small methods took 4.3 seconds, with 2000 it took 16 and with 4000 it took 73. Five places took time with the square of the number of functions or members. What each stage accepts and reports is unchanged. Native CorePrep verifier. For every function it entered every function of the module into a table of its own, building the function type of each, and searched the whole module for closures that capture into the function. That was 14.4 of 17.7 seconds for 2000 methods. The functions, their spellings and the captured parameters are now collected once for the module; a function looks its own definitions up beside them. A function of the module still takes precedence over a local definition with its identity, a function's own spelling still comes first, and a conflict among the function symbols of the module is still reported for every function that sees it. Renamer. The names in scope were a list of pairs, so every lookup and every duplicate check walked all members of the type. They are a map; a later binding replaces an earlier one, which is the shadowing the list gave. Type checker. A method was compared with every earlier member to find two overloads with the same parameters. It is compared with the earlier methods of its own name. Haskell Core verifier. Every function was looked up in the list of source owners; the owners and the source files are sets. CorePrep lowering. The functions lifted out of closures were appended to the list of functions still waiting once for every function, also when there were none, which wrapped the list once more each time. Lifted functions now wait in batches of their own and are prepared in the same order as before. Now 1000, 2000, 4000 and 8000 methods take 1.4, 2.9, 4.8 and 10 seconds. Benchmarks/2026-10-07-Many-Methods.md has the measurements by stage. Tests. CorePrepVerifierTests.cpp pins what a function sees of its module: a call of a function declared later, a module of 3000 functions, an undefined symbol, a conflicting spelling in a use and among the functions, the precedence of a function over a binding with its identity, and a parameter that may be assigned only when a closure captures into it. The same cases pass on the verifier as it was. The CorePrep benchmarks gain a case by number of functions, which Google Benchmark fits to linear time.
The value of an enum member could only be an integer literal with an optional minus sign; the grammar allows an expression there. A member value is now a constant integer expression: integer literals and earlier members of the same enum, joined by the arithmetic, shift and bitwise operators, as in `WRITE = READ << 1` and `ALL = READ | WRITE | EXECUTE`. Language decision. Inside the declaration of its enum the name of a member stands for its number: that is where the numbers behind the names are defined. Everywhere else a member is a value of the enum, and the rules that there is no arithmetic on enum values and no conversion between an enum and an integer stand unchanged. A value names earlier members only, of its own enum only. Examples 316 and 317 of Spec/Language/Decls.vxs specify this. The parser keeps the expression as it was parsed; its names are members, which are named by spelling and take no symbols. The type checker gives the expression the underlying type of the enum, replaces earlier members by their numbers and lets the constant evaluator of the language compute it, so the arithmetic, the rounding of `//`, the complement of `!` for the width and signedness of the type and the refusal of a division by zero are those of any other constant. A member without a value follows the computed one. VXT0070 reports a value that is not such an expression or has no value; VXT0068 still reports a value its type cannot hold. A member whose value is refused is numbered as if nothing had been written, so one mistake is reported once. VXP0041 is retired. The nesting limit of expressions covers member values. Tests run enums with computed values in the reference evaluator on both Core forms and through LLVM in both pipeline modes, and pin each refusal. Documents/BRANCHING.md and Documents/IMPLEMENTATION.md still said that enum case patterns are not implemented; they are, and the two passages now say so.
Arguments by need. An argument of bool, numeric or enum type that has no effect is computed when the method first needs it, at most once however often the method reads it and however many methods it is handed through, and not at all when no method needs it. A value the caller needs as well is computed once for both. - Core has a new primitive, Memoize: a callable without parameters whose result is bool or numeric becomes a callable of the same type that calls it at most once and remembers the result. CorePrep, Xpp and Xmm carry it, each verifier checks its rule (VXC1073, VXC0025, VXC1074, VXP1047, VXL1053), and LLVM lowers it to an AARC object with an invoke thunk and a destructor of its own. Wire versions: Core 9, CorePrep 7, Xpp 6, Xmm 6. - The frontend decides per method which parameters are passed by need: a parameter of a suspendable type that the method is not certain to need before anything else it does that can be observed. Such a method gets a second function that takes suspended computations; its own function and ABI are unchanged, and a call with nothing worth suspending still uses it. - A local whose value may be handed on is suspended in a computation of its own, so caller and method share one computation. - Still computed at the call: an argument of a call through a callable, an argument a closure of the method captures, arguments of other types and arguments of template methods. Native executables. Building the programs above into executables and running them showed four defects, none of which a test could have seen, because no test ran a produced executable with more than a trivial body: - A method without a result whose body ended without a return was closed with an unreachable marker by both Core-to-CorePrep adapters, so a Main without a final return executed an invalid instruction. Reaching the end of such a body now returns. - An executable is linked without any library, and code that creates a closure calls the ownership runtime, so such a program did not link. The module that holds the entry now defines allocate, retain and release itself, over a 64 MiB arena whose released blocks are reused. - An integer quotient or remainder by zero, and a shift outside the width of its operand, were lowered to instructions LLVM gives no meaning, and an optimized program ran on past them. Each is now preceded by a check that stops the program. Example 14 of Spec/Language/Evaluation.vxs states the rule. The least value divided by minus one is carried out so that it wraps like a sum that does not fit. - vxs run -File with a relative path looked for the executable it had just built on PATH. Tests: MemoizeTests.hs and FallThroughTests.hs (cabal: 5766 pass), MemoizePipelineTests.cpp, FallThroughTests.cpp, ComputabilityExecutionTests.cpp, 23 by-need programs in source_feature_smoke with a leak check each, and executable_run_tests, which builds programs into executables and runs them as processes. Cost. A suspended argument is two runtime objects and, in generated code, a function for the argument and the entry it is called through. Three changes keep that down: a module has one entry, one destructor and one metadata record for the callables that remember a result of one type; a closure that owns nothing has no destructor; and the native Core verifier enters a branch, a loop body, a let or a closure without copying every visible definition, which had made its time quadratic in the size of a function (2.05 s to 0.04 s on a function of 400 calls with four suspended arguments each). The execution tables pass every input as a call, so their programs still grew past the 240 second watchdog under the sanitizers; the expression and leaving tables now run in source_expression_smoke. The watchdog is unchanged and no case was removed.
The format check of the Windows job rejected the lambda in main, which was written by hand and never run through clang-format. No other tracked source differs from the formatter.
Programs can write. Console.Print, Println, Printf, Printfn, the four Error forms and Console.Format are implemented, with System imported implicitly, and Examples/HelloWorld/HelloWorld.vxs compiles to a native executable and runs. Language, decided here and written into the specification: - A format is a string literal, checked when the program is compiled. Conversions %d %u %x %f %s %c %b, with %n and %%; flags - 0 + space # and '; a width or precision written as * is an int argument before the value. Which flags each conversion takes, that %f rounds the exact value with a tie to the even digit, and that a line ends with the terminator of the platform are ConsoleIO.vxs examples 69 to 85. - + joins strings and writes a value that is not a string as text; += appends; == and \= compare the characters two strings hold, where they compared the places the strings were kept. Operators.vxs examples 86 to 89. - Output is an effect: an expression that writes, itself or through a method it calls, is evaluated where it stands and in order, while a value that only computes is still by need. Evaluation.vxs examples 15 and 16. Compiler: - Core has a new primitive, the runtime call: a function of the runtime named by a literal first operand, its identity in a catalog of thirteen. CorePrep, Xpp and Xmm carry it and every verifier checks a call against the catalog (VXC1075, VXC0026, VXC1076, VXP1048, VXL1054). Wire versions: Core 10, CorePrep 8, Xpp 7, Xmm 7. - The type checker reads the format and rewrites a console call and the string operators into runtime calls (VXT0071 to VXT0079); no later stage knows what a format is. - The frontend infers which methods may write, from all methods of a program together, and evaluates an expression that calls one where it stands instead of deferring or suspending it. - Ownership placement gives a string literal used as an operand a symbol of its own. Such a literal was never released. Runtime: - Compiler/Runtime/Text: joining, comparing, the conversions and the console write, without a standard container or a C runtime function. %f is the exact decimal expansion, from a bounded natural number. - A native executable is linked with vxs-runtime.lib: the ownership and text runtime as one object that needs no C runtime, built from the sources a JIT host links. It replaces the ownership functions the compiler generated into each executable and their 64 MiB arena. The linker makes the import library for the seven kernel32 functions it uses from a list of names. The helper and the release bundle put the library beside the compiler. Tests: ConsoleTests.hs and RuntimeCallTests.hs with a second implementation of the text functions in RuntimeText.hs (cabal: 6607 pass); text_runtime_tests, which compares %f with the host C library; RuntimeCallPipelineTests.cpp and RuntimeCallVerifierTests.cpp; source_console_smoke, 83 programs through the JIT with a leak check each; and executable_run_tests, which now compares the bytes an executable writes to standard output and standard error.
Tests EffectTests.hs holds the inference of which calls write from both sides. Programs whose writes lie one and two calls down, in mutual recursion, in callables and in methods used as values must write in the order they say, before and after optimization. Programs that only compute must keep their deferred bindings and their suspended arguments, so the inference cannot buy correctness by giving up evaluation by need. FormatSweepTests.hs writes every combination of flags, width and precision for every conversion letter, 5184 formats, and compares what the reader of formats makes of each with the rules stated a second time. It also checks properties of a field against the reference text functions: never shorter than its width, padding only, zeros after the sign, groups of three. TextRuntimeTests.cpp compares %d, %u and %x with the C library of the host over deterministic pseudo-random values of every magnitude, and %f over 3000 numbers drawn from every exponent and over the multiples of small powers of two that lie on a rounding tie. ConsoleTests.hs and SourceConsoleSmoke.cpp gain twelve programs each: strings that callables take, capture, make and return, appending beside another name of the same string, string operations in arguments that are and are not needed, nested Console.Format. The smoke program checks each for objects left behind. ExecutableRunTests.cpp gains six cases that run a linked executable: a method held as a value, strings through callables, appending, a shift by the width of the type, a program that writes to standard error only, and that a link leaves no import library or object beside the executable. Fix The new tests found that a method that writes lost its output when it was used as a value. `auto f = Log; int x = f(1);` and `int x = Run(Log);` were taken to compute only, so the binding was deferred and, when nothing read it, the call never happened. A call through a callable was taken to write only when a method that writes also created a callable expression. A call through a callable is now taken to write when the body of any callable of the program writes or calls a method that does, or when a method that writes is used as a value. A program whose callables do not write keeps their calls by need even when the method around them writes, which the old rule gave up. Version The version is 0.5.0. 0.4.2 is skipped: evaluation by need and string equality change what existing programs mean, three words became reserved, every serialized stage is incompatible with 0.4.1, and an executable now needs the runtime library that ships beside the compiler. CHANGELOG.md gives the reasons under the release heading. The formatter, linter and analyzer packages now bound the compiler to >=0.5.0 && <0.6; their own versions are unchanged.
Three forms the tests of the last commit ran into. Two were missing; the third was written in a way the specification does not have. Method reference `Type::Method` is the static method as a callable value, as section 24.4 of Spec/Language/Operators.vxs has it: `auto f = Counter::Next;`, `Apply(Counter::Next)`. The lexer has the token, the parser a postfix form, and the syntax tree a node that every traversal carries. The type checker replaces a reference it resolves with the name of the method, so the stages after it are unchanged, and the inference of which calls write sees a method that is held as a value. Where the method has overloads, the type the place expects selects one; VXT0080 reports a place that selects none. A reference through a value is a bound method and reports VXT0081 until values have members. A selector with a dot and no call, `Counter.Next`, is still rejected with VXT0034: it is not the form the specification gives a reference. Names through the namespace A declaration may be named through the namespace it is declared in. Inside `namespace Demo;`, `Demo.Program.Run()` is `Program.Run()`, `Demo.Color.Red` is `Color.Red`, and `Demo.Program::Run` is `Program::Run`. The renamer keeps each declaration of the namespace under its qualified spelling as well, which no identifier can have, and turns a selector that spells one into the name. A name of the program spelled like the first part of the namespace hides it. Other namespaces are still unknown here, and a qualified name in a type position is not yet the same type as the bare name. String conditional `condition ? first : second` selects strings. A string is an owned object, so the result is selected into a variable by an assignment in each arm, which the rules of ownership already cover, and never by an expression that holds both. The neutral value of a string slot is the empty string. `?:` is unchanged: its left operand is its test. Tests QualifiedNameTests.hs runs the new forms before and after optimization and holds the rejected ones. ConsoleTests.hs, the leak-checked smoke program and the tests that run a linked executable select strings, including a loop of a million passes, and call methods through references and through the namespace.
Leitwolf11
left a comment
There was a problem hiding this comment.
Review notes, second pass
Scope. This pass read the two commits after the console work: bce33a02 (tests, the effect fix, 0.5.0) and 5b3d8322 (method references, names through the namespace, string conditionals). It is a self-review by the author of the branch, not an independent one; the pull request still has no review from a second person.
What was run. On Windows, on the tree of 5b3d8322: cabal test all (6822 cases), 28 of 28 native suites, the sanitizer build, the fuzz targets with the leak-checked console smoke program, the documentation and example verifiers, and the clang-format dry run. develop tidy last ran on bce33a02; the C++ changed since only by program text in two test files.
Not verified. Linux and macOS for both commits. The new comparisons with the host snprintf have only met the UCRT.
The inline comments are the places I would look at first. None of them blocks the merge in my reading; three of them describe behaviour that is narrower than a reader of the changelog might assume.
Scalar buffer Code scanning reported the growth of `Text::Scalars` as an allocation that cannot return null followed by a check for null. The allocation was `new (std::nothrow) char32_t[n]`, which does return null, so the check was right; but the form is one an analysis has to recognize, and the ownership runtime already allocates an object by calling the allocation function by name. The buffer now does the same: `::operator new(bytes, std::nothrow)` with `::operator delete`, and the check for null stays, since it is the only report of failure in a runtime built without exceptions. The size in bytes cannot wrap: the capacity is bounded before it is multiplied. Formatter and linter 0.1.1 Neither has a new rule. Both are built with the compiler of 0.5.0 and so accept what it accepts; their tests now hold the forms that arrived with it. The formatter keeps method references, names written through the namespace, conditionals over strings and console formats as written while it indents their blocks, and reaches a fixed point on a source that uses all of them. The linter is clean on such a source, reports a selector with a dot and no call, a method reference that selects no overload, a reference through a value and a mismatched format under its `compiler` check, and keeps a reference intact when it applies its safe fixes. Their READMEs say which compiler they are built with.
Draft. No human review has been submitted yet; for a change of this size across language semantics, the optimizer, the wire format, the native stack and the fuzzing infrastructure, one is recommended before merging.
The problem
Deep nesting ended
vxswith a stack overflow (0xC00000FD on Windows) and no diagnostic. Reproduced separately onmain:mainelse ifchain of 2000 linksCore::CorePrep::Preparecopied every function; theStatementcopy constructor recursed about 1550 deepifnested 100 levels deepCore::Wire::Decode, 14.8 KiB of stack per levelSeparately, compile time grew exponentially with the depth of nested loops, quadratically with the length of an
else ifchain, and with about the 3.5th power of the length of a chain of&&.What changes
Chains are not nesting. An
else ifchain, the arms of amatchand a chain of a binary operator are as deep in the tree as they are long. Every native stage now walks them in a loop, the native Core expression and statement release their children from a list, and neither the frontend limit nor the wire depth limit counts them. A sum of 50000 operands and anelse ifchain of 20000 links compile, and the native stages handle 20000 of either on a one-megabyte stack.Real nesting, measured.
//Compiler/Support/Tests:stack_proberuns one native stage on a thread with a stack of a chosen size; the smallest size that completes was found by bisection. KiB of stack per level:mainelse ifchainAn earlier version of this description gave 0.42 KiB per expression level. That figure was for an operator chain; nesting in a second operand had not been measured. It is now, with a shape of its own.
Compile time.
vxs check, one run each, seconds:mainforloopselse ifchainifstatements&&All measurements, and Criterion before and after, are in
Benchmarks/2026-10-04-Nesting-And-Chains.md.Smoke programs. The execution tables compile every distinct body once, up to eight small bodies per program, and check every run through a bit mask that must be zero in both pipeline modes. They are now a program of their own,
source_execution_smoke, besidesource_fuzz_smoke; as one program they had grown to 221 seconds in the fuzzing configuration under the 240-second watchdog, and a second attempt that passed was not a fix. No run was removed and the watchdog is unchanged. Measured in that configuration on this machine, three runs each: 77 to 110 seconds for the execution tables (expression 23, branching 35, leaving 17, ownership programs 2 in the run that was broken down) and 100 to 105 seconds for the differential program. Other work ran on the machine at the time. The tables written by hand for single features (inferred returns, evaluation by need, enums, closures) later became a third program,source_feature_smoke, for the same reason: with them the execution program ran past the watchdog. Apart they take 91 and 45 seconds in that configuration.Language
Implemented from the decisions on the open questions;
Spec/Language/Decls.vxsgains examples 295 to 310 and the grammar allows-before a numeric literal in a match pattern.A numeric constant pattern may be negative. The minus sign belongs to the literal; no other expression is a pattern. The range is checked against the subject's type, including the minimum of a signed type.
A match expression must be exhaustive: a wildcard, a type pattern of the subject's own type, or both values of a
bool. A guarded arm never counts. Enum and nullable coverage are specified; enums and nullable subjects are not implemented.A statement
matchthat selects no arm continues with the next statement.A block used as a value may leave with
return,breakorcontinueunder their ordinary rules. A block that leaves has no value; the expression takes its type from the blocks that complete. An expression none of whose branches completes is valid: whether an expression completes is tracked apart from types (Visual.XSharp.Completion), what would have received the value is neither typed against it nor lowered, and no result slot or placeholder is created. Such a block may stand in a loop header and in a loop used as an expression. The condition and the update clause belong to their loop, as decided and now stated inSpec/Language/Iteration.vxs, examples 79 to 83: abreakin either leaves that loop; acontinuein the condition evaluates the condition of the same loop again, without the body and without the update of afor, and a condition that always continues is an endless loop; acontinuein the update clause skips the rest of the update and goes on to the condition; a callable is not inside the loops around the place that creates it. Abreakmay carry a value to the loop expression around the block, andreturnleaves the method from inside a loop expression. The return type of a callable is inferred from the returns inside its expressions as well, up to nested callables.A method declared with
autocan be called. Its return type is inferred before any caller is checked, across classes and against declaration order, through chains and mutual recursion; a method with no result independent of itself isVXT0063(example 140 ofDecls.vxs), and one whose returns disagree isVXT0062. Onmainsuch a call fails in the Core verifier withVXC1018.A callable may be created inside a callable. The outer one captures what the inner one reads from further out and nothing that belongs to the inner one. On
mainthis fails in the Core verifier withVXC1020.An
ifwith anelse, or amatch, that ends a block used as a value is its value, without parentheses. Method bodies are unchanged.The comma after an arm is optional. A parenthesized pattern after an expression body without a comma is read as a call;
VXP0038says so at the arrow.A type pattern over a scalar applies no numeric conversion.
The
elseblock of aguardis checked by its control flow.See "Open: stack and limits".
Classic enums:
enum Name { A, B = 2, C }, with an optional underlying integer type. Three points the specification left open were delegated and are now examples 311 to 315 ofDecls.vxs: values are compared with==and\=with values of the same enum and nothing else; there is no conversion between an enum and an integer in either direction; and the target-typed.Memberspelling is the same as for an enum class.matchover an enum uses.Memberpatterns, treats two members with one value as one case, and needs no catch-all arm when every value is named..Memberis accepted in an expression wherever an enum type is expected. An enum is stored as its underlying integer and never reaches Core as a named type.Evaluation by need. Visual X# is a lazy, call-by-need language;
Spec/Language/Evaluation.vxsnow says so. The compiler implements the first part: a local binding ofbool, numeric or enum type whose initializer has no effect, that is not assigned again and not captured, is computed by its first read and not at all when nothing reads it. An expression that is certain to read a value by need more than once computes it once ahead of itself; before that, the Core of a chain of bindings that each read the one before twice doubled with every link (found in review of this branch). Reads in different branches of one expression still carry the computation each. Results, other types and assigned or captured variables are still computed where they are written;Documents/EVALUATION.mdlists them. The body of a callable evaluates by need as well, and an expression written as a statement is evaluated. Arguments are passed by need as of item 14. Four rules of the new specification file were delegated and are decided as written: effects happen where they are written; a value means what its variables held where it was bound, so laziness never changes the result of a program that also ends under strict evaluation; a value that cannot be computed fails where it is needed; and arguments are passed by need.Arguments by need. An argument of
bool, numeric or enum type that has no effect is computed when the method first needs it, at most once however often the method reads it and however many methods it is handed through, and not at all when no method needs it; a value the caller needs as well is computed once for both. Core has a new primitive,Memoize: a callable without parameters whose result isboolor numeric becomes a callable of the same type that calls it at most once. CorePrep, Xpp and Xmm carry it, each verifier checks its rule, and LLVM lowers it to an AARC object with an invoke thunk and a destructor. The wire versions are Core 9, CorePrep 7, Xpp 6 and Xmm 6. A method with a parameter by need gets a second function that takes suspended computations; the method's own function and ABI are unchanged, and a call with nothing worth suspending still uses it. Still computed at the call: an argument of a call through a callable, an argument a closure of the method captures, arguments of other types and of template methods. A suspended argument costs two runtime objects and two generated functions; that cost is stated inDocuments/EVALUATION.mdand the changelog.Native executables. Building the programs of item 14 into executables and running them showed four defects that no test could have seen, because no test ran a produced executable with more than a trivial body. A method without a result whose body ended without a
returnwas closed with an unreachable marker by both Core-to-CorePrep adapters, so aMainwithout a finalreturn;executed an invalid instruction. An executable is linked without any library, so a program that created a closure did not link; the entry module now defines allocation, retain and release itself over a 64 MiB arena whose released blocks are reused (weak and unowned handles, strings and type tests still do not link). An integer quotient or remainder by zero and a shift outside the width of its operand were lowered to instructions LLVM gives no meaning, and an optimized program ran on past a division by zero it needed; each is now preceded by a check that stops the program, and example 14 ofSpec/Language/Evaluation.vxsstates that such a value has none.vxs run -Filewith a relative path looked for the executable it had just built onPATH. A new suite,executable_run_tests, builds programs into executables and runs them as processes; the linker is the Windows one, so those cases run on Windows.Compile time of the native Core verifier. It copied every visible definition on entering a branch, a loop body, a let or a closure, so its time grew with the square of a function's size: 2.05 seconds for a function of 400 calls with four suspended arguments each, 0.04 seconds now. The execution tables pass every input as a call, so with item 14 their programs grew past the 240 second watchdog under the sanitizers (344 seconds). The expression and leaving tables now run in
source_expression_smoke; the watchdog is unchanged and no case was removed.Console output and strings.
Console.Print,Println,Printf,Printfn, the fourErrorforms andConsole.Formatare implemented, withSystemimported implicitly;Examples/HelloWorld/HelloWorld.vxscompiles to a native executable and runs. A format is a string literal checked at compile time: conversions%d %u %x %f %s %c %b,%nand%%, flags-,0,+, space,#and', and a width or precision written as*.+joins strings and writes a value that is not a string as text,+=appends, and==and\=compare the characters two strings hold, where they used to compare the places the strings were kept. Decided here and written into the specification:ConsoleIO.vxsexamples 69 to 85 (which flags each conversion takes,%b, exact rounding of%fwith a tie to the even digit, the line terminator of the platform),Operators.vxsexamples 86 to 89, andEvaluation.vxsexamples 15 and 16: output is an effect, so an expression that writes, itself or through a method it calls, is evaluated where it stands, while a value that only computes is still by need. New diagnosticsVXT0071toVXT0079. Console input, the stream objects,%A,%O, a text form of floating-point numbers outside%fand 128-bit numbers as text reportVXT0079;Documents/CONSOLE-IO.mdlists them.The runtime call. Core has a second new primitive: a call of a function of the runtime, named by a literal first operand that is its identity in a catalog of thirteen. CorePrep, Xpp and Xmm carry it, every verifier checks a call against the catalog, and LLVM lowers it to a call of the function's symbol. The type checker rewrites console calls and the string operators into runtime calls, so no later stage knows what a format is. The wire versions are Core 10, CorePrep 8, Xpp 7 and Xmm 7.
The runtime library. A native executable is linked with
vxs-runtime.lib: the ownership runtime and the new text and console runtime as one object that needs no C runtime, built from the sources a JIT host links. It replaces the ownership functions that item 15 generated into each executable, and their 64 MiB arena; weak and unowned references, strings and type tests now link. The linker makes the import library for the seven kernel32 functions it uses from a list of names. The development helper and the release bundle put the library beside the compiler. Ownership placement now gives a string literal used as an operand a symbol of its own; such a literal was never released.More tests, and what they found.
EffectTests.hsholds the inference of which calls write from both sides: writes one and two calls down, in mutual recursion, in callables and in methods used as values must happen in the order the program says, and programs that only compute must keep their deferred bindings and suspended arguments.FormatSweepTests.hswrites all 5184 combinations of flags, width and precision over the nine conversion letters and compares the reader of formats with the rules stated a second time.TextRuntimeTests.cppcompares%d,%uand%xwith the C library of the host over 44 400 fields and%fover 83 020 numbers. The console tests, the leak-checked smoke program and the tests that run a linked executable gain programs with strings in callables, appending and nested formats. The new tests found a defect of item 17: a method that writes lost its output when it was used as a value (auto f = Log; int x = f(1);,int x = Run(Log);), because the binding was taken to compute only and was deferred. A call through a callable is now taken to write when the body of any callable writes or a method that writes is used as a value; callables that do not write stay by need even inside a method that writes.Version 0.5.0. 0.4.2 is skipped, and
CHANGELOG.mdsays why under the release heading: evaluation by need and string equality change what existing programs mean,match,guardandenumbecame reserved, every serialized stage is incompatible with 0.4.1, and an executable needs the runtime library that ships beside the compiler. The formatter, linter and analyzer packages bound the compiler to>=0.5.0 && <0.6; their own versions are unchanged.Method references, names through the namespace, string conditionals.
Type::Methodis the static method as a callable value, as section 24.4 ofSpec/Language/Operators.vxshas it; with overloads the type the place expects selects one (VXT0080), and a reference through a value reportsVXT0081until values have members. A selector with a dot and no call stays rejected: it is not the specified form. Insidenamespace Demo;,Demo.Program.Run(),Demo.Color.RedandDemo.Program::Runname the declarations of that namespace; other namespaces, and a qualified name in a type position, are not connected.condition ? first : secondselects strings, through a variable assigned in each arm so that ownership is that of an assignment.QualifiedNameTests.hs, the console tests, the leak-checked smoke program and the tests that run a linked executable cover them.The scalar buffer, and the tools. Code scanning alert 17 reported the growth of
Text::Scalarsas an allocation that cannot return null followed by a check for null. The buffer now calls the allocation function by name,::operator new(bytes, std::nothrow), as the ownership runtime does for an object, and keeps the check. Visual Formatter and Visual Linter are 0.1.1: neither has a new rule, both are built with this compiler, and their tests now hold method references, names through the namespace, string conditionals and console formats. Xide tokenizes::in Tokenize the method reference sign of Visual X# xide#21.Pending implementation. These are parts of the specified language, not restrictions of it; each is listed with what it needs under "Pending branching and loop forms" in
Documents/IMPLEMENTATION.md:guard,ifandwhile(VXP0035): needs optional values;enum class;matchover a subject that may be null, and type patterns over class hierarchies.Closures run in the reference Core evaluator of the frontend tests and, through LLVM with the AARC runtime, in
source_feature_smoke, unoptimized and optimized: created, called, nested, returned and alive across loop transfers. The smoke program links the runtime and exports its entry points for the JIT; no product target changes.Ownership placement. Running closures through LLVM showed that every AARC object leaked: no stage wrote a retain or a release. A new Xpp pass,
PlaceOwnership, gives every AARC value an owner. Parameters are borrowed and results are owned; a local is released after its last use on each path, on the edge of a branch where it dies there, and directly after its definition when nothing uses it; a copy retains or takes over the reference; a closure owns its captures. A method used as a value, which used to be the address of the method's code, becomes a closure without captures. The pass is checked by an independent reference-count model along every path of each placed function, by the ownership verifiers of Xpp and Xmm, and by the smoke program, which runs 26 closure programs one at a time and fails the one that leaves an AARC allocation behind. Cycles are not collected, and a value now dies at its last use rather than at the end of its source scope.Generated tables. The branching and leaving cases are written once, in
Compiler/Fuzzing/Cases. The Go helperexecution-casesgenerates the C++ rows and the Haskell module from them;check, the helper tests and CI fail on a stale table. One source keeps the two tables equal; it is not an independent oracle. The checks that do not come from these files remain: the expression table, a hand-written table of inferred methods, thematch-against-iforacle tests and the differential generator with its host model.Verification
Local, Windows, on the pushed tree:
cabal test all: five suites pass (6832 cases across them).develop test: 28 of 28 native suites.develop sanitize address-undefined: 28 of 28, no report.develop tidy: no first-party finding in 139 translation units.develop fuzz: the five source smoke programs, the wire smoke program and all nine libFuzzer campaigns complete under ASan and UBSan, and the Haskell coverage campaign completes.verify-helpers,verify-docs,verify-benchmarksandexecution-cases checkpass.Stack and limits
The limits count real nesting only; chains are not counted. 256 nested statements, 1024 nested expressions and a compiler stack reservation of 256 MiB are the limits this version ships with. They are resource limits of this implementation; the specification states none.
Stack committed by whole compilations (
source_stack_probe), Windows x86-64, clang-cl. The figure is committed stack: an upper bound on what the thread used, to the page and with the guard page, not an exact count of bytes in use. Frames that a sanitizer keeps on a heap-allocated fake stack are not in it; the sanitizer runs measured had use-after-return detection off, which is the default of the runtime on Windows.if, the limitelse ifchain of 1400,matchof 2000 arms&&with a storing right operand becomes a conditional statement), so Core from the frontend stays within about 256 + 1024 levels of either kind, inside the wire bound of 4096.mainand 31.0 MiB here; eight at once commit 247.1 and 247.7 MiB in total.Measured on every CI platform for the worst shape, 1023 nested calls, as the stack the compiler thread committed (KiB; ordinary build / ASan and UBSan / TSan): Windows x86-64 2580 / 4736 / not run; Ubuntu 26.04 x86-64 2588 / 3060 / 2880; Fedora 43 x86-64 2588 / 3060 / not run; macOS 15 arm64 2480 / 5536 / 2752; macOS 26 arm64 2480 / 5536 / 2768. The largest figure is 2.1 percent of the reservation. The Linux sanitizer figure leaves out the frames the sanitizer runtime keeps on its fake stack.
Not fixed here
else iflinks. There is no bound on the number of statements of a function.