infer an empty collection literal's type from the first value stored into it - #621
Merged
Conversation
…into it a binding declared as a bare empty literal (mut tasks := []) checked as an error, and every later use of it was silently untyped. awaiting a task pulled out of such a list lost its result type, so extracting the ok payload lowered to an unresolvable call (unwrap_or) or an offset-less field read (.is_ok/.ok) that the ir contract check rejects. the checker now retypes the binding from the first push/insert/add, and records the type on the literal node so the emitter builds a tagged container. the regression covers every payload kind in both extraction spellings, plus ! propagation and catch, through the previously broken shape.
…ntry the heap payload extracted from an awaited task result changes hands exactly once: the ok arm hands the payload out and frees the shell, the err arm releases the error and builds the fallback in place. the new leak case churns both arms; the regression joins the curated memcheck list.
the shared-list-handle workaround existed because task results with list payloads miscompiled; now that they work, each worker returns its own latency list and the caller merges after the joins. output format is unchanged.
the collections guidance now says where an empty literal gets its element type, and the concurrency example fans out over an unannotated task list.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
summary
mut tasks := []left the binding untyped: an empty literal has no element type, so it checked as an error and every later use of the name was silently untyped. the visible damage was awaited task results. a task pulled out of such a list lost its result type, so(await t).unwrap_or(...)compiled to a bare method call the runtime cannot resolve, andr.okemitted a field read with no offset, which the ir contract check rejects at build. the payload kind did not matter — int, string, and list payloads all broke in this shape — while every payload kind worked with a directt := spawn f()or an annotated list.the checker now retypes the binding from the first value stored into it (
pushfor a list,insertfor a map,addfor a set) and records the type on the literal node, so the emitter builds a tagged container that retains its elements. storingnoneor another empty literal decides nothing; the binding stays open until a typed store arrives.the grpc bench client carried a workaround for exactly this: workers pushed latencies into shared list handles because task results with list payloads were avoided. each worker now returns its own latency list through its task result and the caller merges after the joins.
what was tested
tests/cases/test_task_result_payloads.pith: int, string, bytes, list, map, struct, bool, and float payloads through the previously broken inferred-list shape, in both spellings (unwrap_orand.is_ok/.ok), with!propagation andcatchon top; verified on green default, one green worker, andPITH_GREEN=0tests/leaks/leak_task_result_payloadchurns both extraction arms (ok payload handed out, err released with the fallback built in place) and is flat across 200k/800k rounds; the regression also joins the curated memcheck list and runs valgrind-clean withPITH_STRUCT_FREELIST=0make test,PITH_GREEN=0 make test,verify-green-corpusandverify-osthread-corpus(283 passed each),make memcheck,make leak-check,make run-examples,make docsite-check,make bootstrap-verify; the bootstrap seed was regenerated and rebuilds a working driver from scratchexamples/grpc_chat.pithandexamples/grpc_reflect.pithrun to completion; the bench client ran against the local tls server at conc=1 and conc=8 and prints the same row format as beforenotes
a pre-existing leak surfaced while checking ownership, unrelated to this change: rebinding one name to results with different payload kinds in a single function releases later bindings with the first binding's kind, so
r := await ta(anInt!) followed byr := await tb(aBytes!) leaks the bytes payload. it reproduces with direct spawns on main. the regression test uses distinct names per section to stay out of its way; it deserves its own fix.