fix(cfg): harvest Kotlin x++/--x as a def (#2195)

The Kotlin harvester had no postfix_expression/prefix_expression case, so an
increment/decrement fell to the default descent and recorded its operand as a
use only — never a def. Every sibling harvester (Java/C#/C++/Dart/TS/PHP) models
inc/dec, so a Kotlin counting loop (while/for using i++) silently dropped the
loop-carried reaching-def of the counter. Add the case: def AND use the operand
when it is a plain simple_identifier and the operator is ++/-- (other pre/postfix
forms — -x, !x, x!!, x? — stay pure reads, byte-identical to the old descent).
Characterization tests for postfix + prefix added; kotlin suite 30 passed,
grammar gate + bench --check green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Gergo Magyar 2026-06-14 19:27:20 +00:00
parent a805e8fa6f
commit 33446d61c3
2 changed files with 41 additions and 0 deletions

View file

@ -425,6 +425,20 @@ export class KotlinHarvester {
}
return;
}
case 'postfix_expression':
case 'prefix_expression': {
// `x++` / `--x` — def AND use the operand when it is a plain identifier
// and the operator is an increment/decrement. Other pre/postfix forms
// (`-x`, `!x`, `x!!`, `x?`) are pure reads → walk the operand as a use.
const operand = node.namedChild(0);
if (operand?.type === 'simple_identifier' && this.isIncDec(node)) {
this.def(operand, acc);
this.use(operand, acc);
} else if (operand) {
this.walkValue(operand, acc);
}
return;
}
case 'navigation_expression': {
// `a.b` / `a?.b` — value read of the chain root only; the suffix name is
// not a scalar binding.
@ -467,6 +481,15 @@ export class KotlinHarvester {
}
}
/** True iff `node` carries a `++` / `--` operator token (`x++` / `--x`). */
private isIncDec(node: SyntaxNode): boolean {
for (let i = 0; i < node.childCount; i++) {
const c = node.child(i);
if (c && !c.isNamed && (c.text === '++' || c.text === '--')) return true;
}
return false;
}
/** The `= value` expression of a `property_declaration` (the child after `=`). */
private propertyValue(node: SyntaxNode): SyntaxNode | undefined {
let sawEq = false;

View file

@ -316,6 +316,24 @@ describe('Kotlin CfgVisitor — def/use harvest', () => {
expect(definesBinding(cfg, x)).toBe(true);
expect(usesBinding(cfg, x)).toBe(true);
});
const defStmtCount = (cfg: FunctionCfg, idx: number): number =>
cfg.blocks.flatMap((bl) => bl.statements ?? []).filter((s) => s.defs.includes(idx)).length;
it('postfix `x++` defines AND uses the operand (#2195 P2)', () => {
const cfg = kotlin.cfgOf(`fun f() { var x = 0; x++ }`);
const x = bindingIdx(cfg, 'x');
// `var x = 0` defs x once; `x++` must def it AGAIN (the loop-counter
// reaching-def the bug dropped) — not record x as a use-only.
expect(defStmtCount(cfg, x)).toBe(2);
expect(usesBinding(cfg, x)).toBe(true);
});
it('prefix `--x` defines the operand too (#2195 P2)', () => {
const cfg = kotlin.cfgOf(`fun f() { var x = 0; --x }`);
const x = bindingIdx(cfg, 'x');
expect(defStmtCount(cfg, x)).toBe(2);
});
});
describe('Kotlin CfgVisitor — functionStartColumn', () => {