Skip to content

steins-syntax: a statement lists each read its expressions write, at the read's own span with the construct that carries it (#1050 S1, ADR-0104) - #1062

Merged
zonuexe merged 4 commits into
masterfrom
claude/rp-s1-read-positions
Oct 11, 2026
Merged

zonuexe merged 4 commits into
masterfrom
claude/rp-s1-read-positions

Conversation

@zonuexe

@zonuexe zonuexe commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Slice S1 of the read-position run (#1050). It adds the IR that S2–S4 will judge: each statement lists the call, offset, property and class-constant reads written in its own expressions. Each read sits at its own span and carries what the construct around it means for the read. ADR-0104 records the owner's decision D1: a finding about a read is reported at the read's own span.

What changed

  • steins-syntax:
    • ReadPosition { kind, span, carrier } holds one read. kind is Call (span only), Offset { place, key }, Prop or ClassConst.
    • carrier is one of Plain, Argument { call, position }, Conditional, IssetLike, Suppressed, CompoundRead or FirstClassCallable.
    • Stmt.read_positions is Option<Box<[_]>>, so a statement with no reads costs one word.
  • lower_read.rs (new):
    • One CST scan over the four entry-env statement kinds and static.
    • It stops at function-likes (property hooks included), class-likes and any match. An anonymous class keeps only its constructor arguments.
    • The arrow-body, hook and hoisted-match-arm routes fill the list too.
  • Carriers:
    • When several apply, the weakest claim wins: IssetLike > Conditional > Suppressed > CompoundRead > Argument > Plain.
    • Argument.position is None for a named argument or one at or after a spread.
  • What is not recorded:
    • Write targets.
    • An offset or property read under isset, empty, ?? or ??=. A class constant read there stays as IssetLike.
  • No consumer yet. check output is byte-identical. No SCHEMA_VERSION bump is needed: PackageShard holds no Stmt, and trace payloads are decoded past the gate.

Memory

The first cut cloned a CallExpr per call read and re-lowered nested calls, which cost 25–31% peak RSS. The reworked payload stores spans, a small Place/Key and boxed names instead. size_of::<ReadPosition>() is at most 40 bytes, and a test pins that bound.

Peak RSS, release build, strict, median of 3 runs:

package base head change
phpunit 439.2 MB 450.2 MB +2.5%
Carbon 411.4 MB 411.7 MB +0.1%
PHP-Parser 137.2 MB 140.7 MB +2.6%

Wall time is within noise.

Tests

crates/steins-syntax/tests/it/read_positions.rs has 30 tests. They cover every carrier and every boundary, plus the size bound and the place/key content. The syntax, infer and xtask suites pass, and workspace clippy and rustdoc pass with -D warnings.

Measurement

Ten public packages × strict and default: all 20 outputs are byte-identical.

Not measured

  • The private half of the local fp-gate. The orchestrator runs it before merge.

Review outcome

Adversarial review: three rounds, now approved.

  • Round 1: two blockers, both readings where PHP is silent or never evaluates the read.
    • The base of an array append was read as Plain. It is now a Write and records nothing.
    • Nullsafe short-circuit was not modelled. Everything past a ?-> is now Conditional.
    • The first cut also cost 25–31% peak RSS. The payload was reworked to spans and small keys.
  • Round 2: approved, with one should-fix: a ?-> in a static-call class operand ($n?->s::m(…), ($n?->p)::m(…)). This is fixed in e72776f.
  • First-class callables are their own kind, ReadKind::CallRef, with the context folded into the carrier. S2's "FCC existence" row consumes kind == CallRef.
  • IR gap, not a false positive: the key deprecation in $a[$n?->p]['k14'][] = 1 is not recorded. ADR-0104 notes it.
  • phpunit peak RSS: +2.6% to +3.3% across measurements.

…ss-constant read its expressions write, each at its own span with the construct that carries it (#1050)

Adds ADR-0104 and the trace-IR note. No consumer reads the field, so check output is unchanged.
…fset a place and a literal key, and writes and isset-silent reads are not recorded (#1050)
…nditional, a first-class callable folds its context, and the rule table is in the module doc with a test per row (#1050)
@zonuexe
zonuexe force-pushed the claude/rp-s1-read-positions branch from 5ee567b to a4d269c Compare October 11, 2026 09:45
…its arguments, and the ADR names the CallRef consumer and the key-deprecation gap (#1050)
@zonuexe
zonuexe marked this pull request as ready for review October 11, 2026 10:21
@zonuexe
zonuexe merged commit 5a66ba7 into master Oct 11, 2026
7 checks passed
@zonuexe
zonuexe deleted the claude/rp-s1-read-positions branch October 11, 2026 10:21
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