Skip to content

infer an empty collection literal's type from the first value stored into it - #621

Merged
kacy merged 4 commits into
mainfrom
empty-literal-inference
Jul 29, 2026
Merged

infer an empty collection literal's type from the first value stored into it#621
kacy merged 4 commits into
mainfrom
empty-literal-inference

Conversation

@kacy

@kacy kacy commented Jul 29, 2026

Copy link
Copy Markdown
Owner

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, and r.ok emitted 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 direct t := spawn f() or an annotated list.

the checker now retypes the binding from the first value stored into it (push for a list, insert for a map, add for a set) and records the type on the literal node, so the emitter builds a tagged container that retains its elements. storing none or 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

  • new regression 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_or and .is_ok/.ok), with ! propagation and catch on top; verified on green default, one green worker, and PITH_GREEN=0
  • new leak case tests/leaks/leak_task_result_payload churns 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 with PITH_STRUCT_FREELIST=0
  • make test, PITH_GREEN=0 make test, verify-green-corpus and verify-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 scratch
  • examples/grpc_chat.pith and examples/grpc_reflect.pith run to completion; the bench client ran against the local tls server at conc=1 and conc=8 and prints the same row format as before

notes

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 (an Int!) followed by r := await tb (a Bytes!) 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.

kacy added 4 commits July 29, 2026 19:03
…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.
@kacy
kacy merged commit 60ed362 into main Jul 29, 2026
2 checks passed
@kacy
kacy deleted the empty-literal-inference branch July 29, 2026 19:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant