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
98 changes: 96 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ jobs:
- name: Parity, differential fuzz and engine mode matrix
run: >-
mvn -B test -Dgpg.skip=true -Dtinyexpression.skipRailroad=true -Dmaven.repo.local="$MAVEN_REPO"
-Dtest=DeclarativeLexicalConformanceTest,UbnfcParityTest,UbnfcDifferentialFuzzTest,P4EngineModeMatrixTest,P4ParserEngineTest,P4DslJavaCodeCalculatorTest,P4DslJavaCodeCalculatorClassicEngineTest,P4AstEvaluatorCalculatorTest,P4AstEvaluatorCalculatorClassicEngineTest,P4PreferredAstMapperDiagnosticsTest,P4SourceMappingTest,P4PackratFraudFormulaTest,FraudAcceptanceTest
-Dtest=DeclarativeLexicalConformanceTest,UbnfcParityTest,UbnfcDifferentialFuzzTest,P4EngineModeMatrixTest,P4ParserEngineTest,P4DslJavaCodeCalculatorTest,P4DslJavaCodeCalculatorClassicEngineTest,P4AstEvaluatorCalculatorTest,P4AstEvaluatorCalculatorClassicEngineTest,P4PreferredAstMapperDiagnosticsTest,P4SourceMappingTest,P4SingleRecognitionProjectionTest,P4ProjectionTextReaderTest,P4PackratFraudFormulaTest,FraudAcceptanceTest
- name: Require Rust production fraud acceptance and collector negative gates
env:
PYTHONDONTWRITEBYTECODE: '1'
Expand Down Expand Up @@ -245,7 +245,7 @@ jobs:
-Dtinyexpression.expected.mapper.snapshot=true
-Dtinyexpression.expected.mapper.nodeAlias=true
-Dtinyexpression.expected.mapper.optionalQualifier=true
-Dtest=P4LayoutMigrationTest,P4SourceMappingTest,P4OwnedSliceSourceTest,P4TypedAstEvaluatorTest,P4JavaCodeEmitterSourceTextTest,DslGeneratedAstJavaEmitterTest,P4PreferredAstMapperPrecedenceTest,P4OptionalExternalQualifierTest,P4RustRootExpressionAcceptanceTest,P4RustNumericEvaluatorOracleTest,P4RustScalarEvaluatorOracleTest,CodeBlockSourceTest,CodeBlockNoCompilerTest
-Dtest=P4LayoutMigrationTest,P4SourceMappingTest,P4OwnedSliceSourceTest,P4TypedAstEvaluatorTest,P4JavaCodeEmitterSourceTextTest,DslGeneratedAstJavaEmitterTest,P4PreferredAstMapperPrecedenceTest,P4OptionalExternalQualifierTest,P4RustRootExpressionAcceptanceTest,P4SingleRecognitionProjectionTest,P4ProjectionTextReaderTest,P4RustNumericEvaluatorOracleTest,P4RustScalarEvaluatorOracleTest,CodeBlockSourceTest,CodeBlockNoCompilerTest
- name: Verify shared Java/Rust parser fixtures with the pinned source generator
if: matrix.generator == 'source'
working-directory: tinyexpression
Expand Down Expand Up @@ -471,3 +471,97 @@ jobs:
name: tinyexpression-p4-lsp-vsix
path: tools/tinyexpression-p4-lsp-vscode/target/*.vsix
if-no-files-found: error

bounded-default-mapper:
name: Bounded default mapper (fixed upstream candidate)
runs-on: [self-hosted, Linux, X64, tinyexpression-ci]
env:
BOUNDED_PARSER_COMMIT: bee36381b0ec1dd938766b21679cefb984e1b0b0
BOUNDED_PARSER_VERSION: 3.3.0
defaults:
run:
working-directory: tinyexpression
steps:
- uses: actions/checkout@v6
with:
path: tinyexpression
# This source includes unlaxer-parser #680. The ordinary compatibility jobs
# retain their separate historical dependency; Central 3.3.0 is not substituted.
- uses: actions/checkout@v6
with:
repository: opaopa6969/unlaxer-parser
ref: ${{ env.BOUNDED_PARSER_COMMIT }}
path: bounded-unlaxer-parser
- uses: actions/setup-java@v5
with:
java-version: '21'
distribution: 'temurin'
- name: Set up Rust 1.85
run: bash .github/scripts/setup-rust.sh 1.85.0 --component rustfmt --component clippy
- name: Install and identify the fixed bounded mapper dependency
run: |
task_maven_repo=$(mktemp -d "$RUNNER_TEMP/bounded-mapper.XXXXXX")
echo "MAVEN_REPO=$task_maven_repo" >> "$GITHUB_ENV"
parser_source="$GITHUB_WORKSPACE/bounded-unlaxer-parser"
test "$(git -C "$parser_source" rev-parse HEAD)" = "$BOUNDED_PARSER_COMMIT"
mvn -B -q -f "$parser_source/pom.xml" -pl unlaxer-common,unlaxer-dsl -am install \
-DskipTests -Dgpg.skip=true -Dmaven.repo.local="$task_maven_repo"
python3 - "$parser_source" "$task_maven_repo" <<'PY'
import hashlib, json, os, subprocess, sys, xml.etree.ElementTree as ET
from pathlib import Path
source, m2 = map(Path, sys.argv[1:])
revision = ET.parse(source / "pom.xml").getroot().find(
"{http://maven.apache.org/POM/4.0.0}properties/{http://maven.apache.org/POM/4.0.0}revision").text
assert revision == os.environ["BOUNDED_PARSER_VERSION"], revision
commit = subprocess.check_output(["git", "-C", str(source), "rev-parse", "HEAD"], text=True).strip()
assert commit == os.environ["BOUNDED_PARSER_COMMIT"], commit
artifacts = {}
for name in ["unlaxer-common", "unlaxer-dsl"]:
jar = m2 / "org/unlaxer" / name / revision / (name + "-" + revision + ".jar")
artifacts[name] = hashlib.sha256(jar.read_bytes()).hexdigest()
target = Path("target/bounded-mapper-evidence")
target.mkdir(parents=True, exist_ok=True)
target.joinpath("dependency.json").write_text(json.dumps({
"tiny_commit": subprocess.check_output(["git", "rev-parse", "HEAD"], text=True).strip(),
"parser_commit": commit, "parser_version": revision, "origin": "pinned-source",
"artifacts_sha256": artifacts, "measurement_eligible": False
}, indent=2) + "\n")
PY
- name: Verify bounded large inputs and the existing facade corpus
run: |
mvn -B test -Dmaven.repo.local="$MAVEN_REPO" -Dunlaxer.version="$BOUNDED_PARSER_VERSION" \
-Dgpg.skip=true -Dtinyexpression.skipRailroad=true \
-Dtest=P4SourceMappingTest,P4ImplicitMappingTest,P4BoundedMappingConformance,P4PreferredAstMapperDiagnosticsTest,UbnfcParityTest,P4ParserEngineTest
set -o pipefail
cargo test --locked --manifest-path rust/Cargo.toml -p tinyexpression-rs --test default_mapping_inputs 2>&1 | tee target/bounded-mapper-evidence/rust.log
- name: Require all bounded mapper assertions without skips
if: always()
run: |
python3 - <<'PY'
import json, xml.etree.ElementTree as ET
from pathlib import Path
evidence = Path("target/bounded-mapper-evidence")
json.loads(evidence.joinpath("dependency.json").read_text())
counts = {}
for name, expected in [("P4SourceMappingTest", 26), ("P4ImplicitMappingTest", 3),
("P4BoundedMappingConformance", 2),
("P4PreferredAstMapperDiagnosticsTest", 12), ("UbnfcParityTest", 2),
("P4ParserEngineTest", 6)]:
reports = list(Path("target/surefire-reports").glob("TEST-*." + name + ".xml"))
assert len(reports) == 1, (name, reports)
suite = ET.parse(reports[0]).getroot()
count = {key: int(suite.get(key, "0")) for key in ["tests", "failures", "errors", "skipped"]}
assert count == {"tests": expected, "failures": 0, "errors": 0, "skipped": 0}, (name, count)
counts[name] = count
assert "test result: ok. 1 passed; 0 failed; 0 ignored;" in evidence.joinpath("rust.log").read_text()
evidence.joinpath("java-results.json").write_text(json.dumps(counts, indent=2) + "\n")
PY
- uses: actions/upload-artifact@v6
if: always()
with:
name: bounded-default-mapper-evidence
path: |
tinyexpression/target/bounded-mapper-evidence/
tinyexpression/target/surefire-reports/
tinyexpression/rust/tinyexpression-rs/tests/fixtures/default-mapping-inputs.tsv
if-no-files-found: error
136 changes: 136 additions & 0 deletions catalog/fixtures/operator-diagnostics.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
[
{
"input": "$a&&$b",
"operator": "&&",
"failureCp": 3
},
{
"input": "$a && $b",
"operator": "&&",
"failureCp": 4
},
{
"input": "true&&false",
"operator": "&&",
"failureCp": 5
},
{
"input": "true && false",
"operator": "&&",
"failureCp": 6
},
{
"input": "1&&2",
"operator": "&&",
"failureCp": 1
},
{
"input": "1 && 2",
"operator": "&&",
"failureCp": 2
},
{
"input": "/*😀*/\r\ntrue && false",
"operator": "&&",
"failureCp": 13
},
{
"input": "/* x */ true && false",
"operator": "&&",
"failureCp": 14
},
{
"input": "&& true",
"operator": "&&",
"failureCp": 0
},
{
"input": "$a||$b",
"operator": "||",
"failureCp": 3
},
{
"input": "$a || $b",
"operator": "||",
"failureCp": 4
},
{
"input": "true||false",
"operator": "||",
"failureCp": 5
},
{
"input": "true || false",
"operator": "||",
"failureCp": 6
},
{
"input": "1||2",
"operator": "||",
"failureCp": 1
},
{
"input": "1 || 2",
"operator": "||",
"failureCp": 2
},
{
"input": "/*😀*/\r\ntrue || false",
"operator": "||",
"failureCp": 13
},
{
"input": "/* x */ true || false",
"operator": "||",
"failureCp": 14
},
{
"input": "|| true",
"operator": "||",
"failureCp": 0
},
{
"input": "\"&&\" +",
"operator": null
},
{
"input": "\"||\" +",
"operator": null
},
{
"input": "true &",
"operator": null
},
{
"input": "false |",
"operator": null
},
{
"input": "\"&&\" @",
"operator": null
},
{
"input": "/* && */ 1 +",
"operator": null
},
{
"input": "true & false",
"operator": null
},
{
"input": "true | false",
"operator": null
},
{
"input": "'😀 &&' +",
"operator": null
},
{
"input": "true & & false",
"operator": null
},
{
"input": "false | | true",
"operator": null
}
]
7 changes: 7 additions & 0 deletions catalog/scripts/catalog-lib.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,13 @@ export function countsOf(catalog) {
return counts;
}

/** The unsupported double operator touching a failure UTF-16 offset, without scanning elsewhere. */
export function operatorAtFailure(text, offset) {
const c = text[offset];
if (c !== '&' && c !== '|') return null;
return text[offset + 1] === c || (offset > 0 && text[offset - 1] === c) ? c + c : null;
}

/**
* Maps a parser failure to a TE code with the catalog's diagnosticRules. `expected` is the
* list of terminals the parser could accept at the failure (without quotes), `snippet` the
Expand Down
83 changes: 83 additions & 0 deletions docs/quality/p4-default-mapper-selection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Default mapper selection (Tiny #259)

Classic's default facade request has a nonempty list of implicit AST preferences, such as
IfExpr, SinExpr, BinaryExpr and FormulaExpr. It is not a request to enumerate every mapped
candidate. Previously the facade called `mapParsedTree`/`mapSubtreeTree` even for this request;
the current source generator's intentional 4096-candidate inventory limit rejected established x64 benchmark inputs.

When `allowDefault` is true, the adapter now calls the existing public single-selection
`selectParsedTokenWithSourceMap`/`selectSubtreeTokenWithSourceMap` once, retaining its owned AST
and source snapshot. If both the snapshot selector and legacy `sourceSpanOf` API are absent,
whole-source AST indexing is unavailable: the adapter retains the old per-preference selection
path instead of inventing source positions. Other older generators use the existing capability
fallback only when an API is absent. Invocation failures still propagate; mapping failure does
not trigger another mapping API or parse.

The P4 AST retains whole-input wrappers and their captured children. The adapter indexes
whole-source AST nodes in ancestor order, stopping at partial-source AST branches. In this
P4 schema the relevant whole-input wrappers form an ancestor chain; the outermost matching
record is the same one preferred by the generated selector's shallowest-CST rule. It then
uses the facade's existing implicit preference order, whole-source check and default fallback.
A selected child retains the default selection's committed token as a whole-source anchor,
its own identity-based AST span, and the same owned snapshot. This adapter is not a replacement
for the generated mapper's general preferred-candidate API.

`mapDefaultOnce` itself accepts only null/blank preferences. `mapImplicitOnce` resolves only
implicit whole-source preferences already retained in that default AST. Explicit result or
AST preferences keep the existing `mapOnce` path. With the current source generator, full inventories remain bounded at 4096,
and all mapping node/edge/depth/work/text limits remain unchanged. Older generators retain
their historical inventory and validation behavior. No retry, grammar change,
unsafe memo policy, or resource-limit increase is part of this change.

This change depends on Tiny #256's root/family projection. Its canonical projection runs
after the single mapping and before facade preference selection, retaining its historical
bare-boolean/string result shape. Java UBNFC and Rust's production parser are unchanged by
this adapter; Rust still returns owned Formula/Expression wrappers. The shared fixture records
these public root differences explicitly rather than calling them identical APIs.

## Verification

`P4ImplicitMappingTest` compares old full-inventory and new implicit selection on the same
committed CST for the existing corpus: 324 accepted inputs, every eligible whole-source
implicit type, final AST, CP span, source text and selection mode. Twelve inputs fail parsing
before mapping. Four rows (two unique x64 inputs present in both the formula corpus and file
fixtures) exceed the unchanged full-inventory cap and have separate positive expectations.
The Java Classic/UBNFC 350-case facade comparison also compares all AST fields and node CP spans.

`default-mapping-inputs.tsv` pins the two established positive benchmark files, SHA-256, CP
lengths and expected public root families. Java verifies hashes, source spans, facade modes,
retained snapshots across subsequent small partial-input rejection and the unchanged explicit inventory rejection.
The Rust test consumes the same files and expected CP/root families through `parse`, checks
the same small partial-input rejection, and retains the complete owned tree across later parses.

Use Java 21 and Rust 1.85 with an isolated Maven repository. The complete x64 regression requires
an unlaxer build containing parser #680's bounded tree-reachability index. The local candidate
was installed as 3.3.0 from source `de53a229`; this is not a new Central publication. Tiny's CI
source pin `fc75fe1e` / 3.3.0-SNAPSHOT already exposes the single-selection API, but its generated
mapper predates both the public-CST MappingGuard and the preferred-inventory 4096 limit. Its
existing 350-case facade comparison passes (328 accepted, 22 both rejected); that proves API
compatibility, not the newer bounded guard contract. The candidate's large-input success and
unchanged inventory rejection are separate results. Run the new bounded large-input assertions
against a fixed upstream source containing #680 in a separate repository/version override;
do not silently relabel the old pin or skip these assertions as successful.

```sh
RUSTUP_TOOLCHAIN=1.85.0 mvn -Dmaven.repo.local=/path/to/isolated-m2 -Dunlaxer.version=3.3.0 \
-Dtinyexpression.skipRailroad=true \
-Dtest=P4SourceMappingTest,P4ImplicitMappingTest,P4PreferredAstMapperDiagnosticsTest,UbnfcParityTest,P4ParserEngineTest test
RUSTUP_TOOLCHAIN=1.85.0 cargo test --locked --manifest-path rust/Cargo.toml \
-p tinyexpression-rs --test default_mapping_inputs
```

These are semantic/resource-boundary checks on a shared development host. They do not attest
to an exclusive measurement environment or close parser #197/#184's final latency conditions.
Classic SAFE and Java/Rust UBNFC production timings remain separate evidence.

The dedicated bounded-default-mapper CI job builds immutable upstream source
bee36381b0ec1dd938766b21679cefb984e1b0b0 in its own Maven repository and explicitly selects
its actual 3.3.0 artifact version. It requires all three bounded implicit-mapping assertions,
the existing 350-case comparison, and the Rust shared-large-input test without skips.
Artifacts retain both source identities, dependency hashes, Surefire reports and the Rust log.
The ordinary fc75fe1e compatibility jobs and their dependency remain separate. This Tiny
change is merged only after upstream parser PR #682 has merged; candidate CI success alone
does not claim that dependency is already on master or published to Central.
Loading