Landin prototype 4 — a hosted application
Current with specification 0.2.5. Its own findings W1-W7 are all resolved below.
A log filter: read a file, run every line through a chain of filters picked on the command line, hand what survives to a destination picked the same way, print a summary. Deliberately ordinary, because the point is not the program.
What it presses on, none of which the first three prototypes touched:
any— real heterogeneous dispatch. The chain is built from argv, so its shape is unknown until run time and no generic can express it- arenas — explicit program and scratch providers, with caller-owned lifetimes
Io— which the tour has never specified at all- the root — where a capability comes from when nobody handed you one
- entry — argc and argv, and what a hosted program is handed
- callbacks — a function and a state pointer, since nothing captures
Where a spelling had to be invented, the line is marked [Wn] and the question is written out at the end.
D211 preserves this program's genuinely runtime-selected filters, destinations and memory/system worlds. A homogeneous static instance elsewhere is not proof about an any loaded from this chain. Optimization preserves state changes, partial writes, immediate errno capture and cleanup order; unchecked adds no no-alias or undefined-behavior license. C callback addresses retain their convention and identity rather than becoming interchangeable with ordinary Landin entry points. The profile matrix applies each executable fixture's original oracle independently, including the allocator and logger pressures shared with prototypes 2 and 3.
D212 [0820] supersedes W7's historical resolution below: a helper can retain an independent allocator result in module state without returning it through a lexical boundary. Both builtin arena forms are withdrawn. The derivative uses an ordinary core/region allocator over an explicit provider, with explicit bulk cleanup; its allocations keep [0790]'s independent results. The compiler does not promise transitive lifetime checks for those results. Direct tracked references, source-derived views, and erased callback state retain their existing local checks. In particular, [1910] rejects retaining a frame or non-escaping reference by writing through a caller's pointer, slice or inout field. A known local alias cannot conceal that stored origin. This preserves the retained text and callback obligations shared with prototypes 2 and 3; independent allocator results and same-origin updates remain permitted. D222 additionally prevents a writable accessor from hiding module storage behind a parameter-only from clause. An explicit fallback argument preserves that choice and its caller-visible origins, including through a function value or concept entry. Independent region allocations still have no from, and read-only text results retain their existing dependency rule. This is the same accessor contract used by prototype 3, not a new lifetime promise for regions. The historical W7 finding remains unedited.
The bounded memory-world pressure uses ordinary core/io.memory with explicit caller file tables, output/error buffers and injected argument descriptors. Its nonempty reads distinguish EOF from zero-progress failure; partial writes retain their completed prefix, and a close error still consumes valid open state. Source-derived output and argument views retain local origin checks. If a handle is consumed out of an inout reader, [0910] requires replacement even when close fails and its caller recovers; consuming the whole reader by sink leaves no such returned-storage obligation. Applicable cleanup can restore a field before that exit check. D223 consumes a handle only when its close call is entered. A later argument that returns or propagates failure first leaves the pending handle live; a failure from close itself consumes it. The bounded positive/r491-sink-call-entry derivative distinguishes those edges and shares the descriptor/count argument pattern with prototype 3. D220 applies the same boundary to a handle table: a literal fixed-array element is contained storage, while indexing a slice table follows referenced backing and is not a sink place. Whole descriptor fields keep the container consumption form shared with prototype 3. Descriptors, nested backing, nonoverlap and copied-handle validity remain manual obligations under D153. Small complete library clients exercise these contracts. The complete derived hosted application is examples/derived_hosted, whose compiler/tests/fixtures/runtime/derived-hosted-memory/DERIVATION.md maps read, filter, dest, config and the command-line root to their executable sources and support.
At the system boundary D207 keeps errno in the explicit provider state, readable through hosted.last_errno, rather than adding payloads to these atoms. Capture follows the failing libc operation before another host call. Safe no-progress EINTR attempts may retry open/read/write, preserving completed transfers; close consumes its handle once even when it fails and is not blindly retried. The memory provider's injected failures do not invent a thread-local host errno.
For the argument path touched by this slice, both providers expose only user arguments and index zero is the first of them, never argv[0]. The ordinary adapter copies a pointer-and-length io.argument into exact caller scratch and returns its initialized prefix for text.from_bytes; it does not reinterpret the pointer as a C string or manufacture a slice. The complete application copies every retained argument into its explicitly supplied region before building configuration, so later changes to memory-world argument bytes do not change paths or match needles.
core/mem — one addition to what prototype 3 sketched
A single object rather than a slice. No from clause: what comes back is independent of the allocator, which is why two live allocations from one allocator are unremarkable [0790].
public new: (t: type, provider: type is allocator, inout state: provider, escaping value: t) -> (p: ptr mut t) ! out_of_memory = ... end
This inherits [1360]'s byte-count contract. In particular, core/mem's arena providers align the absolute returned address and turn impossible extent or address arithmetic into out_of_memory without consuming allocator state.
core/io — the host capability
public not_found: atom public no_access: atom public io_failed: atom public at_end: atom
A descriptor, distinct so it cannot become arithmetic by accident.
public file: type = distinct i32
The program passes this world to the routines that block or use host I/O. The parameter makes their chosen provider visible, but another hosted routine could call the public host constructor itself [1680].
A concept and not a type, which is the whole point: the program below never learns which one it got. A first draft of this file had it as a struct, and the capability story was worthless, because nothing else could be put in its place.
close takes the handle by sink, so using that place afterwards is refused rather than merely wrong. Say exactly what that is worth, though: the handle is a copyable value, so a copy taken before the close is not refused anything, and closing through both is the double-close this does not prevent. It is a use-after-consume check on one place, not ownership. Affine values would be the other thing, and they are parked with a condition in ROADMAP.md's register. And inout on the read buffer means, since 0.0.10, hand me a writable view; nothing is written back to the slice value and there would be nothing to write it back to.
public world: type = concept (world_type: type) open_read: (inout h: world_type, path: utf8) -> (f: file) ! not_found | no_access open_write: (inout h: world_type, path: utf8) -> (f: file) ! no_access close: (inout h: world_type, sink f: file) -> none read: (inout h: world_type, f: file, into: []mut u8) -> (n: usize) ! io_failed write: (inout h: world_type, f: file, bytes: []u8) -> none ! io_failed out: (h: world_type) -> (f: file) err: (h: world_type) -> (f: file) end world
The real one.
public system: type = struct reserved: u32 end system system is world (open_read: sys_open_read, open_write: sys_open_write, close: sys_close, read: sys_read, write: sys_write, out: sys_out, err: sys_err)
And here is the one place a capability appears from nowhere. Every other one in this program is passed in; this one is minted, because the entry point is where the program meets the machine.
public host: () -> (h: system) = ... end
A second root, for tests, built out of ordinary data rather than minted. This is what makes the concept worth having.
public memory: type = struct ... end memory is world (...) public in_memory: (files: [](name: utf8, body: utf8)) -> (h: memory) = ... end public written: (h: memory) -> (text: utf8 from h) = ... end
The sketch's future args adapter exposes only user arguments as a slice over initialized, process-lifetime C string descriptors; index zero is the first user argument. The hosted adapter owns that construction and backing; D151 supplies no slice_from operation or core-only exemption. Both run and config.build require retainable argument backing because configuration and match-filter objects keep text borrowed from it. An in-memory caller must supply equally retainable backing or make an allocated copy before the call.
public args: () -> (a: []cstring) = ... end
app/read — a buffered line reader
The buffer is an allocation owner with its complete extent, not a freeable slice. mem.new_bytes initializes every byte before returning; mem.bytes borrows a writable view and mem.drop_bytes releases the owner's original backing. Object allocation likewise receives a complete initial value before publishing its pointer. These library operations leave copied aliases and the application's arena lifetime under manual control.
An open reader holds one io.file. shut consumes the whole reader, so it has no returned closed state to represent. The atom-plus-integer union from W3's historical sketch is outside the enabled [1790] grammar; that original finding is retained below, without presenting its type as an enabled form.
import core/mem import core/io public reader: type = struct f: io.file buf: mem.byte_buffer fill: usize pos: usize end reader public open: (provider: type is mem.allocator, inout h: any io.world, inout a: provider, path: utf8, size: usize) -> (r: reader) ! ... = f := try io.open_read(h, path) undo io.close(h, f) buf := try mem.new_bytes(state: a, count: size) r = (f: f, buf: buf, fill: 0, pos: 0) end open
Two fallible acquisitions and one undo entry, which is the shape prototype 3 argued for, in the small. Nothing fallible follows the commit — and here the commit is one assignment at the end, so the discipline [1200] asks for is easy to keep rather than merely stated.
The line is a view into the reader's own buffer, so the signature says so. The caller may read it and may not refill while holding it, which is the bug this would otherwise be.
public next_line: (inout r: reader, inout h: any io.world) -> (line: []u8 from r) ! io.io_failed | io.at_end = loop do k := begin buf := mem.bytes(r.buf) newline_in(buf[r.pos ..< r.fill]) end match k not_present: try refill(r, h) found (at): start := r.pos r.pos = r.pos + at + 1 buf := mem.bytes(r.buf) line = buf[start ..< start + at] return end match end loop end next_line refill: (inout r: reader, inout h: any io.world) -> none ! io.io_failed | io.at_end = ... end
shut consumes the reader, which is what makes it read like close. The alternative was to sink r.f out of an inout parameter and then have nothing to assign back, since there is no i32 that means closed. [W3]
public shut: (provider: type is mem.allocator, sink r: reader, inout h: any io.world, inout a: provider) -> none = io.close(h, r.f) mut owned := r.buf mem.drop_bytes(a, owned) end shut
app/filter — runtime dispatch, one
The hosted library's runtime/generic-any-nominal-transport derives the mutable-dispatch pressure below without allocating an uninitialized object: two initialized providers with different layouts and methods retain their data and evidence through generic nominal copies and typed pointer reads. Their original pointees receive the mutations. runtime/fixed-array-any-shapes extends this evidence to genuine singleton and larger erased arrays, preserving both data and evidence through typed array-pointer stores, slices, generic copies and array fields. Initialized allocation and vector growth are exercised by runtime/r420-object-buffer-providers and runtime/r420-reference-provider-matrix; the stateful filter composition is executed by runtime/r420-stateful-filter-list.
self: ptr mut t, because a filter may count. The permission is in the type since 0.1.0, so the entry says what it does without also claiming it might re-point the pointer — which the older inout spelling did claim, and which was never true.
import core/text public filter: type = concept (t: type) keep: (self: ptr mut t, line: []u8) -> (yes: bool) end filter
- Threshold on the level field.
public level_filter: type = struct least: u8 end level_filter level_keep: (self: ptr mut level_filter, line: []u8) -> (yes: bool) = yes = level_of(line) >= self.val.least end level_keep level_filter is filter (keep: level_keep)
- Substring.
public match_filter: type = struct needle: utf8 end match_filter match_keep: (self: ptr mut match_filter, line: []u8) -> (yes: bool) = candidate: utf8 = text.from_bytes(line) else (encoding) _ = encoding yes = false return end yes = text.contains(candidate, self.val.needle) end match_keep match_filter is filter (keep: match_keep)
- Every nth line, which is why the concept is inout: this one writes to itself on every call.
public sample_filter: type = struct every: u32 seen: u32 end sample_filter sample_keep: (self: ptr mut sample_filter, line: []u8) -> (yes: bool) = self.val.seen = self.val.seen + 1 yes = self.val.seen % self.val.every == 0 end sample_keep sample_filter is filter (keep: sample_keep)
app/dest — runtime dispatch, two
import core/mem import core/io public dest: type = concept (t: type) emit: (self: ptr mut t, inout h: any io.world, line: []u8) -> none ! io.io_failed done: (self: ptr mut t, inout h: any io.world) -> none ! io.io_failed end dest
- Write the lines out, through a buffer of its own.
public text_dest: type = struct f: io.file buf: mem.byte_buffer used: usize end text_dest public open_text_dest: (provider: type is mem.allocator, inout h: any io.world, inout a: provider, path: utf8, size: usize) -> (d: text_dest) ! ... = f := try io.open_write(h, path) undo io.close(h, f) buf := try mem.new_bytes(state: a, count: size) d = (f: f, buf: buf, used: 0) end open_text_dest public discard_text_dest: (provider: type is mem.allocator, inout h: any io.world, inout a: provider, sink d: text_dest) -> none = io.close(h, d.f) mut owned := d.buf mem.drop_bytes(a, owned) end discard_text_dest text_emit: (self: ptr mut text_dest, inout h: any io.world, line: []u8) -> none ! io.io_failed = ... end text_done: (self: ptr mut text_dest, inout h: any io.world) -> none ! io.io_failed = ... end text_dest is dest (emit: text_emit, done: text_done)
- Count by level and print a table at the end.
The counts are a fixed array rather than a map, and that was not a shortcut: the concept has no allocator among its entries, because the other implementation has no use for one. So this one cannot ask for memory while emitting, and the honest answer was to pick a representation that never needs any. [W4]
public count_dest: type = struct by_level: [8]u32 total: u32 end count_dest count_emit: (self: ptr mut count_dest, inout h: any io.world, line: []u8) -> none ! io.io_failed = lvl := usize(level_of(line)) self.val.by_level[lvl] = self.val.by_level[lvl] + 1 self.val.total = self.val.total + 1 end count_emit count_done: (self: ptr mut count_dest, inout h: any io.world) -> none ! io.io_failed = ... end count_dest is dest (emit: count_emit, done: count_done)
app/config — building the chain from argv
The chain inherits D194's checked vector growth and initialized-prefix transaction: an unrepresentable capacity reports out_of_memory before any provider call, and a failed replacement keeps the already-built filter chain. Its copy and cleanup traversal use bounded stack; the complete application derivation remains owned by the roadmap.
import core/mem import core/text import core/vec import config/diag import core/io import app/filter import app/dest public bad_argument: atom
The reason any exists. The chain's length and the types in it are decided by the command line, so []t cannot hold it and no generic function can be written over it. [1400] says so; here it is.
public config: type = struct chain: vec.list(any filter.filter) to: any dest.dest input: utf8 end config public build: (provider: type is mem.allocator, inout h: any io.world, inout a: provider, escaping args: []cstring, inout d: any diag.log) -> (c: config from args) ! ... = mut chain := vec.new_list(t: any filter.filter) mut input: utf8 = "" mut to_file: utf8 = "" mut k: usize = 0 while k < lenof args do arg := text.from_c(args[k]) else (encoding) _ = encoding fail bad_argument end if text.eq(arg, "--level") then k = k + 1 fail bad_argument when k >= lenof args level_text := text.from_c(args[k]) else (encoding) _ = encoding fail bad_argument end initial: filter.level_filter = (least: try level_named(level_text)) f := try mem.new(state: a, value: initial) try vec.push(chain, a, any(f)) elsif text.eq(arg, "--match") then k = k + 1 fail bad_argument when k >= lenof args needle := text.from_c(args[k]) else (encoding) _ = encoding fail bad_argument end initial: filter.match_filter = (needle: needle) f := try mem.new(state: a, value: initial) try vec.push(chain, a, any(f)) elsif text.eq(arg, "--every") then k = k + 1 fail bad_argument when k >= lenof args
[0950] in one line: a bad number here is foreseeable from what we already hold, so it is reported and worked around rather than routed through the channel. The else arm yields the value, and nobody had to invent a placeholder.
number_text := text.from_c(args[k]) else (encoding) _ = encoding d.note(text.nowhere, diag.error, "--every argument is not UTF-8, using 1") "1" end mut n := text.to_u32(number_text) else (e) d.note(text.nowhere, diag.error, "--every wants a number, using 1") 1 end
and zero is a number, which would be a modulo by zero three hundred lines away. Foreseeable from what we hold, so [0950] says check it here.
if n == 0 then d.note(text.nowhere, diag.error, "--every 0 makes no sense, using 1") n = 1 end if initial: filter.sample_filter = (every: n, seen: 0) f := try mem.new(state: a, value: initial) try vec.push(chain, a, any(f)) elsif text.eq(arg, "--out") then k = k + 1 fail bad_argument when k >= lenof args to_file = text.from_c(args[k]) else (encoding) _ = encoding fail bad_argument end else input = arg end if k = k + 1 end while
Every successful adapter above retains the selected argument's origin. That is why args remains escaping and config remains from args; error recovery does not erase either promise. The --every encoding diagnostic is distinct from the following valid-UTF-8 decimal recovery. The only place the two destinations are chosen between, and the only place their concrete types appear. Everything downstream sees 'any dest'.
mut chosen: any dest.dest if lenof to_file > 0 then initial := try dest.open_text_dest(h, a, to_file, 8 * 1024) undo dest.discard_text_dest(h, a, initial) t := try mem.new(state: a, value: initial) chosen = any(t) else initial: dest.count_dest = (by_level: zeroed, total: 0) t := try mem.new(state: a, value: initial) chosen = any(t) end if c = (chain: chain, to: chosen, input: input) end build
app — the program
import core/mem import core/heap import core/region import core/text import core/vec import config/diag import core/io import app/read import app/dest import app/config
Everything retained for the whole run comes from one explicit region: the filters, the destination, copied arguments and reader storage. Its caller releases the region after closing the files and retiring every alias. A region's allocator contract is ordinary; the lifetime discipline is manual. args is a parameter and not io.args(): a run handed an in-memory world must be handed its command line too, or the root is only half replaced and the test cannot say what it is testing.
run: (provider: type is mem.allocator, inout h: any io.world, inout a: provider, inout d: any diag.log, escaping args: []cstring) -> (kept: u32) ! ... = mut cfg := try config.build(h, a, args, d) fail config.bad_argument when lenof cfg.input == 0 mut r := try read.open(h, a, cfg.input, 64 * 1024) defer read.shut(r, h, a) kept = 0 loop do line := read.next_line(r, h) else (e) break when e == io.at_end fail e end
The chain is the prototype's heterogeneous dispatch boundary. Each any filter retains the mutable data pointer authorized when it was constructed, so its ptr mut t entry can update the original counting filter. Copying the pair preserves that authority (D146); calling keep does not require replacing the stored pair or writing a copy back. This indexed initialized view also permits replacing elements. Whether the container meets [1320]'s source-free copied item_type contract is a separate origin question from the mutable dispatch itself [1160].
mut pass := true xs := vec.used(cfg.chain) for k in 0..<lenof xs do pass = xs[k].keep(line) break when not pass end for if pass then try cfg.to.emit(h, line) inc kept end if end loop try cfg.to.done(h) end run
A callback not worth a concept: one use, one shape, no second implementation on the horizon. So it is the pair from [1000], written out, and the contrast with the chain above is the point. A concept earns its place when the set of implementations is open; a pair is enough when it is not. This pair is internal Landin dispatch, not a C-compatible record: a C callback field must carry an explicit extern(c) function type under [1975], with independently declared retention and state-lifetime contracts.
public on_progress: type = struct call: (state: ptr u8, done: u32) -> none state: ptr u8 end on_progress
Hosted entry. The sketch names its argument adapter io.args; the complete derivative uses the supplied world's argument_count and argument entries, then admits their bytes directly into run-lifetime raw storage and appends one NUL for paths. The ordinary io.copy_argument remains available for callers with initialized scratch. Its hosted entry routine imports core/io/hosted and is the only place that acquires the heap and system world. No uninitialized view constructor is implied.
public main: () -> (code: i32) = mut h := hosted.host() mut w := any(addr h) begin mut backing := heap.host() mut program := region.new_region(addr backing) defer region.release_region(program) mut logger := diag.to(w.err()) mut d := any(addr logger) kept := run(w, program, d, io.args()) else (e) report_failure(w, e) code = 1 return end print_summary(w, kept) code = if d.failed() then 1 else 0 end if end end main
And what the root buys, which is the reason for all of it. run never learns which world it was handed. The test argument descriptor array has module backing, as do its string literals, so both levels satisfy run's retention contract.
test_args: [5]cstring = ["--level", "ERROR", "--out", "out.log", "in.log"] mut test_backing: [64 * 1024]u8 = zeroed test_drops_debug_lines: () -> none = mut h := io.in_memory([(name: "in.log", body: "DEBUG a\nERROR b\n")]) mut w := any(addr h) begin mut backing := mem.arena_over(addr test_backing[0], lenof test_backing) mut scratch := region.new_region(addr backing) defer region.release_region(scratch) mut logger := diag.new_log(capacity: 32) mut d := any(addr logger) kept := run(w, scratch, d, test_args[0..<5]) else 0 assert(kept == 1) assert(text.eq(io.written(h), "ERROR b\n")) end end test_drops_debug_lines
The bounded executable library slice keeps this sketch's provider distinction but uses D146's exact object-safe receiver shape: every world entry starts with a ptr world_type or ptr mut world_type, and both system and memory providers may travel through any world. The system provider retains the actual host argument table and returns each pointer-and-length argument view from itself, excluding argv[0]; an in-memory provider returns its caller-supplied argument backing under the same origin contract. Dynamic UTF-8 paths are converted to bytes and copied into explicit caller scratch with a checked trailing NUL, never a hidden allocation or truncation. diag.streaming retains a pointer to the erased world and a borrowed stream rather than naming the system provider. The historical sketches and findings below retain their original spelling.
The ordinary block supplies cleanup scope, not a new allocation origin. defer region.release_region runs on normal, failure and control-transfer exits [0820]. Its provider determines capacity: the hosted root explicitly selects the heap, while the memory example names its byte extent. Metadata uses that same authority, so finite capacity includes the allocation ledger. The executable region starts with an empty ledger variant and initializes its list on the first recorded payload, preserving the constructor's explicit from parent contract under D222. Individual arena frees do nothing, and releasing a region over an arena does not restore the arena's used offset. A new explicitly backed arena is a separate lifetime chosen by its caller. W7's former block-escape argument is not a guarantee of either provider.
The any C implementation makes this prototype's any pressure executable without changing the sketch: construction erases an exact pointer/conformance, every filter and dest entry uses the object-safe first self pointer already written here — world and diag.log gain theirs in the executable slice above — and the pair carries the pointee origin through the config aggregates. Parent conformances stay separate identities while an erased table flattens their function words; mutable authority is proved when the pair is constructed rather than stored as a third runtime field (D145--D147).
WHAT THIS ONE FOUND
The resolutions below cite the pre-release revisions this specification passed through, 0.0.1 to 0.0.17, on the way to 0.1.0. They are kept because when one thing was settled relative to another still carries information.
Seven, which is fewer than the container library and about what a program that uses the language rather than stretching it should produce. All are worked into tour 0.0.13. Two of them changed something larger than themselves: W1 made [1680] tell the truth about its own claim, and W3 resolved itself by pushing the design somewhere better while the file was being written.
W1 RESOLVED at 0.0.13, and it did more than fill a gap: it made an existing principle honest. [1680] said a function given no allocator cannot allocate, enforced by nothing more exotic than an argument list. That claimed more than the language delivers, because any function can reach for a root — hosted.host() here, or ptr(0x4002_0000) in a driver. The resolved principle is that arguments name supplied providers, not an enforced upper bound on authority below a call. An in-memory Io or bounded allocator can replace the host provider for code that uses the supplied value, but an ordinary helper may independently mint a host root. The call tree must be inspected before claiming host exclusion. The public constructors keep that systems access simple; restricting host minting to the entry module would add a privileged-module rule and still leave foreign calls and driver address literals. The roadmap reopens a checked boundary when trusted code needs it for testing or when untrusted code must run; D258 records the choice.
Writing this also found an error in this file. Io was a struct, which made the whole capability story worthless: nothing else could be put in its place. It is a concept now, travelling as 'any io.world' rather than as a type parameter, because an indirect call in front of a system call costs nothing where an allocator sits in hot loops and is threaded generically.
The original finding, for the record.
Where a capability comes from. Every capability here is passed in from somewhere — the allocator, the Io, the diagnostics sink. Follow the chain up and it ends at main, where hosted.host() mints one out of nothing. That call is the whole testability story of the language in a single line, and the tour never mentions it.
It should say at least this much: capabilities bottom out at the entry point, that is the only place one is created rather than passed, and everything below main can be handed a different one — which is what makes a hosted program testable and what lets the same code run freestanding on a different root.
Whether minting is restricted to the entry point or merely conventional is a real question. Restricting it would make "this subtree cannot touch the world" a checkable claim rather than a habit, which is a large promise and should be made or refused deliberately rather than by omission.
W2 RESOLVED at 0.0.13: the no-argument main is the ordinary form and the arguments come from core as a slice. The C shape stays available for whoever wants it.
The original finding, for the record.
What a hosted program is handed. [1650] says main follows the system C ABI, so it gets argc and argv. Turning those into something indexable needs slice_from, which is core-only by [0500]. So either the runtime hands the program a []cstring or core provides the helper written here as io.args(). Either is fine; neither is written down, and until one is, a program cannot read its own arguments.
W3 RESOLVED while writing, and worth recording because the rule pushed the design somewhere better. shut first sank r.f out of an inout parameter, which [0910] then wants assigned again before returning — and there is nothing to assign, since io.file is a distinct i32 and no i32 means closed. Inventing a reserved invalid descriptor would have reintroduced exactly the sentinel that removing null was meant to avoid.
Two things came out of it instead. The field became none_open | io.file, which is honest — a reader that has been shut has no file — but not free, and this finding said it was. [0480] promises the niche for an atom and a pointer, because a pointer has a bit pattern nobody else can use. io.file is a distinct i32 and every i32 is a plausible descriptor, so the union carries a tag and the reader is a word wider. That is a fair price and it should be stated as one. And shut took the whole reader by sink, so it reads like close and the question does not arise. Both are better than what was there.
W4 RESOLVED at 0.0.13, written beside [1260] where the same thing is said about error sets: a concept fixes the shape for every implementation there will ever be, and widening one until it fits the hungriest hands everyone the sum of everyone's needs.
The original finding, for the record.
A concept fixes what every implementation may ask for. count_dest would like to allocate while emitting and cannot, because the concept it shares with text_dest has no allocator among its entries and text_dest has no use for one. The honest answer was to pick a representation that never needs memory — a fixed array of counts rather than a map.
That is not a defect. It is what [1260] says about error sets, one level up: a concept fixes the shape for every implementation there will ever be. It belongs written down beside it, because the pull to widen a concept until it fits the hungriest implementation is strong, and gives every implementation the union of everyone's needs. When it genuinely does not fit, the answer is two concepts.
W5 RESOLVED at 0.0.13, and smaller than this finding claimed. [1150]'s example is correct: items there is a slice, and a slice element is a place. What was missing is the boundary — inout on a loop binding is for arrays and slices, and over anything else that satisfies iterable the binding is a value, because item hands out a copy. A container that wants to be walked and changed hands out a writable view, which is what this file does. No second concept, no pointer as a loop binding, no change to [1150].
The original finding, overstated, for the record.
[1150] and [1320] contradict each other, and it is reached at once.
for inout item, idx in items do
item = item + i32(idx)
end forThat is [1150]. But iterable at [1320] has
item: (s: T, c: Cur) -> (v: Item)
which hands out a copy, so assigning to the binding writes to the copy. There is no entry a traversal can write back through, and so [1150]'s example cannot work for any type that satisfies iterable — which is every container in prototype 3.
Written around here by taking a writable view and indexing it, which works and reads acceptably. But it means the mutating for loop the tour advertises does not exist.
Two ways to fix it. Have item return a reference derived from the collection, (v: Item from s), and let the loop's convention demand a writable one — 0.0.9's machinery doing what it was built for, needing Item to be able to be a place rather than a value. Or a second concept for mutable traversal whose item hands back a ptr, which is more honest about what is happening and costs a concept. The first is smaller and fits what is already decided, and needs a careful look at what Item then is.
Current D146 qualification: construction checks the pointer against every exposed receiver's permission. Copying or reading the pair through immutable storage preserves that authority; only replacing the pair needs writable storage. The older resolution below attributes permission to how the pair was reached, which D146 supersedes. Its origin remains reference-bearing under the existing local checks.
W6 RESOLVED at 0.0.13: the pair carries both. Whether the pointee may be written through, which comes from how the pair was reached, so a stateful implementation behind an inout self works — which is most of them. And the pointee's origin for [0840], so an 'any' over something with frame origin cannot be stored where it would outlive the frame.
The original finding, for the record.
Whether an 'any' carries the permission of what it points at. The chain is walked through used_mut so the elements are writable, and the entries take inout self, since a filter may count. Nothing says the pair's data pointer inherits writability from how the pair was reached. It has to — a stateful implementation behind runtime dispatch is not exotic, it is most of them — but [1370] describes the pair as a data pointer and a table and stops there.
Unstated and related: whether the origin of what an 'any' points at travels with the pair for [0840]. It should, and then a frame-origin any could not be stored in a longer-lived list, which is the bug worth catching here.
W7 RESOLVED at 0.0.13 the way this finding argued: passed on as a parameter, an arena is an ordinary allocator again, and what comes out of it there is allocated rather than frame. It has to be, or a block arena would be useless beyond its own function, and it is sound because the block is the outermost extent.
The original finding, for the record.
What happens to an arena block's frame origin when the arena is passed on. [0820] says everything from a block has frame origin and nothing from it may leave the block. main opens a block and hands the arena to run, which allocates from it and returns a config holding those pointers.
If frame origin travelled through the parameter, run could not return anything it allocated, and a block arena would be useless beyond the function that opened it — which is most of what one is for. If it does not travel, the block's guarantee holds only where the block is, and the tour overstates it.
The second is right and is also sound, and the reason is worth stating rather than leaving implicit: the block is the outermost extent, so anything that would outlive it has to pass the block on its way out, and the check at the block catches it there. Through a parameter the arena is an ordinary allocator. That sentence is missing, and without it [0820] and [0770] disagree.