On this page
- v0.0.74
- v0.0.73
- v0.0.72
- v0.0.71
- v0.0.70
- v0.0.69
- v0.0.68
- v0.0.67
- v0.0.66
- v0.0.65
- v0.0.64
- v0.0.63
- v0.0.62
- v0.0.61
- v0.0.59
- v0.0.58
- v0.0.57
- v0.0.56
- v0.0.55
- v0.0.54
- v0.0.53
- v0.0.52
- v0.0.51
- v0.0.49
- v0.0.48
- v0.0.47
- v0.0.46
- v0.0.45
- v0.0.44
- v0.0.43
- v0.0.42
- v0.0.41
- v0.0.40
- v0.0.39
- v0.0.38
- v0.0.37
- v0.0.36
- v0.0.35
- v0.0.34
- v0.0.33
- v0.0.32
- v0.0.31
- v0.0.29
- v0.0.28
- v0.0.27
- v0.0.26
- v0.0.25
- v0.0.24
- v0.0.23
- v0.0.22
- v0.0.21
- v0.0.20
- v0.0.19
- v0.0.18
- v0.0.17
- v0.0.16
- v0.0.15
- v0.0.14
- v0.0.13
- v0.0.12
- v0.0.11
- v0.0.10
- v0.0.9
- v0.0.8
- v0.0.7
- v0.0.6
- v0.0.5
- v0.0.4
- v0.0.3
- v0.0.2
Changelog
v0.0.74
Removed
intois gone. The undocumentedxs |> into "cmdline"ransh -cpast every effect check (pure,readonly,plan, module loading,--can). Pipe instead:xs | sh -c "…", or betterxs | prog args.
Changed
- A lazy value runs where it is used, unless it was written inside a
within.let here = lsthencd srcthenherelistssrc/(it listed the old directory). A value written insidewithin cd/within envstill keeps that scope however late it runs. An init file’slet gl = git log …now logs the repo you are in. - The REPL echo of a command-backed
letnever runs it. It showsgl : seq<string> (command-backed — runs on each use; type gl to run it);let cleanup = rm -rf buildno longer deletes on Enter. A frozen or reified value still echoes its data. - A lazy command
letis allowed in the init file and in modules (let gl = git log --oneline -n 10); eager forms (| line,$(…)read at once,Seq.freeze) stay refused, with a message that says why. - A module’s body runs once per program, shared by every import of it (diamonds, aliases, symlinks — identity is the real path). It ran once per import site, so nested imports could take exponential time.
- No command runs while a module is imported — refused at run time too, so a lambda or a helper passed as a value can’t slip through.
servebounds request bodies:maxBody(aSize, default 16 MiB), answered 413 past it.Uuidparsing refuses surrounding whitespace, inUuid.parseand in JSON fields alike.- A source file must be valid UTF-8: an invalid byte is a located error, not a silent U+FFFD.
- Statements in a block don’t count toward the nesting limit, so a
function or
withinbody can be any length.
Fixed
- A long flat script no longer overflows the stack (6,000 lines crashed with exit 134; 700 on a 1 MB stack).
- A
Secretargument works in captured commands —curl -H $auth … | complete,| line,$e(…),NAME=$s cmd,within env A=$s. - Module members named like a builtin are exported (
Doc.Text). - SIGTERM stops every child tree once, including
pmapworkers’ grandchildren, which could be orphaned. weir restoresends the host token asadddoes, never writeslock.json, and names the entry and field of a malformed lock entry instead of crashing.- HTTP client: one header pair per received line (
Set-Cookieno longer merged, values never rewritten); an unknown charset is a precise error;authplus an explicitAuthorizationis refused;Authorization/Cookieare dropped on a cross-origin redirect; a redirect loop is a named error. serve: no body on HEAD, 1xx, 204 or 304; framing and hop-by-hop headers from a handler are dropped; aStreamelement is split into onedata:line per line, so a newline can’t forge an SSE event.gen typesleaves a self-referential field out (with a note), so the type reads withfrom json.- REPL:
HOME=no longer crashes it;#savewrites a script that runs; the line editor moves and deletes an emoji whole; path completion quotes spaces and quotes; typed-ahead input and Enter before the first prompt work; a piped blank line ends an unfinished entry; anonymous-record fields complete;#quit now,#alias let,#help map, piped#historyand destructuringletechoes behave. - One formatter for the CLI and the LSP: the shebang stays
verbatim, error lines count from line 1,
fmtrefuses rather than flip-flopping, and trims only what the parser treats as whitespace. - LSP: every request gets an answer, with the JSON-RPC error codes;
exitwithoutshutdownexits 1; header names are case-insensitive. - Checker: copy-and-update fields align under the first field;
imported unions type an unannotated parameter; a union reached only
through another module doesn’t make a local constructor ambiguous;
let un (W x) = xworks for a single-case union. - Weir-worded errors for an unreadable or directory script,
importing a directory or device,
within lockon a directory, a negative or zeropoll interval, a lone surrogate in JSON,from jsontype mismatches, a comma between record fields,cmd |> try,-e ''. - Smaller fixes: run time is linear in a statement’s lines (a 64k-
line heredoc took 53 s);
Path.globbracket classes never reach the regex engine raw;showprintsSome (1, 2); a timed-outretryreports the attempts it made; float-holding==namesFloat.near;build --helpon a union-onlyArgs.loadis scoped; error columns stay on their line.
v0.0.73
Fixed
Plan.applyruns the same checks as the direct call. In a plan,Dir.deleteof a non-empty directory deleted the whole tree, andFile.copy/Dir.moveaccepted the wrong kind of path. The planOpunion gainsDeleteTree,CopyDirandMoveDir; an exhaustive match onOpneeds the three new cases.weir fmtnever re-spaces a command’s arguments.chmod u+x fbecamechmod u + x fandecho a+bbecameecho a + b.- A raise in a producer piped into a command fails the script.
xs |> Seq.map (… fail …) | sha256sumhashed the truncated input and exited 0;exitinside the producer was lost too. - A closed stdout ends the script.
weir s.weir | head -1ran the whole script (or spun forever on an endless child); it now tears down likeSIGPIPE— children killed,alwaysruns — and exits 141. - No statement runs after SIGINT/SIGTERM. Code after
Proc.waitcould keep running while the scope tore down. The main thread now unwinds; if it is blocked where it cannot notice (a stdin read, a network call), the old teardown takes over after one second. - Int arguments past 32 bits raise instead of wrapping.
Seq.item 4294967296 [10;20]returned10;Seq.take,Str.sub,Str.replicate,Str.padLeftand indexing wrapped the same way. - A match on a not-yet-resolved payload is checked for totality.
match o with | Some 0 -> … | None -> …passedweir checkand failed at run time with “no arm matched”. purerefusescd,pwd,Path.home*,Term.*andColor.*;readonlyrefusescd.- YAML reads
null,Null,NULLand~as null (quoted forms stay strings), soto yaml |> from yamlround-trips anOption. - A mis-indented YAML line is an error, not silently dropped.
~andPath.homeuse$HOMEas set, even when the directory is missing;HOME=/nonexistentmade~/xexpand to/x. An emptyHOMEraises.- A raise inside a
Seq.pmap/piter/pfirstarm is reported at the arm’s line, not at the parallel call. Self.promptreads only its own line from stdin, so a command run afterwards gets the rest (printf 'a\nb\n' | weir s.weir).weir fmtfixes: no crash on a comment outdented inside a heredoc or yaml block; a///line inside a block is never moved into its content.//inside a string inside an interpolation hole is data, and a trailing comment after such a string is stripped.- A malformed
${…}in a$$<<<heredoc is a parse error, not literal text. |>after a multi-linematchclosed inside parens pipes the paren, instead of failing with an indentation error.Seq.groupByaccepts any key that supports equality, ascountBydoes; tuple, record and option keys failed at run time.- Applying a returned function works:
id (fun x -> x + 1) 2,(snd t) 5,Map.get "k" handlers x. - A tuple pattern types a value whose type isn’t known yet
(
let f = function | (a, b) -> a - b). planrefuses a command reached through a helper at check time, not only at run time.- Types a module gets from its own imports work for its importers
(fields,
show,==). A local type of the same name takes precedence; the imported one is then shown qualified (A.Pt). - A unit-suffixed literal too large for its unit is a parse error
(
9999999999TiBwrapped silently). - Quoted YAML keys unescape, and an empty key written by
to yamlreads back. Dir.copycopies symlinks as links and refuses a destination inside the source.Instant.parsereads a space separator and nanosecond fractions, so TOML offset date-times load.from jsonlskips blank lines, so a file ending in an extra newline reads.Json.inferShape/Yaml.inferShapedraftfloatfor an integer past int64, which the readers already typed that way.Args.loadteaches--name valuefor--name=value, and a field that would derive--helpis a check error.weir fmtreads no flag as a script path: an unknown flag or a missing script is a usage error (exit 2).- Clearer errors: a dangling symlink and a directory passed to
File.size/File.deleteare named as such;xs[^n]out of range names^nand the length; Duration/Size/Instant overflow andStr.replace ""fail in weir’s words instead of .NET’s;0x10,1_000and1.5.2get their own messages, and unit suffixes no longer clutter “Expecting” lists;Some 1 < None, Show/Ord leftovers, opaque built-in types and a failed import are reported as what they are. Duration.sleeppast 24.8 days sleeps instead of failing.- Deep
Seq.append/Seq.mapchains no longer overflow the stack (100k nested); deepwhere/choosechains still recurse. - A
planbody ending in a non-unit value is an error, like any other discarded value. weir fmtkeeps the layout inside an open bracket. A multi-line value inside a{ … }entry was flattened to the entry’s column, and lines inside an open(were re-indented level by level (elselanding underthen’s body). Inside a bracket fmt now only shifts a line by as much as its opener moved.- Bidi controls are shown at a terminal. Data printed to a tty
renders U+202A–202E, U+2066–2069, LRM/RLM and ALM as a visible
\uXXXX, as C1 controls already were, so displayed text matches the bytes.weir check --jsonand the--canreport (both forms) get the same treatment. Pipes and files stay byte-for-byte. - Deep lazy
Seqchains don’t overflow the stack:where,choose,take,skip,collect,distinct,scanand the other lazy combinators now walk iteratively, likemap/appendalready did. - Long waits are honoured. Retry/poll delays and Http timeouts past ~24.8 days wait instead of silently timing out or failing.
exit noutside 0..255 raises (a literal is a check error);exit 256exited 0.SIGTERMto weir’s pid also stops the foreground child, which used to keep running after weir exited.- Regex matches time out after 5 seconds with a named error;
catastrophic backtracking (
^(a+)+$) hung the script. from tablealigns columns with emoji and other non-BMP text.- A module
letthat calls the module’s own command-running helper is refused, like a direct command; it ran at import. - Match arms resolve by the union that owns all their cases, so a
script-declared
Okno longer breaks anErrorarm. """…"""is one command argument;@"…"in arguments is refused with its own explanation (@is meaningful argv text).- An outdented
//inside a heredoc is an error, not silently dropped. weir runrefuses a fileweir checkrefuses, including a misaligned///.- Smaller fixes:
-9223372036854775808is a valid literal; the literal-0.0reads as0.0;Instant.parserefuses12:34:56.Z; a quoted yaml-block key"$x":is literal;Seq.take/Seq.skipwith a negative count raise;Path.globwith a trailing/matches directories only; a genericto json/to yamland an over-long flat chain get errors that say what to do. - YAML: a plain value containing
:is refused (“quote the value”);- x:with a wide gap after the dash reads;...ends a document. - XML: DTDs are refused (an external entity silently became
""); a repeated element read into a single-value field is refused (“declareseq<string>”).
Changed
to json,to jsonlandweir check --can --jsonwrite non-ASCII text raw."é <b> & \"q\""was written as"\u00E9 \u003Cb\u003E \u0026 \u0022q\u0022"; it is now"é <b> & \"q\"". Characters outside the BMP (emoji) stay an escaped pair. Readers are unaffected; a script comparing JSON bytes sees the new form.- Field paths in
from json/from xmlerrors index from 0 (refs[0].Include), as weir indexing does. - Indentation is spaces only, inside heredoc and yaml blocks too. A tab in a content line’s leading whitespace is an error on every line (it was refused only on the first). A tab after the first non-whitespace character is still content.
- Strings count Unicode code points.
Str.length,Str.sub, slicing and padding no longer count UTF-16 units, so they can’t split an emoji;Str.length "🎉"is now 1 (was 2). Env.get,Env.varsandEnv.loadsee thewithin envoverlay, aspwdseeswithin cd; they read the outer value before.- Reading data into a
Secretfield is allowed (from json/yaml/toml); writing a Secret out stays refused.
v0.0.72
Fixed
- An error inside
within … alwaysis reported at the body’s line. The cleanup runs while the error unwinds, and v0.0.71 reported the error at the cleanup’s line — even when the cleanup itself succeeded. - A module
letthat runs a command through a reifier is refused.let v = cmd | line(or| text,| succeeds,| complete) in a module passedweir checkand ran the command at import; it now gets the same error as$(cmd): wrap it in a function. - An unknown program at the start of a line gets the right message.
nosuchtool argfailed at run time with “unbound variable … wrap it: $(…)”; it now says the program isn’t on PATH and how to name one that only exists at run time (^$tool), aslet x = nosuchtooldid. M.fwith no moduleMsuggests modules, not a constructor (“Did you mean ‘Ok’?”), and says how to import one.[<Positional>]on anArgs.loadfield is an error. It only describes a tool’s operands in a command signature; on a script’s own arguments it silently did nothing.- Smaller fixes: a command inside
planis reported as “commands are refused” (it said “‘proc’ is refused”);weir check --canreports each command of a pipeline at its own column; parse errors no longer list the removed!as an expected token.
v0.0.71
Fixed
- Runtime errors name the line that failed. A command failing
inside an
if,match,for,within,retrybody, a pipeline or a lambda was reported at the block’s first line (a deploy script’sazfailure on line 15 blamed theifon line 11). The error now points at the failing statement asfile:line:col, and a command whose output is only read later is still blamed on the command itself. A failure inside a function reports the function’s line, then onecalled from file:line:colline per call, innermost first — across files, so a module’s function names the module’s own line.
v0.0.70
Added
-
|> tryturns a failure into a value. End any expression with|> tryto getSome value, orNoneif it failed — a missing file, a malformed document, afail:File.read "Cargo.toml" |> from toml Cargo |> try.|> try resultgivesOk valueorError messageinstead, withResult.map,defaultValue,toOptionandisOk. It’s the expression twin of| complete: everything to the left is captured and fully evaluated inside,exitstill exits, and nothing that already happened is undone. -
id, the identity function (F#‘s):xs |> Seq.choose id. It is not reserved, solet id = …andfor id in idsstill bind. -
Str.isEmptyandStr.nonEmpty, so a filter reads point-free:lines |> Seq.where Str.nonEmpty. (isEmptynow has two homes, so in the REPL writeSeq.isEmptyorStr.isEmpty.)
Changed
- A script’s own union cases shadow the prelude’s. With
Resultin the prelude, a script declaring its ownOkorErrorcase keeps constructing it; the same now holds forMoveandCopy(fromOp). - Hover text reads as paragraphs. A
///comment wrapped over several lines shows as one paragraph, and each hover line ends with a space, so editors that show hovers on one line (micro’s info bar) no longer glue words together. tryis a keyword with a meaning. It was reserved only to say weir had notry; the teaching now points at|> try.
v0.0.69
Added
from toml Treads TOML.File.read "Cargo.toml" |> from toml Cargoreads a TOML 1.1 document (so any 1.0 file too) into your types, with the same field rules asfrom yaml: tables are records, arrays and[[arrays of tables]]are seqs,[<Wire "requires-python">]names an awkward key. Dates and times read as text, or into anInstantwith[<Iso8601>]. Duplicate keys and tables defined twice are errors with the line. The parser is weir’s own (no new dependency) and passes the official toml-test suite for TOML 1.1.- The REPL init file can import modules.
import "./prompt.weir"ininit.weirloads the module before the first prompt, and its members are available in the session asPrompt.member. This keeps a large prompt in its own file. from yamlreads indentation indicators. A block scalar header liketoken: |2(written by some kubeconfig tools) made the whole read fail;|2,|2-and|-2now read as YAML defines them, with the content indented relative to the key.
Changed
- Imports resolve from a symlinked file’s real location. A script
or init file linked from a dotfiles repo or into
~/binimports the files beside the original.Self.scriptPathstill reports the link.
Fixed
| orafter a command with a$@splat crashed.echo $@xs | or echo noraised an internal error at check and at run; it now works like| andand the other reifiers.| line,| textand| execfollowed by a word teach.cmd | line fooreported “command not found: line”; it now sayslineis a reifier and how to run a program of that name (^line), as| complete fooalready did.- A name from a pattern
letis a binding in anifcondition. Afterlet a, b = …in a function body,if a == "" thentreatedaas a command. Names bound by tuple and record patterns now work like a plainlet’s.
v0.0.68
Changed
| andand| orcan’t be mixed in one chain.a | and b | or cgrouped from the right —a | and (b | or c)— unlike bash’sa && b || c, and quietly did something else. It’s now an error that points atif a | succeeds then b else c. Chains of one word (a | and b | and c,a | or b | or c) work as before.- The
&&and||warnings suggest| andand| or.
Added
- A warning for
[1, 2]. That’s a one-item list holding the tuple(1, 2), which is almost never what’s meant;weir checknow says to separate list items with;. A list of several pairs (["a", 1; "b", 2]) and[(1, 2)]don’t warn.
v0.0.67
Changed
- Clearer errors around
| and,| orand misspelled reifiers.git status | or lsnow sayslsis weir’s own name, not a program, and to write^lsfor the program, instead of a parser dump. A misspelled reifier after|gets a suggestion (| success→ did you meansucceeds?). For a command missing from PATH, the error now suggests a dynamic head (let tool = "x"then^$tool) for tools that only exist when the script runs, instead ofsh -c.
Added
| ortakes a pipeline on its left.a | b | or crunscwhen the pipeline fails — its exit is the leftmost failing code, as for| exitCode.
Fixed
(%)works as a function value, like(+)and(*):xs |> Seq.reduce (%).(=),(!=)and(<|)now say what to write instead ((==),(<>),|>) rather than showing a parser dump.- Tab completes the command after a
|. In the REPL,git status | s<Tab>offered only files from the current directory. It now offers programs from PATH, and right after|also the reifiers andand/or; a stage’s command is colored like any other command,and/orare colored as keywords, and an unknown command after|is red like one at the start of the line. - A reifier can’t end a
| and/| orchain. Inlet r = a | and b | complete,completeapplied only tob, and the record was silently discarded, sorwas(). It’s now an error that says so (| execstill works there). Errors in a pipeline’s structure are no longer replaced by a parser dump when the line is alet. - A missing program inside a captured pipeline is reported. In
let c = a | missing | exitCode,weir checkgave no warning and running it printed a pipe hint; both now namemissing. | and/| orcan’t follow a reifier. Ina | succeeds | and b, the bool was dropped andbran anyway. It’s now an error that points atif a | succeeds then … else ….- No false “command not found” after
NAME=value. When a line with an environment prefix failed for another reason, the error claimed the command didn’t exist; the real error is reported now.
v0.0.66
Changed
- Plainer help text. The hover help, the website reference and the
reference pages now explain things in ordinary sentences instead of
terse slogans like “the reifier law: the meaning is the code”.
“Reifier” is now a defined term — a
|stage after a command that turns its run into a value (complete,succeeds,exitCode,orFail,line,text,exec) — and the docs say plainly where each one sends the command’s output.
v0.0.65
Added
- A reifier follows a whole pipeline.
git log | head -1 | line,a | b | complete,| text,| succeeds,| exitCodeand| orFailnow work after a chain of commands, not just one. The code is the first failing stage’s (counting from the left, where the fault began), or 0;completecarries the last stage’s output and every stage’s error output in order.execstill takes a single command.
Changed
weir check --canlists each file once. A program run from many places used to repeat the full path for every site; now it readsgit × 10 init.weir:11:72, 15:5, 17:14, …. The second heading is justmutations.weir checktakes its flags in any order.--json --canworks as well as--can --json. A missing script, an unknown flag, or--strictwithout--canis now a usage error instead of “no such script: check”.check --cannames the variablesNAME=valuesets — for one command and forwithin env A=1 B=$x— instead of reporting them as not statically known.
Fixed
- A pipeline whose last command stops reading early no longer hangs.
seq 1 1000000 | head -1,git log | head -1and similar waited forever once the producer filled the pipe; they now end as in a shell, and the producer’s exit is not reported as a failure. Commands run by weir also stop quietly when their reader goes away, instead of printing “write error: Broken pipe”.
v0.0.64
Added
-
A typed
~is your home directory in a command line.cat ~/.config/weir/init.weir,~/bin/toolandcd ~now work as in bash — and as Tab completion already suggested: an unquoted word that is~or starts with~/expands when the line runs. Quoted strings, interpolations and spliced values stay literal,~useris not expanded, and$HOMEstill never expands; in an expression,Path.home ()remains the spelling. Tab completes a leading~to the literal home path, so the line shows what will run, andcdtakes a string argument literally (cd "~/x"no longer expands). -
NAME=value cmdsets an environment variable for one command.EDITOR=nano git commitworks as in bash: one or moreNAME=valuewords before the program name (a word, a quoted string,$xor an interpolation;NAME=is empty), each pipeline stage taking its own. The program is still resolved at check time, and after the program nameCC=gccis an ordinary argument. A loneFOO=1teacheslet. -
Declared codecs:
Instant,DurationandSizefields cross JSON and YAML. Name the encoding on the field —[<Iso8601>] at: Instant,[<EpochMs>],[<EpochSec>],[<Millis>]/[<Seconds>]for aDuration,[<ByteCount>]for aSize— andto/from json,jsonlandyamlall read the one declaration, so a record round-trips without a hand-written wire twin. An unannotated field still refuses, now naming the attributes first. A codec that would drop sub-second precision raises instead of truncating;showand==are unaffected. -
Or-patterns in
match.| "a" | "b" -> …and| Debug | Info -> …share one arm — F#‘s spelling. Each alternative counts toward exhaustiveness and shares the arm’s guard. An alternative cannot bind a name yet (| (n, 0) | (0, n) -> nasks you to split the arm), and an arm takes at most 64 alternatives — for a longer list, test membership in a guard (| x when Seq.contains x values ->). -
Record fields splice into commands:
$cli.tag.git tag $cli.tag,$cli.out.dir,$@cli.filesand a dynamic head^$cli.bineach read the field path as one word — the(cli.tag)detour retires, and a head held in a record no longer needs its ownlet. Only a name continues the path ($x.and$x/ystill refuse as glued words), so bash’s$f.bakis an error on a string that names$"{f}.bak". -
cmd | textcaptures the whole output as one string. The multi-line sibling of| line: lines joined with newlines, trailing blank lines dropped, and a nonzero exit raises (| completekeeps the code) —let notes = git log -1 --format=%B | text. -
within env A=1 B=$x. TheNAME=valuewords that set variables for one command now scope a block too, without binding anEnv.ofPairslist first. Awithin env ewith a bound list works as before. -
alwaysafter anywithin.within tmp d,cd,env,lock,procandserveall take a trailingalwaysblock. The cleanup runs while the resource is still held — the temp directory still exists, the lock is still held, the process is still alive — and the release follows, socp $"{d}/report.html" .works there. It used to need a barewithinnested inside the scope.
Changed
-
pwdis astring. It was a one-lineseq<string>, so every use neededpwd |> Seq.head; nowlet here = pwdis the current directory, read where it is used. Replacepwd |> Seq.headandpwd |> Seq.exactlyOnewithpwd.cd dirstill returns the absolute directory it moved to. -
!(cmd)and!e(cmd)are gone. Every statement position runs a bare command, so writegit pullinstead of!(git pull), and give a command its own line instead of!(a); b. For an environment, useNAME=value cmdorwithin env e.$(…)and$e(…)still capture. A block that ends in a command now runs it when more lines follow, even inside alet(let x =+ awithin tmp dblock + the value), which is where!()used to be needed. -
<<is gone — weir composes left to right.f << gwas exactlyg >> f; it is now an error that says so, as is F#‘s back-pipe<|(writef (x)orx |> f). -
An interpolation hole is generic.
let addr s = $"{s.name}:{s.port}"used to makeporta string, soaddr { name = "a"; port = 1 }failed;let dash n = $"-{n}"took only strings. A hole on a parameter or field now accepts anything a hole can render, asshowdoes — the$"{show x}"workaround is no longer needed. ASecretpassed to such a function is refused at the call. A hole nothing generalizes still defaults tostring. -
A lambda body takes command
lets.xs |> Seq.iter (fun f ->followed bylet r = git log $f | completeused to ask for$( )unless the lambda sat on a top-levellet’s right-hand side; a lambda body now works like aforbody wherever it sits. Paren interiors still need$( ).
Fixed
-
A cleanup ran twice when Ctrl-C interrupted a failing command. When the signal reached weir just as the interrupted child made the body fail, both the signal handler and the scope could run the
alwaysblock. Exactly one of them runs it now. -
A match arm and an
elsetake a text block on their own line.| 1 -> <<<andelse <<<failed with a parser dump althoughthen <<<worked — andweir fmtturned the working next-line form into the failing one. Both arm their block now. A block written as a trailing argument (File.write p <<<) is an error that names the spelling that works: start with the block and pipe it on its closing line,|> File.write p. -
Shell and C reflexes teach instead of dumping a parser error. A trailing
\(bash line continuation) used to pass a literal\to the program — a command line continues by indentation, and a\word is now an error saying so;\;names the quoted spelling and\lsnames^ls.!=teaches<>,!xteachesnot x, a type annotation ((x: int),let x: int =) teaches inference, andfor i in 1..3teaches[1..3];String.trimnamesStr.trim, and alist<string>field type namesseq<string>. Aletthat fails to parse no longer causes a second “unbound variable” error at its uses. -
Editors and pagers get the terminal wherever a command runs as a statement. Only a top-level bare command used to inherit the terminal;
!(cmd),!e(cmd), a command inside awithin,matchor function block, and every pipeline handed the child a pipe — so!e(vi f)warned “Output is not to a terminal” andgit log | lessprinted likecat. All of those now inherit at a terminal (a pipeline’s last stage does), in scripts and the REPL; redirected output is unchanged. A command whose output becomes a value (let x = …,$( ),| complete) still captures, so an editor belongs in statement position. -
A command in an
ifbranch ends atelse/elif.if ok then echo hi else echo byeran a singleechowith argvhi else echo bye, andif ok then git pull else ()failed with “unbound variable ‘git’” — only anifwithoutelsetook commands. Each branch is now its own command, on one line or indented; quote"else"to pass the word to a command there. -
printtakes aUuid.Uuid.v7 () |> printwas refused; aUuidnow prints its canonical text, like the other types with one text form (printerrtoo).Instant,DurationandSizestill print throughshowor an interpolation. -
Uuidcrosses YAML, andInstant’s YAML refusal names the fix. AUuidfield reads and writes throughto yaml/from yamlas it already did through JSON. AnInstantfield at the YAML boundary now teaches the conversion (Instant.epochMs, orshow) instead of the generic “cannot cross” error, and every refusal reads the same in JSON and YAML, in both directions, naming the field.
v0.0.63
Added
Uuid: a first-class identifier type and module.Uuid.v7 ()is time-ordered and strictly increasing within the process — ids generated in the same millisecond still sort in creation order, as uuids and as text — so it keys an append-only log.Uuid.v4 ()is random,Uuid.v5 ns nameis name-based (Uuid.ns.dns,.url,.oid,.x500), andUuid.v7At tstamps a given instant.Uuid.parse/tryParseread the hex-and-dash form, 32 bare hex digits, orurn:uuid:, in any case;Uuid.version,Uuid.instant(v7’s creation time),Uuid.toString,Uuid.nil/maxandUuid.toBytes/fromBytes(big-endian) inspect and convert. Uuids compare, sort, show, splice into argv, and cross JSON as their canonical string, so records withUuidfields round-trip.Uuid.v4/v7/v7Atare effects likeInstant.now;v5is pure.
v0.0.62
Fixed
-
The LSP sends Private-Use glyphs raw, not
\u-escaped. A Powerline/Nerd-Font icon (U+E000–U+F8FF) in atextDocument/formattingedit arrived\u-escaped, and a client that mis-decodes the escape (micro’s lsp plugin) dropped it on format-on-save. System.Text.Json escapes the Private-Use Area under a hardcoded policy no built-in encoder overrides, so the LSP/CLI JSON writer now uses a thin encoder that emits every BMP non-ASCII scalar raw; mandatory"/\/control escapes are untouched, and astral chars stay a valid surrogate pair.weir fmton the CLI was never affected. -
An alias head submits on one Enter.
co -(aliasco = git checkout) andcb burnt*no longer need a double Enter — the line-completeness check parsed without the alias table, so an alias head read as a bare name and a trailing-/*looked like an unfinished expression. The completeness resolver now carries the alias table, like the eval path. -
A short custom prompt keeps its continuation marker. The REPL continuation prompt is now one dim ellipsis (
…), right-aligned to the prompt width. A custom prompt narrower than four columns (a❯whose status line is printed separately above) used to fall to blank spaces; it shows the hint at any width now, and the marker is dimmed at a colour tty. -
A
#sessionfield key hovers as the key, not the rewritten binder.envreported_nv : seq<string * string>andpromptreported_rompt : { ok: bool; .. } -> string— the LSP’s internal field rewrite leaking. A field key now hovers as the key and its#sessionschema type (env : seq<string * string>,prompt : string | unit -> string | PromptStatus -> string). -
ls -lateaches the force-external escape. Applying a command-name builtin value (ls,pwd) to arguments reported a bare “not a function” error; it now points at^ls -la(or an alias).lsstays weir’s typed listing — it is not silently rerouted to the external. -
An unquoted path in an expression teaches quoting.
let x = cd /workparses ascd / work— a division whose RHSworkis unbound. The “unbound variable” error now appends a hint to quote the path (cd "/work"); an unquoted path is an argv word only in a command. A gluedtotal/coutntypo keeps its plain did-you-mean.
v0.0.61
Added
-
Http.expect— the raising read. Request in, body out:Http.get url |> Http.expect |> from json Tfor the bare read, and{ Http.get url with auth = Bearer t } |> Http.expectthe moment it needswith. A non-2xx raises naming the method, the redacted url, the status and a capped snippet of the error body — when the error body is data to inspect,Http.sendstill binds status and body as values. Per-method effect class likesend’s (a GET expect is legal inreadonly); a mutating-method expect inside aplanrefuses — there is no response to return, andHttp.sendcaptures the Op. -
Breaking:
Http.fetchis retired.Http.get url |> Http.expectis the spelling — one raising read, bare or built, and--canstill names a literal url through the pipe. Pre-1.0: no retirement shim; the ledger carries the ruling [D:fetch-retired]. -
Form— the urlencoded body.body = Form [("grant_type", "client_credentials")]percent-encodes each pair at send (thewithQueryescaper) and setsapplication/x-www-form-urlencoded— the token-endpoint shape, never a hand-builtk=v&…. Shared withserveresponses like the rest ofHttpBody. -
The prompt provider carries the status.
promptin the init file’s#sessionblock now also takes aPromptStatus -> stringfunction:okis the red-prompt tint —truewhen the last entry ran clean; a bare command’s nonzero exit counts as not clean, a reified exit (| exitCode,| complete) stays data — andexitcarries that bare command’s code (Option<int>). A custom prompt owns its colors, so the status arrives as data instead of a paint:let prompt st = if st.ok then Color.green "❯ " else Color.red "❯ "— and the record grows without breaking a consumer. Records are nominal and init takes no signatures, so a provider types as a row; the loader accepts any field setPromptStatussatisfies.
Changed
-
A
Jsonbody takes the value.body = Json { name = "w"; count = 3 }checks byto json’s own law and renders at the boundary — theJson (x |> to json)mouthful retires from the common path. Not breaking: aseq<string>payload is read as the pre-rendered document, so the explicit spelling means what it always did. -
weir fmtpads the unambiguous operators.let x =dirty || staged||stashedformats tolet x = dirty || staged || stashed— one space around=,==,<>,<=,>=,&&,||,|>and+where it was missing. Never-(adjacency is meaning:f -1passes an argument) and never bare</>/*//(types, globs, paths); command argv stays byte-inert under the existing parse-shape guard, which gained an expression-only twin so a spaced ident chain is not mistaken for argv. -
File.writeAtomic— the whole-file swap. Same signature asFile.write: the payload lands in a same-directory temp, is fsynced, and renames over the target (the directory fsynced after) — a concurrent reader sees the complete old contents or the complete new, never a window, and a write that dies mid-way leaves the original intact, no temp litter. A symlink’s target is replaced (the link survives); the target’s mode and an existing BOM are preserved. The trade againstFile.writeis the inode change: hardlinks split, held handles andtail -fkeep the old file, and a Docker single-file bind mount breaks. In a plan it captures the sameWriteFileop. A temp stranded by a kill is swept by the nextwriteAtomicto the same target — age-keyed (ten minutes), never pid-keyed. On Windows the rename retries briefly while a reader without delete-sharing holds the target. -
A
matchtypes its scrutinee from the constructor patterns.let f x = match x with | Some n -> … | None -> …now checks with no annotation: a case naming a single union types the scrutinee as that union, nested payloads included (Some (Ok n)resolves the inner union too). An ambiguous case — one name two unions share — still asks you to type the value, and exhaustiveness is unchanged (a missing case is still named). This narrows a standing divergence from F#, which infers the same.
Fixed
-
weir’s file readers no longer block Windows writers.
File.read/readSecret/readBytes/File.sha256(andEnv.fromFile) open withReadWrite|Deletesharing — a Windows share grant cuts both ways, so the old read default denied any live writer and a held reader brokewriteAtomic’s rename. Unix locking is unchanged (every non-Noneshare is the same shared lock). -
File.writecannot destroy a file it failed to write. The old shape truncated first and streamed lines after, so a payload that raised mid-enumeration — a lazy seq whose element fails, a command that dies mid-stream — left the old contents destroyed and the file partial, and a concurrent reader could watch the empty window. The whole payload now encodes (LF, BOM decision included) before the truncate, and lands in one write — a failing payload leaves the file byte-identical.Plan.apply’sWriteFileperforms the same way. -
File.appendis safe under concurrent appenders. .NET’sFileMode.Appendnever opens withO_APPEND— it seeks in userspace and writes withpwriteat offsets computed from a stale end, so two appenders overwrote each other’s bytes (a stress of 8 writers lost lines and corrupted tails; .NET 10’s ownFile.AppendAllBytescarries the same race). The fd now opensO_WRONLY|O_APPEND|O_CLOEXECvia libc on Unix and the whole call goes out in one unbufferedwrite(2)— every call lands contiguous and intact, and no lock ever blocks a concurrent reader. On Windows the handle opens append-only (FILE_APPEND_DATA, full sharing) — the same kernel-atomic append, no writer mutual exclusion. The payload encodes once —utf8Strict, LF, no BOM. The atom is one completed append (a short write retries, so disk-full edges can split a call), and the guarantee is local-filesystem: same-host bind mounts hold, a network filesystem does not. -
The one-line echo’s type tail recedes.
() : unitafter a unit entry printed as normal text while every other footer dimmed; it is metadata and now recedes, and a scalar echo’s: inttail dims the same way (the value itself stays plain). Piped bytes unchanged. -
The init file declares itself with
#init. A required head directive (first line) replaces path-based detection: a file carrying#initis checked as an init wherever it lives, so the config file can be authored in a dotfiles repo — editor support and all — and symlinked into<configHome>/weir/init.weir. The loader refuses the config file without the marker (pre-1.0, no shim). What the REPL loads is unchanged — only<configHome>/weir/init.weir, so which init runs never depends on the working directory;#initis the authoring marker, the symlink is the apply step. Running a marked file as a script refuses: an init is loaded, not run. -
A session binding shadows a same-named alias. The alias table hijacked a bound head:
#alias hi = echonext tolet hi () = …made the function uncallable —hi ()was even a parse error — and both definitions were accepted silently, live and frominit.weir. Resolution now follows the line-decider’s law: a bound name heads an expression; the alias table resolves unbound heads (still above builtins and PATH, so#alias ls = ls --colorkeeps working). Defining an alias over an existing binding refuses with a teaching — at the prompt, and as a located all-or-nothing error at init load. -
The editor no longer flags an init declaration as unused. An init file declares names for the session — the session is the importer of every top-level name — and the loader never judges unused-ness. The LSP ran the whole-script law there, so every prompt helper wore an
[unused-binding]error the loader would never raise. That judgement now stays out of any#initfile; an ordinary script keeps it. -
A heredoc’s leading
//line is content, not a comment. A comment-only line as a block’s first content line was swallowed by comment transparency before the district activated — a// <auto-generated/>header silently vanished from a<<<value. It now arms the district as its first byte line, matching the stated law (every byte below the marker is content) and the mid-block behavior.<<<,$<<<and$$<<<alike; ayamltemplate is untouched — there//lines are the template’s own comments, and a leading one stays transparent.
v0.0.59
Changed
- The dynamic-head teaching names
| line. A seq-typed^$(…)head’s refusal now readsbind and pick (let tool = cmd | line), then run ^$tool— the$(…) |> Seq.exactlyOnespelling it taught predates the| linereifier. The guide, the commands reference and the skill carry the same spelling.
v0.0.58
Added
- From-the-end indexing and slicing (
^n).xs[^1]is the last element,xs[..^2]is all-but-last,xs[^2..]the last two, andxs[^3..^1]counts from the end at both bounds — F#‘s spelling, where^n= length − n. Works on strings, seqs, andBytes. This reverses the earlier decline:^is the command-force sigil only at a command head, and a bracket interior is unambiguous. A^-bound resolves against the length, so a from-end sequence slice forces the sequence (forward open-ended slices stay lazy).
v0.0.57
Added
- Command chaining:
| andand| or(bash&&/||).cmd | and nextrunsnextonly ifcmdsucceeded;cmd | or nextrunsnextonly ifcmdfailed. Both stream their output and chain (mkdir d | and cd d | and build), and the right-hand side is a full command line so a builtin likecdworks as an operand. They fill the one-liner gap the statement rule left — especially in the REPL — where before you needed separate lines orif … | succeeds then …. Right-associative (differs from bash’s left-associativity for mixedand/orchains);| or’s left must be a single external command.
v0.0.56
Fixed
-
An alias to the
cdbuiltin runs the builtin.#alias up = cd ..now performs weir’scd, not the external/usr/sbin/cd(which failed with “too many arguments”). Aliases remain resolution-table entries, not macros —$(…), pipes and&&/||in a body are literal argument words; for a shortcut that needs those, write a nullary function ininit.weir(let root () = cd $(git rev-parse --show-toplevel | line)). -
weir fmtaligns a parenthesized match’s arms under them. A(match … )nested in an arm body had its arms indented to the block depth instead of under thematchkeyword (the head detector missed the leading(). Now(match— and the arms below it — line up as a top-level match does. -
REPL history dedupes properly (erasedups, keep-last). A re-entered command now moves to the most-recent position and its earlier copy is erased, so history is a clean recency-ordered set — Up-arrow,
#historyand the Ctrl+R finder each show a command once. Previously only adjacent duplicates were dropped (readlineignoredups), and the finder fed every entry to fzf so re-runs listed repeatedly. Duplicate lines already in the history file are collapsed on load. Off withhistoryDedup = false. -
Tab completion works inside an interpolation hole.
$"{Path.<TAB>now completesPath’s members — the{ … }interior is an ordinary expression. It had offered nothing: the opaque$"…"made the completer read the line as a command and try a filesystem completion ofPath.. -
ls,pwdand a relativePath.globcapture the directory where they’re written, not where they’re forced. Awithin cd "dir"block whose trailingls/pwd/globwas echoed (or otherwise forced) after the block exited had listed the outer directory — the lazy seq read the cwd at force time, after the scope restored it. They now snapshot the cwd at evaluation (the same closure rule command values already follow, and matching the eagerDir.list/Dir.stat). A userlet ls = …shadow is untouched. -
A parse error’s
Expecting:list no longer doubles a token.within tmp d -> …reportedExpecting: the scope's block or ';' or ';'; the alternatives are de-duplicated now (… or ';').
v0.0.55
Added
-
The LSP understands
init.weir. The REPL’s own<configHome>/weir/init.weirno longer flags#session/#aliasas parse errors, and inside the#sessionblock the field values complete and check as ordinary expressions (prompt = <TAB>offers your declared functions) while a field-name slot completes the keys (cwd,env,logLevel,echoCap,prompt). An unknown key and a value of the wrong type for its key are flagged, and a#aliascommand completes against PATH. A real declaration error still shows, and a strayinit.weirelsewhere stays a normal script. -
Term.width— the terminal columns, live.Term.width () : int, queried each call (a resize shows), falling back to 80 when there is no terminal (piped/redirected) rather than raising. Thetput colsprimitive for sizing output. -
Table.render— a seq of records to aligned column lines. The table layout the REPL echoes, exposed forprint:rows |> Table.render |> Seq.iter print. Display, not a wire format (noto tableback out). Tracks the terminal width interactively, unclamped when piped; an empty seq prints nothing, a non-record seq is a located error. -
A
Colormodule for terminal color.Color.red/green/yellow/blue/magenta/cyan/grayandbold/dim/underline(eachstring -> string, nesting to combine), plusColor.sgr code sfor 256-color/truecolor. Context-aware: emits the escape only at a terminal with color on, plain when piped orNO_COLOR— soprint (Color.green x)colors interactively and stays plain in a pipe, no guarding.
Changed
printkeeps color at a terminal. The tty data-sanitizer now lets SGR (color/attribute) escapes through — display-only, so they can’t drive the terminal — while still neutralizing every other family (OSC title/clipboard, DCS, cursor/screen moves, carriage-return overwrites) to visible\xNN. Colored output (theColormodule, a colored prompt banner) reaches the screen; untrusted data still can’t hijack it.
Fixed
- A quoted directory completion keeps the caret inside the quotes.
Completing
cd /wor<TAB>tocd "/work/"now leaves the cursor before the closing quote, so the next segment continues the same string literal and a re-TABnests (cd "/work/sub…"). It had landed past the quote, where the next keystroke fell outside the string and re-TABcompleted against the whole environment.
v0.0.54
Added
-
#infer let’s RHS colors and completes as a statement. In#infer let x = <src> …, the source now gets the same syntax coloring and tab-completion (bindings, command heads, members) a barelet x = <src>would — the leading#inferhad been hiding the statement context. -
A successful init load is silent.
init.weirloading clean no longer printsinit: N name(s) from …— the session opens straight on the prompt. Only failure still reports (init: not loadedplus the located error); a missing init stays silent as before. -
Parked library members, unparked.
Str.rsplitOnce/tryRsplitOnce(split at the last separator —user@host:port,[::1]:port),Option.orElseWith(orElse with a lazyunit -> Optionfallback), andSeq.tryReduce(reduce’s Option twin —Noneon empty). Each fills in next to its existing sibling. -
Bytesnow slices —b[a..b],b[..b],b[a..]. The range slice operator was string/seq only;Bytesjoins as the third target, same inclusive-clamping contract, slicing toBytes. A singleb[i]still isn’t a bracket form (useBytes.sub start len); it now teaches that instead of leaking aseqtype error.
v0.0.53
Added
-
#infer let— draft, declare, and bind in one shot.#infer let z = <src> |> from json as Podsevaluates the source once, drafts and injects the types, and bindszto the value parsed from the same sample — noSeq.freeze, no re-run, no third statement.as _auto-names the root (Type1), in the classic form too; a top-level array binds asseq<Name>.#savedistills the honest explicit spelling (typedecls +let z = src |> from json Name), never the directive line.from … as <Name>in a file now teaches the#inferspelling instead of a bare parse error. -
A custom REPL prompt:
promptin the init file’s#sessionblock. A string, or the name of aunit -> stringfunction declared ininit.weir— it may run commands (git branch --show-current | line) and is called once per entry read, never per keystroke. Colors work (SGR is zero-width for the column math, a reset is appended) and only SGR passes — every other escape family in the output is stripped; the continuation prompt pads to the same width; a raising provider falls back toweir>with one stderr note. Redirected sessions keep the fixed default.
Changed
- Breaking:
promptis nowSelf.prompt. The interactive read moves in withSelf.stdin; the bare name is unbound (no alias, pre-1.0). The REPL and-enow carry aSelfmodule withpromptalone — the script facts (args,stdin,scriptPath…) stay script-only.
v0.0.52
Added
-
$$<<<— a splice-interpolated heredoc for templating JSON/config. A third block form alongside<<<(literal) and$<<<({expr}holes): in$$<<<,$nameand${expr}substitute,$$is a literal$, and braces/quotes are literal — so a pasted JSON blob templates directly, no brace-doubling:$$<<< { "server": "$sqlFqdn", "type": "AzureSqlMI" } |> File.write "ls.json"Like any text template it substitutes raw (a value with a
"/newline can break the output — useto jsonwhen a value is untrusted).
Fixed
- A
Ctrl+R/#findselection made while candidates were still streaming to fzf was silently discarded as a cancel: the finder’s exit breaks the feed pipe, and the broken-pipe error swallowed the selection. The feed now wears its own guard — the selection is read regardless.
v0.0.51
Added
-
exec: process replacement.cmd | execreplaces the running weir process with the command (POSIXexecve; Windows spawns, waits, and exits with the child’s code). weir keeps its pid, so as a container entrypoint the application receives signals directly — no forwarding or reaping layer. It is a reifier likecomplete/orFail, diverging likefail/exit(never returns), so it is a legal bare statement; it takes a literal or dynamic (^$cmd) head and an env-sigil overlay ($e(cmd | exec)), but refuses a piped stdin — there is no parent left to feed the replacement. -
cmd | line— the single-line capture reifier. Reads a one-value command (az … --query X -o tsv,git rev-parse,id -un) as its one trimmed line of stdout, as astring— replacing the$"{$(cmd) |> Seq.exactlyOne}"mouthful withlet x = cmd | line. It joins the reifier family (complete/succeeds/exitCode/orFail/exec), raises on a nonzero exit or on 0-or-2+ lines, and composes with the env sigil ($e(cmd | line)) and a value head (xs | grep foo | line). -
Path.homeand the XDG directory trio (Path.configHome,Path.stateHome,Path.cacheHome). The typed stand-in for~/$HOME, which never expand in argv — build a path with an interpolation instead:cat $"{Path.home ()}/.bashrc",cat $"{Path.stateHome ()}/weir/history". Each is a pureunit -> stringquery with platform-native output:configHome/stateHome/cacheHomeresolve%APPDATA%/%LOCALAPPDATA%on Windows and$XDG_CONFIG_HOME/$XDG_STATE_HOME/$XDG_CACHE_HOME(falling back to~/.config,~/.local/state,~/.cache) on POSIX — the same resolution the REPL uses for its own history file. -
#historyREPL directive. Shows the session’s history with the file path in the header (a quick way to find where history lives, since~never expands): bare#historydumps every entry numbered,#history Nshows the last N. Entries render one per line, matching the history search’s display. -
REPL kill-ring:
Ctrl+W/Ctrl+Y.Ctrl+Wkills the previous word,Ctrl+Yyanks (pastes) the last killed text, andCtrl+U/Ctrl+K(kill to line start / end) now feed the same ring. The ring is session-wide, so a kill on one line yanks into a later one. (Copying from elsewhere remains your terminal’s own mouse selection — there is no shift-arrow selection, matching every terminal line editor.) -
from tablereadsaz … -o tableoutput. Azure CLI (and other tabulate-style tools) draw a------ ----------dashes rule under the header;from tablenow skips that separator when it is the first data row, so bothfrom table Tand#infer … from tablework on az output. kubectl/docker tables (no separator) are unchanged, and a real row that merely contains a lone-cell is kept — only an all-dashes row is dropped. -
A checker warning for the
Path.newTempDircleanup footgun. APath.newTempDir ()binding that is laterDir.delete/Dir.deleteAll’d in the same scope now draws an advisory warning pointing atwithin tmp, which cleans up on scope exit and on Ctrl+C/kill (a manual delete misses the signalled case).newTempDirremains the right tool for a directory that must outlive its scope, so a binding with no in-scope delete stays silent. Warning severity —checkstill exits 0.
Changed
- A failed command in the REPL is now a quiet exit-code status, not a red
error. When a bare command you type at the prompt exits nonzero, the
REPL shows a dim
↳ exit Ninstead oferror: command failed with exit code N: …— the output already streamed and the session continues, so it reads like a shell’s$?(and since weir has no$?, showing the code is the only way to see it). The raise itself is unchanged: scripts still fail-fast, and a command failure inside a value ($(…), a reifier, a binding) still raises the full error, because there it aborted a computation.
Fixed
-
A top-level
if … then … else …block now parses. The block form at column 0 (if c then+ an indented body, then a dedentedelse/elif) was rejected as a strayelsekeyword — the assembler treated the dedentedelseas a new statement. A col-0else/elifnow continues itsif, the same way a dedented|/until/alwaysalready did. (The indented form inside a function body always worked.) -
#inferno longer mis-drafts a heterogeneous object as a homogeneous map. When sibling objects in a sample array carried different keys whose values only coincidentally agreed in an early pair (e.g. a KubernetessecurityContextwith aboolfield, then anintfield two elements later), the pairwise merge committed toseq<string * bool>and a laterfrom jsonrejected the int (expected bool, got Number). The open-map verdict is now decided over the whole set of siblings, so an object with mixed value types stays a typed record; genuinely uniform data-keyed objects (a ConfigMap’sdata) are still drafted as an open mapping. -
Tab-completing a
$namesplice in a command argument now offers session bindings.az … --location $l<TAB>completes to$location(any binding whose name starts withl); before, an argv position offered only filesystem paths, so a splice never completed. A bare (non-$) argument still completes paths only. -
execis now refused inside aplanblock. A helper that invokescmd | execinside aplanis refused at runtime, matching the guard on every other spawn —execvedoes not funnel through the spawn path, so the guard is applied inProc.execdirectly (it could otherwise replace the process inside a dry run).
v0.0.49
Fixed
-
A deeply-nested value echoed at a terminal no longer crashes the process. The tty binary-echo probe (which decides whether to sanitize a value’s bytes at a terminal) walked record/tuple/union/map structure with no depth bound — the last value walk that wasn’t iterative or depth-capped. A checker-accepted program producing a deeply-nested recursive-record value, echoed under
weir -eat a tty or in the REPL, aborted with an uncatchable stack overflow. The probe is now bounded at depth 100, matching theshowrenderer. -
A heavily-documented module no longer stalls
weir check. The doc-comment attachment pass appended each///line to a growing list one at a time, which is O(N²) over a contiguous run — a 50,000-line module pinned a core for ~20s. The run now conses in O(1) and is reversed once at the point of use, restoring linear time. -
The top-level “internal error” fallback now sanitizes its message at a terminal. The CLI’s residual-exception guard printed a .NET exception’s message verbatim to stderr; a message interpolating attacker-influenced data (a filename, URL, or value) could emit terminal escape/control bytes, the one error-echo site that had missed the tty-data neutralization.
v0.0.48
Added
-
HttpMethodgains anOther of stringcase for a well-formed but unlisted request verb. Awithin servehandler can now route on a verb the union does not name (match req.method with | Other v -> …) and answer 405 by its own choice, instead of the verb being silently reported asGet. On the client side a request built withOther "…"sends that verb verbatim. -
within serve’s config gains an optionalbodyTimeoutfield that bounds the request-body read (default 30s). A slow client dribbling its body is refused with a 408 instead of parking a handler slot. Existing{ port; maxConcurrent }configs are unchanged — omitting the field keeps the 30s default. -
Server.streamErrors— the designated channel for a streaming failure. When aStreamresponse body’s producer raises mid-flight, its message is recorded on the handle rather than raised out of the handler, so a monitoring loop can readServer.streamErrors srvand learn its producer died (the failure used to be reported to nobody).
Fixed
-
An HTTP request method
within servecannot name no longer becomesGet.TRACE,QUERY,FROBNICATEand any other verb outside theHttpMethodunion all arrived at the handler asGet, so the read path ran for a request that was not a GET and a handler could never answer 405 for an unknown verb.QUERYnow reads asQuery(a case the client side already had), a well-formed unlisted verb reads asOther v, and a malformed method token (control chars, whitespace, non-token bytes) is refused at the boundary with a 400 — while a well-formed unknown verb a proxy may forward is preserved. -
A
Streamresponse body whose producer raises mid-flight no longer reports success to everyone. The producer’s failure used to still emit a proper terminating chunk and a 200, so the client read a complete stream and the script exited 0 — a silent truncation. The failure is now surfaced to the script throughServer.streamErrors(the designated channel, not a raise out of the handler), and the response is aborted. (The managed HttpListener emits the chunk terminator even on abort and offers no AOT-safe way to suppress it, so the wire-level truncation is best-effort on this platform; the script-observable channel is the reliable signal.) -
within servebounds the request-body read. A client that declares a largeContent-Length, sends a few bytes and holds the socket used to park a handler slot indefinitely. The read is now bounded by the newbodyTimeoutconfig field (default 30s) and exhaustion refuses the request with a 408. -
Terminal escape sequences from data no longer reach the terminal verbatim. A filename or field carrying ANSI/OSC escapes, a bare ESC, or other control bytes could clear the screen, set the window title, leave the terminal coloured, or use a carriage return to hide the real name. Tty-bound data renderers —
print, command streaming, the REPL echo and table, and error text — now render those bytes as a visible\xNNescape. weir’s own colouring is unaffected, and redirected output (a pipe or file) stays byte-faithful. -
A
plannow captures filesystem paths as absolute, bound to the directory where they were captured. A plan stored the caller’s relative path andPlan.applyre-resolved it against the apply-time working directory — so aFile.write "marker.txt"captured underwithin cd "planning"and applied underwithin cd "application"wrote toapplication/marker.txt, not the previewedplanning/marker.txt. Capture now resolves every filesystem path to absolute at capture time andPlan.applywrites that exact path, so whatPlan.previewshows is exactly what apply performs, regardless of the working directory at apply. This covers every captured filesystem op —File.write/delete,Dir.create/delete/deleteAll, and the two-pathFile/Dircopy/move(both source and destination are bound). Plans built from absolute paths are unchanged (they already resolved to themselves), so plan equality andPlan.opscomparisons on absolute paths behave as before. -
A CRLF (or NUL) in an HTTP header no longer forges a second header or is silently dropped. A header name or value carrying CR, LF or NUL is now refused at both HTTP crossings. On the
Httpclient an outbound request header with such a byte (from a config file, an API response, a tenant name) used to arrive at the server as two headers —X-Evil: aplus a forgedInjected: yes— the response-splitting class.Http.sendnow refuses before sending, with a located error naming the header, the byte, and whether it sat in the name or value. On thewithin serveside a handler-returned header with such a byte used to be silently dropped; it is now refused too — the response is a 500 without the injecting header (never a silent drop), and the located message reaches the script throughServer.streamErrorsso a handler bug is surfaced rather than swallowed. A benign control header is unaffected in both directions. -
secretHeadersno longer leak across a cross-origin redirect.Http.sendfollowing a redirect to a different origin used to re-sendsecretHeaders— a credential channel carrying aSecret— to the new host, while the typedauthunion was protected only because .NET happens to dropAuthorization. weir now dropssecretHeaders(andauth) on a cross-origin redirect exactly as the BCL dropsAuthorization, so the two credential channels agree: the destination a redirect names can no longer harvest a credential the caller sent to the original host. A same-origin redirect keeps the credential, and a non-sensitive header still crosses (the drop is credential-specific). -
The contract-fetch client no longer leaks credentials across a cross-origin redirect.
weir add module/add schema/restore/verifydownload over a separate HTTP client fromHttp.send, and it was a bareHttpClientwith automatic redirects that re-sent the credential (a GitLabPRIVATE-TOKEN, a GitHubAuthorization: token) to a redirect target — the BCL strips onlyAuthorization, soPRIVATE-TOKENin particular leaked. The contract client now disables automatic redirects and follows them explicitly, dropping the credential headers on a cross-origin change, across both the API-resolution request and the artifact download. (The leak mechanism is closed; whether a real provider can be induced to redirect to an attacker host is an unproven precondition, recorded in the ledger.) -
An HTTP error no longer prints a URL’s credentials in the clear.
Http.fetch’s non-2xx error quoted the whole URL (http://…answered 500) andHttp.send’s transport error did the same when the URL could not be parsed — so a credential inhttp://user:pass@hostprinted verbatim to the terminal, a CI log, or the REPL. Both now redact the userinfo:http://***@host answered 500. A URL with no credentials is still shown in full, so the target is named. (Display only — a credential still rides the wire and isps-visible in argv, unchanged.) -
Parse and decode errors no longer echo a huge invalid input back.
Str.toInt,Str.fromBase64,Bytes.fromBase64,Bytes.fromHex,Duration.parse,Size.parse,Float.parseandInstant.parseembedded the caller’s whole input in the error message, so a 500KB invalid value produced ~500KB of stderr. They now excerpt: an input of 64 characters or fewer is quoted in full (unchanged — a short typo stays readable), a longer one shows a 64-character head and names the true length (… (200000 chars)), so a multi-megabyte invalid input yields a bounded error. (A residual short-prefix leak for a value derived from a revealedSecretis recorded in the ledger.) -
A NUL byte in a path no longer crashes weir — it is refused with a located error.
Path.GetFullPaththrows a rawArgumentException: Null character in pathon a NUL-bearing argument, and every File/Proc/completion builtin plus the parser’s command-head classifier reach it throughSession.resolve. A.weirline like./a<NUL>b caborted the process (SIGABRT, exit 134) with no diagnostic, because the parse-time head resolution runs outside the runner’s exception handler; a NUL-bearing path value at run time (e.g. a command line or file line carrying a NUL) leaked the raw .NET message.Session.resolvenow rejects a NUL-bearing path with a located weir error (path contains a NUL byte — paths are NUL-free; NUL-bearing data is binary, not a path), closing the whole run-time class, andExtern.existstreats a NUL-bearing program head as not-found so the parser emits its ordinary missing-command diagnostic instead of aborting. NUL-free paths are unchanged. -
An out-of-range attribute integer is now a located error, never a crash. The tiny literal lexer behind attribute arguments (
[<Default n>]and friends) cast the digits toint64with a raw cast that throwsOverflowExceptionpast the 64-bit range — and nothing above the parser caught it, so[<Default 99999999999999999999>]on any field, union case, or type abortedcheck/run/fmt/-e/the REPL with a raw stack trace (exit 134). The digits are now parsed withInt64.TryParse, and an out-of-range value is a located parse error —attribute argument out of range (64-bit): <digits>— mirroring the integer-literal parser. The duration/size unit arms (…TiB,…h) additionally bound their scaling multiply, so a large-but-in-range base can no longer silently wrap negative. In-range attributes (30s,10MiB,42,0.5) are unchanged. -
A bareword
;-spine no longer parses in O(N²). Alet x = b;b;b;…line (N barewords) cost quadratic time: a bare;is a literal argv word in command position, so the doomed command-mode attempt’s head-word scanner consumed the whole remaining line before failing, once per bareword. The command head-word scan is now bounded (a resolvable command head is a PATH entry, ≤255 bytes), which caps the doomed attempt and makes the line linear — 20k barewords fell from ~27s to ~5s. Resolvable command heads and the diagnostic on the spine are byte-identical; argv words longer than the bound still parse as arguments (no grammar change). -
The top-level CLI verb dispatch has a residual-exception guard.
check/run/fmt/-erun weir’s front end, but the verb dispatch had no top-leveltry/with, so any residual front-end exception aborted with a raw stack trace and exit 134. A residual exception now becomes a locatedinternal error: …diagnostic on stderr with a non-zero exit, mirroring the LSP’s per-document guard. Kept narrow: a legitimateexit/failis never swallowed. This backstops the whole front-end crash class behind the two point fixes above. -
weir check/--can/ the LSP no longer multiply a per-statement inference-budget burn across a whole file. The type checker’s inference budget bounds one statement, but the whole-file analysis (analyzeLines) collects errors and continues after each — so a file with several independent budget-exhausting statements multiplied the single-statement cost with no whole-file ceiling (linear in blocks, re-paid on every LSP keystroke: a CPU-exhaustion DoS). A budget- exhaustion diagnostic is now a stop-and-fix, exactly as the runner and module loader already abort on the first error: the fold stops at the first budget diagnostic and checks no further statement, so one burn bounds the file. Ordinary type errors are unaffected — a file with three plain type errors still reports all three.[D:budget-stop-first] -
weir lspno longer allocates for a client-suppliedContent-Lengthbefore reading the body. The framing layer allocated a buffer of exactly the declared length, soContent-Length: 2147483647(26 bytes) triggered anOutOfMemoryExceptionthat killed the editor session (an OOM DoS). The transport now caps accepted messages at 64MB and never allocates for a larger declared length: an oversized body is drained in small fixed chunks to keep the stream synced and dropped as an id-less no-op, so the server keeps serving. The header-line accumulator is independently capped at 64KB so an unbounded header line cannot grow memory.[D:lsp-transport-caps] -
The line assembler no longer scales quadratically on hostile inputs (algorithmic-DoS) [D:assemble-quadratic]. Four O(N²) paths in the logical-line assembler let a crafted hundreds-of-KB source stall
weir check/fmt/REPL/LSP for tens of seconds to minutes:- the pending statement’s text was rebuilt (
text + sep + piece, plus a full-textTrimEnd) on every continuation join — now a single growingStringBuilderis mutated in place and materialized to the immutableLogicalLine.Textexactly once at statement close; - the bracket-continuation dangle predicates called
TrimEnd()on the whole accumulated text per line — now answered from an incrementally-tracked last-non-white index; - the
within proc/within servebinder-head scan re-LastIndexOf’d and re-scanned the whole growing text per join — now it reads an incrementally-tracked last-segment start (and aContainsfast reject skips the per-char scan on marker-free segments); - the record-update (
{ … with) header check allocated a whole-remaining-line substring per opening bracket, so a bracket-heavy single line was quadratic — now an index-based, allocation-free check. The emitted diagnostics and the assembled span tables are byte- identical (a pure performance fix, no grammar change): the join arithmetic derivesjoinedStartfrom the buffer length at exactly the point the old code read the string length, and the same separator literals apply. Measuredweir checkon the standing fixtures: continuation-line flood 80k lines 9.3s → 1.3s; the barewordwithin procmarker case 80k lines 24.1s → 3.6s; a bracket-heavy single line 400k record openers 29s → 0.5s — quadratic to linear.
- the pending statement’s text was rebuilt (
-
A NUL byte can no longer silently truncate a value at any process boundary. weir’s word-integrity guarantee (SECURITY.md) refuses a NUL-bearing value at the spawn hand-off — argv and env are NUL-terminated C strings, so an unrefused NUL would truncate the word at the child, which runs the prefix and exits 0 with no diagnostic. The refusal lived only in the evaluator’s command-statement and pipe constructors; four other spawn paths assembled argv/env downstream and skipped it, so a NUL entering as external data (a command whose stdout carries a NUL,
File.read, base64-to-bytes) leaked through: the exit-code reifiers (| complete/succeeds/exitCode/orFail), the ambientwithin envoverlay and its$e(...)twin,into, and — the worst — the dynamic command head^$name, where a NUL-bearing head resolved through PATH to the prefix program and ran it with the remaining words as argv. The NUL refusal now lives atProc.spawn, the single point every process start funnels through, validating the program name, every argument, and every environment key and value — so all four downstream paths inherit the boundary and refuse with the same located diagnostic (exit 1, no child spawned). Two adjacent shapes gain weir-shaped diagnostics too: an empty program name, and a NUL-bearing path-like program name (which previously leaked a raw “Null character in path” platform exception throughPath.GetFullPath). The pre-existing statement-path refusal is unchanged.[D:spawn-nul-funnel] -
A deeply-nested YAML document no longer hangs the parser.
Yaml.parse(andfrom yaml T,#infer,Yaml.inferShape, andweir checkon a deep-ladder yaml district) recursed once per mapping/sequence nesting level with no limit, and each mapping level re-scanned its remaining extent — so a hostilea:ladder was cubic in depth and hung for tens of seconds to hours. The parser now caps nesting at 500 and fails with a located diagnostic (“yaml nesting is too deep (limit 500) — the subset reads real manifests, not adversarial ladders”). Real manifests nest far below the cap and are unaffected. External command output piped into a yaml adapter is the realistic hostile source this closes. -
A deeply-nested value no longer crashes
to yaml. The YAML emitter recursed per nesting level with no limit, so a value built iteratively (e.g. aSeq.foldnesting aYSeq~100k deep) crashed the process with an uncatchable stack overflow (exit 134) — hostile or malformed input must never crash the tool. The emitter now caps nesting at 1000 and fails with a clean located error (“to yaml: the value nests deeper than 100 — the emitter needs finite trees”). Finite, real-depth trees render unchanged. -
==on a deeply-nested value no longer crashes the process with an uncatchable stack overflow. A legally-built recursive-record value — anOption-linked record folded ~100k deep viaSeq.fold, which the checker accepts — crashed the wholeweirprocess (StackOverflow, exit 139/134) when compared with==, because value equality recursed one stack frame per nesting level. Equality now walks an explicit heap work-list of pending value pairs instead: records (order-insensitive), union payloads, tuples, and map entry values queue their children, seqs compare lockstep via enumerators (short-circuiting at the first mismatch), and a mismatch drains the list. Every existing equality semantic is unchanged — closures/builtins/procs/servers still compare by reference, bytes structurally, floats by value. Theshow/ interpolation renderer shared the same crash class and now carries a finite depth bound (100, past every real value; deeper nesting renders a teaching ellipsis), matching the REPL echo’s existing bound.
v0.0.47
Fixed
-
A parallel or race combinator inside a
plannow refuses instead of escaping capture.plan’s mutation capture is thread-local, butSeq.pmap/pmapWith/piter/piterWith/pfirst/pfirstWithrun their callbacks on worker threads that did not inherit the capture frame — so aFile.write(or any native mutation) inside a parallel callback underplanexecuted for real while the plan reported empty. The combinators now refuse on the calling thread before any worker is scheduled, with a located error naming the combinator. The same combinators outside a plan are unchanged, and a native mutation reached through a serial helper still captures normally. -
An indirect process started inside a
plannow refuses at runtime. The static plan check does not follow helper-function references, so a helper that forced a command (e.g.sh -c "…") spawned the process for real during plan construction while the plan reported empty. The process spawn point now refuses when a plan is capturing on the same thread, with the same “proc refused inside plan” message. The directsh -cform still refuses at check time (belt and suspenders), and a weir-nativeFile/Dir/Httpmutation reached through a helper still captures — only process spawns are refused. -
within servereaches the handler overlocalhost, not just127.0.0.1. The scoped listener registered a prefix for127.0.0.1only, so a client addressing the server aslocalhostgot .NET’s built-in404before the weir handler ran (theHost: localhostrequest did not match the127.0.0.1prefix). The listener now registers the common loopback names beside each other on the one port —127.0.0.1,localhost, and[::1]— so all three reach the handler. The posture stays loopback: no+/*all-interfaces bind, so the server is not exposed beyond loopback. The[::1]name is guarded — a host without IPv6 loopback still serves on the two IPv4 names. Bind, teardown, port-free, andServer.port/Server.runningare unchanged. -
File.isExecutable ""raises instead of returningtrue. An empty string resolved to the current directory (which exists and carries a directory’s execute bit), so the query answeredtruefor a path that is not a path.""now raises the same locatedno such patherror a missing path raises. A real missing path still raises, unchanged. -
restoreandverifyconfine a lock entry’s path to.weir/. A hand-edited or tampered lockfile whose entry recorded a path outside the vendor directory (absolute, or../) maderestorewrite andverifyread outside.weir/—restorecrashed on the write. Each entry’s path is now confined per entry: a hostile one is a located refusal naming the entry, while its benign siblings still restore.restoreoverwrites only its own artifacts inside.weir/. -
Contract-path confinement now resolves symlinks. The path check above was purely lexical, so a lock path with no
../and no absolute root still escaped.weir/when a component on the way — an intermediate directory like.weir/schemas, or the final file itself — was a symlink pointing outside.restorefollowed it and wrote outside the vendor directory. Confinement now resolves the real filesystem object (symlinks chased on every existing component) and requires it under the real.weir/root, across every sink:restore,verify,gen types,add schema, andadd module. The write opens the final component without following a symlinked leaf (O_NOFOLLOWon Unix), so the check and the write refer to the same object, thenfchmods that open descriptor to0o644(libcopen(2)is variadic, and its mode argument was unreliable on ARM64 macOS — a vendored file could land unreadable, so a laterverifyfailed with a permission error). The fullopen(2)flag set (O_CREAT/O_TRUNC/O_NOFOLLOW) is now branched per OS: the Linux values had been hardcoded, so on macOSO_TRUNCwas never set and a re-write over a longer existing file (repairing a tampered vendored file) left stale trailing bytes and failed the post-repair hash. A user-chosengen types --out <path>stays unconfined by design. -
add schema --asrefuses a name that is not a plain name. An absolute or traversal--asname vendored the schema outside.weir/. The plain-name ruleadd modulealready enforced is now shared byadd schema,add module, andgen types, refused before the fetch with nothing written. -
An unreadable import is a located diagnostic, not a crash. An imported module that existed but could not be read (permissions) crashed
check,check --json,check --can, and the LSP. The import source is now read once into a result: an unreadable file gives a locatedcannot read importdiagnostic (a missing file still saysno file at …), and the previous read-twice shape no longer races a file deleted mid-check. -
A
forloop no longer tripspure,readonly,plan, or--strict. Thefordesugar targets a library member (Seq.iter), but the effect classifiers treated every|-prefixed internal name as a command — so aforbody that reached no effect was refused inside apure/readonly/planblock, the internal desugar key leaked into the refusal message, and eachforadded a phantom dynamic-head capability that failed--strict. The classifiers now distinguish a command reifier from a library desugar and read the latter as its target member; no internal|-name reaches a user-facing message. -
A multi-statement
readonlybody parses. Areadonlyblock whose body ran a unit statement before its result value space-joined the statement onto its successor and mis-parsed (a spurious arity error), becausereadonlywas missing from the block-sentinel registration thatpureandplanhave.readonlynow sequences its statements likepure; its mutation ceiling is unchanged.
v0.0.46
Added
-
within serve— the scoped HTTP listener, weir’s first server surface.within serve srv = { port = 8080; maxConcurrent = 4 } handleropens a loopback listener for the block, runs a plain synchronous handler (HttpServerRequest -> HttpServerResponse) per request, and closes the socket on every exit — normal, raise, SIGINT and SIGTERM — so the port frees (a second bind on it then succeeds). Same no-orphan discipline aswithin proc, port edition: the scope IS the lifetime. Three primitives, deliberately no more — scoped by the ring protocol study, which proved this surface needs no WebSockets (commands are request→response, pushes are SSE):- A scoped listener with a sync handler. The handler matches
on
req.path(weir’s union/match, not a routing framework) and returns a response record. Async-under-sync: the runtime multiplexes connections on tasks; the handler surface stays synchronous. - A streaming response body:
Stream of seq<string>, a new case on the sharedHttpBodyunion besideNoBody/Json/Text. The runtime pulls and flushes each element as it is produced — chunked, SSE-shapeddata:lines — so a slow producer streams incrementally (the client sees early elements before the seq ends). The lazy-seq-streams-out precedentprintsets, server side. - A handler concurrency ceiling:
maxConcurrenton the config, theSeq.pmapWithconcurrency law on the scope — N handlers run at once, excess queues. One type family:HttpMethod, header pairs and the body union are shared with theHttpclient; the server records differ only where the wire differs (HttpServerRequestcarriespath+query, not aurl; noauth/timeout— client concerns;HttpServerResponse’sbodyis anHttpBody, soStreamis expressible). The handle surface isServer.portandServer.running(the scope is the teardown — there is nostop). Bounded out of v1, each a stated non-goal: TLS (a reverse-proxy posture), a routing DSL, WebSockets, request-body streaming, HTTP/2.
- A scoped listener with a sync handler. The handler matches
on
-
Dynamic command heads:
^$name/^$(…). The^force-external head gains a$-splice alternative beside the literal — a runtime string names the program, and argv stays typed argv, so the computed-command shape that used to forcesh -c "$path …"(plugin callbacks, asdf-styleexecdispatch, lookup-table tools) runs injection-safe:^$tool $userArgpasses one entry per argument and a head value with spaces is one program, never re-lexed. The value must be astring; a seq capture refuses with a bind-and-pick teaching (let tool = $(…) |> Seq.exactlyOne, then^$tool), while a string-typed capture (^$(… |> Seq.exactlyOne) args) heads directly. Resolution moves to run for exactly this head:weir checkdraws no cmd-not-found diagnostic (there is nothing to look up yet), a missing program is a located run error naming the value, andweir check --canreports the head as not statically known, counted with the opaque sites (--strictexits 2). Pipes,$()captures, the| completefamily, value-headed stdin and env sigils compose exactly as with a literal head;^$@xsand^$"…"refuse with teachings. -
Width members:
Str.replicate,Str.padLeft,Str.padRight.Str.replicate n sconcatenates n copies (0 → empty; a negative count raises — Seq.replicate’s convention).padLeft/padRightpad with spaces to a TOTAL width, .NET Pad semantics: an already-longer string is unchanged, a negative width raises. The primitives every columnar CLI port hand-rolled (asdf’scurrent,printf "%-15s").replicatenow has two homes (Seq and Str), so the bare REPL name retires — qualified-only, the derived bare-partition rule. -
Seq.equal : seq<'a> -> seq<'a> -> bool. Element-wise, length-sensitive, Eq-constrained likeSeq.contains(functions, seqs and floats refuse at the use site, floats namingFloat.near). Lockstep and short-circuiting — never pulls beyond need, so a finite seq compares safely against an infinite one. The honest spelling of output-vs-expected; a join-then-compare is lossy when elements contain the separator. -
Hex, slicing and HMAC on
Bytes:fromHex/toHex,sub,hmacSha256.toHexemits lowercase (sha256 parity);fromHexreads either case and refuses odd-length and non-hex input with located teachings (fromBase64’s posture).Bytes.sub start len bisStr.sub’s exact shape on bytes, out-of-range raising with the same detail.Bytes.hmacSha256 key msg : Bytesis .NET’s HMAC-SHA256 (key first, then message; RFC 4231 vectors pinned) — a Secret key exits viaSecret.reveal |> Str.toUtf8, deliberately. Erases the ~70 hand-rolled hex/codec lines of the acme.sh port and its External-Account-Binding openssl fallback. -
File.isExecutable : string -> bool. The mode’s OWNER execute bit as a bool — replaces the stringlyFile.mode |> Str.contains "x". Follows a symlink and raises on a missing path likeFile.mode; on Windows (no execute bit) it answers by extension (.exe/.bat/.cmd/.com) — a stated posture, never a guess.
Changed
- BREAKING:
Seq.forceis renamedSeq.freeze(bare aliasforce→freeze); the old spelling is gone, no compatibility alias. “force” named the moment (evaluate now) and misleadingly suggested repeatability; the durable property of the result is a frozen snapshot — pulls never re-run. Migration:s/Seq.force/Seq.freeze/, bareforce→freeze. A stray old spelling teaches the new name at both lookup sites.
Fixed
-
Detached SIGINT now tears scopes down. A
kill -INTon a weir backgrounded in a non-interactive session (setsid, a CI runner, a systemd unit) was a no-op: scopes did not unwind,alwayscleanups were skipped,within procchildren orphaned, and only SIGKILL stopped it. The cause: such a shell sets SIGINT toSIG_IGNbefore exec (the job-control nohup convention) and .NET honours an inheritedSIG_IGN, so weir’s signal handler never installed. A detached weir (no controlling terminal) now resets SIGINT to its default disposition, so SIGINT runs the same scope-unwind and child-reap SIGTERM and a tty Ctrl+C always ran, exiting 130. A second signal during teardown hard-exits (the double-Ctrl+C escape). Aweir … &/nohupin an interactive shell keeps its controlling terminal and its ignored SIGINT unchanged — a terminal Ctrl+C still does not reach it (send SIGTERM, orkill -INTthe pid). SIGTERM was already correct and is unchanged; POSIX only (Windows keepsConsole.CancelKeyPress). -
The operator partial-application teach no longer offers two different functions as interchangeable.
(op) vstill refuses; for a commutative operator (==,<>,*) the message keeps both lambda spellings as alternatives, and for every other operator it now says the directions differ (fun x -> x - v and fun x -> v - x differ) instead of the(or …)form.+takes the differ-form too: it concatenates strings by context, and the operand type is not known at the refusal site. -
A qualified case in pattern position teaches the bare law.
| Core.System ->errored with a bare expecting-list; it now names the repair — writeSystem, notCore.System: the scrutinee’s type resolves the case, imported unions included. -
A qualified type in the adapter slot teaches the flat-import law.
from json Acme.Poderrored unhelpfully; it now says writePod, notAcme.Pod— imported types resolve by their plain name (the module alias qualifies values, not types in this slot). -
A capture sigil composed with a reifier names the spelling that works.
$e(cmd) | completestill refuses (the sigil closes its chain at the)), but the error now names the composition that exists:$e(cmd | complete). -
A block-local binder piped into inside the same statement is no longer a phantom command. Under check’s assume-command rule,
… |> padwherepadis aletearlier in the same function body mis-read as feeding a program ('|>' applies functions; feed a program with '|'). The pipe-target walk now tracks statement-local binders (lets, params, match/for/within binders) — bindings beat PATH, block-locals included. -
A helper ending in
exit/failused as a statement names its cause. Such a helper infers a polymorphic return (diverging calls are never unit), so using it as a statement was a bare “computes a ‘a1 and discards it”. The discard error now explains the polymorphism and both repairs: return the exit code and exit at the call site, or bind the value.
v0.0.45
Added
-
The checker warns on possible re-enumeration. A
letwhose RHS is a command-backed seq with no visible force (|> Seq.force, an appliedSeq.force, an eager literal or a comprehension) re-runs its command on every pull. When such a binding is enumerated at two or more sites,weir checknow warns at the second and later sites — naming the command and the one-line repair (snapshot one run: let pods = kubectl get po -A |> Seq.force). Advisory: warning severity, exit stays 0, and the LSP shows it as a squiggle. Conservative and stated: any read of the name counts as a possible pull (argv splats included); a plain alias (let y = pods) does not. -
The REPL binding echo states the bound seq’s state. At a tty,
let pods = kubectl get po -Anow echoespods : seq<string> (command-backed — re-runs on each use), and a materialized bind echoes(frozen)— the same forcedItems probe the echo and completion already share. A pure lazy seq stays unannotated. Display only; the piped surface is unchanged. -
weir gen types --schema <name>— generate weir types from a locked JSON Schema. The locked.weir/schemas/<name>.json(fromweir add schema) becomes a declaration-only weir module at.weir/types/<name>.weir, imported asimport "weir:<name>" as …(theweir:namespace now resolves vendored modules first, then generated types).#inferdrafts from a sample and can only see what the sample had; the schema carries the facts no sample can — a member ofrequiredgenerates a plain field, anything elseOption<…>, the nullable spellings (type: [.., "null"]/nullable: true) fold intoOption,additionalPropertiesgenerates theseq<string * V>mapping,$refdefinition names become type names (io.k8s.api.core.v1.PodSpec→PodSpec, parent-segment prefix on collision), anenumgeneratesstringplus a//note listing the values,anyOftakes the first variant with a verify note, and a self-referential definition stays opaque (Yaml) with a note — never a silent guess. Field names ride#infer’s own[<Wire>]sanitizer; the emitter is shared, only the frontend differs. The generated file is user-owned and deliberately unlocked: the provenance header records the schema name and lock hash, regeneration is an explicit re-run (--asnames the top type,--outmoves it,--out -streams to stdout), output is byte-deterministic, and the emitted module runs through the real checker before anything lands — a schema producing unrepresentable weir refuses with the reason. -
The schema subset reads the raw k8s OpenAPI shape.
weir add schema(andschema=district validation) now accept in-document$ref(#/definitions/…,#/$defs/…,#/components/schemas/…, resolved against the root holders — a dangling ref refuses at add),allOfof one schema plus annotations (the k8s $ref-with-description idiom),anyOf(all-scalar folds likeoneOf’s IntOrString; mixed alternatives validate as unchecked), andnullable: true. Cross-file$refstill teaches the standalone variants;not/if/then/elsestay outside the subset. -
from json/from yamlmissing-field errors teach the Option repair.missing field 'env' …now carries the from-table spelling: if the field is sometimes absent, declare itOption<…>— a type drafted from a sample only sees what the sample had.
v0.0.44
Added
-
Str.fieldsandStr.rsplit— the shell’s field splitting, as members.Str.fields ssplits on whitespace runs and never yields an empty piece (leading/trailing whitespace produces nothing; a blank or empty string is the empty seq) — POSIX field splitting with trim’s whitespace class, so the kubectl-column idiom isline |> Str.fields |> Seq.item 1instead ofStr.rmatchAll @"(\S+)" line |> Seq.map Seq.head.Str.rsplit pat ssplits on every regex match, pattern-first like the rest of the r-family:Str.split’s empties law verbatim (adjacent matches and edges yield""), capture groups never add pieces, and a bad pattern raises likermatch. -
Tab completes a bound map’s keys. With open maps first-class, keys are data — no static pool can offer them. The REPL now reads them from the value you already hold: inside the open key literal of a pipe-form
Map.get/Map.tryGet/Map.has,d |> Map.tryGet "Cor<TAB>completes the bound value’s own keys, prefix-filtered, inside the quotes (no closing quote is added — the path-in-quotes convention). The receiver must be a bare session binding (itcounts) whose value is already materialized — a map or a forced pair-seq. Completion still never runs anything: a pipeline receiver would need evaluating, so it completes nothing (bind first —let d = …— and the keys complete), and an unforced seq is never pulled. -
from table Treads aligned column output. The shapekubectlanddockerprint — one header row, aligned data rows — reads into declared row records:kubectl get po |> from table Podyieldsseq<Pod>. Columns slice at header offsets, never whitespace runs, so a spaced value (Up 2 hours) survives; a header boundary is a run of two or more spaces, soCONTAINER IDstays one column (both tools pad columns with three spaces). Fields match headers by normalized name, case-insensitively (podTemplateHashreadsPOD-TEMPLATE-HASH);[<Wire "HEADER">]matches a raw header verbatim. Cells trim and type by the declared field (string/int/float/bool); anOptionfield reads an empty or<none>cell asNone, and a required field refuses an absent cell naming theOptionrepair. Extra columns are ignored, blank lines skip, a header-only table is the empty seq, and every error is located — a missing column lists the headers seen, a bad cell carries its row line and column. Read-only: there is noto table.tablestays an ordinary identifier outside adapter position. -
#infer … from table as Poddrafts the row record. The REPL directive (and its composable core,Table.inferShape) scans a live sample per column — all-int columns draftint, float/bool by token, elsestring; a column with empty/<none>cells draftsOptionwith a note — and prints that the value reads asseq<Pod>. Headers sanitize to field names with[<Wire>]where the spelling needs it, the taken-name guard included.Note for the release flow: the external tree-sitter-weir grammar needs the matching
tableadapter bump before the next release — the grammar-currency gate will go red until it lands.
Changed
-
#infermerges array elements. A drafted element type was the first element’s shape only, so real kubectl lists died on read (kubectl get cm -o json→missing field 'items[2].data.networkYml'— every ConfigMap’sdatacarries its own keys). The element type is now the UNION of all elements: a key absent in some elements draftsOption<T>; a genuine type conflict for one key keeps the first element’s type with a printed verify note. Differing key sets across elements are ordinary now — no note. -
#inferdrafts data-keyed objects as open mappings. An object whose entries carry one value shape and whose keys look like data — a majority of non-identifier keys (k8slabels/annotations), or key sets that differ across the array’s sibling elements (a ConfigMap’sdata) — drafts asseq<string * V>with a note instead of a[<Wire>]-riddled record. Mapping keys are data, so no wire attribute is drafted;metadata-shaped objects (identifier keys, identical across elements) stay records. An empty{}is an open map with zero entries and draftsseq<string * string>with a note — the old opaqueYamldraft could not cross the json boundary, so a json-sourced draft carryingresources: {}checked but never read. -
from json/to jsonspeak theseq<string * T>mapping. The yaml boundary’s pair-seq law, now on json too: a field (or nested position) typedseq<string * T>reads a JSON object as pairs in document order and writes back as one object, so the drafted mappings read and roundtrip on both wire formats. An empty mapping writes[]and reads back empty — the roundtrip holds;Map,jsonl, and every existing shape are unchanged. -
itfollows F# Interactive: expressions and commands always rebind it;letnever does. Every expression and command statement now bindsit— unit included: a streamed bare command bindsit := ()(weir never held the bytes, so unit is the truth) and the tty echoes() : unitfor unit statements; aletbinds its name and nothing else (let o = 10binds noit, FSI’s own rule — previously theletRHS bound it and unit statements left it alone). Directives still leaveituntouched and a fresh session’sitstays unbound. The v0.0.43 streamed parenthetical ((streamed — not bound to 'it'; let x = … captures)) reverts to the plain: seq<string>— a standing teach on every command was noise. The teach now fires only at misuse: using the unit-bounditwhere unit fails the check (#infer it from yaml,it |> map …) appends the capture repair to the located type error, with the recorded command verbatim —to capture: let x = kubectl get po -A -o yaml. The piped REPL’s byte surface is unchanged (a piped bare command still binds the value; unit stays invisible there). -
A named function value echoes a mini-help. A bare expression evaluating to a function no longer echoes the opaque
<builtin> : ty/<fun> : ty. A builtin (a bare alias likefindincluded — it names its home,Seq.find) echoes the qualified signature plus the doc’s first line, composed from the exact#helpsources; a session-defined function echoesname : schemeplus a dim line with its definition’s first physical line (a redefinition shows the last accepted; multi-line definitions clip with an ellipsis). Anonymous and composed closures keep<fun> : ty. Echo type metadata also normalizes type-variable display —'a1 -> 'a2reads'a -> 'b(the REPL echo and the-eecho; error messages keep the checker’s names).
Fixed
- The
let-RHS is a head slot — tint, completion, and aliases.let svc = kubecttyped at the prompt got no live head tint and no head completion, though the RHS has been a first-class command position all along. Both surfaces now read ONE position predicate: the RHS head tints by the session verdict exactly as a statement head does (known bold, PATH bold-blue with dim argv, unresolved red — an unknown uppercase RHS head stays casing-yellow:let n = Some 42is a constructor, not a failing command), and Tab at the RHS offers the statement head’s pool (PATH executables, the command-callable builtins, bindings and keywords), with argv after the RHS head completing as directory entries like any command argv. The^force-PATH sigil now completes too — PATH names only, at the statement head and the RHS alike. Folded in from a live report: session#aliasnames were missing from the known-head membership everywhere — after#alias k = kubectl,k get popaintedkred; alias heads are now known (tint) and offered (Tab) at both head slots, and^kstill bypasses the table on both surfaces.
v0.0.43
Changed
- The help tint covers the signature line and the example block.
#help’s signature renders structurally at a tty — name bold, types in the colorizer’s own yellow, punctuation dim — and the example block goes through the live prompt’s colorizer itself, so the two can never drift. Piped output,--repl-doc, andNO_COLORkeep the plain bytes.
Fixed
-
A streamed REPL statement no longer looks like it bound
it. A bare command at a tty streams straight to the terminal (the colour-inherit path — weir never holds the bytes), soitdoes not bind — but the meta line read a bare: seq<string>, as if a value landed, and the nextiterrored with the generic unbound message. The meta now says: seq<string> (streamed — not bound to 'it'; let x = … captures), and anitright after a streamed statement gets a targeted teach naming the repair with the command verbatim:to capture (and bind 'it'): let x = kubectl get po -A -o yaml. A fresh session’sitkeeps the ordinary error; the piped REPL’s byte surface is unchanged (a piped bare command still bindsit). -
#inferderived names dodge taken type names. A drafted type landing on a name the session already resolves was injected-and-shadowed: a k8s volume’ssecret:sub-object draftedtype Secret, everysecret: Secretfield resolved to the builtin redaction type instead, andfrom json/from yamlrefused with “a Secret must not cross” (a prelude name likeYamldied earlier, at injection). A derived name colliding with any in-scope type — primitive spelling, prelude/builtin nominal, or session-declared — now parent-prefixes (VolumeSecret) with a printed note naming the rename; same-shape dedup between inferred types is unchanged. Theasname stays the user’s: one that collides with a builtin refuses with a teaching instead of being renamed silently. -
Multi-line quoted scalars read.
kubectl get po -A -o yamlevicted-podmessage:values — a quoted scalar whose closing quote sits on a later, deeper-indented line — errored'message' has both an inline value and a nested block(the continuation lines were taken for a nested block). The yaml subset now reads multi-line single- and double-quoted scalars with YAML’s flow folding, PyYAML-refereed: a line break folds to one space, each empty continuation line contributes a newline, continuation indentation strips, and trailing space before the closing quote is content.''and the double-quote escape set work across lines (a\-escaped line break is not in the subset); the continuation lines belong to the scalar — map-value and sequence-item position, any depth, zero-indent sequences included. Unterminated quotes error at the opening line in the unclosed family; content after the closing quote errors at its own line. Quotedness stays load-bearing across the fold (a foldednois still a string), and the write side is untouched — weir never emits the multi-line quoted form.
v0.0.42
Changed
- Help renders its code spans at a tty.
#help/#finddocs’`code`spans now print tinted (the colorizer’s cyan) with the backticks themselves dropped — the span reads as code, not markdown source. One renderer covers every REPL help surface. Piped output andNO_COLOR/TERM=dumbkeep the literal backticks: the piped byte surface (and--repl-docequality) is unchanged, and a stripped terminal never loses the span boundary. - Help copy drops uppercase emphasis. Doc strings shouted for
stress (
It DRAFTS what the sample has,NOT check-time inference,READ-ONLY, …); the corpus — builtin docs, module blurbs, attribute docs, the directive list — is rewritten in normal case, syntax depictions (KEY=value) moved into code spans, and a unit pin now rejects any non-acronym all-caps word in user-rendered prose (real acronyms live on a commented allowlist beside the pin). Theinit: NOT loadedteaching line is nowinit: not loaded.
v0.0.41
Fixed
#infersanitizes KEYWORD keys and the empty-string key — the two [D:infer-wire-sanitize] residuals closed. A keyword JSON/YAML key ({"in": 2}—let,match,fun, …) passed the char-class-only identifier test and draftedin: int, which the parser rejects in field position (probe-pinned: EVERY keyword is); it now rides[<Wire "in">]over the parser’s own repair spelling (inField), collision-deduped, and the draft checks AND reads. The reserved set is the parser’s ownParser.keywords, threaded in as a parameter — the hand-copied three-word set is gone, so the set cannot drift. An empty-string key ({"": 1}) drafted[<Wire "">], which the checker refuses (“expects the wire key as a string”) and no field name can spell — it is now DROPPED with a printed note, and the drafted type still reads the sample (the readers tolerate an undeclared key). The e2e drafted-type diagnostic recast onto the remaining un-checkable class: a DUPLICATE JSON key.- The release pipeline uploads with retry and refuses incomplete
releases. v0.0.40 published with 5 of 10 assets:
gh release createhit a transient HTTP 500 mid-asset-upload, a manual recovery re-created the release with what had survived, the draft review missed it — and a published release is immutable, so v0.0.40 stays incomplete forever (missingweir-v0.0.40-win-arm64.exe,SHA256SUMS,install.sh,install.ps1,grammar-manifest.json; install from v0.0.39 or this release). Now the draft is created with NO assets, uploads run separately with 3 retries and--clobber(a 500-ghost cannot block a retry), andci/release-assets.weir— the one copy of the expected-asset list — fails the publish job on any missing, part-uploaded, or empty asset before a human sees the draft.ci/release-published.weirruns the same completeness check against the newest published release on every CI run, so an incomplete published release stays red on main until the next release supersedes it. A permanently-incomplete release is acknowledged inci/release-known-incomplete.yaml(reason required, swept: a listed-but-complete release fails as stale), so the standing CI check cannot stay red blocking its own supersede.
v0.0.40
Added
-
#helpis glanceable.#help <Module>lists one member per line — the name plus the FIRST LINE of its doc (the same builtinDocs source hover and#help Module.memberread, so the glance cannot drift), clipped to the terminal. Bare#helpgives every module a one-line blurb (Seq — lazy sequence pipeline ops: map, where, fold, pmap) from a new one-source table (moduleBlurbs), completeness unit-pinned two ways against the derived module list — a new module without a blurb fails loud. -
#find [query]— fuzzy help search. Every module (Seq — blurb) and every member (Seq.map — glance) feeds fzf at a tty, with a LIVE PREVIEW of the highlighted name’s full doc; Enter prints the exact#helpanswer for the selection, Esc returns quietly. The preview runs the session’s own binary headlessly — a newweir --repl-doc <name>prints the exact#help <name>bytes (builtin docs only, nothing evaluated; byte-equality e2e-pinned). Without fzf, or piped,#find queryis a deterministic case-insensitive substring filter over the same candidate lines — never an “install fzf” message.--no-extendedleads the fzf argv as in Ctrl+R (weir glyphs are fzf operators;finderFlagscan restore--extended),#findTab-completes with the other session directives, and a typo’d directive now gets a did-you-mean from the same one-source list.docs/reference/lexical.mdgained the consolidated directive table (script file / init.weir / prompt — the three contexts), gated in e2e so a new directive cannot skip it. -
#alias name = cmd [args...]— command-head aliases, REPL-only. A short head maps to a real program and a fixed prefix of arguments, consulted ONLY in command-head position and BEFORE PATH. Declare them ininit.weir(the canonical place), one per line:#alias k = kubectl #alias kb = kustomize buildNow
k get po -o yamlrunskubectl get po -o yamlandkb overlays/prodrunskustomize build overlays/prod— the fixed prefix is inserted after the exe, your argv appended. It is a RESOLUTION-table entry, not a textual macro: your argv stays typed argv, sok get $xpasses$xas ONE argument (the injection law holds), and only the HEAD token in command-head position is rewritten — akin a string, a variable, or an argument is untouched. The resolution order is alias table → PATH, and the^force-PATH sigil skips the table, so a shadowing alias (#alias ls = ls --color) is bypassable with^ls. Aliases are single-hop (the target is a program, never another alias — an alias-of-alias is rejected at load) and REPL-only: scripts and-enever loadinit.weir, so their command resolution is unchanged and an alias name there is an ordinary unknown command. The target need not exist when defined (like bash); a malformed#aliasline is a loud init error (init stays all-or-nothing). A live#aliasworks at the prompt too (bare#aliaslists the table), butinit.weiris the canonical home. -
#savedesugars aliases. The saved script has no alias table, so each kept line’s command HEAD is span-rewritten back to the real invocation —let pods = k get posaves aslet pods = kubectl get po, a keptkb overlays/prodaskustomize build overlays/prod. The rewrite is head-only (akin a string is untouched), and the output is alias-free andweir checkclean.
Changed
- The unknown-directive message dropped its
#sig/#schemaparenthetical. It was noise for an unrelated typo (a bare#or#timedoes not care about file directives). Now#sig/#schemaat the REPL get their own “file directive, read at check time — no effect in the REPL” redirect (mirroring#session’s), and the generic catch-all is justunknown directive '…' — #help lists them.
v0.0.39
Added
- REPL path completion quotes in an expression, stays bare as a
command argument. Tab-completing a filesystem path after a function
head —
File.read ./weir-pods.yaml<TAB>— used to insert the bare path (File.read ./weir-pods.yaml), which then failed to parse on Enter: a bare path is not a valid weir expression. The completion is now a string literal —File.read "./weir-pods.yaml"— which parses. The distinction is the slot’s: a COMMAND-argv path (cat ./x,ls ./dir) stays BARE, the way argv wants it. If you already opened the quote (File.read "./x<TAB>), the completion lands inside it — no second quote is added; a directory keeps its trailing/within the quotes. Completion still runs nothing — a directory read at most.
Fixed
-
A truncated
let-bound seq echo now shows the same unforced teaching as a bare echo — visibly. Echoing aseq<string>value showed a footer: seq<string> (first 100 of an unforced seq — Seq.force to echo everything)at the BOTTOM, where you’d see it. Butlet xs = <over-100-line command>printed that footer FIRST and the 100 lines after it, so the teaching scrolled off the top and the bind looked like it had silently dropped data — and the footer rendered a dangling=before the parenthetical. Theletecho now prints the lines (or table) first and thename : type (hint)footer last, identical in shape and ordering to the bare-expression echo. A forced seq still echoes whole with no teaching; the piped/-esurface is unchanged. -
A failing
#inferdrafted type now points at the bad field. When a type#inferdrafts from a sample fails to check for any reason, the message was opaque —#infer: a drafted type did not check: Expecting: ':'with no location and no snippet. It now reports theline:colwithin the drafted text and prints the offending drafted line with a caret under the column (the same rendering weir uses for ordinary parse and type errors), so you can see which generated field is wrong. -
The owned YAML subset now reads kubectl’s zero-indent block sequences. A block sequence written at the SAME column as its parent mapping key — the form
kubectl get -o yaml(and most k8s tooling) emits,items:/- apiVersion: v1flush-left under the key — failed with'- apiVersion' has both an inline value and a nested block: the parser only consumed more-indented lines as a valueless key’s value, so a same-column sequence was reparsed as sibling mapping keys. A valueless key followed by a-sequence at its own indent now takes that sequence as its value, top-level and nested (a sequence item’s map containing its own same-column sequence, e.g.metadata:→ownerReferences:). The classic indented-dash style still reads, and the two styles read to the same value. A genuinely malformed inline value plus a more-indented nested block still errors. -
Empty flow collections
{}and[]are now read as values. Real kubectl output is full ofresources: {},securityContext: {},emptyDir: {},lastState: {}(and occasionally[]); the block-only subset rejected all flow style, so a real List would notfrom yaml. The two EMPTY forms (inner whitespace tolerated) are now the narrow exception — unambiguous at zero elements — in both map-value and sequence-item position:{}reads the empty mapping,[]the empty sequence, and both render back so they round-trip. POPULATED flow ({a: 1},[1, 2]) still rejects with the block-only teaching. An opaqueYamlfield now reads structure whole, so#infer’s empty-{}fallback is readable. -
#infersanitizes keys weir cannot spell as field names. JSON/YAML keys emitted verbatim as field names — k8s labels likek8s-app,pod-template-hash,node.kubernetes.io/os,app.kubernetes.io/instance— drafted atypethat would not parse (Expecting: ':'). A key that is not a legal, non-reserved weir identifier now camelCases into a valid identifier (k8s-app→k8sApp,node.kubernetes.io/os→nodeKubernetesIoOs) and rides a[<Wire "original-key">]attribute the readers honor; a clean key stays bare with no attribute; two keys colliding on one identifier disambiguate deterministically (aB/aB2/aB3). The drafted type now CHECKS and READS the real data, JSON and YAML alike.
v0.0.38
Fixed
- A command line ending in
yamlis argv, not a district marker.kubectl get po -o yaml,docker … --format yaml— everyday ops commands whose last word isyaml— failed at assembly with “line-end ‘yaml’ needs an indented block below it”, misread as arming ayamldistrict. The marker now arms token-precisely: only a bareyaml(the next-line form) or a= yamlassignment RHS (let d = yaml) arms;yamlpreceded by argv (-o yaml,--format yaml, a trailing bare word) stays a command.weir checkandweir <file>agree — the check gap is closed. #savenow distills a session to its checkable definitions. The old#savedumped the raw transcript: a multi-line heredoc came out flattened onto one line by the assembler’s join sentinel (anillegal control characterparse error), a redeclaredtypewas written twice (dup-type), andlet x = itwas saved thoughitis REPL-only (unbound in a script) — so the “runnable” file did notweir check.#savenow DISTILLS (option B — a session is scratch;#savecrystallizes definitions): it keepstypedecls and self-contained namedletbindings with their REAL multi-line source, DEDUPS a redeclared name to its last form (survivor order preserved), and DROPS the bare-echo scratch. The written file is GUARANTEED to check clean — any surviving statement that still references session-only state (alet x = it) is removed and#saveprints a dropped-count note.#help <TAB>now completes session types (and modules and forms).#help Patatasdocumented an#infer’d type fine, but#help Pat<TAB>offered onlyPathandPatch— neverPatatas, because the argument completed against the general pool, which never surfacesenv.Typesnames. The#helpargument now completes the same set#helpcan document, so an injected type is completable, not just documentable.
v0.0.37
Fixed
- REPL Tab completes record fields through a pipe. A record piped
into
_.or a lambda param —x |> from yaml T |> _.,r |> _.,x |> … |> (fun row -> row.— offered no fields (or, for the lambda, every declared record’s fields, unioned). The completer only unwrapped aseqelement and gave up on a scalar record, butfrom yaml Tyields one document, not a sequence. It now types the value flowing into the position — aseqstill unwraps to its element, a scalar record surfaces its own fields — so#infer’s payoff (a typed record → field completion) lands in the pipe positions people actually use. - Tab on an empty prompt no longer dumps the universe. A fresh Tab
used to return over a thousand candidates — every PATH executable plus
every module, keyword and constructor, sorted — because the empty
prefix matched everything. It now offers the session directives,
#helpfirst, so a bare Tab teaches the REPL’s affordances instead. A prefix (Fi,Wr) filters as before, and a Tab in argument position still lists the directory. - Union-case constructors are no longer offered as statement heads.
WriteFile,Some,Get,Bearerand the other constructors leaked into head completion, but a constructor as a statement head is a discarded value (a check error). They are subtracted from the head pool while staying available in expression and argument positions, where they are valid.
v0.0.36
Added
#infer— draft named types from a JSON/YAML sample, in the REPL.#infer <source> from <json|jsonl|yaml> as <Name>evaluates the source ONCE (a binding, a$(…)capture, or a bare command), parses it by the named adapter, walks it to a set of namedtypedeclarations, and INJECTS them into the session — sofrom json <Name>checks and field completion lights up. Auto-naming: a nested record takes its field name capitalised (metadata→Metadata), a seq-of-record element the field name singularised best-effort (items→Item,datastaysData), a same-name-different-shape collision a parent prefix (PodSpec/ContainerSpec), a same-shape dedup to one type. A single sample cannot see optional/absent fields, so absent/null/empty-array/heterogeneous cases PRINT a note rather than guess. Scaffolding, not a language change:checkstays evaluation-free andfrom jsonnever sniffs — theweir add schemacategory (external structure → a declaration you own and edit). The source defaults toit(the last result) when omitted. The same inference is a composable builtin —Json.inferShape/Yaml.inferShape : seq<string> -> stringreturn the declaration text outside the REPL.#save <path>— dump the session to a runnable script. Writes the accepted statement lines to a.weirfile, auto-qualifying bare aliases (map→Seq.map,startsWith→Str.startsWith) via the same map the checker’s did-you-mean reads, then formatting the result. Errored lines drop; injected#infertypes come out as ordinarytypedecls; a bare non-unit expression echo is saved as alet _rN = …discard so the strict unused-binding law holds. The saved fileweir checks clean.#inferto explore,#saveto keep.it— the last result, bound at the prompt. Every REPL line that produces a value (an expression, a command, or aletRHS) also binds it toit(ghci’s convention). REPL-only; a unit statement or a directive leaves it untouched. Lets a just-built pipeline feed the next line (and the no-source#infer).
Fixed
$<<<heredocs now dedent byte-identically to plain<<<. The interpolated form over-stripped leading whitespace, flattening deeper-indented lines to column 0, while<<<correctly preserved indentation relative to the first content line. The two forms now differ ONLY in whether{holes}interpolate — indentation, interior blank lines, deeper indentation and trailing-blank clipping match exactly, as the docs always promised.- A piped REPL (
printf '…' | weir) now assembles multi-line statements. The redirected-stdin loop read one physical line per prompt, so a statement spanning lines (a heredoc body, a multi-linetype, an offsideif/matchblock, a leading-|>pipeline) never assembled —let block = <<</ indented body errored “unbound variable”. It now accumulates the way a script does, reusing the interactive editor’s completeness rule and the assembler’s own statement boundary (no second parser); a single statement per line is unchanged, and directives stay one line.
v0.0.35
Added
readonly— a read-only assertion, one tier up frompure. A barereadonlyhead + an indented block asserts the body reaches no EXTERNAL MUTATION; unlikepure(which forbids every effect), AMBIENT READS are fine —fs.read,Env/Args, the clock, the query HTTP methods (Http.fetch/Http.queryandHttp.sendof a GET/HEAD/OPTIONS/QUERY request), andSelf.stdin. A reachable mutation (File.write, a command, a mutating HTTP method, anywithinresource) is a located check error naming the offender AND its class (“this ‘readonly’ block forbids external mutation, but ‘File.write’ writes the filesystem — reads are allowed”).readonly == only ambient-input, so apurebody (only ∅) is trivially read-only. Opt-in only, effect-normal outside, its own head — neverwithin readonly.readonlyis a new keyword (the grammar-manifest and lexical table gain it; the tree-sitter-weir grammar owes the addition, thepure/xmlposture).- The ambient/mutation partition, consultable at check AND eval
time. Every effect label ({fs.read, fs.write, net, proc, env,
clock}) now classifies as ambient-input (reads the world) or
external-mutation (changes it), with the
netsplit resolved per-METHOD from the request (Http.sendreads its HttpMethod case). The classification drives thereadonlyceiling,--can’s new class grouping, and — resolvable where the interpreter runs — the reads-run / mutations-capture rule the plan/apply build consumes. plan/apply— effects reified into an inspectable Plan (dry-run as a language primitive). Aplanblock runs its body but CAPTURES its external mutations asOpvalues instead of performing them, while ambient reads still RUN — yielding aPlan, an equatable and showable value over aseq<Op>you inspect, diff, confirm, then apply. Two plans compare directly (both capture, neither performs), soplan <actual> == plan <expected>is the mock-free test. The members:Plan.ops(the raw ops),Plan.preview(human lines; wrote nothing),Plan.isEmpty, andPlan.apply(perform, in capture order). TheOparms are the mutation surface:WriteFile of string * seq<string>,DeleteFile of string,Copy/Move of string * string(File/Dir),MakeDir of string,DeleteDir of string(Dir.delete/deleteAll), andHttpSend of HttpRequest(a mutating HTTP method only —showmasks the auth Secret). Content is snapshotted at plan time, sopreview == apply. The refusals:procinside a plan (a spawned binary is uncapturable — plan covers weir-native mutation only),applyinside a plan (a capture cannot be captured), and a known-after-apply read (reading a path an earlier captured mutation targets is a located error).applyis NOT transactional — it stops at the first failing op with prior ops done, no rollback.planis a new keyword, the third standalone head besidepure/readonly(the grammar-manifest and lexical table gain it; the tree-sitter-weir grammar owes the addition, thepure/readonlyposture). Consumes the ambient/mutation partition [D:pure-stage2].
Changed
weir check --cangroups its report by class. The capabilities now render under two headings — ambient reads (inform, change nothing) and mutations (change the world) — so “what does this script CHANGE?” is answerable, not just “what can it touch”. The--jsonoutput gains aclassfield per capability.
v0.0.34
Changed
- A
letin any statement body takes command lines, exactly like top level.withinbodies (every kind,alwaysblocks included), if/elif/else bodies,forbodies, and match arms now give a blockletthe full command-RHS law — bare chains, reifiers, splices, param-ful lets, bindings-beat-PATH — solet r = npx … | completeinsidewithin tmp/within cdno longer needs its$()wrapper.pureblocks admit the grammar and the purity checker refuses at the command, its own located teaching. The expression positions keep the refusal, with a hardened teaching that names$()and the actual context (“inside a lambda body, a command needs$(…)”) and fires even when the RHS argv would have died at a--flagbefore the pipe: paren interiors, single-linelet … in, and lambda bodies off a top-level let’s spine.
Fixed
- An HTTP response body reads under the same line law as everything
else.
resp.bodysplit on\nraw, so a body ending in a newline carried a trailing empty line (a one-line file came back as two elements), a CRLF body left a stray\ron each line, and an empty body was one empty line rather than none. It now reads the wayFile.readand command output do — a trailing newline terminates rather than separates,\r\nnormalizes, an empty body is zero lines — so a URL fetched withcurl urland withHttp.fetch urlagree exactly. Bodies piped intofrom jsonare unaffected.
v0.0.33
Added
- Breaking: an unread
letbinder is a hard check error. The strictness family grows its next member (statement rule, exhaustiveness, unreachable arms — weir has no warnings): binding is exactly how weir silences raise-on-nonzero, so alet r = cmd | completenothing ever reads is a swallowed failure, and the checker now refuses it — scripts and module bodies, top-level and block-local, destructured names each judged, and a name rebound before its earlier binding was read errors at the earlier binder. A module’s unsigned member unread at home is dead private code and errors with the signature repair. The escape is a_-prefixed name (let _r = …— deliberately unused, never errors, still readable); a bare_let binder now refuses (name the discard). Exempt: function params, match-arm binders,for/until/withinbinders, signed module members (the signature is the use), and#sigcontract files (the sig loader is their reader). Errors are collected and located at the binder; every consumer agrees —check, run, import, the LSP, and the fidelity oracle. F# accepts unread binders silently by default (FS1182 is opt-in), recorded as theunused-bindingdivergence.
Fixed
-
weir checkaccepts every patch districtweir runaccepts. Check’s assume-resolver read theyaml patch [by=<key>]marker line as a command head (yamlis command-shaped), so the district body then failed as statements — any$namesplice or multi-line mapping item inside ayaml patchdistrict checked red while the same file ran green (a kustomize-shaped port hit both). The command grammar now refuses the patch marker face glued to the district sentinel, exactly as it already did for plainyamlandschema=, so check == run. -
Block-let params shadow PATH in their own RHS. A param heading an if-condition inside a nested
let f pairs = if pairs |> … then …resolved as a PATH command (cmd-not-found warnings plus bogus type errors) until the condition was parenthesized — the block-let RHS never extended the resolver with its params, though top-level lets and lambdas did. Bindings-beat-PATH now reaches block-let depth; a genuine external head in that condition position (if test -f $p | succeeds then) still chains. -
A
)on its own line closes a multi-line application anywhere.YMap(with arguments on deeper lines and the close paren alone at the body indent was a parse error at the paren inside if/match arm bodies (and everywhere else the closer sat at the sibling level) — the assembler sequenced the)line as a block statement. While a plain paren is open a)-headed line now continues the statement, the rule multiline lambdas already had; a stray)with no paren open still refuses. -
A returning match arm after a multi-statement body assembles. An arm body ending in an if/else (after a
let) left the if compound open, and the next arm then died with “this arm sits left of its match (head at column N)” pointing at the if — healthy code. A pipe line landing exactly on its own arm group’s column is a returning arm (the deeper compound offside-closes as it always did); genuinely misaligned or left-of-match arms keep their errors. -
Module signatures can name every builtin type.
let mk : string -> YamlPatch(andProc) refused as “unknown type” — the def-less builtin nominals were unnameable in signatures, even though the module-privacy error itself suggested exactly that signature. Both now validate in signatures and field types (arity 0), likeMap; the suggested signature round-trips. -
A qualified type name teaches the bare-name law.
let f : M.Spec -> stringdied with a bare parse error at the dot; every type position now refuses with “a signature names types bare — an imported type resolves by its plain name”. -
pureno longer admitsSelf.stdin. Reading it drains the process’s live input stream — an effect the classifier missed because the value is injected per run rather than called through a builtin. A pure region now refuses it with a located teaching (“‘Self.stdin’ reads the process’s input stream”), and a function touching it loses the(pure)hover badge. The per-run constants (Self.args/pid/scriptPath/entryPath) stay pure-admissible. -
pureaccepts union constructors. A data constructor is pure by construction, but a payload constructor’s function type fell into the unknown-callable bucket and refused as an unknown callable. Applied and partially applied constructors now pass in pure regions, and a constructor-building function keeps its(pure)badge.
Changed
- The ctor-pattern refusal teaches its repairs. A constructor
pattern on an unresolved param (
let step (m, wd) = match m with | Ctor …) now says params are not typed from patterns and names both ways out — inline the lambda at its use site (a typed pipe position types the binder there), or match on already-typed data — instead of “needs a union value; this one has type ‘a1”.
v0.0.32
First release published since v0.0.29: this ships everything under
## v0.0.31below as well — that tag was cut but never published (see its note).
Added
prompt— interactive input as a builtin.prompt "msg?"writes the message to stderr (piped stdout stays data) and reads one line from stdin; EOF refuses rather than inventing phantom input. AndSelf.stdinnow states its law: it is a live stream read once — a second enumeration used to silently yield empty and now raises, naming both repairs (bind one enumeration, orpromptper interaction).
Changed
weir fmtputs district markers on the binding line. Canonical layout islet x = <<</let p = yaml patch by=name— never the marker alone on a continuation line. Both spellings still parse and run identically (district content is relative to its first line, so the rewrite is lossless);fmtnow merges the next-line spelling up, outdenting a district-pipe close (|> f) with the marker, andfmt --checkflags it. A binding line with a trailing comment keeps the next-line spelling.
v0.0.31
Neither v0.0.30 nor v0.0.31 was ever published. v0.0.30’s tag was cut but its release run failed on the zed-pin/grammar-main race (the pin bump had not landed when the gate ran). v0.0.31’s tag was then cut and its run died on the same race, re-armed by a publish-wait that trusted a bare 200 from releases/latest — so nothing shipped under it either. Everything below first reached users in v0.0.32.
Added
-
Breaking:
Seq.groupByreturns(key, items)pairs. F#‘s own shape, replacing theGroup { key; items }record —countBy,zip, andpairwisealready speak tuples, and string keys now feedMap.ofPairsdirectly. Destructure withfun (k, g) ->(orfst/snd); code that pattern-matched the record updates mechanically. TheGrouptype is retired. -
pureregions are enforced — Stage 1 of the effects plan. A barepurehead + indented block asserts the body reaches no effect (filesystem, commands, network, environment, console, clock, anywithinresource); a reachable effect is a located check error naming the offender (this 'pure' block forbids effects, but 'File.write' writes the filesystem).let pure f x = …— weir’s first post-let modifier — asserts a whole binding and keeps the(pure)hover badge; an impure body is a check error. Judged by the Stage 0 classifier: transitive through earlier bindings, conservative at unknown callables (a refusal can be over-careful, an acceptance is never wrong). Opt-in only — nothing outside apureregion is gated, andpureis its own head, neverwithin pure.pureis now a reserved keyword. -
The
withinkind is a union. A long-deferred internal restructure: Check/Eval dispatch kind-first onAst.WithinKindId, so a new kind (likepureabove) is a build failure at every consumer instead of a silent gap. No surface change. -
fail(andexit) now diverge:string -> 'a. A failing arm sits opposite a value arm —match o with | Some v -> v | None -> fail "absent"types as the value, andif bad then fail "no" else 5likewise; the throwawayfail "x" ; 0scaffolding is no longer needed (it stays legal). A constraint nothing determines still refuses, and the else-lessif bad then fail "usage"guard is untouched. Supersedes the earlier unit-typing ruling on its own revisit trigger. -
Fixed: every deeper line continues a multi-line application. An application with arguments on their own deeper-indented lines used to join only the first — the rest sequenced as block statements (“a sequenced expression must be unit”). The sibling floor is now the statement’s start column, in scripts and module bodies alike; a block statement after a deeper continuation also sequences instead of dying at the dedent floor.
-
Module signatures — a signature is the export. In a module,
let name : int -> intwith no=declares a member’s type and exports it; an unsigned member is module-private (full inference, invisible to importers — importing one names the exact signature to add, inferred type included, with did-you-mean over the signed members). Implementations stay annotation-free: the signature’s types flow into their checking, so params can pattern-match their declared unions and generic signatures ('a -> 'a) are honoured or refused as less-general. Signature without implementation errors at the signature;///docs live on the signature line (hover and definition read the sig);typedeclarations stay auto-exported; scripts refuse the form (scripts infer). Breaking: previously every module member was public — export now requires the signature; the import error teaches the migration. -
Path.normalize— lexical../.collapse. No filesystem touch, no cwd, symlinks never followed.Path.combinekeeps..(paths you control) andPath.underrefuses an escape (paths you do not);normalizeis the third spelling, for the legitimate escape both siblings decline — a project reference leaving its own directory. Relative paths keep their leading..s; at an absolute root..swallows. -
Fixed: a lazy command escaping a
withinscope now spawns under the scope it was written in. Command values are lazy; one bound insidewithin cd/within envand forced after the block used to spawn under the restored outer ambient. The ambient (cwd + env overlay) is now captured at the expression’s written site, closure-style, and replayed at spawn. (The related, different defect — axs | cmdpipe written as a block’s tail line associating to the outside of the block — is fixed below.) -
Fixed: a pipe on a
withinbody line belongs to the body. A value-headed pipe written wholly on a block’s body line (["x"] | sh -c "…"underwithin cd/within env) used to associate to the whole block expression —(within …) | cmd— so the command spawned after the scope restored: the cd invisible, the env overlay invisible. It is now part of the body statement and sees the scope; a non-final body pipe (previously a bare parse error at the statement boundary) works too. The offside law is unchanged: a pipe dedented to the block’s head column still closes the block and pipes its value. -
Command pipes carry bytes. A command→command hop is now a raw byte pipe — no decode, no line split, no appended newline — so binary flows through pipelines untouched (
gzip -c f | sh -c "cat > out"is byte-identical to bash; previously every non-UTF-8 byte was silently replaced with U+FFFD). Only the ends of a chain are text edges: a value head feeds stdin as UTF-8 lines, and output becomesseq<string>where a value is made ($(), reifiers,|>). A failing stage raises at the leftmost fault, as before. -
Pure functions are visibly pure: the
(pure)hover badge. A top-level function whose body can reach no effect — no file/dir/env/process/network/console touch, no command, no clock, transitively through the bindings it calls — hovers asslug (s) : string (pure). The display is deliberately asymmetric: weir is effect-normal, so the rare pure function is surfaced and effectful code stays exactly as it was (effect detail remains inweir check --can). The badge is conservative — a call through a function-typed parameter or any unknown callable forfeits it — so a missing badge is possible, a lying badge is not. Display only: nothing is gated on purity. -
yaml patch+Yaml.parse/Yaml.merge— edit YAML you did not fully declare.Yaml.parsereads one document intoYamlnodes (typeless: structure held whole, undeclared keys included — wherefrom yaml Twould drop them on a rewrite), andYaml.mergeapplies ayaml patchdistrict whose structure is the address, kustomize’s strategic-merge model with no path language: maps upsert recursively; a sequence appends-if-absent, or upserts by the marker line’sby=<key>; scalars replace; the$-tombstone removes (in value position, its key; as- $- <content>, the matching item). Update-or-insert is the semantics of the keyed merge, not a branch you write, and merging is orderless and idempotent. A patch types asYamlPatchand does not render —to yamlon it is a check error, so a tombstone can never leak into a file;$-outside a patch district andpatch schema=both refuse with teachings. Editing a file is the visible round-trip:let p = yaml patch by=name images: - name: app newTag: v2 File.read f |> Yaml.parse |> Yaml.merge p |> to yaml |> File.write f -
Graph and tree walks without recursion:
Graph.reach,Tree.walk,Frontier.fold. The frontier/visited worklist as builtins, bounded by construction — the visited set caps a finite graph, and a 100000-step budget turns a non-finite walk into an error, never a hang.Graph.reach keyOf neighbors startyields every node reachable from a start node, breadth-first, each exactly once (cycles and diamonds are safe); the neighbor function is the graph.Tree.walk step rootwalks a tree for effects, parent before children — the step runs each node’s effect and returns its children, so a child may depend on its parent’s effect (create the directory, then the files inside it). Both derive fromFrontier.fold keyOf seed step frontier, whose step folds an accumulator and discovers children at once (fun acc n -> (acc', children)); a""key opts a node out of dedup.let deps = Map.ofPairs [("app", ["core"; "util"]); ("util", ["core"])] let neighbors p = Map.tryGet p deps |> Option.defaultValue [] Graph.reach (fun p -> p) neighbors "app" // ["app"; "core"; "util"] -
Option.bindandOption.flatten.bindapplies a function that itself returns anOption, flattening as it goes — the chain reaches through nested optionals (user.address |> Option.bind _.zip);flattencollapsesOption<Option<T>>toOption<T>. -
from xml Treads XML into a declared record. A read-only, typed boundary over an XML subset — point it at a.csproj/.slnx(or any XML document) and walk the result as ordinary weir data. The document’s root element is the top record; a field name matches a child element by local name (a defaultxmlnsis stripped, so field names stay plain);[<Attr>](optionally[<Attr "Include">]) reads an attribute;[<Elem "ProjectReference">]names the repeated child aseq< >field reads (defaulting to the element type’s name forseq<record>, the field name forseq<string>); a nested record reads a child element recursively. Every leaf is text, so fields arestring,Option<string>(present-or-absent), a record, or aseqof either — numbers and booleans are declaredstringand converted (Str.toInt), which the checker teaches. There is noto xml: XML is read-only.type Ref = { [<Attr>] Include: string } type Pg = { IsPackable: Option<string> } type Proj = { [<Elem "PropertyGroup">] groups: seq<Pg> [<Elem "ProjectReference">] refs: seq<Ref> } File.read "App.csproj" |> from xml Proj
v0.0.29
Fixed
- A bare lambda field/element no longer swallows the next one. In a
multi-line record literal or list, an unparenthesized lambda value —
{| act = fun () -> () <newline> name = "x" |}— used to have its body run past the field separator and consume the following field (error: unbound variable 'name'); the same bit list elements. Record and list fields are now separated by a dedicated sentinel the assembler inserts for newline joins, which a field value’s own;-sequencing cannot cross — so the fields split cleanly with no parentheses. (An explicit one-line;between a lambda field and the next still needs the lambda parenthesized.) File.writepreserves an existing file’s UTF-8 BOM. Reading a file that starts with a UTF-8 BOM, transforming its lines, and writing it back no longer silently strips the BOM (config files like.csprojthat MSBuild saves with one were being churned).File.writenow re-emits the BOM when it is overwriting a file that already had one; a new file, or an existing file without a BOM, stays bare — so this never adds a BOM. Byte-exact round-trips still useFile.readBytes/File.writeBytes.
Changed
- The over-application error teaches the dangling block form. When a
statement is slurped as an indented continuation of the line above
(
'f' takes at most N argument(s)), the hint no longer says only “start at that line’s indent, not deeper” — it now adds that a block body written inline (after->orthen) takes no second statement below it, so multiple statements start on their own line. - A value-bound lambda hovers as its type, not a partial signature.
let fun2 = fun () -> fun () -> 1now hovers asfun2 : unit -> unit -> int(the flat value type, like F#‘sval fun2 : …) instead offun2 () : unit -> int. The rule: a binding with a named parameter still hovers as its signature (apply (f) (x) : …); a binding whose parameters are all()(unit) — or none — hovers as the flat type, since()names nothing. This makes the binding hover agree with the use-site hover.
v0.0.28
Fixed
- Each match arm keeps its own body column. A regression in v0.0.26:
a continuation
|>in one arm was judged against an earlier arm’s body column, so a later arm with a shallower body (e.g. a| None ->whose pipeline sits left of a preceding| Some p -> …’s inline body) was wrongly rejected as “left of the arm body”. Each arm’s body offside is now tracked independently;ci/grammar-currency.weirand the like parse again.
v0.0.27
Tag burned — released from the wrong commit. Version number consumed; no artifacts.
v0.0.26
Changed
- The match-arm pipe floor is the arm body’s column. Corrects
v0.0.25: a continuation
|>now extends an arm only when it lines up at or under the arm body; anywhere left of the body (but right of the|) is rejected with a message naming the body column. v0.0.25 used the arm’s pattern column as the floor, which quietly swallowed a|>sitting at the pattern — well left of the body — into the arm.|>at the|still closes the whole match. The floor is cleaner than F#‘s relaxed offside (which tolerates a|>hanging a few columns left of the body); weir is stricter there — line the pipe up under the body.
v0.0.25
Changed
- A
|>under a match arm continues that arm. Completing v0.0.24’s offside work: a|>indented at or past an arm’s pattern now extends that arm’s body — the same whether the body is inline or on its own line — instead of being rejected. The full rule:|>at the arm’s|closes the whole match; under the arm body continues the arm; in the gap between the|and the pattern it’s rejected with a message naming both fixes. This replaces v0.0.24’s reject-the-deeper-form behavior, which was inconsistent (it depended on whether the arm body was inline or dangling) and diverged from F#.
v0.0.24
Changed
- A trailing
|>closes the match, F#‘s offside. A|>dedented to the arm column now closes the wholematchand pipes it —match x with | … | _ -> vthen|> fon its own line is(match …) |> f, notfburied in the last arm (which used to make the arms disagree). This matches F#: a|>indented past the arm body still extends the arm, and — the one kept divergence — weir rejects that deeper form rather than F#‘s warn-accept, so write the one-line| _ -> v |> fto pipe inside an arm.
v0.0.23
New features
- Range slicing:
x[a..b]. Inclusive at both ends and clamping — an out-of-range or reversed range gives the empty result, never a raise — on strings and sequences alike, with open endsx[..b]andx[a..]."weir"[1..2]is"ei";xs[3..100]truncates to what is there; an infinite sequence stays usable (nats[2..5]returns).x[i]remains the single-element accessor (raises out of range), and from-the-end (x[^1]) is not supported —^is the command-force sigil, soSeq.last/Seq.revreach the end. - Higher-order parameters infer.
let apply f x = f xandfun f -> f 1now typecheck: a parameter applied in a body is inferred as a function, exactly as F# does. This was previously refused as “a bare parameter cannot be applied.” - A function type is writable. An arrow may now be written in any
type position — a union payload (
Custom of (string -> bool)), a record field ({ matches: string -> bool }), a generic argument — with F#‘s precedence:->is right-associative and looser than*and generics, and a function domain parenthesises ((unit -> int) -> string). Such a value constructs and its functions call; the data boundaries (==,to json,to yaml,show) refuse it, naming the offending field, so a scalar-only record still serialises while a function-bearing one is turned back at the wire.
Changed
- A
yaml/<<<district written with a trailingdo(for x in xs do) now names the mistake — a districtforis bodyless, the entries sit indented below it — instead of a bare parser expecting-list.
Docs
- A Coming from Nushell section — weir’s closest neighbour —
written and checked against a real
nu. The Emacs editor section is validated on Emacs 30.2 (it was marked untested). The reference page’s duplicate section anchors are fixed and gated, and the showcase gains an “On this page” navigation. Smaller corrections:withincleanup runs on SIGINT/SIGTERM (it was described as a leak), thewithinfamily is five kinds, and the homepage leads with the check reaching a tool’s flags and a manifest’s schema, not just a program name.
v0.0.22
New features
- A heredoc or
yamlblock pipes like anyseq<string>.<<<… then|> Seq.lengthon the closing line now composes with the whole block, and the block may sit anywhere an expression can — piped, bound in a body, followed by a sibling. (A|>deeper than the content is still content; one at the marker’s column closes the block and pipes it.) Previously only a top-levelletbinding was accepted, and a trailing pipe was silently glued into the last content line — that corruption is gone.
Fixed
--helpshows a value placeholder for every value-taking flag —--timeout <duration>,--max <size>,--rate <float>,<instant>,<secret>— where only<int>and<string>rendered one before.boolflags stay presence-only.usage:names the invoked script:usage: deploy.weir [flags]instead of the bareusage: [flags], in every form (records, subcommands, shared flags, case-scoped help).#help printand the reference no longer leak an internal type sentinel ('__print); the accepted set is stated in the member’s own text. Unit-parameter members render uniformly (Instant.now () : Instant,Path.newTempDir () : string).
Docs
- Function parameters do take patterns —
let dist (x, y) = …,let label { names = n } = n— and three documents said otherwise; corrected, with running examples. The guide and reference each state the statement-layout and heredoc shape rules once, in one home; the heredoc-built fixtures now read as the files they write. - The homepage leads with a script that ends in a real command, and its first section shows the check reaching three surfaces at once — a tool’s flag (against a generated signature), a manifest’s key (against a vendored schema), and a program name (against PATH) — in one report. The reference’s duplicate anchors are fixed and gated.
v0.0.21
Editors
- Go-to-definition and hover reach into declarations. A type name
written in a declaration — the payload after
of, a field’s type in a record — now jumps to its definition and hovers its shape; before, only expression-position names resolved. The attribute names ([<Tag>],[<Other>],[<Wire>],[<Short>]…) hover a one-line explanation, and thestreamcardinality word both hovers its meaning and colours as an adapter keyword instead of a plain identifier (in every editor grammar — VS Code, micro, and now tree-sitter/Zed).
Docs
- The website was rebuilt: the fold leads with a worked example, the three-bar mark and its hues carry the identity, quoted outputs sit in terminal frames captioned with the command that produced them, and the prose is set in Iosevka Aile — the sans sibling of the code font. (Deploys independently of the release.)
v0.0.20
Tag burned — the release gate went red when a grammar push landed between the tag and its build, and the tag’s pinned grammar rev can no longer match the moved grammar main. No artifacts; version consumed. The content shipped as v0.0.21.
v0.0.19
New features
- Breaking:
to yamlwrites one document;to yaml streamwrites the---bundle. The write side now mirrors the read side exactly, completing the grid json got in v0.0.17:[1; 2; 3] |> to yamlrenders one sequence document (- 1,- 2,- 3— json’s array, one format over), a record seq renders a sequence of mappings, and the multi-document bundle takes the same word the reader uses:docs |> to yaml stream. Every form now reads back through its own name —to yaml |> from yaml seq<T>andto yaml stream |> from yaml stream Tboth roundtrip (the first never did before). A pair-seq still renders one mapping document.
Docs
- The docs pages finished what v0.0.18 started in the help text:
the guide, the reference, and the translation tables dropped
their emphasis caps — “the process TREE is killed” reads “the
process tree is killed” now. Acronyms and file names stand, and
!still means do it, in italics.
Checks clean, behaves differently
xs |> to yamlon a seq previously wrote----separated documents; it now writes one sequence document. Scripts that meant the bundle should sayto yaml stream— same bytes as before.
v0.0.18
Docs
-
Help text reads as prose. The hover/
#helpdocs (and the reference pages built from them) dropped their emphasis caps and in-house shorthand:to jsonnow says “render one value as one JSON document” rather than “ONE value as ONE JSON document”, and phrases like “more algorithms on receipt” or “the X/tryX rule” became plain statements (“sha256 only for now”; “Seq.tryLast answers None instead”). Same facts, said normally. Theweir check --cansecrets line joined in: a Secret in argv is now reported as “visible in ps — weir does not hide argv”. -
The homepage shows the language above the fold — beat 1’s refusal sits beside the install lines — and shared links unfurl with a preview card (the mark, the tagline, and that same genuine refusal).
Chores
- The installer distinguishes “gh not authenticated” from “network
or attestation unavailable” when verifying build provenance —
gh attestationrequires auth even for public repos, and the note now saysgh auth loginis the repair. The embedded checksum remains the primary verification either way.
v0.0.17
New features
-
from yaml stream T— multi-document yaml. A----separated stream reads asseq<T>, each document oneT. Point it at a tagged union and a kubernetes apply file types end to end:bundle |> from yaml stream KDoc |> Seq.iter (fun d -> ...)A homogeneous stream of one record type works too; an empty stream is zero documents.
from yaml Tstill reads exactly one document — its multi-document error now names the stream form instead of telling you to split by hand. There is noto … streamword:to yamlon a seq already writes the stream, and that write reads back through the stream form. -
Tagged unions cross the wire. Documents that come in several shapes discriminated by a field — kubernetes kinds, webhook event types — now read into a union:
[<Tag "kind">]names the discriminator, each case carries the record for one kind, and reading dispatches on the tag at both boundaries (JSON and YAML, top level or nested — sofrom jsonl KDocreads mixed NDJSON andfrom json seq<KDoc>a mixed array):[<Tag "kind">] type KDoc = | Deployment of DepSpec | Service of SvcSpec | [<Other>] Unknown of stringOne
[<Other>]case opts into open-world reading — kinds you didn’t declare land there carrying the raw tag, and yourmatchdecides their fate; without it an unknown tag errors naming the declared cases. Writers reinsert the tag first, so the roundtrip holds; a case’s tag value defaults to its name,[<Wire "v1">]overrides. Untagged unions stay off the wire, the error naming the attribute. -
Attributes on unions. Union declarations and union cases now host attributes, F#‘s syntax —
[<Tag "kind">]above atype,| [<Other>] Unknown of stringon a case. The registry stays closed and is now position-aware: a registered attribute in the wrong position says where it belongs ('Tag' attaches to a union declaration), an unknown one keeps the did-you-mean. -
Breaking:
to jsonwrites one document;to jsonlwrites NDJSON. The write side now mirrors the read side:value |> to jsonrenders one minified document — a record becomes an object, a seq becomes an array — and the newto jsonldoes whatto jsondid before, one document per element, lazily. No more wrapping a single value in a list to serialize it (payload |> to jsonreplaces[payload] |> to json), and every adapter now pairs with its own name:to json |> from json T,to jsonl |> from jsonl T.to yamlis unchanged: a seq still writes a----separated stream — YAML’s plural is the stream, JSON’s is the array.
Bugfixes
- A
forloop’s binder now types from its source, sofor d in docs do match d with | Deployment s -> …works — it used to refuse with “constructor patterns need a union value” while the equivalentdocs |> Seq.iter (fun d -> match d …)passed. The loop desugars to the piped shape now (the spelling it was always sugar for), and the comprehension form is fixed the same way.
Checks clean, behaves differently
xs |> to jsonon a seq previously wrote one JSON document per element (NDJSON); it now writes one array document. Scripts that meant NDJSON should sayto jsonl— same bytes as before. This is the section’s first real entry since v0.0.2 defined it.
v0.0.16
New features
-
Anonymous record literals.
{| key = key; n = 3 |}builds a record value without declaring a type first — handy for one-off shapes on their way out the door:[{| key = k; value = v |}] |> to json |> File.write "x.json". Fields can mix types (whereMap.ofPairsneeds one value type), multi-line literals work, and hover and dot-completion know the fields. A literal has the same type as the matching anonymous type (so it unifies withfrom json {| … |}’s result), while a declared record with the same fields stays a distinct type. No field punning, no empty{||}, no{| r with … |}— each error says what to write instead. -
Bare commands in match arms. A match arm now runs a command directly, no sigil needed:
match target with | "build" -> sh -c "make" | "test" -> dotnet test | other -> print $"unknown: {other}"In statement position the chosen arm streams its output; on a
letright-hand side it captures asseq<string>, like any command. The command ends at the next| pattern ->, so an argv word that happens to spellx ->needs quotes. With this, commands run bare in every block position —!()remains only for sequencing a command with an expression on one line (!(setup); print "done").
Docs
- Examples no longer wrap commands in
!()where a bare command works — inside anifbody or any other block, a command is just a statement. The docs now show the sigil only where it is actually required.
v0.0.15
Tag burned — released from the wrong commit. Version number consumed; no artifacts.
v0.0.14
Bugfixes
-
The subcommand walk learns all-caps command banners — jira’s
MAIN COMMANDS/OTHER COMMANDScarry no trailing colon, so v0.0.13’s detector saw no subcommands to walk and jira sigs stayed at the four global flags. And the walk’s outcome is now observable either way: a help-sourced sig that probed subcommands without profit says so —source: help (walked 12 subcommand help(s), 0 answered, none yielded flags). -
Bare-word probes are gated on advertisement —
weir add sig coderancode completion fish, which opened VS Code on two files.completion fishand theversionword now run only when the tool’s own--helpadvertises that subcommand; flag probes (--version,--help) remain universal. -
The recorded version identity is
--version’s first line, whitespace-collapsed — az’s multi-page environment report (with machine paths) is not an identity. And az’s help dialect parses:--flag --alias -s [Required] : docrows record the postfix short, each alias as its own flag, and a clean description. -
A path-y tool (
weir add sig ./lib/jp, an absolute path) mints a legal module name and one flat sig file under.weir/sigs/(separators become_) — the absolute case had aimed the write outside.weirentirely, and a leading/or.broke the module line. -
weir add sigon a file that exists but will not run (a stray.yaml) says so, instead of “not on PATH”. -
Subcommand tokens complete at every depth —
kustomize edoffersedit,kustomize edit aoffersadd— in the token spelling, never the case or record name; the segments un-glue from the sig’s own path keys. -
Completion fires on
-(a declared trigger character now) — a bare--offers the longs, a single-the shorts beside them. -
Go-to-definition reaches into the sig: a flag lands on its field declaration (the scoped record the line resolves to), a subcommand token on its case’s record.
-
The walk follows nesting to depth 4 (
kustomize edit add resource), reads gh’s colon-suffixed command tables, and the not-on-PATH message dropped its aside. -
A sub-less line on a scoped sig checks the globals — flag-only usage (
claude --scop2 --scope) squiggles again: the flags riding every case are the global set, so the case intersection checks it; hand-written unions that share nothing keep the partial-surface skip. -
Sig generation scopes to subcommand paths. The walk keeps its provenance to depth 4: a walked surface generates a union with a case per subcommand path — the longest match picks it, so
jira issue list --summarywarns namingjira issue listwhilejira issue create --summarychecks clean (sibling paths no longer share flags;issue createandproject createname distinct records by construction). A case checks its own flags plus its ancestors’ and the globals; a line with no subcommand checks the globals; completion offers the matched path only. Flat sigs (hand-written or scrape-poor) load unchanged. -
Sig flags complete in the editor — a
--word after a sig’d tool offers the sig’s own longs in kebab spelling (--s→--session-id, not the field name or a comment word; the old dropdown was the editor’s word-fallback over the sig file). claude-style dual spellings (--allowedTools, --allowed-tools) merge into one field, both accepted. -
broot’s box-drawing options table parses — the border glyphs strip to spaces and the rows read as standard columns, shorts and docs included.
-
Two more help dialects generate: micro’s Go-flag rows (single-dash longs, description on the next line), and BSD grep’s usage-only page — which exits nonzero, so the dump now feeds the harvest. And a surface that recorded no shorts no longer warns on short flags: no evidence, no claim.
-
Probe output is stripped of ANSI escapes — a tool that colors its help when piped (jira on macOS) broke banner detection and leaked
[1mbytes into sig docs; a colored--versionwould have stored escapes as the identity. -
A flag whose long is a weir keyword (docker’s
--type, kubectl’s--for) no longer aborts generation — the generator emits[<Wire "type">] typeFlag: bool, and the sig checker reads the Wire spelling for matching and did-you-mean. A keyword long with a short (jira’s-t, --type) shares one attr bracket, and when walked subcommands reuse a short (docker’s-aonallandall-tags) the first holder keeps it — longs still check. A generator-bug refusal now names the offending sig line.
v0.0.13
New features
-
Sig generation walks subcommands. Cobra-family tools (jira, docker, kubectl, kustomize) keep the real flags under
tool sub --help— generation now reads the advertised commands sections (grouped headings included), probes each subcommand’s help breadth-first to depth 4 under a 60-probe budget, and unions the flags into the flat surface, labeledhelp+subs. Sojira issue list --jql …stops warning on every flag the top-level help never mentioned. Every probe is guarded: null stdin, temp cwd, bounded wait with kill. -
A usage-table help with no flag rows (weir’s own) yields a harvested surface — every
--flagtoken, docless, labeledhelp-scan— instead of “found no flags”.
v0.0.12
Bugfixes
-
weir add sigon a tool that refuses--version(jira-style) no longer records the error’s usage dump — user paths included — as the version. The probe reads the exit code and climbs a two-rung ladder:--version, then theversionsubcommand (jira versionworks), with null stdin and a temp cwd so a bare-word probe can neither hang nor serve a local VERSION file as the identity. A tool answering neither records no identity, the sig says so in a comment, andweir verifytakes the hash-only arm.let versionis now optional in sig files. -
#sig "jira"(a quoted tool name) searched for a file literally named"jira".weir— it now teaches: drop the quotes. -
The sig help scraper learns fzf’s
+s, --no-sortoff-toggle spelling, and stops eating the first word of an argless flag’s description. -
Pattern-let binders hover:
let key, title = …shows each name’s own type.
v0.0.11
Docs
- Builtin hover/
#helpexamples are statement-style — the elevenlet … inone-liners now read as scripts actually read, and a gate keeps the form out.
v0.0.10
New features
-
Str.splitOnce/Str.trySplitOnce— split at the first separator, tail intact. Rust’ssplit_onceshape:splitOnce sep syields(before, after)and raises when the separator is absent;trySplitOnceis the Option twin. The KEY=VALUE spelling —Str.splitplus a[k; v]pattern silently misses when the value contains the separator;splitOncekeeps the tail whole. -
Seq.exactlyOne/Seq.tryExactlyOne— the cardinality assertion.headtakes the first and silently accepts more, so a wrong-arity command output passes quietly;exactlyOneraises on none and on more, with distinct messages (they are different bugs). The try twin answers None for both shapes, and stops at the second element — an infinite source never hangs it. The guide now teaches it for one-line expectations (git rev-parse HEAD |> Seq.exactlyOne). -
Seq.firstis retired;takestands. A preference reversal: the synonym’s readability reason did not fall — it was outweighed by one-name-per-operation, the rule that already retiredfilter. The error teaches (weir's first is 'Seq.take'), and the freed name binds (let first = …). -
Bare
dirteaches the listing. The module-qualified redirect (use 'Path.dir') now also says “for a directory listing, use ls” — DOS muscle memory pointed at the parent-of-a-path function was a wrong turn.
v0.0.9
New features
-
%— integer remainder. F#‘s spelling at*//’s precedence, truncated — the sign follows the dividend (-7 % 3is-1, matching F#/.NET; Python’s floored%gives2there). A zero divisor raises (“modulo by zero”,/’s discipline); floats are refused with a teach — finite-only floats cannot hold IEEE’s NaN remainder. In command argv%stays a literal byte (echo 50%,date +%N). -
Dir.stat— a directory’s entries as rows.Dir.list’sseq<FileRow>form:ls’s own rows over a named directory, same order, sorted by name, eager. Three names, one mapping —Dir.listgives paths,Dir.statgives rows,File.statgives one row. And the consequence:lsis no longer a reserved binder — withDir.stat "."as the escape a shadow leaves open,let ls = …is a user preference now, not an error.
v0.0.8
Bugfixes
- A heredoc block’s trailing blank lines clip — the blank line
separating the block from the next statement is layout, not a
trailing empty element. Interior blanks stay content, matching
YAML block scalars (whose keep-trailing
|+form weir rejects).
Docs
- The showcase tours the heredoc block, and Showcase joined the site’s top navigation.
v0.0.7
New features
<<</$<<<— the heredoc block. A line-end<<<marker opens a plain multiline literal: every byte below is content ($and{included), blank lines and relative indentation survive, and the value isseq<string>— one element per line, ready forFile.write, a pipe, or theSeqmodule.$<<<is the interpolated twin with exactly the string forms’ hole rules:{expr}substitutes,{{/}}are literal braces,$still stays a byte. The markers are glyphs, so no identifier is reserved and the interpolated form can never read as a splice; the arming law is yaml’s, and the errors teach.
Bugfixes
- A parse error inside a yaml
$(…)splice or a$<<<hole now reports its message and exact column instead of an empty error (the extraction landed on FParsec’s “Other error messages:” trailer).
Editors
- Heredoc highlighting everywhere the grammars reach: VS Code
(Marketplace/Open VSX) and micro ship rules with this release;
the tree-sitter grammar gained the heredoc district (byte-verbatim
body lines, whole-
{expr}hole tokens) and the Zed extension pins it.
v0.0.6
New features
weir add module— remote modules, vendored. Share code across repos as a fetch, not a package manager: no registry, no resolver, no version ranges.weir add module github.com/org/repo//lib/x.weir@v1.2.0 --as xresolves the ref to a full commit sha, fetches, validates (the file must be amodule, must typecheck, and must notimport— vendored modules are leaves for now), and vendors it into.weir/modules/with a content-hashed lock entry. The//separates repo from in-repo path; an explicit@refis required; the shorthand knows github.com and gitlab.com, and any host takes the full raw URL. Import from anywhere under the project withimport "weir:x" as X— a new, distinct spelling: both existing import forms resolve exactly as before. A re-add updates and prints the old and new sha. Private repos: setWEIR_TOKEN_GITHUB_COM/WEIR_TOKEN_GITLAB_COM(needed only at add/restore — the committed file needs neither). Andcheck --canreports a vendored module’s commands, writes and network access in your own report, at the module’sfile:line.
Bugfixes
- A REPL init
letwhose evaluation raises (File.readon a missing path) no longer crashes the REPL with a raw .NET stack trace — it reports the located error, printsinit: NOT loaded, and the session starts with none of the init’s names. weir restorenow repairs a present-but-modified vendored artifact by refetching it (schemas and modules alike) — the lock is the intent. Previously it only materialized absent files, leaving local drift in place; a deliberate local edit is a re-add, not an edit-in-place.
Chores
.weir/lock.jsonnow carries"schemaVersion": 1. Locks without the field read as version 1; a lock newer than the binary understands is refused with an upgrade teach.
Checks clean, behaves differently
- Nothing — the two import spellings that existed keep their exact
resolution (pinned);
weir:is new surface only.
v0.0.5
New features
- The REPL init file.
init.weirbeside the REPL config ($XDG_CONFIG_HOME/weir/,%APPDATA%\weir\on Windows) loads before the first prompt: declaration-onlytype/letbindings for the prompt (aliases are functions —let pu () = git push …), plus one#sessiondirective for four settings:cwd,env(seq<string * string>, set into the process environment once — visible toEnv.vars, every spawn, and layered underwithin env/sigils),logLevel(theWEIR_LOGlevels, same parsing), andechoCap. Loading is all-or-nothing: a broken init prints its located error and the session starts with none of it — safe because nothing in the file can run.#helpon an init name shows its///doc. A missing init is silent. weir --helpnow listsadd sig,check --can, and--version— three arms the usage string had silently omitted.
Bugfixes
weir docs-jsonemits LF on every platform (its dump is a generated, diffed artifact; Windows emitted CRLF).
Chores
#sessionoutside its home teaches: in a script it names the init file; typed at the prompt it says edit-and-restart.
Checks clean, behaves differently
- Nothing — scripts are untouched; the init file is REPL-only.
v0.0.4
New features
- Nothing.
Bugfixes
- Breaking: adjacent argv pieces no longer silently split into
separate arguments — they are refused at check time. Previously,
rm -rf $root/*ran as two arguments ($root, then/*— the Steam-bug shape, reproduced), and--flag="value"passed--flag=andvalueseparately. Both now fail with an error naming the repairs (an interpolated arg, quoting the whole word, orPath.under). Scripts that relied on glued adjacency were already silently wrong; they now fail loudly instead. - Error messages that suggest building a filesystem path now name
Path.under(which refuses a result that escapes its base) instead ofPath.combine(which follows it).
Chores
- Nothing.
Checks clean, behaves differently
- Nothing — the argv change above is the inverse: scripts that previously checked clean may now be refused, loudly.
v0.0.3
New features
- Nothing.
Bugfixes
- The installer served at weir.sh no longer prints six harmless
not founderrors before downloading — a generator defect planted a stray copy of the release checksums at the top of the script. The install itself was never affected; it was noise.
Chores
- The site’s post-release check waits out edge propagation instead of failing on a seconds-old deploy.
Checks clean, behaves differently
- Nothing.
v0.0.2
First release.
New features
- The language: F#-shaped expressions and real commands in one syntax, the whole file typechecked — PATH lookups included — before anything runs.
- Typed command output:
from json/from jsonl/from yamlinto declared records,Regexmatch patterns for everything line-shaped. - Typed boundaries:
Args.load(flags with derived--help),Env.load,Secret,Duration,Size,Instant. - Exit codes as data:
| succeeds,| complete,| exitCode,| orFail— a failing command raises by default. - Parallel fan-out (
Seq.pmap/piter/pfirst), retry/poll with deadlines, scoped processes, typed HTTP and YAML. - The tooling: REPL with completion and live coloring,
weir check [--json],weir fmt, an LSP, command signatures (#sig). - One static AOT binary per platform (Linux/macOS/Windows, x64 and arm64), millisecond start, no runtime. Installers verify checksums before installing.
Bugfixes
- Nothing — first release.
Chores
- Nothing — first release.
Checks clean, behaves differently
- Nothing — first release, no “before” to differ from. (This section
tracks changes where a script that passed
weir checkstill passes but does something else at runtime.)