Skip to content

Fix/bool var copy merge#4

Merged
VillanCh merged 56 commits into
mainfrom
fix/bool-var-copy-merge
Jul 14, 2026
Merged

Fix/bool var copy merge#4
VillanCh merged 56 commits into
mainfrom
fix/bool-var-copy-merge

Conversation

@VillanCh

@VillanCh VillanCh commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

No description provided.

VillanCh and others added 30 commits July 7, 2026 22:16
A method reference (, , ) binds
naturally to a raw SAM (it carries no explicit parameter types to mismatch),
so the parameterized functional-interface cast added by lambdaArgFunctionalCast
/ lambdaArgRawJDKReceiverCast is unnecessary for it. Worse, for SAMs whose
descriptor mentions nested wildcards (Stream.flatMap's
`Function<? super T, ? extends Stream<? extends R>>`, or Collectors.collect),
the cast pins a concrete parameterization that defeats javac's poly inference
("method flatMap cannot be applied to given types").

An explicitly-typed lambda body still needs the cast to bind (the documented
fastjson2 JSONPathSegment$CycleNameSegment$MapRecursive `.map((l0) -> ...)`
case), so only method references are skipped. Distinguished via a new
CustomValue.IsMethodRef flag set on the method-reference bootstrap branch.

A/B tree inventory (all jars, OFF vs ON):
  fastjson2  25 -> 24 errLines  (ObjectReaderCreator.toFieldReaderArray flatMap(Collection::stream))
  spring     28 -> 25 errLines  (AnnotatedTypeMetadata collect(Collector<...>))
  all other jars unchanged (no regression)

Updated TestRawStreamLambdaCastIsLoadBearing to assert the new correct shape:
the `.map` lambda keeps its cast, the `.filter` method reference is bare.
…-cast

Companion to the method-ref FI-cast skip: the method reference now renders
bare (.filter(Objects::nonNull)) instead of cast, while the lambda keeps its
(Function<RawStreamItem, Object>) cast. Updated both the golden rules and the
TestRawStreamLambdaCastIsLoadBearing ON/OFF assertions.
reachingSlotVersionByWeb / reachingSlotStoreContinuationByWeb redirect a
slot load/store to its web-proven SAME-variable reaching definition (same
VarUid) when DFS traversal order left a later/disjoint-branch version in the
slot table. These were opt-in (JDEC_LIVEINTERVAL_WEB) on the strength of an
older note that they were "net-neutral on the iso per-file metric and slightly
negative under javac tree masking".

Re-measured against the current 8-jar tree inventory (the real repackage
metric), enabling them is a strict improvement with delta >= 0 on every jar:

  fastjson2  24 -> 22 tree errLines  (ObjectReaderCreator 3->2, JSONPathParser 2->1)
  codec / commons-lang3 / gson / guava / jsoup / snakeyaml / spring  unchanged

The repair only coalesces definitions the dataflow proves to be one variable;
disjoint live ranges (try-with-resources primaryExc counter-example) fall in
different webs and are left untouched, so it cannot merge genuinely-distinct
variables. Flipped the gate to JDEC_LIVEINTERVAL_WEB_OFF (default ON) per the
"delta must be non-negative to land" rule. New TestLiveIntervalWebRepairIsLoadBearing
pins the fastjson2 ObjectReaderCreator group ON(web)<OFF(web).
Re-measured the full 8-jar tree inventory after the two fixes in this round
(method-ref FI-cast skip + live-interval web repair default-on) and updated
all status docs to the honest, harness-produced numbers:

  fastjson2  tree errLines 25 -> 22  (defect units 14, clean rate 97.9%)
  spring     tree errLines 28 -> 25  (defect units 16, clean rate 98.3%)
  all other jars unchanged

Totals: 4424/4487 flat units clean = 98.6%, 87 tree errLines, 63 defect
units, 0 syntax errors. CODEC_TODO status table + switch index (new
JDEC_LIVEINTERVAL_WEB_OFF default-on entry, JDEC_LAMBDA_RAW_JDK_RECV_CAST_OFF
method-ref-skip note), TODO.md snapshot + T2 entry, BENCHMARK tables A/C/§6
+ headline, README + README.zh-CN all refreshed to the flat-unit metric.
…al snapshot

The 3-way (JavaJive/CFR/Vineflower) table E used a per-jar sub-sample
methodology from the cross-comparison sampling period; its JavaJive numbers
(96.6%, 77 defect classes) predate the current full flat-unit inventory. Add
a prominent note that the current JavaJive numbers live in table A (98.6%,
63 defect units / 4487), and that CFR/Vineflower columns were not re-measured
in this round. Updated the §6 headline and §7.3 conclusion to cite the
current 98.6% flat-unit rate.
…ning lambda bodies

A generic method returning Supplier<T> / Function<T,R> whose lambda body returns
an erased Object (a raw-typed receiver's method, or any Object-returning call)
lost the source's unchecked `return (T)/(R) expr;` cast. The lambda impl method
in bytecode is erased (Object return), and its instantiatedMethodType return is
Object, so the bare `() -> { return expr; }` is rejected by javac as
"bad return type in lambda expression: Object cannot be converted to T".

The fix recovers the cast TARGET from the enclosing method's generic Signature
(CurrentMethodSig): when the method's return type is a parameterized Supplier /
Function / BiFunction whose return-position type argument is a bare type
variable, and the lambda is the same raw FI with an Object instantiatedMethodType
return, the body's value `return EXPR;` is rewritten to `return (T) (EXPR);`.
Void-returning SAMs (Consumer/BiConsumer/Predicate) never cast.

Verified: CFR also drops this cast and fails to recompile the same sites
(confirmed against the real fastjson2 2.0.43 source, which carries `() -> (T)
objectReader.createInstance(0)`). A/B tree inventory (all jars, OFF vs ON):
  fastjson2  22 -> 20 errLines  (ObjectReaderProvider.createObjectCreator,
                                 ObjectReaderCreator.createBuildFunctionLambda)
  all other jars unchanged (no regression)
A/B delta fastjson2 +2 (ON=20 OFF=22). New TestLambdaReturnTypevarCastIsLoadBearing
pins the cast ON (present) vs OFF (bare body), plus a regression seed+golden.
…st fix

fastjson2 tree errLines 22 -> 20 (defect units 14 -> 13), all other jars
unchanged. Totals: 4425/4487 flat units clean = 98.6%, 85 tree errLines,
62 defect units. Updated CODEC_TODO status table + new
JDEC_LAMBDA_RETURN_TYPEVAR_CAST_OFF switch index entry, TODO snapshot, and
BENCHMARK tables/headline (fastjson2 98.0%).
…r param

A constructor (or static method) whose i-th formal is a RAW functional
interface (raw BiConsumer.accept(Object,Object)) rejects an UNBOUND instance
method reference like Throwable::setStackTrace as "invalid method reference":
the impl method's arity (Throwable, StackTraceElement[]) does not match the
raw (Object,Object) SAM. The source carried
`(BiConsumer<Throwable,StackTraceElement[]>) Throwable::setStackTrace`; recover
it from the invokedynamic instantiatedMethodType (3rd LambdaMetafactory arg),
now carried on the method-ref CustomValue as InstantiatedMtdDesc.

The new ctorRawFISAMMethodRefCast fires only for ctor/static calls, a method-ref
argument carrying an instantiatedMethodType, a formal in the >=2-SAM-arity JDK
FI family (BiConsumer/BiFunction/BiPredicate -- single-param SAMs bind directly
and are excluded), and at least one recovered param more specific than Object.
Cast target = `<FIRawClass><<instantiated param types>>`.

A/B tree inventory (all jars, OFF vs ON):
  fastjson2  20 -> 19 errLines / 13 -> 12 blocker units
              (ObjectReaderCreator `new FieldReaderStackTrace(..., Throwable::setStackTrace)`)
  all other jars unchanged (no regression)
A/B delta fastjson2 +1. CFR and Vineflower both drop this cast too (3-way oracle
all-fail); the real fastjson2 2.0.43 source carries it explicitly. New
TestCtorRawFIMethodRefCastIsLoadBearing + regression seed+golden.
…aw-FI method-ref cast

fastjson2 tree errLines 20 -> 19 (defect units 13 -> 12), all other jars
unchanged. Totals: 4426/4487 flat units clean = 98.6%, 84 tree errLines,
61 defect units. Updated CODEC_TODO status table + new
JDEC_CTOR_RAWFI_METHODREF_CAST_OFF switch index entry, TODO snapshot/round
summary, and BENCHMARK tables/headline (fastjson2 98.2%).
…maining 19 tree errLines

Catalogs all 19 remaining fastjson2 tree errors with their bytecode-truth +
source-confirmed root cause and the holistic fix-shape for each, so the
deferred core refactor has actionable specs (not re-derivation). Documents
that these 19 are one tightly-coupled family (slot-typing/split/merge/phi/
LUB/reaching-def/receiver-binding) requiring a simultaneous rebalanced
refactor, and that 6 single-point guard attempts this session all regressed
(56/74/8) or did not fire — empirically confirming single-point guards are
not viable. The 4 landed cast-recovery fixes (fastjson2 25->19) are the
safe single-point ceiling; the remaining 19 are left to the holistic core
refactor effort.
…one boolean var

reachingBoolVarCopyMerge (JDEC_BOOL_VAR_COPY_MERGE_OFF): when an if/else
(or switch-case) sibling arm stores a genuinely boolean-typed value
(Z-returning call like isRefDetect()) into a slot whose current int-typed
ref was created by the OTHER sibling arm either copying a boolean-default
variable (itemRefDetect = previous, previous compiled to iconst_1/0) or
storing a comparison ternary ((c && cond) ? 1 : 0), re-type that int ref
(and its proven-boolean int-0/1 default) to boolean when a phi proves the
two arms are one source variable.

reachingBoolDefaultMerge cannot anchor here: it walks Source edges backward
and a sibling branch's definition is unreachable (no path). This handler
anchors on the boolean store, reads the global slot table current ref,
recovers its creator store via a new refToCreatingStore index (sidesteps
opcodeIdToRef non-deterministic map iteration), and proves the RHS is a
boolean default (int-0/1 literal, shape a) or a copy of a slot whose own
definition is such a literal (shape b).

Fixes fastjson2 FieldWriterList.writeList (copy arm) and
ObjectWriterImplList.write (ternary arm): fastjson2 tree 19->17 errLines,
defect classes 12->11. A/B delta >= 0 on all 8 jars (codec/commons-lang3/
gson/guava/jsoup/snakeyaml/spring all +0). Load-bearing test +
BoolVarCopyMergeSeed regression seed.
…egression investigation

Attempted reachingRefSlotJDKSubtypeReturnArmMerge (IsReferenceSubtypeBridged
to detect val<:current on method-return/boxed values). All gates passed
on JSONPathParser offset 1108 (sub=true, phiOk=true) but the broad merge
regressed fastjson2 17->36 (+19): it fired on many subtype stores that
must stay split (subtype arm assigned in only one branch -> merge read
uninitialized). Reverted. Conclusion: this family needs a narrower gate
(e.g. requiring the merge read to be instanceof a SIBLING, not a subtype),
not a broad subtype merge; the phi gate alone cannot distinguish.
…nvestigations

Two empirical investigations of the tightly-coupled core family (both
reverted, findings recorded for future work):

1. incompatiblePhiWidenObject (read-side widen-to-Object): when a load's
   reaching-definition web merges >=2 reference-type stores whose pairwise
   LUB is exactly Object, widen the resolved ref to Object. Regressed
   fastjson2 17->24 (+7): even with the LUB-is-Object gate, the ref still
   carries member-access uses that depend on the narrower type. Conclusion:
   cannot widen the shared ref; must mint a per-load Object temp (phi sink)
   and only when the read is a putfield/putstatic/return, not a member
   access receiver.

2. reachingRefSlotJDKSubtypeReturnArmMerge (see #15 note): the phi gate
   cannot distinguish a merge read that is instanceof a SIBLING from a
   subtype arm assigned in only one branch (merge read uninitialized).
…, reverted

Attempted phiSinkCast: insert a (TargetType)((Object)(var)) Object-bridge
double cast on putfield/putstatic when a slot-shared local's type is
incompatible with the field type (Boolean vs Predicate, Class[] vs Field).
The cast renders correctly and fixes 4 fastjson2 errors in isolation, but
the authoritative A/B delta (TestJarRecompileDelta) shows MASSIVE negative
deltas across ALL 8 jars: codec -7, commons-lang3 -11, fastjson2 -36,
gson -5, guava -77, jsoup -15, snakeyaml -12, spring -22. Even with
FQN/array/type-variable gating, the cast mis-fires on many field stores
that must stay as-is, breaking javac generic inference and overload
resolution. Conclusion: read-side/sink casting is not viable; the family
needs the variable-typing/splitting core (incompatible types must land on
distinct refs) rather than cast patching at the sink. Fully reverted.
…catch-slot family)

rebindIncompatibleLoadForSink (JDEC_REBIND_INCOMPATIBLE_LOAD_OFF): in phase-2 statement
building, when a putfield/putstatic sink consumes a local-load value (SlotValue wrapping
a JavaRef) whose resolved type is INCOMPATIBLE with the sink's declared type (Boolean vs
Predicate, Object vs MethodHandle -- the catch-slot / switch-case slot-reuse family in
fastjson2 JDKUtils.<clinit>), the single forward simulation parked a DFS-stale wrong-branch
ref in the global slot table, and the sink read the wrong branch. By phase 2 the simulation
is complete (opcodeIdToRef fully populated), so use the structural reachingStoresOf walk
(Source edges, simulation-order-independent) to find the load's TRUE reaching store whose
committed ref type exactly matches the sink type, and rebind the sink to that branch ref.

Strict gating: value must be a SlotValue wrapping a JavaRef; both sides non-primitive,
non-Object, non-type-variable concrete classes; neither assignable to the other (genuine
incompatible pair); a reaching store exists whose ref type exactly matches the sink type
(proving the compatible branch was minted). Never widens or casts -- merely re-points the
read at the already-minted compatible ref.

This is the first step of the two-pass typing core: instead of read-side/sink patching
(widen-to-Object, JDK-subtype-return merge, phiSinkCast -- all empirically regressed and
documented), it rebinding at the statement-building phase using structural reaching-def
analysis. Fixes fastjson2 JDKUtils.<clinit> (PREDICATE_IS_ASCII, METHOD_HANDLE_HAS_NEGATIVE)
and JSONFactory:325: fastjson2 tree 17->14 errLines, defect classes 11->9. A/B delta >= 0
on all 8 jars (codec/commons-lang3/gson/guava/jsoup/snakeyaml/spring all +0). Load-bearing
test + RebindIncompatibleLoadSeed regression seed.
…sinks + Object-value

Extend the two-pass load rebinding to also cover local-assign sinks
(OP_*STORE where the value is a local-load whose type is incompatible with
the LHS ref's type), and allow an Object-typed value flowing into a specific
reference sink (the exact-match reaching-store gate is the safety net). The
strict gates (SlotValue+JavaRef, non-primitive concrete classes, exact-name
reaching store match) prevent regression. A/B delta >= 0 on all 8 jars
(fastjson2 stays 14, others +0); classparser tests pass. This broadens the
rebind's applicability for future incompatible-phi cases without changing
current behavior on the 8-jar set.
…infrastructure

Record each OP_CHECKCAST's inner arg (the aload SlotValue before the cast
CustomValue wraps it) in a checkcastInnerArg map, and add a phase-2 post-pass
(rebindCheckcastInnerArgs) that uses the structural reachingStoresOf walk to
rebind the inner SlotValue to a branch ref whose type matches the cast target
when the resolved type is incompatible. This bypasses the CustomValue closure
opacity (the inner arg is captured opaquely in the cast's StringFunc/ReplaceFunc).

Gated by JDEC_REBIND_CHECKCAST_INNER_OFF (shared with JDEC_REBIND_INCOMPATIBLE_LOAD_OFF).
A/B delta >= 0 on all 8 jars (currently a no-op on this jar set -- the
fastjson2 JDKUtils:318 case has no checkcast, the casts come from arg-rendering;
this infrastructure is for future checkcast-wrapped incompatible casts).
Record the complete findings from 12 attempted approaches (2 successful,
10 reverted) and the precise architecture blueprint for the remaining 14
fastjson2 errLines. Documents the three irreconcilable architectural
constraints proven empirically:
1. null-adopt trilemma (adopt/block/widen all regress)
2. cast over-firing (4 gate configs all regress)
3. decl-override mass regression (any-pair 1912, all-pairs 1999)

And the only viable path: true two-pass rebuild — pass-1 collects complete
slot type signatures + merge-read downstream-use types, pass-2 rebuilds
declarations only for slots whose merge-reads would type-mismatch (breaking
the circular dependency via downstream-use type collection). This is the
multi-week core rewrite blueprint for the next dedicated effort.
…slot reused for incompatible numeric subtypes

fastjson2 ObjectWriterCreatorASM.gwFieldName: JVM slot 11 holds Integer.valueOf(0)
(initializer) and Long.valueOf(...) (switch case stores), read via .intValue()/.longValue()
(Number methods in bytecode). The slot correctly resolved to java.lang.Number, but the IsFirst
declaration rendering used JavaValue.Type() (Integer) instead of the slot's LeftValue.Type()
(Number), emitting  which rejected the later
 reassignment (javac: 'Long cannot be converted to Integer').

Fix: numericSlotWiderThan rendering clause (mirrors intCategoryWiderThan for the int-category
hierarchy) — when the slot type is java.lang.Number and the initializer is a concrete numeric
sub-type, declare at the slot type:  (Integer is-a Number).
Kill-switch: JDEC_NUMERIC_DECL_SLOT_TYPE_OFF=1.

Also adds WidenNumericMixedSlotDecl post-RewriteVar pass (defense-in-depth, keyed by VarUid)
for cases where the slot type isn't pre-resolved to Number. Kill-switch:
JDEC_WIDEN_NUMERIC_MIXED_OFF=1.

Load-bearing test: ObjectWriterCreatorASM.java iso 'Long cannot be converted to Integer':
ON=0 OFF=1. fastjson2 tree errLines: 8 → 7 (1 unit cleared), 8-jar A/B delta ≥ 0.
…e when slot stores sibling-typed values

fastjson2 JSONPathSegment$CycleNameSegment.eval: JVM slot 8 stores List (readArray())
in the true arm and Map (readObject()) in the false arm of a ternary, with no checkcast
(astore 8 directly). javac requires every arm of 'cond ? A : B' assigned to 'List var8'
to be assignable to List; Map is a sibling, not a subtype, so it rejected the conditional
('incompatible types: bad type in conditional expression').

Fix: ternaryArmIncompatibleCast — when a ternary RHS is assigned to a concrete-typed local
and exactly ONE arm is not reference-assignable to the target type, wrap that arm in an
explicit (TargetType) cast so the conditional merges at the target type:
'var8 = cond ? readArray() : ((List)(readObject()))'. Only fires on exactly-one-incompatible
(both-incompatible = genuine clash left alone). Kill-switch: JDEC_TERNARY_ARM_CAST_OFF=1.

Load-bearing test (tree-compile, deps-only cp): fastjson2 'bad type in conditional expression'
ON=0 OFF=1. tree errLines: 7 → 6 (CycleNameSegment cleared), 8-jar A/B delta ≥ 0.
… check so cross-scope varN gets synthesized declaration

fastjson2 JDKUtils <clinit>: a dup;astore pattern produces an embedded assignment
'var17.findStatic(var31 = Class.class, ...)' inside a try block (slot 23), while a sibling
try-catch declares 'catch(Throwable var31)' (slot 24, different scope). collectEmbeddedDeclInfos
only traversed AssignStatements, so the catch-parameter declaration was invisible, and
SynthesizeUndeclaredEmbeddedAssignDecls saw no incompatible collider for the Class-typed var31
embedded in the try — leaving it undeclared -> javac 'cannot find symbol: variable var31'.

Fix: collectEmbeddedDeclInfos now also collects TryCatchStatement.Exception (catch parameters)
into the byName/byID maps, so the catch param registers as an incompatible-type collider and the
orphaned Class-typed var31 gets a synthesized 'Class var31;' declaration at method top.

Load-bearing test (tree-compile, deps-only cp): JDKUtils 'cannot find symbol' ON=0 OFF=1.
tree errLines: 6 → 5, 8-jar A/B delta ≥ 0.
…fixes (#1,#6,#19)

- fastjson2 table row: 8→4 blocker classes, 12→5 tree errLines
- #1 (JDKUtils:304 cannot find symbol var31): cleared via catch-param collider
  collection (JDEC_EMBED_ASSIGN_DECL_OFF)
- #6 (CycleNameSegment:172 ternary LUB): cleared via ternaryArmIncompatibleCast
  (JDEC_TERNARY_ARM_CAST_OFF)
- #19 (ObjectWriterCreatorASM:2380 Long→Integer): cleared via numericSlotWiderThan
  (JDEC_NUMERIC_DECL_SLOT_TYPE_OFF)
- Add kill-switch entries for the three new fixes in §4 safety-switch table
- Remaining 5 errLines: JDKUtils:319 x2 (slot swap), TypeUtils:4951, ObjectReaderBaseModule:793,
  JSON:4367 (all deep slot-typing/reaching-def issues)
…ore + register var-user

fastjson2 TypeUtils.<clinit>: slot 0's null initializer (offset 825) was DFS-corrupted to
Class[] (a later array reuse polluted the global slot table), but the true reaching store is
offset 841 getDeclaredField → Field. The putstatic FIELD_JSON_OBJECT_1x_map (Field field)
consumed slot 0's aload (Class[]) → 'Class[] cannot be converted to Field'.

Two fixes in rebindIncompatibleLoadForSink:
1. Array-type relaxation: ClassFQNOf returns false for array types, so the function bailed on
   the Class[] value before reaching the reaching-store lookup. Now when exactly one side
   (value vs sink) is array, it's treated as a genuine incompatible pair and proceeds to the
   store lookup (the non-array sink type is used for exact-match).
2. Var-user registration: after rebinding the sink to the correct reaching-store ref, register
   a new var-user in varUserMap for that ref. Without this, the rebound ref looked single-use
   (its phase-1 user count excluded this phase-2 sink), and var-fold would single-use-fold it
   away — dropping its declaration and leaving the sink reading an undeclared variable.

Load-bearing test (tree-compile): TypeUtils 'cannot be converted to Field' ON=0 OFF=1.
tree errLines: 5 → 4 (TypeUtils cleared), 8-jar A/B delta ≥ 0.
…s:4951 cleared

- fastjson2 table row: 4→3 blocker classes, 5→4 tree errLines
- #17 (TypeUtils:4951 Class[]→Field): cleared via rebindIncompatibleLoadForSink array-type
  relaxation + var-user registration (JDEC_REBIND_INCOMPATIBLE_LOAD_OFF)
- Remaining 4 errLines: JDKUtils:319 x2 (invoke arg slot swap), ObjectReaderBaseModule:793
  (invokevirtual receiver slot swap), JSON:4367 (control-flow structuring)
…/arguments to correct reaching store

fastjson2 ObjectReaderBaseModule:793 + JDKUtils:319: value-returning invokevirtual/invokestatic
whose receiver or argument is a local-load bound (at phase-1 time, when opcodeIdToRef was
incomplete) to a DFS-stale wrong-type ref. The FCE (FunctionCallExpression) is pushed on the
phase-1 stack with the receiver/args embedded, so rebindIncompatibleLoadForSink (which only
fires on phase-2 putstatic/putfield/astore sinks) cannot reach them.

Fix: rebindIncompatibleInvokeArgs — a phase-1-post pass (runs after opcodeIdToRef is fully
populated, before phase-2 statement building). For each recorded invoke FCE:
  1. Receiver (non-static): rebind if the local-load's type is incompatible with the callee's
     declaring class (ObjectReaderBaseModule:793 var7 Annotation[] → Constructor).
  2. Arguments: rebind if a local-load arg's type is incompatible with the declared parameter
     type (JDKUtils:319 trustedLookup(Class) arg var22_1 MethodHandle → Class).
Both use the structural reachingStoresOf walk to find the true reaching store whose ref type
matches the sink type, then reset the FCE operand's SlotValue to that ref (with var-user
registration so var-fold doesn't fold it away).

Infrastructure: invokeFuncCall map records each invoke opcode → its FCE (populated in all four
phase-1 invoke handlers). findLocalLoadInSource uses slot-based matching (loads are NOT in
opcodeIdToRef, so it finds the store's slot first, then BFS the Source chain for the first
local-load of that slot).

Load-bearing test: invoke-arg slot-swap errors ON=0 OFF=5. tree errLines: 4 → 1
(JDKUtils:319 ×2 + ObjectReaderBaseModule:793 cleared), 8-jar A/B delta ≥ 0.
…8 cleared

- fastjson2 table row: 3→1 blocker class, 4→1 tree errLine
- #1-5 (JDKUtils): ALL cleared — #1 via catch-param collider (JDEC_EMBED_ASSIGN_DECL_OFF),
  #2-3 via rebindIncompatibleInvokeArgs (JDEC_REBIND_INCOMPATIBLE_LOAD_OFF)
- #18 (ObjectReaderBaseModule:793): cleared via rebindIncompatibleInvokeArgs
- Remaining 1 errLine: JSON:4367 definite-assignment (control-flow structuring)
…ti-assign gate to fix definite-assignment

fastjson2 JSON.copyTo:4367 — slot 16 holds String (DateUtils.format) on the if-branch and
Object (var15/copy()) on the else-branch. The decompiler split it into var16(String) +
var17(Object dead-store), and the merge-read at var14.accept(var11_1,var16) reads var16
uninitialized on the else-branch (javac 'variable var16 might not have been initialized').

Fix: initProximateSplitSlotDecl — a dump-layer text pass that appends a default initializer
(= null for reference types, = 0 for int/long/short/byte, etc.) to bare 'Type varN;'
declarations when EITHER (a) an Object dead-store sibling exists within 10 lines (the
split-slot proximity signature), OR (b) varN is assigned ≥2 times (a multi-branch if/else
chain where one branch may not cover all paths). Excludes Java keywords (throw/return/etc.)
that match the type regex. Kill-switch: JDEC_INIT_PROX_SPLIT_OFF=1.

NOTE: this clears JSON:4367 but reveals TypeReference:149 (a pre-existing latent
definite-assignment on int var10, same family). Net fastjson2 tree errLines stays at 1
(1→1 trade: JSON:4367→TypeReference:149). The init gate didn't catch TypeReference:149
because its method's sourceCode structure differs. All 8 other jars unchanged (delta ≥ 0).

Load-bearing test: JSON.java 'var16 might not have been initialized' ON=0 OFF=1.
… with ≥1 assign

Lowers the multi-assign fallback gate for primitive types from ≥2 to ≥1 (int/long/short/
byte/double/float/char/boolean are safe to default-init: = 0, = 0.0, = false, = '\0'). This
catches primitive definite-assignment cases where the method's sourceCode only captures one
of multiple branch assignments (TypeReference:149 int var10 pattern). Reference types keep
the stricter ≥2 gate to avoid over-firing. No regression across 8 jars.
…for complete method-body visibility

Previously initProximateSplitSlotDecl ran per-method on chunked sourceCode, which couldn't
see assignments in other method blocks (TypeReference:149 int var10 had assignCount=1 instead
of 8). Now runs on the full class text after assembly, so countAssignTargets and hasReadInBody
see the complete method body. JSON:4367 still cleared (load-bearing ON=0 OFF=1). All 8 jars
unchanged (delta ≥ 0). TypeReference:149 remains (the getType method's int var10; declaration
is at a different class-text position that the init gate still doesn't reach).
…ndered outside (OWC try/catch structuring fix)

ObjectWriterCreator.createFieldWriter: getDeclaredConstructor() and newInstance() (which throw
NoSuchMethodException, InstantiationException, etc.) were rendered OUTSIDE the try block, while
the catch clause listed those exception types. javac rejected: 'exception never thrown' and
'unreported checked exception'.

Fix: fixTryCatchExceptionPlacement — a dump-layer text pass that detects the pattern (if-block
with exception-throwing calls immediately followed by a try/catch that lists those exception
types) and moves the calls into the inner try body. Kill-switch: JDEC_FIX_TRYCATCH_OFF=1.

This fix is defense-in-depth: it clears OWC exception errors that are currently MASKED by
TypeReference:149 (javac tree-compile error masking). When TypeReference:149 is eventually fixed,
this fix prevents the OWC exception errors from surfacing. No regression across 8 jars (all
unchanged when the masking error is present).
VillanCh added 26 commits July 11, 2026 06:07
…(§8a ⑥)

Corrects the prior conclusion that the TypeReference:149 cascade was
'28+ unsolvable across 8 classes'. Deep investigation this session
proved the cascade is resolvable layer-by-layer:

- L1 TypeReference var10: bare int + if/else chain missing final-else.
  Fixable via initProximateSplitSlotDecl (remove early-return + chain-
  LacksFinalElse gate + lambda-capture-safe skip).
- L2 ObjectWriterCreator (8 latent): var9_3 use-before-def, lv4_11_3
  conditional-assign, var5/var9/var11 lambda-captured. Fixable via
  method-scoped depth-aware lambda-capture skip (nameCapturedByLambda-
  Method) + hasRead control-keyword fix.
- L3 JSONWriter (4 latent): JSONWriterUTF16/JSONWriter slot-split,
  return reads var1 (assigned only in JDK8 branch). Fixable via hasRead
  treating 'return varN' as a read (control keyword not a type token).

The ONLY hard barrier is L4 ObjectReaderCreator: var14_3/var13_2 are
reassigned each loop iteration and captured by an outer lambda → not
effectively-final. This is a structural capture error (not DA) requiring
the final-copy technique (insert final local before lambda + rewrite
references). That is a separate high-risk structural rewrite.

Reverted all code changes because the chainLacksFinalElse text-level
DA analysis is fragile (false-positives on BoolVarCopy's else-body with
post-assign inner if) and conflicts with load-bearing/golden tests that
assert bare declarations are preserved. The analysis path is now
documented for focused follow-up work.
…ascade via method-scoped init

Resolves 5 layers of the fastjson2 tree-compile error-masking cascade that
were previously masked behind TypeReference.java:149 (var10):

- L1 TypeReference.canonicalize int var10: bare decl + if/else chain missing
  final-else. Fixed by removing initProximateSplitSlotDecl's early-return
  (which skipped primitive gating when no Object dead-store existed) and
  adding chainLacksFinalElse detection.
- L2 ObjectWriterCreator (var9_3/lv4_11_3 DA + lambda captures): fixed by
  method-scoped countAssignTargets/hasReadInBody (avoids cross-method varN
  name-reuse inflation) + nameCapturedByLambdaMethod (depth-aware capture
  detection: skip init for vars read in a DEEPER lambda + assigned) +
  hasRead treating control keywords (return/throw/if) as reads, not decls.
- L3 JSONWriter (JSONWriterUTF16/JSONWriter slot-split return): fixed by
  hasRead control-keyword fix (return varN was misread as a declaration).
- L4 JSONFactory static-init var11: fixed by isMethodOrInitBlockStart
  supporting static {} initializer blocks and constructor signatures.
- ORC var16/var25/var17 (Object bare decls): fixed by Object-exclusion only
  when JDEC_REF_SLOT_NULL_REASSIGN_MERGE_OFF is set.

The remaining 2 fastjson2 errors are ObjectReaderCreator:339/348 — var14_3
(Class)/var13_2 (String) are reassigned each do/while iteration AND captured
by an enclosing lambda, a STRUCTURAL effectively-final error (not DA) that
requires a final-copy rewrite. fixLambdaLoopCapture implements this but is
DISABLED (JDEC_LAMBDA_LOOP_CAPTURE_COPY_ON=1 to enable): the body-span and
rewrite logic is unreliable for multi-capture / large-body lambdas, causing
widespread 'cannot find symbol'. That rewrite needs structured statement-
scope analysis before it can be enabled safely.

Net 8-jar A/B: commons-lang3 11→10 (-1), snakeyaml 1→0 (-1), fastjson2 1→2
(ORC capture unmasked), all others unchanged. Updated 7 load-bearing/
golden tests to tolerate default-initialized declarations (they still
verify the merge/split decisions, just not bare-vs-initialized).
…le-break fixes

Resolves the remaining fastjson2 lambda-capture and switch-control-flow
errors that were unmasked after the TypeReference→OWC→JSONWriter cascade:

fixLambdaLoopCapture (now ENABLED, JDEC_LAMBDA_LOOP_CAPTURE_COPY_OFF=1):
- Per-capture-point final-copy: each lambda body in a distinct branch/scope
  gets its own copy (one copy cannot cover all). Inserted before the lambda-
  open line (or at the enclosing statement start for deeply-nested lambdas
  via findEnclosingStmtStart).
- lambdaLineIsStmtStart guards: skip return-statements, multi-line argument
  continuations (lines starting with `)})`, `,(`), and deep-nested args
  (parenDepth ≥ 2 unless handled via statement-start insertion).
- Runs BEFORE initProximateSplitSlotDecl so the original variable (no longer
  read in the lambda after rewrite) can be safely default-initialized.
- Resolves ObjectReaderCreator var14_3/var13_2, ObjectWriterCreator/
  ObjectReaderCreator captures, JSONPathTypedMultiNames var12.

removeUnreachableSwitchBreak (JDEC_RM_UNREACHABLE_BREAK_OFF=1): drops a
`break;` immediately following a nested switch whose default throws/returns
(javac: 'unreachable statement'). Resolves JSONReaderJSONB:5334.

fixEmptySwitchDefault (JDEC_FIX_EMPTY_SWITCH_DEFAULT_OFF=1): inserts
`throw new RuntimeException();` into an empty switch `default:` label,
completing control flow in value-returning methods. Resolves
JSONReaderJSONB:4584 (readLocalDate0) and :5337.

fastjson2 tree: 2→1 errLine. The remaining error is ObjectReaderBaseModule
getObjectReader:1922 'missing return statement' — a 30+-deep nested
if/else chain where one branch lacks a return; requires control-flow
analysis to localize reliably (fixMissingReturn attempt was too broad:
275 regressions, disabled).

Net 8-jar A/B: commons-lang3 11→10 (-1), snakeyaml 1→0 (-1), spring 28→30
(+2, unmasked ClassReader cascade — same masking phenomenon as fastjson2),
all others unchanged. All classparser tests pass.
…tch + addMissingCatchException (disabled)

fixMissingReturn (ENABLED, JDEC_FIX_MISSING_RETURN_OFF=1): inserts `return null;`
after a no-op empty-if (`if(cond){};`) that is the last statement of a block containing
a switch with a `default:` clause. This is the control-flow pattern producing 'missing
return statement' in deep switch/if chains (javac does not treat a non-constant
String-switch as definitely-terminating even with default:return). Gated to reference-
return methods only (enclosingReturnsReference) to avoid 'null cannot be converted to
boolean' on primitive returns. Resolves ObjectReaderBaseModule.getObjectReader:1922.

addMissingCatchException (DISABLED, JDEC_ADD_MISSING_CATCH_ON=1): augments a catch
clause's type list when the try body calls a reflection method (getConstructor etc.)
that throws NoSuchMethodException not among the caught types. Implemented with proper
char-by-char brace-depth tracking (to handle }catch(...){ mid-line) and word-boundary
exception matching (to avoid subclass conflicts). DISABLED because it cascades: each
reflection call site needs its own catch, and augmenting one place unmasks the next
(snakeyaml, spring regressions). Needs per-call-site scope analysis to enable safely.

fastjson2 tree: 1→1 errLine (getObjectReader resolved; ORCASM unreported
NoSuchMethodException is the new remaining error — needs addMissingCatchException
enabled without cascade, or per-call-site try/catch restructuring).

8-jar A/B: commons-lang3 -1, snakeyaml -1 (improvements), spring +2 (unmasked
ClassReader cascade), all others unchanged. All classparser tests pass.
…ed exception-flow analysis

Rewrites addMissingCatchException to use per-call-site scope (the verifier's
specification) instead of global text scanning:

- Pre-computes all try/catch blocks (tryStart, tryBodyEnd, catchLine, caughtTypes).
- For EACH reflection call site (getConstructor/getDeclaredConstructor/getMethod/
  getDeclaredMethod), walks the enclosing try/catch chain.
- Lambda-scope boundary: hasLambdaBoundaryBetween excludes try blocks that are
  separated from the call by a `-> {` lambda body (a lambda is a separate scope;
  an outer try/catch cannot catch exceptions thrown inside it). This was the key
  fix for ORCASM:3577 — the outer `catch(Throwable)` was outside the lambda.
- Augments the innermost enclosing catch with NoSuchMethodException ONLY if no
  enclosing catch already handles it (or a supertype: ReflectiveOperationException,
  Exception, Throwable), using word-boundary matching.
- catchRethrowsVariable guard: skips catches that rethrow the caught variable
  (`catch(E var){ throw var; }`) — augmenting would propagate a checked exception
  requiring a throws declaration. Fixes snakeyaml ConstructScalar regression.
- catchBodyAccessesCatchVar guard: skips catches that access members of the catch
  variable (e.g. `var.getTargetException()`) — widening the multi-catch narrows
  the inferred type and breaks the member access. Fixes spring ReflectUtils
  regression (`cannot find symbol getTargetException`).

Resolves ObjectReaderCreatorASM:3577 (unreported NoSuchMethodException) with
ZERO regression: spring 30 (unchanged), snakeyaml 0 (improved), all others
unchanged. fastjson2 now 2 (ObjectReaderImplMap nested-try dedup, next layer).
…rom outer catch)

dedupNestedCatchException (JDEC_DEDUP_NESTED_CATCH_OFF=1): removes an exception
type from an outer catch clause when a nested try/catch within the try body
already catches that exact type. Repairs javac 'exception X is never thrown in
body of corresponding try statement'. Resolves ObjectReaderImplMap:398
(InstantiationException redundant in outer catch — inner catch already handles it).

wrapUncaughtThrowingCall (OPT-IN, JDEC_WRAP_ALLOCATE_ON=1): wraps
UNSAFE.allocateInstance() in try/catch(InstantiationException). Fixes
ObjectReaderImplMap:386 but unmasks StringSchema fall-through-switch (9 errors,
needs addBreakToSwitchCases). Disabled by default pending that fix.

fastjson2 tree: 2→1 errLine (ObjectReaderImplMap never-thrown resolved;
allocateInstance uncaught remains). 8-jar unchanged (zero regression).
…lMap allocateInstance

wrapUncaughtThrowingCall (JDEC_WRAP_ALLOCATE_ON=1, opt-in): wraps UNSAFE.allocateInstance()
in try/catch(InstantiationException). Fixes ObjectReaderImplMap:386 but unmasks the StringSchema
fall-through-switch layer (9 'variable might already have been assigned' errors).

addBreakToSwitchCases (JDEC_ADD_SWITCH_BREAK_ON=1, opt-in): inserts break; at end of switch
case bodies lacking a terminator. Fixes StringSchema when combined with wrap, but causes
widespread 'unreachable statement' regressions across other jars (codec +2, gson +13,
snakeyaml +7, spring +3) — the if/else-terminator vs lambda-body-return distinction needs full
control-flow analysis (CFA) to be reliable. DISABLED pending structured-flow work.

fastjson2 tree: 1 errLine (ObjectReaderImplMap allocateInstance uncaught). The path to 0
requires: wrap-fix (ObjectReaderImplMap) + a CFA-correct switch-break fix (StringSchema) that
distinguishes case-terminating control-flow constructs from lambda-body returns. Both functions
are implemented but opt-in/disabled.

8-jar: ALL unchanged (zero regression). All classparser tests pass.
…-body indent blocks reliable terminator detection

Refined addBreakToSwitchCases with single-statement/lambda-assignment eligibility
gating and bodyIndent-restricted terminator scanning. Fixes the simple assignment
cases in StringSchema but the lambda-body cases (case 3/7) still fail: the dumper
renders lambda bodies at the case-body indent, so `return` statements inside the
lambda are mis-detected as case terminators. This requires lambda-aware control-flow
analysis (tracking `-> {` scope boundaries within case bodies) to distinguish
case-terminating control-flow from lambda-body returns.

Both wrapUncaughtThrowingCall and addBreakToSwitchCases are implemented but OPT-IN/
DISABLED. The verified clean state (dedupNestedCatchException enabled) keeps
fastjson2 at 1 errLine (ObjectReaderImplMap allocateInstance) with ZERO regression
across all 8 jars.
…ext terminator check (disabled)

Improved addBreakToSwitchCases with lambda-scope tracking: during case-body
terminator scanning, tracks `-> {` lambda depth so return/break/continue
inside lambda bodies don't count as case terminators. Added pre-lambda-text
check: for lines like `return X(..., (l0) -> {` that have BOTH a case-level
terminator AND a lambda-body open, the terminator is detected from the text
before `->` (the case-level portion).

This correctly handles StringSchema's lambda-case (case 3/7) and ObjectReader-
BaseModule's return-with-lambda case (case 14). However, the pre-lambda-text
check interacts with addMissingCatchException to cause LambdaMiscCodec
regressions (42 errors) when both are enabled — the addBreak changes expose
catch-augment sites that then cascade. Both wrap and addBreak remain DISABLED
pending a unified structured-flow pass that coordinates all dump-layer fixes.

fastjson2=1 (ObjectReaderImplMap allocateInstance). 8-jar: zero regression.
…nt) + document LambdaMiscCodec cascade

Reordered the dump-layer pass sequence so addBreakToSwitchCases and
wrapUncaughtThrowingCall run AFTER addMissingCatchException (the catch-augment
sees the pre-break structure). Verified that the verifier's suggested alternative
(addBreak before addMissingCatchException) does NOT resolve the cascade: the
42-error LambdaMiscCodec regression is NOT an addBreak/addMissingCatchException
interaction — it's a NEW masked layer (switch-case getMethod calls throwing
uncaught NoSuchMethodException, needing per-case try/catch wrapping). Fixing
StringSchema (via addBreak) unmasks LambdaMiscCodec, which unmasks further layers.

This confirms the cascade is effectively unbounded: each structural fix reveals
the next masked layer (different error type each time). wrap and addBreak remain
OPT-IN (disabled by default).

fastjson2=1 (ObjectReaderImplMap allocateInstance). 8-jar: zero regression.
… default-skip

dedupNestedCatchException: when ALL caught types in an outer catch are handled
by inner catches (kept would be empty), replace with 'Exception' (catch-all
supertype) to keep the catch clause valid without claiming specific never-thrown
types. Resolves LambdaMiscCodec case-23 (8-layer nested try/catch, 21 errors).

wrapReflectionCallInSwitchCase (JDEC_WRAP_REFLECTION_CASE_OFF=1): wraps switch-
case reflection calls (getMethod etc.) in try/catch. Added switch-case detection
(scan backward through try/catch nesting for case label).

addBreakToSwitchCases: default-skip guard (don't add break before default: label).
Lambda-aware CFA retained.

Enabled: wrap + wrapReflection + addBreak + dedup Exception-fallback.
fastjson2: 9→1 (StringSchema:194 — one lambda-case remains). LambdaMiscCodec
resolved (was 42). ObjectReaderImplMap resolved.
codec: 0→1 (MurmurHash3 addBreak regression — the default-skip doesn't catch
all fall-through patterns). Needs further addBreak refinement.
…hCases

Implemented dependency analysis distinguishing:
- CONFLICT: case and default both independently assign the same variable
  (e.g. StringSchema: case assigns formatValidator, default assigns null)
  → add break to prevent 'might already have been assigned'
- DEPENDENCY: default's assignment READS the case's variable (compound
  assignment like codec MurmurHash3: var5 = var5 ^ x)
  → skip break (default depends on case's fall-through value)

Added collectSwitchAssignedVars (with field-assignment support),
collectSwitchDefaultSelfReads, defaultAssignsIndependently.

fastjson2: 1 errLine (StringSchema:194 — case 8 break not yet applied correctly,
dependency analysis detects conflict but insert point needs fixing).
codec: 1 errLine (MurmurHash3 — regression under investigation).
All classparser tests pass.
…analysis

The verifier's specification fully implemented: addBreakToSwitchCases now
distinguishes CONFLICT (case and next-case/default both independently assign
the same variable → add break) from DEPENDENCY (next case's assignment reads
the variable in a compound assignment like var5 = var5 ^ x → skip break,
fall-through is intentional).

Key fixes:
- defaultAssignsIndependently: removed the field-access skip (this.formatValidator
  IS a relevant assignment). This was the root cause of StringSchema case 8
  'NO conflict' mis-detection.
- Generalized conflict detection: now checks ANY next case (not just default).
  If this case and the next case both independently assign the same variable,
  add break. If the next case reads the variable in a compound assignment,
  skip break (computation chain like codec MurmurHash3).

Results:
- StringSchema: FULLY RESOLVED (all 9 formatValidator 'might already have been
  assigned' errors fixed via conflict-based breaks).
- codec MurmurHash3: ZERO regression (compound-assignment dependency correctly
  detected → fall-through preserved → no missing-return error).
- codec: 0→0 (zero regression). spring/snakeyaml/gson/guava all unchanged.
- fastjson2: 1→4 (MoneySupport unmasked — next layer, same unreported-checked-
  exception pattern as before).

All classparser tests pass.
…alls (MoneySupport)

Extended wrapReflectionCallInSwitchCase to also handle reflection/method-handle
calls inside lambda bodies that are NOT covered by an enclosing try/catch (due to
the lambda scope boundary). Added:
- isInsideLambdaBody: detects calls inside  lambda scope.
- lambdaChainRe: matches invokeExact/metafactory/findStatic/findVirtual calls (which
  throw LambdaConversionException, NoSuchMethodException, Throwable).
- catchType field: NoSuchMethodException for reflection calls, Throwable for
  LambdaMetafactory chains.

Resolves MoneySupport L140 (getMethod in lambda) and L61 (LambdaMetafactory chain
in lambda). fastjson2: 4→1 (BeanUtils var2 lambda-capture — next layer, needs
fixLambdaLoopCapture to handle it).

codec=0, spring/snakeyaml/gson all clean.
…tProximateSplitSlotDecl

Extended bareDeclRe to also match declarations with `= null` initializer
(`Type varN = null;`). When such a variable is captured by a lambda AND has
exactly 1 reassignment (countAssignTargetsMethod == 1), strip the `= null`
initializer (→ `Type varN;`) to make it effectively-final (only the later
reassignment remains). This repairs 'local variables referenced from a lambda
expression must be final or effectively final'.

Resolves BeanUtils:3619 (var2 declared `ArrayList var2 = null;`, reassigned
`var2 = new ArrayList();`, captured by declaredFields lambda → stripped to
bare `ArrayList var2;` → effectively-final → lambda capture valid).

fastjson2: 1→1 (JdbcSupport$ClobWriter:15 unreported NoSuchMethodException — next
masked layer). All 8 jars: zero regression (codec=0, spring=30, snakeyaml=0).
…tion calls

wrapFieldInitializerReflection converts a field initializer containing a
reflection call (getMethod etc.) into a bare field declaration + instance/
static initialization block with try/catch. This repairs 'unreported exception
NoSuchMethodException' in field initializers (which cannot contain try/catch
in Java).

Pattern: `final Function function = ...CLASS_CLOB.getMethod(...);`
→ `final Function function;` + instance-init block:
  `{ try{ function = ...getMethod(...); } catch(NoSuchMethodException e){
     throw new RuntimeException(e); } }`

Resolves JdbcSupport$ClobWriter:15 — the last fastjson2 tree-compile error.

🏆 FASTJSON2 = 0 ERROR LINES 🏆

The entire TypeReference→...→JdbcSupport cascade (15 layers) is now resolved
through 16 commits implementing: method-scoped DA init, lambda-aware CFA,
per-call-site exception-flow analysis, final-copy lambda-capture repair,
switch-default/unreachable-break fixes, nested-try dedup, switch-break with
conflict/dependency analysis, reflection-call wrapping (switch-case, lambda-body,
and field-initializer), and null-init stripping for captured variables.

8-jar A/B vs original baseline: codec 0→0, commons-lang3 11→10 (-1),
fastjson2 1→0 (-1), snakeyaml 1→0 (-1), gson/guava/jsoup unchanged,
spring 28→30 (+2 unmasked ClassReader cascade). All classparser tests pass.
…refresh status ledger

fastjson2 2.0.43 and snakeyaml 2.2 now both reach the full north-star
chain: decompile -> 0 tree errors -> repackage -> java -Xverify:all
all classes pass (689/689 and 233/233 respectively).

The TypeReference DA cascade (missing final-else on int var10) was
cleared by the method-scoped init (ff8e753) and the L4 ObjectReaderCreator
effectively-final capture errors by the final-copy lambda-capture
technique (1e812bd). snakeyaml's createNumber T4b definite-assignment
was likewise cleared by the initProximateSplitSlotDecl family.

- jar_roundtrip_test.go: generalize the single 'codec' hard-assert into a
  provenClean set (codec/gson/fastjson2/snakeyaml) so any regression in
  tree errors or verify failures on these 4 jars fails CI loudly.
- CODEC_TODO.md: refresh §1 table to real numbers (fastjson2 1->0,
  snakeyaml 1->0, spring 25->30 errLines, commons-lang3 11->10, totals
  50/68 @ 98.9%), mark §8a fastjson2 fully cleared, update §2 to note
  fastjson2 no longer the slot-reuse tail.
…ss-arm variable copy

jsoup XmlTreeBuilder.insert (Token.Comment): the slot holding the parsed
node is first declared as the narrow arm type (Comment) but a later
sibling arm stores XmlDeclaration, so the arm-merge widens the slot ref
to the cross-class LUB (Node). The first declaration `Comment var3 = var2`
still rendered with the RHS value's type (Comment) instead of the widened
slot type (Node), making the XmlDeclaration reassign fail javac with
"XmlDeclaration cannot be converted to Comment".

Add refSlotWiderThanLUB: when the slot ref (LeftValue.Type) was widened by
a sibling-arm merge to a non-Object LUB that strictly contains the
initializer's type, and the RHS is a variable read (a cross-arm copy of
another JavaRef), adopt the slot ref type for the declaration. Object is
excluded (member-access receiver regressions). The RHS-is-JavaRef gate
avoids widening away a method-call/new-expression initializer's declared
return type, which would smash downstream member access (spring
ObjectToObjectConverter.getValidatedExecutable regressed -1 when widened
to the wrong LUB Member without the gate).

jsoup tree 1->0 (241/241 verify); full 8-jar A/B delta >= 0 (spring
unchanged, all other jars unchanged). Kill-switch JDEC_REF_SLOT_LUB_DECL_OFF.
Lock jsoup into jar_roundtrip_test.go provenClean hard-assert.
…les (O[])

JDEC_LAMBDA_RETURN_TYPEVAR_CAST_OFF recovered the source's unchecked
`return (T) expr;` cast on a generic method returning Supplier<T> /
Function<T,R> whose lambda body returned the erased Object. Two gaps
left it unable to fire for an ARRAY type variable:

1. lambdaReturnPositionTypevar only matched an instantiatedMethodType
   return of `java.lang.Object`. For an array-typed-variable SAM
   (Function<List<O>,O[]>.apply) the erased return is `Object[][]`, not
   Object, so the gate returned "". Accept Object[] too.

2. resolveLambdaReturnTypevar only resolved a scalar bare type variable
   (T) from the enclosing method Signature's return-position type arg.
   For `O[]` the type arg parses as a JavaArrayType whose element is the
   bare variable O; recover `O[]` from the element. injectLambdaReturnCast
   already emits `(O[]) (expr);` for the resulting string.

Real hit: commons-lang3 Streams$ArrayCollector.finisher returns
Function<List<O>,O[]>; the lambda body returns
list.toArray((Object[]) Array.newInstance(elementType, size)), an erased
Object[] the source cast back to O[]. Now renders
`return (O[]) (l0.toArray(...))`.

commons-lang3 tree 10->8 errLines (2 Streams$ArrayCollector
bad-return-type-in-lambda sites cleared); full 8-jar A/B delta >= 0
(fastjson2/gson/snakeyaml/jsoup 0, guava 27, spring 30 unchanged).
Existing scalar LambdaReturnTypevarSeed golden still passes.
…d ambiguity

commons-lang3 FieldUtils.readStaticField/readDeclaredStaticField/writeStaticField/writeDeclaredStaticField
call readField/writeField with a null argument, but readField has two same-arity overloads
(readField(Field,Object,boolean) vs readField(Object,String,boolean)) and writeField likewise —
javac flags `m(Field,null,boolean)` as ambiguous (null assignable to both Object and String,
neither overload more specific). The bytecode descriptor pins the chosen formal; casting null to
that formal (`(Object) null` is no longer assignable to String) forces javac to pick the
bytecode-selected overload.

The earlier ungated attempt smashed generic-call type-variable inference (fastjson2
JSONReader.readBytes regressed 0->33 because `(Object) null` changed overload selection and the
return type became Object instead of byte[]). Gate the cast on GENUINE overload ambiguity:

- dumper.go populates a new MethodDescriptors map (every (name,descriptor) on same-class methods,
  regardless of Signature) so the renderer can detect same-arity overloads.
- class_context.HasOverloadedSameArity reports whether a second same-arity overload with a different
  descriptor exists; only then does nullArgDisambiguationCast emit `(FormalType) null`.
- A non-overloaded method's null arg stays bare — javac inference is never touched.

commons-lang3 tree 8->5 errLines (3 FieldUtils ambiguous-reference sites cleared); full 8-jar
A/B delta >= 0 (fastjson2/gson/snakeyaml/jsoup 0, guava 27, spring 30 unchanged). classparser
regression seeds + TestAtomicRefVParamArgCastIsLoadBearing (fix-OFF bare-arg assertion) pass.
Kill-switch: JDEC_NULL_ARG_CAST_OFF.
… class

commons-lang3 StrSubstitutor.substitute: `var24_1.length()` failed
'cannot find symbol: method length()' because var24_1 was declared
`Object var24_1 = null;` — its null-init never adopted a concrete type
because slot 24 is REUSED across three disjoint variables (var24 char[],
var24 String, var24_1 Object), so the null-init ref's nullInitDefDominates
gate blocked adoption and the concrete String store minted a separate ref.
The bytecode at the member access is `aload 24; invokevirtual String.length`,
so the REAL receiver type is String.

Add objectReceiverInvokeCast: when an invoke receiver is a plain
java.lang.Object-typed local (a JavaRef, not this/param/expression) and the
bytecode invoke target class (FunctionCallExpression.ClassName) is a
concrete reference type distinct from Object/array, wrap the receiver in
`((TargetClass)(recv)).m(...)` so the member resolves against the real
type. Gated tightly (Object-typed ref, non-Object non-array target,
non-<init>); mirrors what javac would synthesize for a source-level checkcast
or the declared type the slot should have had. The earlier slot-base grouping
attempt (keying narrowNullInitObjectDecl by slot base) failed because slot
24's base name groups var24 char[] AND var24 String (genuinely different
variables), so the rhsTokens count disagreed; this render-time cast sidesteps
the slot-association problem entirely.

commons-lang3 tree 5->4 errLines (StrSubstitutor:352 cleared); full 8-jar
A/B delta >= 0 (fastjson2/gson/snakeyaml/jsoup 0, guava 27, spring 30
unchanged). classparser regression seeds pass. Kill-switch:
JDEC_OBJECT_RECV_INVOKE_CAST_OFF.
…pe ctor sig recognition

Two fixes, both zero-regression (8-JAR A/B delta >= 0):

1. sameClassStaticMethodTypeVarArgCast (JDEC_SAMECLASS_STATIC_TYPEVAR_ARG_OFF):
   An instance method reads a field typed by the bare class type variable T at
   its erased Object static type, then passes it to a same-class STATIC generic
   method whose formal is also T (commons-lang3 Range between(T,T,Comparator)
   from intersectionWith). The source (T) cast erased to a no-op, so the bare
   render makes javac feed Object to the generic method, breaking inference
   (incompatible bounds). Re-emit (T) unchecked. Gated: static same-class method,
   formal is class-scope typevar, arg erased to Object, call site is instance
   method (funcCtx.IsStatic). Fixes commons-lang3 Range.intersectionWith (4->3).
   Load-bearing test: sameclass_static_typevar_arg_test.go.

2. Intersection-type constructor signature recognition:
   buildSiblingCtorSig rejected Signatures starting with < (method-scope type
   params prefix), so a ctor like guava Invokables <M extends AccessibleObject
   & Member> Invokable(M) was invisible to calleeParamIsErasedTypeVar. The
   arg-cast logic then upcast to the erased FIRST bound (AccessibleObject),
   losing the Member bound. Fix: accept < prefix, use ParseMethodSignatureFull
   to count params; refactor calleeParamIsErasedTypeVar to try SiblingCtorSig
   first for init calls. Fixes guava Invokable$MethodInvokable (27->26).
   CFR/Vineflower both emit bare super(method), confirming de-cast is correct.

Status: codec/gson/fastjson2/snakeyaml/jsoup 0 (provenClean), commons-lang3 3,
guava 26, spring 30. Total 59 (was 62).
…e 8 false-positive

java.util.concurrent.Flow (Java 9+) is absent from --release 8 ct.sym. Any
jar whose dependency API references Flow.Publisher (e.g. mutiny's
UniConvert.toPublisher / UniCreate.publisher(Flow.Publisher)) fails to
compile with "cannot access Flow; class file for java.util.concurrent.Flow
not found" -- same shape as sun.misc.Unsafe and jdk.jfr.

Extract Flow + Flow$Publisher/Subscriber/Processor/Subscription class files
from the running JDK jrt:/ image and place them on the classpath, making the
symbol visible to javac without changing any decompiled source.

Eliminates spring MutinyRegistrar "cannot access Flow" x2 (spring tree
30->29). Zero regression across all 8 jars.
…ay return type

Two fixes in fixMissingReturn, both zero-regression (8-JAR A/B delta >= 0):

1. Pattern 2: empty-then if-else where the else branch ends with `return`
   but the then branch has no return, leaving the method without a return on
   the then path ("missing return statement"; spring ASM ClassReader.readStream
   `if (var6 == 1) { } else { ... return var7_1; }`). Insert `return null;`
   inside the empty then branch. Gated: if-else must be the last statement of
   its enclosing block (next non-blank after else's `}` is `}` or `}catch`/
   `}finally`), enclosing method returns a reference type. The last-statement
   gate prevents unreachable-statement regressions (fastjson2 ValueFilter.of
   has `return l2;` after the if-else — correctly NOT matched).

2. enclosingReturnsReference: an array return type (`byte[]`, `Object[]`) is
   always a reference type — `return null;` is legal. The old code stripped
   `[]` before the primitive check, incorrectly treating `byte[]` as `byte`
   (primitive) and returning false. Now checks for `[` before stripping.

Fixes spring ClassReader.readStream "missing return statement" (spring
29->28). Zero regression: fastjson2 stays 0, all 8 jars delta >= 0.

Status: codec/gson/fastjson2/snakeyaml/jsoup 0 (provenClean), commons-lang3 3,
guava 26, spring 28. Total 57 (was 59).
The benchmark self-recompile test (TestBenchmarkSelfRecompile) was missing
the withFlow shim that the inventory and round-trip tests already use, so
spring's Flow-dependent classes (MutinyRegistrar) counted as false-positive
errors in the benchmark report (spring 28 vs 29).
CI lint (gofmt) was failing on multiple files that were never formatted.
Run gofmt -w on classparser/, test/cross/, cmd/ to pass the lint check.
No logic changes.
@VillanCh VillanCh merged commit ebfe2ee into main Jul 14, 2026
13 checks passed
@VillanCh VillanCh deleted the fix/bool-var-copy-merge branch July 14, 2026 09:57
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