Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,34 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.4.3] - 2026-05-06

Performance — interactive editing focus. 19% faster median, 26% faster p95 on the IncrementalBenchmark editing-session suite. **One breaking change**: SourceSpan record components changed from two SourceLocations to six ints (see migration note).

### Performance (interactive editing on 1,900-line Java fixture)

| Metric | 0.4.2 | 0.4.3 | Change |
|---|---:|---:|---:|
| Median | 13.3 ms | 10.8 ms | -19% |
| p95 | 30.1 ms | 22.4 ms | -26% |
| p99 | 56.4 ms | 53.3 ms | -5% |
| % under 16ms (frame budget) | 83% | 91.5% | +8.5pp |

### Changed (BREAKING)

- **`SourceSpan` stores int triples instead of `SourceLocation` refs.** Component accessors changed from `start()`/`end()` (returning SourceLocation refs) to `startLine()`/`startColumn()`/`startOffset()`/`endLine()`/`endColumn()`/`endOffset()` (auto-generated record component accessors returning ints). The methods `start()` and `end()` are preserved as regular methods that lazily materialize SourceLocation. The factory `SourceSpan.sourceSpan(SourceLocation, SourceLocation)` is preserved. **Migration:** code that pattern-matches `case SourceSpan(SourceLocation s, SourceLocation e)` must be updated to either `case SourceSpan span` + accessor calls, or `case SourceSpan(int sl, int sc, int so, int el, int ec, int eo)`. Code calling `.span().start().line()` should migrate to `.span().startLine()` to skip the SourceLocation materialization. Drove the 26% p95 improvement.

### Performance — internals

- **`PegEngine` interpreter:** no API change. Continued from 0.4.1.
- **`NodeIndex.build` pre-sizes the IdentityHashMap** from a descendant-count pass before populating, eliminating resize churn that was 22.8% of bench allocations. (Commit `fcff78f`)
- **New `IncrementalSessionBench`** in `peglib-incremental/src/jmh/java`: realistic 1,000-edit interactive sessions with cursor-moved-to-edit (Regime B) AND cursor-pinned (Regime A) regimes. Reports per-class median/p95/p99/max + frame-budget hit rate + top-10 outlier diagnostics. Recommended JVM tuning for downstream consumers in `peglib-incremental/README.md`: `-Xms2g -Xmx2g -XX:MaxGCPauseMillis=20` reduces p99 by 33% (60ms → 40ms) — free for downstream. (Commits `716a113`, `77c7c70`)

### Notes for downstream

- **Regenerate generated parsers** with 0.4.3's `ParserGenerator` to pick up the gen-time SourceSpan emission update (also int-based).
- **Architectural rework planned for 0.5.0.** See `docs/incremental/ARCHITECTURE-0.5.0.md` (when published) for the planned overhaul targeting incremental-native data structures.

## [0.4.2] - 2026-05-04

Standalone-parser fix. No API changes for the interpreter / incremental / formatter paths.
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ Quick links:
<dependency>
<groupId>org.pragmatica-lite</groupId>
<artifactId>peglib</artifactId>
<version>0.4.2</version>
<version>0.4.3</version>
</dependency>
```

Expand Down Expand Up @@ -394,7 +394,7 @@ The `peglib-maven-plugin` module (separate artifact, sibling to `peglib`) wraps
<plugin>
<groupId>org.pragmatica-lite</groupId>
<artifactId>peglib-maven-plugin</artifactId>
<version>0.4.2</version>
<version>0.4.3</version>
<executions>
<execution>
<goals>
Expand Down
2 changes: 1 addition & 1 deletion peglib-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<parent>
<groupId>org.pragmatica-lite</groupId>
<artifactId>peglib-parent</artifactId>
<version>0.4.2</version>
<version>0.4.3</version>
<relativePath>../pom.xml</relativePath>
</parent>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1660,13 +1660,40 @@ public static SourceLocation sourceLocation(int line, int column, int offset) {
@Override public String toString() { return line + ":" + column; }
}

public record SourceSpan(SourceLocation start, SourceLocation end) {
public record SourceSpan(int startLine, int startColumn, int startOffset,
int endLine, int endColumn, int endOffset) {
public static SourceSpan sourceSpan(SourceLocation start, SourceLocation end) {
return new SourceSpan(start, end);
return new SourceSpan(start.line(), start.column(), start.offset(),
end.line(), end.column(), end.offset());
}
public static SourceSpan sourceSpan(SourceLocation location) {
return sourceSpan(location, location);
}
public SourceLocation start() {
return SourceLocation.sourceLocation(startLine, startColumn, startOffset);
}
public SourceLocation end() {
return SourceLocation.sourceLocation(endLine, endColumn, endOffset);
}
public int length() { return endOffset - startOffset; }
public String extract(String source) { return source.substring(startOffset, endOffset); }
public SourceSpan merge(SourceSpan other) {
int nsl, nsc, nso, nel, nec, neo;
if (startOffset <= other.startOffset) {
nsl = startLine; nsc = startColumn; nso = startOffset;
} else {
nsl = other.startLine; nsc = other.startColumn; nso = other.startOffset;
}
if (endOffset >= other.endOffset) {
nel = endLine; nec = endColumn; neo = endOffset;
} else {
nel = other.endLine; nec = other.endColumn; neo = other.endOffset;
}
return new SourceSpan(nsl, nsc, nso, nel, nec, neo);
}
@Override public String toString() {
return startLine + ":" + startColumn + "-" + endLine + ":" + endColumn;
}
public int length() { return end.offset() - start.offset(); }
public String extract(String source) { return source.substring(start.offset(), end.offset()); }
@Override public String toString() { return start + "-" + end; }
}

public sealed interface Trivia {
Expand Down Expand Up @@ -1822,11 +1849,11 @@ public String format(String source, String filename) {
sb.append(loc.line()).append(":").append(loc.column()).append("\\n");

// Find all lines we need to display
int minLine = span.start().line();
int maxLine = span.end().line();
int minLine = span.startLine();
int maxLine = span.endLine();
for (var label : labels) {
minLine = Math.min(minLine, label.span().start().line());
maxLine = Math.max(maxLine, label.span().end().line());
minLine = Math.min(minLine, label.span().startLine());
maxLine = Math.max(maxLine, label.span().endLine());
}

// Calculate gutter width
Expand Down Expand Up @@ -1867,13 +1894,13 @@ public String format(String source, String filename) {

private List<DiagnosticLabel> getLabelsOnLine(int lineNum) {
var result = new ArrayList<DiagnosticLabel>();
if (span.start().line() <= lineNum && span.end().line() >= lineNum) {
if (span.startLine() <= lineNum && span.endLine() >= lineNum) {
if (labels.isEmpty()) {
result.add(DiagnosticLabel.primary(span, ""));
}
}
for (var label : labels) {
if (label.span().start().line() <= lineNum && label.span().end().line() >= lineNum) {
if (label.span().startLine() <= lineNum && label.span().endLine() >= lineNum) {
result.add(label);
}
}
Expand All @@ -1885,13 +1912,13 @@ private String formatUnderlines(int lineNum, String lineContent, List<Diagnostic
int currentCol = 1;

var sorted = lineLabels.stream()
.sorted((a, b) -> Integer.compare(a.span().start().column(), b.span().start().column()))
.sorted((a, b) -> Integer.compare(a.span().startColumn(), b.span().startColumn()))
.toList();

for (var label : sorted) {
int startCol = label.span().start().line() == lineNum ? label.span().start().column() : 1;
int endCol = label.span().end().line() == lineNum
? label.span().end().column()
int startCol = label.span().startLine() == lineNum ? label.span().startColumn() : 1;
int endCol = label.span().endLine() == lineNum
? label.span().endColumn()
: lineContent.length() + 1;

while (currentCol < startCol) {
Expand Down
60 changes: 47 additions & 13 deletions peglib-core/src/main/java/org/pragmatica/peg/tree/SourceSpan.java
Original file line number Diff line number Diff line change
@@ -1,36 +1,70 @@
package org.pragmatica.peg.tree;
/**
* A range in source text from start (inclusive) to end (exclusive).
*
* <p>Stored as six primitive ints to avoid long-lived {@link SourceLocation}
* references (which previously promoted to old gen via CstNode → SourceSpan → SourceLocation
* chains, contributing to GC pause tail). {@link #start()}/{@link #end()} materialize
* fresh SourceLocation instances on demand; callers needing a single field should use
* the int accessors directly (e.g., {@link #startLine()}, {@link #endOffset()}) to avoid
* the materialization cost.
*/
public record SourceSpan(SourceLocation start, SourceLocation end) {
public record SourceSpan(int startLine,
int startColumn,
int startOffset,
int endLine,
int endColumn,
int endOffset) {
public static SourceSpan sourceSpan(SourceLocation start, SourceLocation end) {
return new SourceSpan(start, end);
return new SourceSpan(start.line(), start.column(), start.offset(), end.line(), end.column(), end.offset());
}

public static SourceSpan sourceSpan(SourceLocation location) {
return new SourceSpan(location, location);
return sourceSpan(location, location);
}

public SourceLocation start() {
return SourceLocation.sourceLocation(startLine, startColumn, startOffset);
}

public SourceLocation end() {
return SourceLocation.sourceLocation(endLine, endColumn, endOffset);
}

public int length() {
return end.offset() - start.offset();
return endOffset - startOffset;
}

public String extract(String source) {
return source.substring(start.offset(), end.offset());
return source.substring(startOffset, endOffset);
}

public SourceSpan merge(SourceSpan other) {
var newStart = start.offset() <= other.start.offset()
? start
: other.start;
var newEnd = end.offset() >= other.end.offset()
? end
: other.end;
return new SourceSpan(newStart, newEnd);
int newStartLine, newStartColumn, newStartOffset;
int newEndLine, newEndColumn, newEndOffset;
if (startOffset <= other.startOffset) {
newStartLine = startLine;
newStartColumn = startColumn;
newStartOffset = startOffset;
}else {
newStartLine = other.startLine;
newStartColumn = other.startColumn;
newStartOffset = other.startOffset;
}
if (endOffset >= other.endOffset) {
newEndLine = endLine;
newEndColumn = endColumn;
newEndOffset = endOffset;
}else {
newEndLine = other.endLine;
newEndColumn = other.endColumn;
newEndOffset = other.endOffset;
}
return new SourceSpan(newStartLine, newStartColumn, newStartOffset, newEndLine, newEndColumn, newEndOffset);
}

@Override
public String toString() {
return start + "-" + end;
return startLine + ":" + startColumn + "-" + endLine + ":" + endColumn;
}
}
4 changes: 2 additions & 2 deletions peglib-core/src/test/java/org/pragmatica/peg/TriviaTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,8 @@ void triviaSpan_hasCorrectPositions() {
assertThat(node.leadingTrivia()).hasSize(1);

var trivia = node.leadingTrivia().getFirst();
assertThat(trivia.span().start().offset()).isEqualTo(0);
assertThat(trivia.span().end().offset()).isEqualTo(3);
assertThat(trivia.span().startOffset()).isEqualTo(0);
assertThat(trivia.span().endOffset()).isEqualTo(3);
assertThat(trivia.text()).isEqualTo(" ");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -287,8 +287,8 @@ record LspDiagnostic(int line, int column, int severity, String message) {}

var lspDiagnostics = result.diagnostics().stream()
.map(d -> new LspDiagnostic(
d.span().start().line() - 1, // LSP uses 0-based
d.span().start().column() - 1,
d.span().startLine() - 1, // LSP uses 0-based
d.span().startColumn() - 1,
d.severity() == Diagnostic.Severity.ERROR ? 1 : 2,
d.message()
))
Expand All @@ -314,8 +314,8 @@ record ErrorRange(int start, int end) {}

var ranges = result.diagnostics().stream()
.map(d -> new ErrorRange(
d.span().start().offset(),
d.span().end().offset()
d.span().startOffset(),
d.span().endOffset()
))
.toList();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
*/
class ChoiceDispatchAnalyzerTest {

private static final SourceSpan SPAN = new SourceSpan(
private static final SourceSpan SPAN = SourceSpan.sourceSpan(
new SourceLocation(1, 1, 0),
new SourceLocation(1, 1, 0));

Expand Down
2 changes: 1 addition & 1 deletion peglib-formatter/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<parent>
<groupId>org.pragmatica-lite</groupId>
<artifactId>peglib-parent</artifactId>
<version>0.4.2</version>
<version>0.4.3</version>
<relativePath>../pom.xml</relativePath>
</parent>

Expand Down
2 changes: 1 addition & 1 deletion peglib-incremental/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<parent>
<groupId>org.pragmatica-lite</groupId>
<artifactId>peglib-parent</artifactId>
<version>0.4.2</version>
<version>0.4.3</version>
<relativePath>../pom.xml</relativePath>
</parent>

Expand Down
Loading
Loading