From 9f2f2b9c9671f8cf7485b1cc2a5b931619b88aa7 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Thu, 16 Jul 2026 02:06:18 -0400 Subject: [PATCH] bugc: compose combined debug contexts by gather/flat, not pick MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit combineDebugContexts wrapped multiple combined contexts in `pick`, but pick means "one of these is true (choose one)" — whereas contexts combined here (constant propagation, CSE, read-write merging, block merging) ALL apply simultaneously. That is what `gather` means. Compose correctly per the context-composition guidance: when the combined contexts have disjoint discriminator keys, merge them flat as sibling keys on one object; when a key collides (e.g. two `code` ranges from merged writes), gather them. Enclosing gather/pick wrappers are flattened first so they re-compose rather than nest. Effect: read-write merging (coalesce) now gathers its merged code ranges instead of picking one. --- packages/bugc/src/ir/utils/debug.ts | 33 +++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/packages/bugc/src/ir/utils/debug.ts b/packages/bugc/src/ir/utils/debug.ts index 13516fe713..e34120f672 100644 --- a/packages/bugc/src/ir/utils/debug.ts +++ b/packages/bugc/src/ir/utils/debug.ts @@ -2,8 +2,9 @@ import type * as Ir from "#ir"; import type * as Format from "@ethdebug/format"; /** - * Combine multiple debug contexts into a single context. - * If multiple contexts have source information, creates a pick context. + * Combine multiple debug contexts into a single context that asserts + * all of them. Contexts with disjoint keys compose flat as siblings; + * contexts with a colliding key (e.g. two `code` ranges) are gathered. * Filters out empty contexts. */ export function combineDebugContexts( @@ -19,10 +20,14 @@ export function combineDebugContexts( return {}; } - // Flatten pick contexts - if a context has a pick, extract its children + // Flatten enclosing gather (and legacy pick) wrappers so their + // children re-compose here rather than nesting. `gather` children + // all apply simultaneously, which is exactly what combining means. const flattenedContexts: Format.Program.Context[] = []; for (const context of contexts) { - if ("pick" in context && Array.isArray(context.pick)) { + if ("gather" in context && Array.isArray(context.gather)) { + flattenedContexts.push(...context.gather); + } else if ("pick" in context && Array.isArray(context.pick)) { flattenedContexts.push(...context.pick); } else { flattenedContexts.push(context); @@ -66,10 +71,23 @@ export function combineDebugContexts( return { context: uniqueContexts[0] }; } - // Multiple unique contexts - create a pick context + // Multiple distinct contexts that all apply simultaneously. If + // their discriminator keys are disjoint, compose them flat as + // sibling keys on one object; if any key collides (e.g. two + // `code` ranges), they cannot be siblings, so gather them. Both + // mean "all apply" — pick would wrongly mean "choose one". + const keys = uniqueContexts.flatMap((context) => Object.keys(context)); + const hasCollision = new Set(keys).size !== keys.length; + + if (!hasCollision) { + return { + context: Object.assign({}, ...uniqueContexts) as Format.Program.Context, + }; + } + return { context: { - pick: uniqueContexts, + gather: uniqueContexts, } as Format.Program.Context, }; } @@ -145,7 +163,8 @@ export function extractContexts( * - The operation's debug context * - Debug contexts from all operands/fields * - * Uses pick contexts to preserve all distinct debug information. + * Distinct contexts are gathered (or composed flat when their keys + * are disjoint) so all applicable debug information is preserved. */ export function combineSubInstructionContexts( operationDebug: Ir.Instruction.Debug | Ir.Block.Debug | undefined,