{"atom":"regression/ast-cache-publication","family":"regression","slug":"ast-cache-publication","header":{"atom":"regression/ast-cache-publication","kind":"whole-program — a minimal repro, not yet reduced to an atom","source":"curated"},"axes":null,"cols":null,"engines":["rakudo-2026.07","rakupp-3.14.0"],"kinds":["ref","pp"],"defFrom":"rakupp:t/regression/ast-cache-publication.raku","counts":{"a":1,"f":0,"d":0,"c":0,"r":0,"n":0},"files":["atoms/regression/ast-cache-publication.grid"],"records":[["0001","","# Regression: the per-node eval caches published a pointer without publishing\n# what it pointed at.\n#\n# Two caches on AST nodes that every thread shares:\n#   * Binary::litVal — the literal operand of `$var OP literal`, built on first\n#     evaluation and reused. Stored through DecidedOnce, i.e. a RELAXED atomic:\n#     a reader could see the pointer without seeing the Value it addressed. On\n#     x86 the store buffer usually hides that; arm64 is weakly ordered and does\n#     not, and rakupp ships arm64 binaries.\n#   * NumLit::ratCache — a Rat literal's BigInt parts. Worse: two plain\n#     shared_ptrs, assigned concurrently, which races on the control block\n#     itself and can corrupt a refcount rather than merely read a stale pointer.\n#\n# Both now go through PublishedOnce (Ast.h): release on publish, acquire on\n# read, and a compare-exchange so a racing double-build has one winner and the\n# loser frees its copy instead of leaking it.\n#\n# WHAT THIS FILE CAN AND CANNOT DO. A memory-ordering bug does not reliably\n# produce a wrong answer — the values here were already correct when it was\n# found. ThreadSanitizer is the real detector:\n#\n#     cmake --build build-tsan -j8\n#     TSAN_OPTIONS=\"halt_on_error=1\" build-tsan/rakupp -e \\\n#       'my @r = await ^8 .map: -> $i { start { $i + 1 } };'\n#     TSAN_OPTIONS=\"halt_on_error=1\" build-tsan/rakupp -e \\\n#       'my @r = await ^8 .map: { start { 3.14 + 0 } };'\n#\n# So what this guards is the BEHAVIOUR: every expression below is evaluated for\n# the FIRST time by several threads at once — the exact window the caches are\n# built in — and each must still produce the right value. Each expression is\n# spelled differently on purpose, because a repeat of one is a second\n# evaluation and no longer races.\n#\n# Contract: exit 0 + last line PASS.\nmy @fail;\n\n# --- Binary::litVal: `$var OP literal`, first evaluated concurrently ---------\n# Each block is a distinct AST node, so each one races on its own first eval.\n{\n    my @got = await (\n        start { (^8).map({ $_ + 1  }).sum },   start { (^8).map({ $_ + 2  }).sum },\n        start { (^8).map({ $_ + 3  }).sum },   start { (^8).map({ $_ + 4  }).sum },\n        start { (^8).map({ $_ * 5  }).sum },   start { (^8).map({ $_ * 6  }).sum },\n        start { (^8).map({ 7 + $_  }).sum },   start { (^8).map({ 8 * $_  }).sum },\n    );\n    my @want = 28+8, 28+16, 28+24, 28+32, 28*5, 28*6, 56+28, 8*28;\n    @fail.push(\"litVal: got {@got.raku}, want {@want.raku}\") unless @got eqv @want;\n}\n\n# --- and the shape where the literal is on the LEFT (fastShape 2) ------------\n{\n    my @got = await (\n        start { 100 - 1 }, start { 200 - 2 }, start { 300 - 3 }, start { 400 - 4 },\n    );\n    @fail.push(\"literal-on-left: {@got.raku}\") unless @got.List eqv (99, 198, 297, 396);\n}\n\n# --- NumLit::ratCache: Rat literals, first evaluated concurrently ------------\n# Rats, not Nums: the cache only exists for the exact-rational path.\n{\n    my @got = await (\n        start { 1.1 + 0 }, start { 2.25 + 0 }, start { 3.5 + 0 }, start { 4.75 + 0 },\n        start { 0.1 + 0.2 }, start { 1.5 * 2 }, start { 3.14 + 0 }, start { 2.5 - 0.5 },\n    );\n    my @want = 1.1, 2.25, 3.5, 4.75, 0.3, 3.0, 3.14, 2.0;\n    @fail.push(\"ratCache values: got {@got.raku}, want {@want.raku}\") unless @got eqv @want;\n    # The whole point of the Rat path is exactness, and the cache hands out the\n    # SHARED BigInt parts — so a corrupted one would show up here.\n    @fail.push(\"ratCache exactness: 0.1+0.2 != 0.3\") unless (await start { 0.1 + 0.2 }) == 0.3;\n    @fail.push(\"ratCache type: not a Rat\") unless (await start { 1.1 + 0 }).WHAT.^name eq 'Rat';\n}\n\n# --- the same literal read by many threads AFTER it is cached ----------------\n# The published pointer is now shared by everyone; every reader must see the\n# same, complete value.\n{\n    my @got = await ^24 .map: { start { 6.25 + 0 } };\n    @fail.push(\"shared Rat readers disagreed: {@got.unique.raku}\")\n        unless @got.elems == 24 && @got.unique.elems == 1 && @got[0] == 6.25;\n    my @g2 = await ^24 .map: -> $i { start { $i + 9 } };\n    @fail.push(\"shared literal readers: {@g2.raku}\") unless @g2.List eqv (9..32).List;\n}\n\n# --- and the values are still right single-threaded --------------------------\n@fail.push(\"single-threaded litVal\") unless (^8).map({ $_ + 1 }).sum == 36;\n@fail.push(\"single-threaded Rat\")    unless 1.1 + 2.2 == 3.3;\n\nif @fail {\n    note \"FAILED:\\n\" ~ @fail.map({ \"  - $_\" }).join(\"\\n\");\n    exit 1;\n}\nsay \"PASS\";","output PASS","a",["PASS","PASS"],0,[["budget","20s"]]]]}