From 64b3e3a22c66598aae2fc79a40dbc85574c4e478 Mon Sep 17 00:00:00 2001 From: piotr-blue Date: Wed, 29 Jul 2026 19:47:25 +0200 Subject: [PATCH 01/13] feat: finalize BEX 2.0 publication boundary --- .../scripts/run-final-publication-gates.sh | 166 + .github/workflows/release-rc.yml | 10 +- .github/workflows/release.yml | 10 +- README.md | 134 +- build.gradle.kts | 1755 +++++- docs/BEX_CONFORMANCE.md | 149 + docs/FIXTURES.md | 275 +- docs/GAS.md | 396 +- settings.gradle.kts | 27 + specifications/blue-bex-specification-2.0.md | 2248 ++++++++ src/main/java/blue/bex/api/BexEngine.java | 40 +- .../blue/bex/api/BexExecutionContext.java | 133 +- .../java/blue/bex/api/BexGasLedgerHost.java | 85 + .../blue/bex/api/BexIntrinsicInvocation.java | 86 +- .../blue/bex/api/BexIntrinsicRegistry.java | 348 +- .../blue/bex/api/FrozenBexDocumentView.java | 16 +- ...cessorExecutionContextBexDocumentView.java | 15 +- ...essorExecutionContextBexGasLedgerHost.java | 136 + .../blue/bex/compile/BexCompiledProgram.java | 12 +- .../bex/compile/BexCompiledProgramKey.java | 33 +- .../java/blue/bex/compile/BexCompiler.java | 731 ++- .../blue/bex/compile/BexContainsCache.java | 26 +- .../java/blue/bex/compile/BexExpressions.java | 1455 ++++- .../blue/bex/compile/BexNodeFingerprint.java | 3 +- .../java/blue/bex/compile/BexOperands.java | 107 +- .../java/blue/bex/compile/BexStatements.java | 21 +- .../bex/compile/CollectionExpressions.java | 93 +- .../bex/compile/LogicNumericExpressions.java | 96 +- .../bex/compile/ObjectResultExpressions.java | 116 +- .../bex/compile/TypeStringExpressions.java | 262 +- src/main/java/blue/bex/gas/BexGasCharge.java | 293 + src/main/java/blue/bex/gas/BexGasCounter.java | 133 + src/main/java/blue/bex/gas/BexGasLedger.java | 155 + .../bex/gas/BexGasLimitExceededException.java | 146 + .../java/blue/bex/gas/BexGasManifest.java | 215 + src/main/java/blue/bex/gas/BexGasMeter.java | 852 ++- .../java/blue/bex/gas/BexGasSchedule.java | 385 +- .../java/blue/bex/gas/BexSizeEstimator.java | 82 - .../blue/bex/output/BexAdmittedValue.java | 63 + .../bex/output/BexEstablishedIdentity.java | 34 + .../blue/bex/output/BexOutputAdmission.java | 143 + .../java/blue/bex/output/BexOutputKind.java | 22 + .../output/BexSemanticIdentityBoundary.java | 26 + ...ionContextBexSemanticIdentityBoundary.java | 30 + src/main/java/blue/bex/result/BexEvents.java | 19 + .../blue/bex/result/BexExecutionResult.java | 72 +- src/main/java/blue/bex/result/BexMetrics.java | 16 - .../java/blue/bex/result/BexPatchEntry.java | 20 + .../blue/bex/result/BexResultOverlay.java | 40 +- .../bex/runtime/BexExecutionAccumulator.java | 44 +- .../java/blue/bex/runtime/BexRuntime.java | 375 +- .../java/blue/bex/runtime/CompileScope.java | 37 +- .../java/blue/bex/runtime/CompiledFrame.java | 26 + .../blue/bex/type/BexBlueTypeMatcher.java | 946 +++- .../blue/bex/value/AdmittedExactBexValue.java | 192 + .../blue/bex/value/BexBlueNodeWriter.java | 362 +- src/main/java/blue/bex/value/BexEquality.java | 9 + .../blue/bex/value/BexFrozenNodeFactory.java | 20 - .../java/blue/bex/value/BexFrozenWriter.java | 47 +- .../java/blue/bex/value/BexSimpleWriter.java | 9 +- .../java/blue/bex/value/BexUnicodeOrder.java | 144 + src/main/java/blue/bex/value/BexValue.java | 26 + src/main/java/blue/bex/value/BexValues.java | 241 +- .../blue/bex/value/FrozenNodeBexValue.java | 468 +- src/main/java/blue/bex/value/MapBexValue.java | 18 +- .../java/blue/bex/value/NodeBexValue.java | 35 +- .../value/NodeRoundTripFrozenNodeFactory.java | 55 - .../blue/bex/value/OverlayMapBexValue.java | 35 +- .../blue/bex/value/PointerSetBexValue.java | 209 +- .../java/blue/bex/value/ScalarBexValue.java | 3 + .../blue/bex/gas/blue-bex-gas-2.0.yaml | 91 + .../BexAccumulatorPointerConsistencyTest.java | 54 +- .../blue/bex/BexBlueTypeMatchingGasTest.java | 647 +++ .../java/blue/bex/BexBlueTypeSupportTest.java | 63 +- src/test/java/blue/bex/BexCompiler20Test.java | 250 + .../BexCompilerScalarNormalizationTest.java | 99 + .../BexCompositeExhaustionEvidenceTest.java | 1165 ++++ .../java/blue/bex/BexContextReadTest.java | 1 - .../BexDiagnosticAdmissionOrderingTest.java | 279 + .../java/blue/bex/BexDiagnosticsTest.java | 2 +- .../blue/bex/BexEngineConformanceTest.java | 62 +- .../java/blue/bex/BexExactGasRuleTest.java | 336 ++ .../bex/BexExactReferenceDocumentTest.java | 527 ++ .../bex/BexExecutionEvidenceLedgerTest.java | 210 + .../blue/bex/BexFocusedConformanceTests.java | 8 +- .../java/blue/bex/BexFrozenValueTest.java | 59 +- src/test/java/blue/bex/BexIntrinsicTest.java | 371 +- .../java/blue/bex/BexLazyBindingTest.java | 2 +- .../java/blue/bex/BexLocalBenchmarkTest.java | 8 +- src/test/java/blue/bex/BexNodeCursorTest.java | 13 - .../java/blue/bex/BexPointerSet20Test.java | 184 + .../BexPrimitiveExhaustionEvidenceTest.java | 73 + .../java/blue/bex/BexRichFixtureTest.java | 106 +- .../java/blue/bex/BexSchemaValueTest.java | 16 +- .../BexStructuredReferenceEvidenceTest.java | 212 + .../blue/bex/BexTranslatedCorpusTest.java | 6 +- .../blue/bex/BexUseCaseConformanceTest.java | 10 +- .../blue/bex/api/Bex20ApiSurfaceTest.java | 261 + .../conformance/BexBinaryApiManifestMain.java | 249 + .../BexConformanceFixtureTest.java | 35 + .../BexConformancePackageIntegrityTest.java | 541 ++ .../BexConformancePropertyTest.java | 300 ++ .../conformance/BexConformanceReportMain.java | 4755 +++++++++++++++++ .../BexConformanceReportTruthfulnessTest.java | 143 + .../conformance/BexEngineFixtureAdapter.java | 734 +++ .../blue/bex/conformance/BexFixtureRun.java | 174 + .../bex/conformance/BexFixtureRunner.java | 793 +++ .../BexFixtureSchemaValidator.java | 363 ++ .../conformance/BexGasMicrofixtureTest.java | 102 + .../BexRepresentationInvarianceTest.java | 760 +++ .../bex/conformance/ConformancePackage.java | 594 ++ .../blue/bex/gas/BexGasPrimitivesTest.java | 355 ++ .../BexSemanticIdentityIntegrationTest.java | 762 +++ .../java/blue/bex/test/BexTestFixtures.java | 6 +- .../blue/bex/value/BexUnicodeOrderTest.java | 154 + .../BexHostedRuntimeWorkSessionTest.java | 1557 ++++++ .../conformance/bex/fixtures/HARNESS.md | 133 + .../conformance/bex/fixtures/README.md | 5 + .../conformance/bex/fixtures/c/bex-c-01.yaml | 23 + .../conformance/bex/fixtures/c/bex-c-02.yaml | 20 + .../conformance/bex/fixtures/c/bex-c-03.yaml | 29 + .../conformance/bex/fixtures/c/bex-c-04.yaml | 28 + .../conformance/bex/fixtures/c/bex-c-05.yaml | 24 + .../conformance/bex/fixtures/c/bex-c-06.yaml | 23 + .../conformance/bex/fixtures/c/bex-c-07.yaml | 22 + .../conformance/bex/fixtures/c/bex-c-08.yaml | 27 + .../conformance/bex/fixtures/c/bex-c-09.yaml | 37 + .../conformance/bex/fixtures/e/bex-e-01.yaml | 27 + .../conformance/bex/fixtures/e/bex-e-02.yaml | 59 + .../conformance/bex/fixtures/e/bex-e-03.yaml | 23 + .../conformance/bex/fixtures/e/bex-e-04.yaml | 22 + .../conformance/bex/fixtures/e/bex-e-05.yaml | 30 + .../conformance/bex/fixtures/e/bex-e-06.yaml | 32 + .../conformance/bex/fixtures/e/bex-e-07.yaml | 25 + .../conformance/bex/fixtures/e/bex-e-08.yaml | 30 + .../conformance/bex/fixtures/e/bex-e-09.yaml | 26 + .../conformance/bex/fixtures/e/bex-e-10.yaml | 31 + .../conformance/bex/fixtures/e/bex-e-11.yaml | 32 + .../conformance/bex/fixtures/e/bex-e-12.yaml | 25 + .../conformance/bex/fixtures/e/bex-e-13.yaml | 32 + .../conformance/bex/fixtures/e/bex-e-14.yaml | 32 + .../bex/fixtures/fixture-schema.yaml | 112 + .../conformance/bex/fixtures/g/bex-g-01.yaml | 21 + .../conformance/bex/fixtures/g/bex-g-02.yaml | 26 + .../conformance/bex/fixtures/g/bex-g-03.yaml | 24 + .../conformance/bex/fixtures/g/bex-g-04.yaml | 25 + .../conformance/bex/fixtures/g/bex-g-05.yaml | 25 + .../conformance/bex/fixtures/g/bex-g-06.yaml | 24 + .../conformance/bex/fixtures/g/bex-g-07.yaml | 31 + .../conformance/bex/fixtures/g/bex-g-08.yaml | 27 + .../conformance/bex/fixtures/g/bex-g-09.yaml | 28 + .../conformance/bex/fixtures/g/bex-g-10.yaml | 22 + .../conformance/bex/fixtures/g/bex-g-11.yaml | 33 + .../conformance/bex/fixtures/g/bex-g-12.yaml | 24 + .../conformance/bex/fixtures/g/bex-g-13.yaml | 26 + .../conformance/bex/fixtures/g/bex-g-14.yaml | 27 + .../conformance/bex/fixtures/g/bex-g-15.yaml | 108 + .../bex/fixtures/gas-micro/bindingRead.yaml | 19 + .../gas-micro/blueOutputBoundary.yaml | 19 + .../gas-micro/collectionItemProduced.yaml | 19 + .../gas-micro/collectionItemVisited.yaml | 19 + .../gas-micro/comparisonNodeVisited.yaml | 19 + .../bex/fixtures/gas-micro/constantRead.yaml | 19 + .../gas-micro/currentContractRead.yaml | 19 + .../bex/fixtures/gas-micro/documentRead.yaml | 19 + .../bex/fixtures/gas-micro/eventAppended.yaml | 19 + .../bex/fixtures/gas-micro/eventRead.yaml | 19 + .../gas-micro/expressionEvaluated.yaml | 19 + .../fixtures/gas-micro/functionCalled.yaml | 19 + .../gas-micro/integerLimbOperation.yaml | 19 + .../fixtures/gas-micro/intrinsicCalled.yaml | 19 + .../bex/fixtures/gas-micro/listItemRead.yaml | 19 + .../gas-micro/nodeIdentityRequested.yaml | 19 + .../fixtures/gas-micro/objectMemberRead.yaml | 19 + .../bex/fixtures/gas-micro/patchAppended.yaml | 19 + .../gas-micro/pointerSegmentRead.yaml | 19 + .../gas-micro/pointerSegmentWritten.yaml | 19 + .../gas-micro/processingEventRead.yaml | 19 + .../fixtures/gas-micro/resultValueRead.yaml | 19 + .../fixtures/gas-micro/sortComparison.yaml | 19 + .../fixtures/gas-micro/statementExecuted.yaml | 19 + .../bex/fixtures/gas-micro/stepsRead.yaml | 19 + .../gas-micro/textBlockConstructed.yaml | 19 + .../fixtures/gas-micro/textBlockExamined.yaml | 19 + .../gas-micro/transientListItemProduced.yaml | 19 + .../transientObjectMemberProduced.yaml | 19 + .../bex/fixtures/gas-micro/variableRead.yaml | 19 + .../conformance/bex/fixtures/h/bex-h-01.yaml | 67 + .../conformance/bex/fixtures/h/bex-h-02.yaml | 30 + .../conformance/bex/fixtures/h/bex-h-03.yaml | 24 + .../conformance/bex/fixtures/h/bex-h-04.yaml | 27 + .../conformance/bex/fixtures/h/bex-h-05.yaml | 28 + .../conformance/bex/fixtures/h/bex-h-06.yaml | 36 + .../conformance/bex/fixtures/manifest.yaml | 581 ++ .../bex/fixtures/operator-coverage.yaml | 331 ++ .../bex/fixtures/operators/bex-op-add.yaml | 23 + .../bex/fixtures/operators/bex-op-and.yaml | 23 + .../operators/bex-op-appendchanges.yaml | 30 + .../operators/bex-op-appendevents.yaml | 24 + .../fixtures/operators/bex-op-boolean.yaml | 20 + .../fixtures/operators/bex-op-changeset.yaml | 32 + .../bex/fixtures/operators/bex-op-choose.yaml | 24 + .../fixtures/operators/bex-op-coalesce.yaml | 24 + .../fixtures/operators/bex-op-default.yaml | 22 + .../bex/fixtures/operators/bex-op-empty.yaml | 20 + .../fixtures/operators/bex-op-emptylist.yaml | 20 + .../operators/bex-op-emptyobject.yaml | 20 + .../fixtures/operators/bex-op-entries.yaml | 26 + .../bex/fixtures/operators/bex-op-events.yaml | 26 + .../bex/fixtures/operators/bex-op-failif.yaml | 23 + .../bex/fixtures/operators/bex-op-filter.yaml | 31 + .../bex/fixtures/operators/bex-op-find.yaml | 29 + .../fixtures/operators/bex-op-findentry.yaml | 31 + .../fixtures/operators/bex-op-flatmap.yaml | 31 + .../bex/fixtures/operators/bex-op-get.yaml | 23 + .../bex/fixtures/operators/bex-op-gt.yaml | 22 + .../bex/fixtures/operators/bex-op-gte.yaml | 22 + .../bex/fixtures/operators/bex-op-haskey.yaml | 23 + .../fixtures/operators/bex-op-includes.yaml | 24 + .../fixtures/operators/bex-op-isempty.yaml | 20 + .../bex/fixtures/operators/bex-op-iskind.yaml | 24 + .../bex/fixtures/operators/bex-op-join.yaml | 25 + .../bex/fixtures/operators/bex-op-list.yaml | 20 + .../fixtures/operators/bex-op-listconcat.yaml | 26 + .../fixtures/operators/bex-op-listget.yaml | 24 + .../bex/fixtures/operators/bex-op-lt.yaml | 22 + .../bex/fixtures/operators/bex-op-lte.yaml | 22 + .../bex/fixtures/operators/bex-op-merge.yaml | 25 + .../bex/fixtures/operators/bex-op-ne.yaml | 22 + .../bex/fixtures/operators/bex-op-not.yaml | 20 + .../bex/fixtures/operators/bex-op-object.yaml | 20 + .../operators/bex-op-objectfromentries.yaml | 26 + .../fixtures/operators/bex-op-objectset.yaml | 26 + .../bex/fixtures/operators/bex-op-reduce.yaml | 31 + .../fixtures/operators/bex-op-sliceafter.yaml | 22 + .../bex/fixtures/operators/bex-op-some.yaml | 29 + .../bex/fixtures/operators/bex-op-split.yaml | 25 + .../fixtures/operators/bex-op-startswith.yaml | 22 + .../fixtures/operators/bex-op-subtract.yaml | 23 + .../bex/fixtures/operators/bex-op-unwrap.yaml | 22 + .../bex/fixtures/projection-catalog.yaml | 80 + .../conformance/bex/fixtures/r/bex-r-01.yaml | 40 + .../conformance/bex/fixtures/r/bex-r-02.yaml | 85 + .../conformance/bex/fixtures/r/bex-r-03.yaml | 29 + .../conformance/bex/fixtures/r/bex-r-04.yaml | 32 + .../conformance/bex/fixtures/r/bex-r-05.yaml | 34 + .../conformance/bex/fixtures/r/bex-r-06.yaml | 30 + .../conformance/bex/fixtures/r/bex-r-07.yaml | 35 + .../conformance/bex/fixtures/r/bex-r-08.yaml | 40 + .../conformance/bex/fixtures/r/bex-r-09.yaml | 26 + .../conformance/bex/fixtures/s/bex-s-01.yaml | 38 + .../conformance/bex/fixtures/s/bex-s-02.yaml | 26 + .../conformance/bex/fixtures/s/bex-s-03.yaml | 33 + .../conformance/bex/fixtures/s/bex-s-04.yaml | 25 + .../conformance/bex/fixtures/s/bex-s-05.yaml | 33 + .../conformance/bex/fixtures/s/bex-s-06.yaml | 23 + .../conformance/bex/fixtures/s/bex-s-07.yaml | 35 + .../bex/fixtures/vector-coverage.yaml | 197 + .../conformance/bex/gas-manifest.yaml | 91 + .../conformance/bex/registry/Compute2.blue | 16 + .../bex/registry/FixtureIntrinsic.blue | 8 + .../bex/registry/SortFixtureIntrinsic.blue | 5 + .../conformance/bex/registry/manifest.yaml | 30 + ...stomer-paynote-snapshot-bex-functions.yaml | 72 +- .../hosted-release/baseline.properties | 22 + .../gas-exhaustion-trace-examples.properties | 45 + .../published-api-inspection.properties | 29 + .../hosted-release/required-public-api.txt | 789 +++ .../current/14-is-blueid-typed-node-true.yaml | 6 +- .../15-is-blueid-wrong-type-false.yaml | 8 +- .../gas/gas-004-custom-expression-base.yaml | 4 +- .../gas/gas-121-custom-function-call.yaml | 4 +- .../gas/gas-122-custom-statement-base.yaml | 4 +- .../gas/gas-123-custom-append-event-base.yaml | 4 +- .../gas-124-custom-append-change-base.yaml | 4 +- .../gas/gas-126-custom-pointer-set-base.yaml | 4 +- .../gas/gas-127-custom-foreach-item.yaml | 4 +- work-status.txt | 1 + 278 files changed, 36979 insertions(+), 1698 deletions(-) create mode 100755 .github/scripts/run-final-publication-gates.sh create mode 100644 docs/BEX_CONFORMANCE.md create mode 100644 specifications/blue-bex-specification-2.0.md create mode 100644 src/main/java/blue/bex/api/BexGasLedgerHost.java create mode 100644 src/main/java/blue/bex/api/ProcessorExecutionContextBexGasLedgerHost.java create mode 100644 src/main/java/blue/bex/gas/BexGasCharge.java create mode 100644 src/main/java/blue/bex/gas/BexGasCounter.java create mode 100644 src/main/java/blue/bex/gas/BexGasLedger.java create mode 100644 src/main/java/blue/bex/gas/BexGasLimitExceededException.java create mode 100644 src/main/java/blue/bex/gas/BexGasManifest.java delete mode 100644 src/main/java/blue/bex/gas/BexSizeEstimator.java create mode 100644 src/main/java/blue/bex/output/BexAdmittedValue.java create mode 100644 src/main/java/blue/bex/output/BexEstablishedIdentity.java create mode 100644 src/main/java/blue/bex/output/BexOutputAdmission.java create mode 100644 src/main/java/blue/bex/output/BexOutputKind.java create mode 100644 src/main/java/blue/bex/output/BexSemanticIdentityBoundary.java create mode 100644 src/main/java/blue/bex/output/ProcessorExecutionContextBexSemanticIdentityBoundary.java create mode 100644 src/main/java/blue/bex/value/AdmittedExactBexValue.java delete mode 100644 src/main/java/blue/bex/value/BexFrozenNodeFactory.java create mode 100644 src/main/java/blue/bex/value/BexUnicodeOrder.java delete mode 100644 src/main/java/blue/bex/value/NodeRoundTripFrozenNodeFactory.java create mode 100644 src/main/resources/blue/bex/gas/blue-bex-gas-2.0.yaml create mode 100644 src/test/java/blue/bex/BexBlueTypeMatchingGasTest.java create mode 100644 src/test/java/blue/bex/BexCompiler20Test.java create mode 100644 src/test/java/blue/bex/BexCompilerScalarNormalizationTest.java create mode 100644 src/test/java/blue/bex/BexCompositeExhaustionEvidenceTest.java create mode 100644 src/test/java/blue/bex/BexDiagnosticAdmissionOrderingTest.java create mode 100644 src/test/java/blue/bex/BexExactGasRuleTest.java create mode 100644 src/test/java/blue/bex/BexExactReferenceDocumentTest.java create mode 100644 src/test/java/blue/bex/BexExecutionEvidenceLedgerTest.java create mode 100644 src/test/java/blue/bex/BexPointerSet20Test.java create mode 100644 src/test/java/blue/bex/BexPrimitiveExhaustionEvidenceTest.java create mode 100644 src/test/java/blue/bex/BexStructuredReferenceEvidenceTest.java create mode 100644 src/test/java/blue/bex/api/Bex20ApiSurfaceTest.java create mode 100644 src/test/java/blue/bex/conformance/BexBinaryApiManifestMain.java create mode 100644 src/test/java/blue/bex/conformance/BexConformanceFixtureTest.java create mode 100644 src/test/java/blue/bex/conformance/BexConformancePackageIntegrityTest.java create mode 100644 src/test/java/blue/bex/conformance/BexConformancePropertyTest.java create mode 100644 src/test/java/blue/bex/conformance/BexConformanceReportMain.java create mode 100644 src/test/java/blue/bex/conformance/BexConformanceReportTruthfulnessTest.java create mode 100644 src/test/java/blue/bex/conformance/BexEngineFixtureAdapter.java create mode 100644 src/test/java/blue/bex/conformance/BexFixtureRun.java create mode 100644 src/test/java/blue/bex/conformance/BexFixtureRunner.java create mode 100644 src/test/java/blue/bex/conformance/BexFixtureSchemaValidator.java create mode 100644 src/test/java/blue/bex/conformance/BexGasMicrofixtureTest.java create mode 100644 src/test/java/blue/bex/conformance/BexRepresentationInvarianceTest.java create mode 100644 src/test/java/blue/bex/conformance/ConformancePackage.java create mode 100644 src/test/java/blue/bex/gas/BexGasPrimitivesTest.java create mode 100644 src/test/java/blue/bex/output/BexSemanticIdentityIntegrationTest.java create mode 100644 src/test/java/blue/bex/value/BexUnicodeOrderTest.java create mode 100644 src/test/java/blue/language/processor/BexHostedRuntimeWorkSessionTest.java create mode 100644 src/test/resources/conformance/bex/fixtures/HARNESS.md create mode 100644 src/test/resources/conformance/bex/fixtures/README.md create mode 100644 src/test/resources/conformance/bex/fixtures/c/bex-c-01.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/c/bex-c-02.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/c/bex-c-03.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/c/bex-c-04.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/c/bex-c-05.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/c/bex-c-06.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/c/bex-c-07.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/c/bex-c-08.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/c/bex-c-09.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/e/bex-e-01.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/e/bex-e-02.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/e/bex-e-03.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/e/bex-e-04.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/e/bex-e-05.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/e/bex-e-06.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/e/bex-e-07.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/e/bex-e-08.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/e/bex-e-09.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/e/bex-e-10.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/e/bex-e-11.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/e/bex-e-12.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/e/bex-e-13.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/e/bex-e-14.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/fixture-schema.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/g/bex-g-01.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/g/bex-g-02.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/g/bex-g-03.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/g/bex-g-04.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/g/bex-g-05.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/g/bex-g-06.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/g/bex-g-07.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/g/bex-g-08.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/g/bex-g-09.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/g/bex-g-10.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/g/bex-g-11.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/g/bex-g-12.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/g/bex-g-13.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/g/bex-g-14.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/g/bex-g-15.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/gas-micro/bindingRead.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/gas-micro/blueOutputBoundary.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/gas-micro/collectionItemProduced.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/gas-micro/collectionItemVisited.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/gas-micro/comparisonNodeVisited.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/gas-micro/constantRead.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/gas-micro/currentContractRead.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/gas-micro/documentRead.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/gas-micro/eventAppended.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/gas-micro/eventRead.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/gas-micro/expressionEvaluated.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/gas-micro/functionCalled.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/gas-micro/integerLimbOperation.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/gas-micro/intrinsicCalled.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/gas-micro/listItemRead.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/gas-micro/nodeIdentityRequested.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/gas-micro/objectMemberRead.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/gas-micro/patchAppended.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/gas-micro/pointerSegmentRead.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/gas-micro/pointerSegmentWritten.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/gas-micro/processingEventRead.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/gas-micro/resultValueRead.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/gas-micro/sortComparison.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/gas-micro/statementExecuted.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/gas-micro/stepsRead.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/gas-micro/textBlockConstructed.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/gas-micro/textBlockExamined.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/gas-micro/transientListItemProduced.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/gas-micro/transientObjectMemberProduced.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/gas-micro/variableRead.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/h/bex-h-01.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/h/bex-h-02.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/h/bex-h-03.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/h/bex-h-04.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/h/bex-h-05.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/h/bex-h-06.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/manifest.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/operator-coverage.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/operators/bex-op-add.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/operators/bex-op-and.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/operators/bex-op-appendchanges.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/operators/bex-op-appendevents.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/operators/bex-op-boolean.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/operators/bex-op-changeset.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/operators/bex-op-choose.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/operators/bex-op-coalesce.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/operators/bex-op-default.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/operators/bex-op-empty.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/operators/bex-op-emptylist.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/operators/bex-op-emptyobject.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/operators/bex-op-entries.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/operators/bex-op-events.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/operators/bex-op-failif.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/operators/bex-op-filter.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/operators/bex-op-find.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/operators/bex-op-findentry.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/operators/bex-op-flatmap.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/operators/bex-op-get.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/operators/bex-op-gt.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/operators/bex-op-gte.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/operators/bex-op-haskey.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/operators/bex-op-includes.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/operators/bex-op-isempty.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/operators/bex-op-iskind.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/operators/bex-op-join.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/operators/bex-op-list.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/operators/bex-op-listconcat.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/operators/bex-op-listget.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/operators/bex-op-lt.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/operators/bex-op-lte.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/operators/bex-op-merge.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/operators/bex-op-ne.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/operators/bex-op-not.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/operators/bex-op-object.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/operators/bex-op-objectfromentries.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/operators/bex-op-objectset.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/operators/bex-op-reduce.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/operators/bex-op-sliceafter.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/operators/bex-op-some.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/operators/bex-op-split.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/operators/bex-op-startswith.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/operators/bex-op-subtract.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/operators/bex-op-unwrap.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/projection-catalog.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/r/bex-r-01.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/r/bex-r-02.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/r/bex-r-03.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/r/bex-r-04.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/r/bex-r-05.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/r/bex-r-06.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/r/bex-r-07.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/r/bex-r-08.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/r/bex-r-09.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/s/bex-s-01.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/s/bex-s-02.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/s/bex-s-03.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/s/bex-s-04.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/s/bex-s-05.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/s/bex-s-06.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/s/bex-s-07.yaml create mode 100644 src/test/resources/conformance/bex/fixtures/vector-coverage.yaml create mode 100644 src/test/resources/conformance/bex/gas-manifest.yaml create mode 100644 src/test/resources/conformance/bex/registry/Compute2.blue create mode 100644 src/test/resources/conformance/bex/registry/FixtureIntrinsic.blue create mode 100644 src/test/resources/conformance/bex/registry/SortFixtureIntrinsic.blue create mode 100644 src/test/resources/conformance/bex/registry/manifest.yaml create mode 100644 src/test/resources/hosted-release/baseline.properties create mode 100644 src/test/resources/hosted-release/gas-exhaustion-trace-examples.properties create mode 100644 src/test/resources/hosted-release/published-api-inspection.properties create mode 100644 src/test/resources/hosted-release/required-public-api.txt create mode 100644 work-status.txt diff --git a/.github/scripts/run-final-publication-gates.sh b/.github/scripts/run-final-publication-gates.sh new file mode 100755 index 0000000..6840aa6 --- /dev/null +++ b/.github/scripts/run-final-publication-gates.sh @@ -0,0 +1,166 @@ +#!/usr/bin/env bash +set -euo pipefail + +readonly SCRIPT_DIR="$( + cd "$(dirname "${BASH_SOURCE[0]}")" + pwd +)" +readonly BEX_REPOSITORY="$( + cd "$SCRIPT_DIR/../.." + pwd +)" +readonly INSPECTION_FILE="$BEX_REPOSITORY/src/test/resources/hosted-release/published-api-inspection.properties" +readonly LANGUAGE_REPOSITORY_URL="${BLUE_LANGUAGE_REPOSITORY_URL:-https://github.com/bluecontract/blue-language-java.git}" +readonly BEX_RELEASE_TEMP_ROOT="$( + mktemp -d "${RUNNER_TEMP:-${TMPDIR:-/tmp}}/blue-bex-publication.XXXXXX" +)" +readonly LANGUAGE_CHECKOUT="$BEX_RELEASE_TEMP_ROOT/blue-language-java" +readonly FIRST_BEX_CHECKOUT="$BEX_RELEASE_TEMP_ROOT/blue-bex-clean-one" +readonly SECOND_BEX_CHECKOUT="$BEX_RELEASE_TEMP_ROOT/blue-bex-clean-two" +readonly RECEIPT_ROOT="$BEX_RELEASE_TEMP_ROOT/receipts" +readonly STANDALONE_FIRST_RECEIPT="$RECEIPT_ROOT/standalone-first.properties" +readonly STANDALONE_SECOND_RECEIPT="$RECEIPT_ROOT/standalone-second.properties" +readonly LOCAL_FIRST_RECEIPT="$RECEIPT_ROOT/local-composite-first.properties" +readonly LOCAL_SECOND_RECEIPT="$RECEIPT_ROOT/local-composite-second.properties" + +property_value() { + local key="$1" + sed -n "s/^${key}=//p" "$INSPECTION_FILE" | head -n 1 +} + +readonly LANGUAGE_COMMIT="$(property_value "source.commit")" +readonly LANGUAGE_TAG="$(property_value "source.tag")" +readonly LANGUAGE_API_STATUS="$(property_value "status")" +readonly BEX_COMMIT="$(git -C "$BEX_REPOSITORY" rev-parse HEAD)" +readonly SOURCE_COMMIT_EPOCH="$( + git -C "$BEX_REPOSITORY" show -s --format=%ct "$BEX_COMMIT" +)" + +if [[ ! "$LANGUAGE_COMMIT" =~ ^[0-9a-fA-F]{40}$ ]]; then + echo "Recorded final Blue Language commit is unavailable." >&2 + exit 1 +fi +if [[ ! "$LANGUAGE_TAG" =~ ^[A-Za-z0-9][A-Za-z0-9._/-]*$ || + "$LANGUAGE_TAG" == *..* ]]; then + echo "Recorded final Blue Language tag is unavailable or invalid." >&2 + exit 1 +fi +if [[ "$LANGUAGE_API_STATUS" != "compatible-with-final-hosted-adapter" ]]; then + echo "Recorded Blue Language artifact is not final-host compatible: $LANGUAGE_API_STATUS" >&2 + exit 1 +fi +if [[ -n "$(git -C "$BEX_REPOSITORY" status --porcelain --untracked-files=all)" ]]; then + echo "Publication requires a completely clean BEX checkout." >&2 + exit 1 +fi + +mkdir -p "$LANGUAGE_CHECKOUT" +git -C "$LANGUAGE_CHECKOUT" init +git -C "$LANGUAGE_CHECKOUT" remote add origin "$LANGUAGE_REPOSITORY_URL" +git -C "$LANGUAGE_CHECKOUT" fetch \ + --depth=1 \ + origin \ + "refs/tags/$LANGUAGE_TAG:refs/tags/$LANGUAGE_TAG" +git -C "$LANGUAGE_CHECKOUT" checkout --detach "refs/tags/$LANGUAGE_TAG" + +if [[ "$(git -C "$LANGUAGE_CHECKOUT" rev-parse HEAD)" != "$LANGUAGE_COMMIT" ]]; then + echo "Blue Language checkout does not match the recorded commit." >&2 + exit 1 +fi +if [[ -n "$(git -C "$LANGUAGE_CHECKOUT" status --porcelain --untracked-files=all)" ]]; then + echo "Publication requires a clean Blue Language composite checkout." >&2 + exit 1 +fi + +export CI=true +export SOURCE_DATE_EPOCH="$SOURCE_COMMIT_EPOCH" +if [[ -n "${GITHUB_ENV:-}" ]]; then + printf 'SOURCE_DATE_EPOCH=%s\n' "$SOURCE_COMMIT_EPOCH" >> "$GITHUB_ENV" +fi + +cd "$BEX_REPOSITORY" + +# Assemble the publication artifacts twice from separate clean checkouts of +# this exact BEX commit, using the standalone published dependency in both. +git clone --no-hardlinks "$BEX_REPOSITORY" "$FIRST_BEX_CHECKOUT" +git clone --no-hardlinks "$BEX_REPOSITORY" "$SECOND_BEX_CHECKOUT" +git -C "$FIRST_BEX_CHECKOUT" checkout --detach "$BEX_COMMIT" +git -C "$SECOND_BEX_CHECKOUT" checkout --detach "$BEX_COMMIT" +mkdir -p "$RECEIPT_ROOT" + +GRADLE_USER_HOME="$BEX_RELEASE_TEMP_ROOT/gradle-clean-one" \ + "$FIRST_BEX_CHECKOUT/gradlew" \ + --no-daemon \ + -p "$FIRST_BEX_CHECKOUT" \ + clean test writeCleanBuildArtifactHashes +GRADLE_USER_HOME="$BEX_RELEASE_TEMP_ROOT/gradle-clean-two" \ + "$SECOND_BEX_CHECKOUT/gradlew" \ + --no-daemon \ + -p "$SECOND_BEX_CHECKOUT" \ + clean test writeCleanBuildArtifactHashes + +cp \ + "$FIRST_BEX_CHECKOUT/build/reports/bex-release/clean-build-artifacts.properties" \ + "$STANDALONE_FIRST_RECEIPT" +cp \ + "$SECOND_BEX_CHECKOUT/build/reports/bex-release/clean-build-artifacts.properties" \ + "$STANDALONE_SECOND_RECEIPT" + +GRADLE_USER_HOME="$BEX_RELEASE_TEMP_ROOT/gradle-evidence-verifier" \ + ./gradlew --no-daemon verifyIndependentCleanBuildReproducibility \ + -PcleanBuildEvidenceOne="$STANDALONE_FIRST_RECEIPT" \ + -PcleanBuildEvidenceTwo="$STANDALONE_SECOND_RECEIPT" + +# Record each supported dependency mode separately after the independent +# archive evidence exists. Fresh Gradle homes make standalone dependency-cache +# provenance explicit and prevent one mode from borrowing resolution state +# from the other. +GRADLE_USER_HOME="$BEX_RELEASE_TEMP_ROOT/gradle-standalone-mode" \ + ./gradlew --no-daemon clean test + +GRADLE_USER_HOME="$BEX_RELEASE_TEMP_ROOT/gradle-local-clean-one" \ + "$FIRST_BEX_CHECKOUT/gradlew" \ + --no-daemon \ + -p "$FIRST_BEX_CHECKOUT" \ + clean test writeCleanBuildArtifactHashes \ + -PblueLanguageCompositePath="$LANGUAGE_CHECKOUT" +GRADLE_USER_HOME="$BEX_RELEASE_TEMP_ROOT/gradle-local-clean-two" \ + "$SECOND_BEX_CHECKOUT/gradlew" \ + --no-daemon \ + -p "$SECOND_BEX_CHECKOUT" \ + clean test writeCleanBuildArtifactHashes \ + -PblueLanguageCompositePath="$LANGUAGE_CHECKOUT" + +cp \ + "$FIRST_BEX_CHECKOUT/build/reports/bex-release/clean-build-artifacts.properties" \ + "$LOCAL_FIRST_RECEIPT" +cp \ + "$SECOND_BEX_CHECKOUT/build/reports/bex-release/clean-build-artifacts.properties" \ + "$LOCAL_SECOND_RECEIPT" + +GRADLE_USER_HOME="$BEX_RELEASE_TEMP_ROOT/gradle-local-evidence-verifier" \ + ./gradlew --no-daemon verifyIndependentCleanBuildReproducibility \ + -PblueLanguageCompositePath="$LANGUAGE_CHECKOUT" \ + -PcleanBuildEvidenceOne="$LOCAL_FIRST_RECEIPT" \ + -PcleanBuildEvidenceTwo="$LOCAL_SECOND_RECEIPT" + +GRADLE_USER_HOME="$BEX_RELEASE_TEMP_ROOT/gradle-local-mode" \ + ./gradlew --no-daemon clean test \ + -PblueLanguageCompositePath="$LANGUAGE_CHECKOUT" + +# Rebuild the exact standalone publication outputs from a fresh dependency +# cache and make the single fail-closed readiness decision consumed below by +# the publication workflows. +GRADLE_USER_HOME="$BEX_RELEASE_TEMP_ROOT/gradle-final-standalone" \ + ./gradlew --no-daemon clean bexReleaseEvidence + +readonly CLEAN_BUILD_EVIDENCE_ARCHIVE="$BEX_REPOSITORY/build/reports/bex-release/independent-clean-builds" +mkdir -p "$CLEAN_BUILD_EVIDENCE_ARCHIVE" +cp "$STANDALONE_FIRST_RECEIPT" \ + "$CLEAN_BUILD_EVIDENCE_ARCHIVE/standalone-first.properties" +cp "$STANDALONE_SECOND_RECEIPT" \ + "$CLEAN_BUILD_EVIDENCE_ARCHIVE/standalone-second.properties" +cp "$LOCAL_FIRST_RECEIPT" \ + "$CLEAN_BUILD_EVIDENCE_ARCHIVE/local-composite-first.properties" +cp "$LOCAL_SECOND_RECEIPT" \ + "$CLEAN_BUILD_EVIDENCE_ARCHIVE/local-composite-second.properties" diff --git a/.github/workflows/release-rc.yml b/.github/workflows/release-rc.yml index b435c37..8be875d 100644 --- a/.github/workflows/release-rc.yml +++ b/.github/workflows/release-rc.yml @@ -67,8 +67,10 @@ jobs: git commit -m "chore: release ${{ steps.version.outputs.version }}" git tag -a "v${{ steps.version.outputs.version }}" -m "Release ${{ steps.version.outputs.version }}" - - name: Execute Gradle build - run: ./gradlew clean build + - name: Prove full BEX publication readiness + env: + BLUE_LANGUAGE_REPOSITORY_URL: https://github.com/bluecontract/blue-language-java.git + run: bash .github/scripts/run-final-publication-gates.sh - name: Execute Gradle publish run: ./gradlew publish @@ -92,6 +94,10 @@ jobs: with: name: rc-artifacts path: | + build/distributions build/libs build/publications + build/reports + build/test-results build/jreleaser + .gradle/bex-hosted-release diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2998b83..7d98e3e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -37,8 +37,10 @@ jobs: - name: Setup Gradle uses: gradle/gradle-build-action@v2 - - name: Execute Gradle build - run: ./gradlew clean build + - name: Prove full BEX publication readiness + env: + BLUE_LANGUAGE_REPOSITORY_URL: https://github.com/bluecontract/blue-language-java.git + run: bash .github/scripts/run-final-publication-gates.sh - name: Execute Gradle publish run: ./gradlew publish @@ -59,6 +61,10 @@ jobs: with: name: artifacts path: | + build/distributions build/libs build/publications + build/reports + build/test-results build/jreleaser + .gradle/bex-hosted-release diff --git a/README.md b/README.md index e5c710f..7db95a6 100644 --- a/README.md +++ b/README.md @@ -110,12 +110,20 @@ processor registry decides whether that operation is supported: ```java BexEngine engine = BexEngine.builder() - .intrinsic(CommonCryptoEd25519Verify.class, invocation -> { - invocation.chargeGas(500); - // Read invocation.field("publicKey"), invocation.field("message"), - // and invocation.field("signature"), then return a BexValue boolean. - return BexValues.scalar(verifySignature(invocation)); - }) + .intrinsic( + CommonCryptoEd25519Verify.class, + COMMON_CRYPTO_REGISTRY_IDENTITY, + Collections.singletonMap("signatureVerification", 500L), + invocation -> { + invocation.charge( + "signatureVerification", + 1, + "ed25519-verification"); + // Read invocation.field("publicKey"), + // invocation.field("message"), and + // invocation.field("signature"), then return a boolean. + return BexValues.scalar(verifySignature(invocation)); + }) .build(); ``` @@ -641,8 +649,15 @@ Rules: a shared compiled-program cache cannot bypass it. - The processor returns a `BexValue`. A `null` Java return is normalized to BEX `undefined`. -- The processor is responsible for its own gas accounting by calling - `invocation.chargeGas(...)`. +- The processor charges only counters declared by its exact registration, + using `invocation.charge(counter, quantity, reason)`. Arbitrary aggregate + intrinsic gas is not accepted. +- Intrinsic namespaces are disjoint physical runtime-session children; they + are never flattened into `bex`. The `/` separator is reserved so several + BEX executions in one host session cannot produce ambiguous namespaces. +- A compiled program opens only the intrinsic namespaces it statically + requires. Success, deterministic failure, evidence unavailability, and + exhaustion are separate host-ledger lifecycle callbacks. The Blue type definition and its description/spec text define what the operation means. For standard intrinsics, keep conformance vectors beside the @@ -756,20 +771,22 @@ separately. The older form with only `item` still binds `{ key, val }`. - `value`, the primary return value; - `changeset`, the standard patch accumulator; - `events`, the standard event/data accumulator; -- `gasUsed`; +- `gasLedger`, the canonical ordered named child ledger (`gasUsed` is a + trace-derived convenience total); - `metrics`. BEX computes these values only. The host decides whether patches are applied, events are emitted, or accumulators are treated as ordinary data. -`$resultValue` reads the document value after applying accumulated patches in -order. Parent reads reflect descendant object patches, so reading -`/hotelOrder` after replacing `/hotelOrder/status` returns the original -`hotelOrder` object with the updated status. Current materialization supports -object paths, list index replacement, and non-shifting list index removal. -Removing a list index creates a sparse overlay slot: the removed index reads as -`undefined`, later indexes keep their positions, and converting the whole sparse -overlay list to Blue output fails under the strict host-boundary profile. +`$resultValue` reads a transient overlay of the document after applying +accumulated patches in order. Parent reads reflect descendant object patches, +so reading `/hotelOrder` after replacing `/hotelOrder/status` returns the +original `hotelOrder` object with the updated status. A list-index removal is +non-shifting only in this `$resultValue` overlay: the removed index reads as +`undefined`, later indexes keep their positions, and converting the whole +sparse overlay list to Blue output fails. This sparse-slot rule does not apply +to ordinary BEX list values, `$pointerSet`, or the host's eventual application +of the changeset. `$appendChanges` validates each patch entry the same way as `$appendChange`. Supported patch operations are `add`, `replace`, and `remove`. `add` and @@ -812,11 +829,11 @@ metrics.nodeMaterializations(); ``` The deterministic gas rules are specified in [docs/GAS.md](docs/GAS.md). The -portable fixture format is documented in [docs/FIXTURES.md](docs/FIXTURES.md). -The rich fixture suite lives under `src/test/resources/rich-fixtures/` and -currently has 159 cases: 53 current behavior fixtures, 3 parse-error fixtures, -and 103 gas fixtures. The local fixture package is kept aligned with the -canonical BEX spec fixtures under `blue-spec/specifications/bex/1.0/fixtures/`. +closed BEX 2.0 fixture format is documented in +[docs/FIXTURES.md](docs/FIXTURES.md). The normative package under +`src/test/resources/conformance/bex/` executes 105 behavior cases and 30 exact +named-counter microfixtures, with direct coverage for all 86 published +operators. Its inventory and package identities are verified before execution. The translated corpus strategy is documented in [docs/TRANSLATED_CORPUS.md](docs/TRANSLATED_CORPUS.md). It contains 80 @@ -828,13 +845,78 @@ query/transform cases, and 10 JSON Patch emission edge cases. ## Tests +The local Blue Language composite is opt-in. Use the explicit property when +developing against the sibling checkout: + +```bash +./gradlew -PblueLanguageCompositePath=../blue-language-java test +./gradlew -PblueLanguageCompositePath=../blue-language-java build +``` + +With no property, Gradle uses the declared published dependency +`blue.language:blue-language-java:3.1.0-rc.19`. Resolution is Maven +Central-only—`mavenLocal` is not a dependency repository—and the resolved JAR +must match the recorded Maven Central SHA-256: + ```bash -./gradlew test --tests '*BexRichFixtureTest' -./gradlew test --tests '*BexTranslatedCorpusTest' -./gradlew test -./gradlew build +CI=true ./gradlew clean bexReleaseEvidence ``` +Each report invocation records its current dependency mode under +`.gradle/bex-hosted-release/`. Publication therefore records standalone and +local-composite runs separately, then makes one final standalone +`bexReleaseEvidence` decision. The release workflows automate this sequence +with `.github/scripts/run-final-publication-gates.sh`. + +Artifact evidence must use the non-snapshot CI version and the same source +epoch in two clean checkouts. Every `GRADLE_USER_HOME` below must be a distinct +fresh empty directory. For the required standalone publication pair: + +```bash +export CI=true +export SOURCE_DATE_EPOCH="$(git show -s --format=%ct HEAD)" + +(cd /first/clean/blue-bex-java && \ + GRADLE_USER_HOME=/tmp/blue-bex-gradle-one \ + ./gradlew --no-daemon clean test writeCleanBuildArtifactHashes) + +(cd /second/clean/blue-bex-java && \ + GRADLE_USER_HOME=/tmp/blue-bex-gradle-two \ + ./gradlew --no-daemon clean test writeCleanBuildArtifactHashes) + +./gradlew verifyIndependentCleanBuildReproducibility \ + -PcleanBuildEvidenceOne=/first/clean/blue-bex-java/build/reports/bex-release/clean-build-artifacts.properties \ + -PcleanBuildEvidenceTwo=/second/clean/blue-bex-java/build/reports/bex-release/clean-build-artifacts.properties + +GRADLE_USER_HOME=/tmp/blue-bex-standalone-mode \ + ./gradlew --no-daemon clean test +GRADLE_USER_HOME=/tmp/blue-bex-local-mode \ + ./gradlew --no-daemon clean test \ + -PblueLanguageCompositePath=/absolute/path/to/clean/blue-language-java +GRADLE_USER_HOME=/tmp/blue-bex-final-standalone \ + ./gradlew --no-daemon clean bexReleaseEvidence +``` + +For an additional local-composite reproducibility pair, add the same explicit +`-PblueLanguageCompositePath=/absolute/path/to/clean/blue-language-java` +argument to both clean-checkout builds. Never use one standalone build and one +local-composite build as a two-run pair. The verifier rejects dirty checkouts, +different commits, versions or dependency modes, and any mismatch among the +four artifact hashes. Its commit-bound evidence is also compared with the +artifacts from the reporting build. The same-working-tree archive gate remains +a separate packaging check. + +The API gate compares the packaged JAR’s complete generated descriptor +manifest with the exact source-controlled first-public BEX 2.0 baseline. +Removals, descriptor changes, reordering, and unexpected public/protected +additions all fail the gate. A clean dependency-cache run is +separate evidence and remains +`not-executed` unless a controlled isolated run records it. At this source +state, the current +hosted runtime session APIs exist only in the sibling working tree and are not +present in the published `3.1.0-rc.19` JAR, so standalone release evidence is +expected to remain blocked until Blue Language publishes that API surface. + ## License MIT. See [LICENSE](LICENSE). diff --git a/build.gradle.kts b/build.gradle.kts index b024a30..37cc718 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,17 @@ import java.io.File -import java.text.SimpleDateFormat -import java.util.Date +import java.nio.ByteBuffer +import java.nio.charset.StandardCharsets +import java.nio.file.Files +import java.nio.file.LinkOption +import java.security.MessageDigest +import java.time.Instant +import java.util.Properties +import java.util.zip.ZipFile +import org.gradle.api.tasks.javadoc.Javadoc +import org.gradle.external.javadoc.StandardJavadocDocletOptions +import org.gradle.api.tasks.bundling.Jar +import org.gradle.api.tasks.bundling.Zip +import org.gradle.api.tasks.testing.Test plugins { `java-library` @@ -12,14 +23,50 @@ plugins { group = "blue.bex" version = determineProjectVersion() +val blueLanguagePublishedVersion = "3.1.0-rc.19" +val blueLanguageDeclaredCoordinate = + "blue.language:blue-language-java:$blueLanguagePublishedVersion" +val blueLanguageCompositePath = + providers.gradleProperty("blueLanguageCompositePath") + .orNull + ?.trim() + ?.takeIf { it.isNotEmpty() } +val blueLanguageDependencyMode = + if (blueLanguageCompositePath == null) { + "standalone-published" + } else { + "local-composite" + } +val publishedBlueLanguageInspection = + Properties().apply { + file( + "src/test/resources/hosted-release/" + + "published-api-inspection.properties" + ).inputStream().use(::load) + } +val publishedBlueLanguageCoordinate = + publishedBlueLanguageInspection.getProperty("coordinate") +val publishedBlueLanguageSha256 = + publishedBlueLanguageInspection.getProperty("artifact.sha256") +val publishedBlueLanguageRepository = + publishedBlueLanguageInspection.getProperty("repository") +val blueLanguageModuleVersionCache = + File( + gradle.gradleUserHomeDir, + "caches/modules-2/files-2.1/blue.language/" + + "blue-language-java/$blueLanguagePublishedVersion" + ) +val blueLanguageModuleVersionCacheInitiallyAbsent = + !blueLanguageModuleVersionCache.exists() + base { archivesName.set("blue-bex-java") } repositories { - if (System.getenv("CI") == null) { - mavenLocal() - } + // Release and developer resolution intentionally share one policy. + // A same-GAV artifact from ~/.m2 must never masquerade as the published + // Blue Language artifact in standalone-published mode. mavenCentral() } @@ -35,8 +82,274 @@ tasks.withType().configureEach { options.release.set(8) } +tasks.withType().configureEach { + isPreserveFileTimestamps = false + isReproducibleFileOrder = true +} + +tasks.withType().configureEach { + javadocTool.set( + javaToolchains.javadocToolFor { + languageVersion.set(JavaLanguageVersion.of(8)) + } + ) + (options as StandardJavadocDocletOptions).apply { + encoding = "UTF-8" + charSet = "UTF-8" + addBooleanOption("notimestamp", true) + } +} + +fun sha256(file: File): String { + val digest = MessageDigest.getInstance("SHA-256") + file.inputStream().buffered().use { input -> + val buffer = ByteArray(8192) + while (true) { + val read = input.read(buffer) + if (read < 0) { + break + } + digest.update(buffer, 0, read) + } + } + return digest.digest().joinToString("") { "%02x".format(it) } +} + +fun sha256(bytes: ByteArray): String = + MessageDigest.getInstance("SHA-256") + .digest(bytes) + .joinToString("") { "%02x".format(it) } + +fun byteIdentical(left: File, right: File): Boolean { + if (left.length() != right.length()) { + return false + } + left.inputStream().buffered().use { leftInput -> + right.inputStream().buffered().use { rightInput -> + val leftBuffer = ByteArray(8192) + val rightBuffer = ByteArray(8192) + while (true) { + val leftRead = leftInput.read(leftBuffer) + val rightRead = rightInput.read(rightBuffer) + if (leftRead != rightRead) { + return false + } + if (leftRead < 0) { + return true + } + for (index in 0 until leftRead) { + if (leftBuffer[index] != rightBuffer[index]) { + return false + } + } + } + } + } +} + +fun writeEvidence(file: File, values: Map) { + file.parentFile.mkdirs() + file.writeText( + values.toSortedMap().entries.joinToString( + separator = "\n", + postfix = "\n" + ) { (key, value) -> "$key=$value" } + ) +} + +fun readEvidence(file: File): Map { + check(file.isFile) { + "Evidence file does not exist: ${file.canonicalPath}" + } + val properties = Properties() + file.inputStream().use(properties::load) + return properties.stringPropertyNames().associateWith { + properties.getProperty(it) + } +} + +fun commandBytes( + directory: File, + vararg command: String +): ByteArray { + val process = + ProcessBuilder(command.toList()) + .directory(directory) + .redirectErrorStream(true) + .start() + val output = + process.inputStream.buffered().use { + it.readBytes() + } + val exitCode = process.waitFor() + check(exitCode == 0) { + "Command failed ($exitCode): " + + command.joinToString(" ") + + "\n" + + String(output, StandardCharsets.UTF_8) + } + return output +} + +fun commandOutput(directory: File, vararg command: String): String = + String( + commandBytes(directory, *command), + StandardCharsets.UTF_8 + ) + +data class GitWorkspaceFingerprint( + val commit: String, + val dirty: Boolean, + val statusSha256: String, + val workspaceSha256: String, + val pathCount: Int +) + +fun splitNul(bytes: ByteArray): List { + val values = mutableListOf() + var start = 0 + for (index in bytes.indices) { + if (bytes[index].toInt() == 0) { + if (index > start) { + values.add( + String( + bytes, + start, + index - start, + StandardCharsets.UTF_8 + ) + ) + } + start = index + 1 + } + } + if (start < bytes.size) { + values.add( + String( + bytes, + start, + bytes.size - start, + StandardCharsets.UTF_8 + ) + ) + } + return values +} + +fun updateLength( + digest: MessageDigest, + length: Long +) { + digest.update( + ByteBuffer.allocate(8) + .putLong(length) + .array() + ) +} + +fun gitWorkspaceFingerprint( + directory: File +): GitWorkspaceFingerprint { + val root = directory.canonicalFile + val commit = + commandOutput(root, "git", "rev-parse", "HEAD") + .trim() + .lowercase() + check(commit.matches(Regex("[0-9a-f]{40}"))) { + "Source fingerprint requires an exact Git commit: $root" + } + val status = + commandBytes( + root, + "git", + "status", + "--porcelain", + "-z", + "--untracked-files=all" + ) + val listed = + commandBytes( + root, + "git", + "ls-files", + "-z", + "--cached", + "--others", + "--exclude-standard" + ) + val ignoredReleaseInputs = + commandBytes( + root, + "git", + "ls-files", + "-z", + "--others", + "--ignored", + "--exclude-standard", + "--", + ".github", + "docs", + "gradle", + "specifications", + "src" + ) + check(ignoredReleaseInputs.isEmpty()) { + "Source fingerprint rejects ignored release inputs: " + + splitNul(ignoredReleaseInputs).joinToString(", ") + } + val paths = splitNul(listed).sorted() + check(paths.isNotEmpty()) { + "Source fingerprint has no tracked or non-ignored files: $root" + } + val digest = MessageDigest.getInstance("SHA-256") + for (relativePath in paths) { + val pathBytes = + relativePath.toByteArray(StandardCharsets.UTF_8) + updateLength(digest, pathBytes.size.toLong()) + digest.update(pathBytes) + val source = File(root, relativePath) + .toPath() + .toAbsolutePath() + .normalize() + check(source.startsWith(root.toPath())) { + "Source path escapes checkout: $relativePath" + } + check( + Files.isRegularFile( + source, + LinkOption.NOFOLLOW_LINKS + ) + ) { + "Source fingerprint rejects missing, symlink, or " + + "non-regular path: $relativePath" + } + digest.update(byteArrayOf(1)) + val length = Files.size(source) + updateLength(digest, length) + Files.newInputStream(source).buffered().use { input -> + val buffer = ByteArray(8192) + while (true) { + val read = input.read(buffer) + if (read < 0) { + break + } + digest.update(buffer, 0, read) + } + } + } + return GitWorkspaceFingerprint( + commit, + status.isNotEmpty(), + sha256(status), + digest.digest().joinToString("") { + "%02x".format(it) + }, + paths.size + ) +} + dependencies { - api("blue.language:blue-language-java:3.0.0") + api(blueLanguageDeclaredCoordinate) testImplementation(platform("org.junit:junit-bom:5.10.2")) testImplementation("org.junit.jupiter:junit-jupiter") @@ -52,7 +365,7 @@ tasks.test { ) useJUnitPlatform() reports { - junitXml.required.set(false) + junitXml.required.set(true) html.required.set(true) } testLogging { @@ -61,9 +374,1412 @@ tasks.test { } } +val mainJar = tasks.named("jar") +val sourcesJarTask = tasks.named("sourcesJar") +val javadocJarTask = tasks.named("javadocJar") +val javadocTask = tasks.named("javadoc") +val sourceReleaseIncludes = + listOf( + ".cz.toml", + ".github/**", + "LICENSE", + "README.md", + "build.gradle.kts", + "docs/**", + "gradle.properties", + "gradle/**", + "gradlew", + "gradlew.bat", + "settings.gradle.kts", + "specifications/**", + "src/**" + ) +val sourceReleaseExcludes = + listOf( + ".git/**", + ".gradle/**", + ".idea/**", + "build/**", + "out/**", + "target/**", + "work-status.txt", + "*.zip", + "*.tar", + "*.tar.gz", + "*.tgz", + "*.7z", + "**/*.zip", + "**/*.tar", + "**/*.tar.gz", + "**/*.tgz", + "**/*.7z", + "**/*.class", + "**/*.log", + "**/*.tmp", + "**/*.bak", + "**/*.swp", + "**/*~", + ".DS_Store", + "**/.DS_Store" + ) +val sourceReleaseInputs = + fileTree(projectDir) { + include(sourceReleaseIncludes) + exclude(sourceReleaseExcludes) + } +val sourceReleaseRoot = + "${base.archivesName.get()}-${project.version}" +val sourceReleaseArchive by tasks.registering(Zip::class) { + group = "distribution" + description = + "Assembles the reproducible BEX source release from release inputs." + archiveBaseName.set(base.archivesName) + archiveVersion.set(project.version.toString()) + archiveClassifier.set("source-release") + destinationDirectory.set(layout.buildDirectory.dir("distributions")) + isPreserveFileTimestamps = false + isReproducibleFileOrder = true + from(sourceReleaseInputs) { + exclude("gradlew") + into(sourceReleaseRoot) + } + from("gradlew") { + into(sourceReleaseRoot) + filePermissions { + unix("rwxr-xr-x") + } + } +} +val rebuiltSourceReleaseArchive by tasks.registering(Zip::class) { + group = "verification" + description = + "Independently reassembles the source release from the same working-tree inputs." + archiveFileName.set(sourceReleaseArchive.flatMap { it.archiveFileName }) + destinationDirectory.set( + layout.buildDirectory.dir("reproducibility/source-release") + ) + isPreserveFileTimestamps = false + isReproducibleFileOrder = true + from(sourceReleaseInputs) { + exclude("gradlew") + into(sourceReleaseRoot) + } + from("gradlew") { + into(sourceReleaseRoot) + filePermissions { + unix("rwxr-xr-x") + } + } +} +val rebuiltMainJar by tasks.registering(Jar::class) { + group = "verification" + description = + "Repackages the current compiled main output for archive byte comparison." + archiveFileName.set(mainJar.flatMap { it.archiveFileName }) + destinationDirectory.set( + layout.buildDirectory.dir("reproducibility/main") + ) + from(sourceSets.main.get().output) +} +val rebuiltSourcesJar by tasks.registering(Jar::class) { + group = "verification" + description = + "Repackages the current source inputs for archive byte comparison." + archiveFileName.set(sourcesJarTask.flatMap { it.archiveFileName }) + destinationDirectory.set( + layout.buildDirectory.dir("reproducibility/sources") + ) + from(sourceSets.main.get().allSource) +} +val rebuiltJavadoc by tasks.registering(Javadoc::class) { + group = "verification" + description = + "Freshly regenerates the public Javadoc for deterministic comparison." + source = sourceSets.main.get().allJava + classpath = sourceSets.main.get().compileClasspath + destinationDir = + layout.buildDirectory.dir( + "reproducibility/javadoc-content" + ).get().asFile +} +val rebuiltJavadocJar by tasks.registering(Jar::class) { + group = "verification" + description = + "Packages freshly regenerated Javadoc for byte comparison." + dependsOn(rebuiltJavadoc) + archiveFileName.set(javadocJarTask.flatMap { it.archiveFileName }) + destinationDirectory.set( + layout.buildDirectory.dir("reproducibility/javadoc") + ) + from(rebuiltJavadoc.map { it.destinationDir }) +} + +val deterministicArchiveEvidence = + layout.buildDirectory.file( + "reports/bex-release/deterministic-archives.properties" + ) +val sourceReleaseEvidence = + layout.buildDirectory.file( + "reports/bex-release/source-release.properties" + ) +val sourceReleaseChecksum = + sourceReleaseArchive.flatMap { archive -> + archive.archiveFile.map { file -> + File(file.asFile.parentFile, "${file.asFile.name}.sha256") + } + } +val verifyDeterministicArchives by tasks.registering { + group = "verification" + description = + "Checks JAR packaging determinism and independently reassembles the source release from the same working tree; this is not an independent clean compilation or checkout." + dependsOn( + mainJar, + sourcesJarTask, + javadocJarTask, + javadocTask, + rebuiltMainJar, + rebuiltSourcesJar, + rebuiltJavadocJar, + sourceReleaseArchive, + rebuiltSourceReleaseArchive + ) + outputs.file(deterministicArchiveEvidence) + outputs.file(sourceReleaseEvidence) + outputs.file(sourceReleaseChecksum) + outputs.upToDateWhen { false } + doFirst { + deterministicArchiveEvidence.get().asFile.delete() + sourceReleaseEvidence.get().asFile.delete() + sourceReleaseChecksum.get().delete() + } + doLast { + val originalMain = mainJar.get().archiveFile.get().asFile + val rebuiltMain = rebuiltMainJar.get().archiveFile.get().asFile + val originalSources = + sourcesJarTask.get().archiveFile.get().asFile + val rebuiltSources = + rebuiltSourcesJar.get().archiveFile.get().asFile + val originalJavadoc = + javadocJarTask.get().archiveFile.get().asFile + val regeneratedJavadoc = + rebuiltJavadocJar.get().archiveFile.get().asFile + val originalSourceRelease = + sourceReleaseArchive.get().archiveFile.get().asFile + val rebuiltSourceRelease = + rebuiltSourceReleaseArchive.get().archiveFile.get().asFile + val originalMainHash = sha256(originalMain) + val rebuiltMainHash = sha256(rebuiltMain) + val originalSourcesHash = sha256(originalSources) + val rebuiltSourcesHash = sha256(rebuiltSources) + val originalJavadocHash = sha256(originalJavadoc) + val regeneratedJavadocHash = sha256(regeneratedJavadoc) + val originalSourceReleaseHash = sha256(originalSourceRelease) + val rebuiltSourceReleaseHash = sha256(rebuiltSourceRelease) + val sourceReleaseByteIdentity = + byteIdentical(originalSourceRelease, rebuiltSourceRelease) + check(originalMainHash == rebuiltMainHash) { + "Main JAR rebuild differs: $originalMainHash != $rebuiltMainHash" + } + check(originalSourcesHash == rebuiltSourcesHash) { + "Source JAR rebuild differs: $originalSourcesHash != $rebuiltSourcesHash" + } + check(originalJavadocHash == regeneratedJavadocHash) { + "Javadoc JAR rebuild differs: " + + "$originalJavadocHash != $regeneratedJavadocHash" + } + check(sourceReleaseByteIdentity) { + "Source-release ZIP replica is not byte-identical to the original" + } + check(originalSourceReleaseHash == rebuiltSourceReleaseHash) { + "Source-release ZIP replica differs: " + + "$originalSourceReleaseHash != $rebuiltSourceReleaseHash" + } + sourceReleaseChecksum.get().writeText( + "$originalSourceReleaseHash ${originalSourceRelease.name}\n" + ) + writeEvidence( + sourceReleaseEvidence.get().asFile, + mapOf( + "archive.bytes" to + originalSourceRelease.length().toString(), + "archive.path" to + originalSourceRelease + .relativeTo(projectDir) + .invariantSeparatorsPath, + "archive.sha256" to originalSourceReleaseHash, + "assembly" to + "two-independent-gradle-zip-tasks", + "byteIdentity" to sourceReleaseByteIdentity.toString(), + "checksum.path" to + sourceReleaseChecksum.get() + .relativeTo(projectDir) + .invariantSeparatorsPath, + "checksum.sha256" to + sha256(sourceReleaseChecksum.get()), + "excluded.patterns" to + sourceReleaseExcludes.joinToString(","), + "hashIdentity" to + (originalSourceReleaseHash == + rebuiltSourceReleaseHash).toString(), + "included.patterns" to + sourceReleaseIncludes.joinToString(","), + "independentCleanCheckout" to "false", + "replica.bytes" to + rebuiltSourceRelease.length().toString(), + "replica.path" to + rebuiltSourceRelease + .relativeTo(projectDir) + .invariantSeparatorsPath, + "replica.sha256" to rebuiltSourceReleaseHash, + "rootDirectory" to sourceReleaseRoot, + "schema" to + "blue-bex-source-release-evidence/1.0", + "scope" to + "independent-archive-assembly-from-the-same-working-tree-inputs", + "status" to "passed" + ) + ) + writeEvidence( + deterministicArchiveEvidence.get().asFile, + mapOf( + "main.original.path" to + originalMain.relativeTo(projectDir).invariantSeparatorsPath, + "main.original.sha256" to originalMainHash, + "main.rebuild.path" to + rebuiltMain.relativeTo(projectDir).invariantSeparatorsPath, + "main.rebuild.sha256" to rebuiltMainHash, + "sources.original.path" to + originalSources.relativeTo(projectDir).invariantSeparatorsPath, + "sources.original.sha256" to originalSourcesHash, + "sources.rebuild.path" to + rebuiltSources.relativeTo(projectDir).invariantSeparatorsPath, + "sources.rebuild.sha256" to rebuiltSourcesHash, + "javadoc.original.path" to + originalJavadoc.relativeTo(projectDir).invariantSeparatorsPath, + "javadoc.original.sha256" to originalJavadocHash, + "javadoc.rebuild.path" to + regeneratedJavadoc.relativeTo(projectDir).invariantSeparatorsPath, + "javadoc.rebuild.sha256" to regeneratedJavadocHash, + "javadoc.freshlyRegenerated" to "true", + "sourceRelease.original.path" to + originalSourceRelease + .relativeTo(projectDir) + .invariantSeparatorsPath, + "sourceRelease.original.sha256" to + originalSourceReleaseHash, + "sourceRelease.replica.path" to + rebuiltSourceRelease + .relativeTo(projectDir) + .invariantSeparatorsPath, + "sourceRelease.replica.sha256" to + rebuiltSourceReleaseHash, + "sourceRelease.byteIdentity" to + sourceReleaseByteIdentity.toString(), + "sourceRelease.hashIdentity" to + (originalSourceReleaseHash == + rebuiltSourceReleaseHash).toString(), + "sourceRelease.independentAssembly" to "true", + "sourceRelease.independentCleanCheckout" to "false", + "scope" to + "jar-packaging-determinism-and-source-release-reassembly-from-the-same-working-tree", + "independentCleanCompilation" to "false", + "status" to "passed" + ) + ) + } +} + +val cleanBuildArtifactEvidence = + layout.buildDirectory.file( + "reports/bex-release/clean-build-artifacts.properties" + ) +val invalidateCleanBuildArtifactEvidence by tasks.registering { + group = "verification" + description = + "Invalidates any prior clean-build receipt before artifact work starts." + outputs.upToDateWhen { false } + doLast { + cleanBuildArtifactEvidence.get().asFile.delete() + } +} +listOf( + mainJar, + sourcesJarTask, + javadocJarTask, + sourceReleaseArchive +).forEach { artifactTask -> + artifactTask.configure { + mustRunAfter(invalidateCleanBuildArtifactEvidence) + } +} +val writeCleanBuildArtifactHashes by tasks.registering { + group = "verification" + description = + "Records all four release hashes from one clean committed checkout." + dependsOn( + invalidateCleanBuildArtifactEvidence, + mainJar, + sourcesJarTask, + javadocJarTask, + sourceReleaseArchive + ) + outputs.file(cleanBuildArtifactEvidence) + outputs.upToDateWhen { false } + doFirst { + cleanBuildArtifactEvidence.get().asFile.delete() + } + doLast { + val checkout = + gitWorkspaceFingerprint(projectDir) + check(!checkout.dirty) { + "Clean-build evidence requires a completely clean checkout:\n" + + commandOutput( + projectDir, + "git", + "status", + "--short" + ) + } + val gitDirectory = + commandOutput( + projectDir, + "git", + "rev-parse", + "--absolute-git-dir" + ).trim() + val languageArtifacts = + configurations.compileClasspath.get() + .resolvedConfiguration + .resolvedArtifacts + .filter { + it.moduleVersion.id.group == "blue.language" && + it.name == "blue-language-java" && + it.extension == "jar" + } + check(languageArtifacts.size == 1) { + "Expected exactly one Blue Language dependency artifact" + } + val languageArtifact = languageArtifacts.single() + val compositeDirectory = + blueLanguageCompositePath + ?.let { file(it).canonicalFile } + val compositeFingerprint = + compositeDirectory + ?.let(::gitWorkspaceFingerprint) + if (blueLanguageDependencyMode == "local-composite") { + check(compositeDirectory != null) { + "Local-composite clean-build evidence requires a source path" + } + check(compositeFingerprint != null) { + "Local-composite source fingerprint is unavailable" + } + check(!compositeFingerprint.dirty) { + "Local-composite clean-build evidence rejects a dirty " + + "Blue Language checkout" + } + } + val artifacts = + mapOf( + "main" to mainJar.get().archiveFile.get().asFile, + "sources" to + sourcesJarTask.get().archiveFile.get().asFile, + "javadoc" to + javadocJarTask.get().archiveFile.get().asFile, + "sourceRelease" to + sourceReleaseArchive.get().archiveFile.get().asFile + ) + val values = + linkedMapOf( + "schema" to + "blue-bex-clean-build-artifacts/1.0", + "status" to "passed", + "commit" to checkout.commit, + "checkout.clean" to "true", + "checkout.root" to projectDir.canonicalPath, + "checkout.gitDirectory" to + File(gitDirectory).canonicalPath, + "checkout.gitStatusSha256" to + checkout.statusSha256, + "checkout.workspaceSha256" to + checkout.workspaceSha256, + "checkout.pathCount" to + checkout.pathCount.toString(), + "project.version" to project.version.toString(), + "dependency.mode" to blueLanguageDependencyMode, + "dependency.coordinate" to + blueLanguageDeclaredCoordinate, + "dependency.effectiveCoordinate" to + ( + languageArtifact.moduleVersion.id.group + + ":" + + languageArtifact.name + + ":" + + languageArtifact.moduleVersion.id.version + ), + "dependency.artifact.bytes" to + languageArtifact.file.length().toString(), + "dependency.artifact.sha256" to + sha256(languageArtifact.file), + "composite.path" to + (compositeDirectory?.path ?: ""), + "composite.commit" to + (compositeFingerprint?.commit ?: ""), + "composite.dirty" to + (compositeFingerprint?.dirty + ?.toString() + ?: "false"), + "composite.gitStatusSha256" to + ( + compositeFingerprint + ?.statusSha256 + ?: "" + ), + "composite.workspaceSha256" to + ( + compositeFingerprint + ?.workspaceSha256 + ?: "" + ), + "composite.pathCount" to + ( + compositeFingerprint + ?.pathCount + ?.toString() + ?: "0" + ) + ) + for ((name, artifact) in artifacts) { + values["artifact.$name.path"] = + artifact.relativeTo(projectDir) + .invariantSeparatorsPath + values["artifact.$name.bytes"] = + artifact.length().toString() + values["artifact.$name.sha256"] = + sha256(artifact) + } + writeEvidence( + cleanBuildArtifactEvidence.get().asFile, + values + ) + } +} + +val independentCleanBuildEvidence = + layout.projectDirectory.file( + ".gradle/bex-hosted-release/" + + "independent-clean-builds-" + + "$blueLanguageDependencyMode.properties" + ) +val verifyIndependentCleanBuildReproducibility by tasks.registering { + group = "verification" + description = + "Compares main, sources, Javadoc, and source-release hashes from two clean checkouts of the same commit." + val firstEvidencePath = + providers.gradleProperty("cleanBuildEvidenceOne") + val secondEvidencePath = + providers.gradleProperty("cleanBuildEvidenceTwo") + inputs.property( + "cleanBuildEvidenceOne", + firstEvidencePath.orElse("") + ) + inputs.property( + "cleanBuildEvidenceTwo", + secondEvidencePath.orElse("") + ) + outputs.file(independentCleanBuildEvidence) + outputs.upToDateWhen { false } + doFirst { + independentCleanBuildEvidence.asFile.delete() + } + doLast { + val firstPath = + firstEvidencePath.orNull?.trim() + ?.takeIf { it.isNotEmpty() } + ?.let(::file) + ?: throw GradleException( + "-PcleanBuildEvidenceOne is required" + ) + val secondPath = + secondEvidencePath.orNull?.trim() + ?.takeIf { it.isNotEmpty() } + ?.let(::file) + ?: throw GradleException( + "-PcleanBuildEvidenceTwo is required" + ) + check( + firstPath.canonicalFile != + secondPath.canonicalFile + ) { + "Independent-build evidence files must be distinct" + } + val first = readEvidence(firstPath) + val second = readEvidence(secondPath) + val currentCommit = + commandOutput( + projectDir, + "git", + "rev-parse", + "HEAD" + ).trim().lowercase() + val expectedSchema = + "blue-bex-clean-build-artifacts/1.0" + val authenticatedRoots = + linkedMapOf() + val authenticatedGitDirectories = + linkedMapOf() + val authenticatedFingerprints = + linkedMapOf() + for ((label, evidence) in + listOf("first" to first, "second" to second)) { + check(evidence["schema"] == expectedSchema) { + "$label clean-build evidence has the wrong schema" + } + check(evidence["status"] == "passed") { + "$label clean-build evidence did not pass" + } + check(evidence["checkout.clean"] == "true") { + "$label build was not produced from a clean checkout" + } + val recordedRoot = + evidence["checkout.root"] + ?.let(::File) + ?.takeIf(File::isAbsolute) + ?.canonicalFile + check(recordedRoot?.isDirectory == true) { + "$label checkout root is unavailable" + } + val actualRoot = + File( + commandOutput( + recordedRoot, + "git", + "rev-parse", + "--show-toplevel" + ).trim() + ).canonicalFile + check(actualRoot == recordedRoot) { + "$label checkout root is not its Git top level" + } + val recordedGitDirectory = + evidence["checkout.gitDirectory"] + ?.let(::File) + ?.takeIf(File::isAbsolute) + ?.canonicalFile + val actualGitDirectory = + File( + commandOutput( + recordedRoot, + "git", + "rev-parse", + "--absolute-git-dir" + ).trim() + ).canonicalFile + check( + recordedGitDirectory?.isDirectory == true + && recordedGitDirectory == + actualGitDirectory + ) { + "$label Git directory is unavailable" + } + val fingerprint = + gitWorkspaceFingerprint(recordedRoot) + check(!fingerprint.dirty) { + "$label checkout is no longer clean" + } + check( + evidence["checkout.workspaceSha256"] == + fingerprint.workspaceSha256 + ) { + "$label checkout source fingerprint changed" + } + check( + evidence["checkout.pathCount"] == + fingerprint.pathCount.toString() + ) { + "$label checkout source path count changed" + } + check( + evidence["checkout.gitStatusSha256"] == + fingerprint.statusSha256 + ) { + "$label checkout Git status fingerprint changed" + } + check(evidence["commit"] == currentCommit) { + "$label build commit ${evidence["commit"]} " + + "does not match $currentCommit" + } + check( + evidence["project.version"] == + project.version.toString() + ) { + "$label build used a different project version" + } + check( + evidence["dependency.mode"] == + blueLanguageDependencyMode + ) { + "$label build used a different dependency mode" + } + check( + evidence["dependency.coordinate"] == + blueLanguageDeclaredCoordinate + ) { + "$label build used a different declared dependency" + } + check( + evidence["dependency.effectiveCoordinate"] + ?.isNotEmpty() == true + ) { + "$label build has no effective dependency coordinate" + } + authenticatedRoots[label] = recordedRoot + authenticatedGitDirectories[label] = + actualGitDirectory + authenticatedFingerprints[label] = + fingerprint + } + check( + authenticatedRoots.getValue("first") != + authenticatedRoots.getValue("second") + ) { + "Independent builds used the same checkout root" + } + check( + authenticatedGitDirectories.getValue("first") != + authenticatedGitDirectories.getValue("second") + ) { + "Independent builds used the same Git directory" + } + check( + authenticatedFingerprints.getValue("first") + .workspaceSha256 == + authenticatedFingerprints.getValue("second") + .workspaceSha256 + ) { + "Independent builds used different source bytes" + } + check( + authenticatedFingerprints.getValue("first") + .pathCount == + authenticatedFingerprints.getValue("second") + .pathCount + ) { + "Independent builds used different source path sets" + } + val artifactNames = + listOf( + "main", + "sources", + "javadoc", + "sourceRelease" + ) + val values = + linkedMapOf( + "schema" to + "blue-bex-independent-clean-builds/1.0", + "status" to "passed", + "commit" to currentCommit, + "first.checkout.clean" to "true", + "second.checkout.clean" to "true", + "first.checkout.root" to + authenticatedRoots.getValue("first") + .canonicalPath, + "second.checkout.root" to + authenticatedRoots.getValue("second") + .canonicalPath, + "first.checkout.gitDirectory" to + authenticatedGitDirectories + .getValue("first") + .canonicalPath, + "second.checkout.gitDirectory" to + authenticatedGitDirectories + .getValue("second") + .canonicalPath, + "first.checkout.gitStatusSha256" to + authenticatedFingerprints + .getValue("first") + .statusSha256, + "second.checkout.gitStatusSha256" to + authenticatedFingerprints + .getValue("second") + .statusSha256, + "first.checkout.workspaceSha256" to + authenticatedFingerprints + .getValue("first") + .workspaceSha256, + "second.checkout.workspaceSha256" to + authenticatedFingerprints + .getValue("second") + .workspaceSha256, + "first.checkout.pathCount" to + authenticatedFingerprints + .getValue("first") + .pathCount.toString(), + "second.checkout.pathCount" to + authenticatedFingerprints + .getValue("second") + .pathCount.toString(), + "first.evidence.path" to + firstPath.canonicalPath, + "second.evidence.path" to + secondPath.canonicalPath, + "first.evidence.sha256" to sha256(firstPath), + "second.evidence.sha256" to sha256(secondPath), + "project.version" to project.version.toString(), + "dependency.mode" to + first.getValue("dependency.mode"), + "dependency.coordinate" to + first.getValue("dependency.coordinate"), + "dependency.effectiveCoordinate" to + first.getValue( + "dependency.effectiveCoordinate" + ), + "dependency.artifact.sha256" to + first.getValue( + "dependency.artifact.sha256" + ), + "composite.path" to + first.getValue("composite.path"), + "composite.commit" to + first.getValue("composite.commit"), + "composite.dirty" to + first.getValue("composite.dirty"), + "composite.gitStatusSha256" to + first.getValue( + "composite.gitStatusSha256" + ), + "composite.workspaceSha256" to + first.getValue( + "composite.workspaceSha256" + ), + "composite.pathCount" to + first.getValue("composite.pathCount") + ) + check( + first["dependency.mode"] == + second["dependency.mode"] + ) { + "Clean builds used different dependency modes" + } + check( + first["dependency.coordinate"] == + second["dependency.coordinate"] + ) { + "Clean builds used different dependency coordinates" + } + check( + first["dependency.effectiveCoordinate"] == + second["dependency.effectiveCoordinate"] + ) { + "Clean builds resolved different effective dependency coordinates" + } + check( + first["dependency.artifact.sha256"] + ?.matches(Regex("[0-9a-f]{64}")) == + true + ) { + "First build has no exact Language artifact hash" + } + check( + first["dependency.artifact.sha256"] == + second["dependency.artifact.sha256"] + ) { + "Clean builds resolved different Language artifacts" + } + val compositeKeys = + listOf( + "composite.path", + "composite.commit", + "composite.dirty", + "composite.gitStatusSha256", + "composite.workspaceSha256", + "composite.pathCount" + ) + for (key in compositeKeys) { + check(first[key] == second[key]) { + "Clean builds used different $key values" + } + } + if (blueLanguageDependencyMode == "local-composite") { + check( + first["composite.commit"] + ?.matches(Regex("[0-9a-f]{40}")) == + true + ) { + "Local-composite source commit is unavailable" + } + check( + first["composite.workspaceSha256"] + ?.matches(Regex("[0-9a-f]{64}")) == + true + ) { + "Local-composite source fingerprint is unavailable" + } + check( + first["composite.dirty"] == "false" + && second["composite.dirty"] == + "false" + ) { + "Local-composite clean builds require a clean " + + "Blue Language checkout" + } + check( + first["composite.gitStatusSha256"] == + sha256(ByteArray(0)) + ) { + "Local-composite Git status is not clean" + } + val activeCompositePath = + blueLanguageCompositePath + ?.let(::file) + ?.canonicalFile + val recordedCompositePath = + first["composite.path"] + ?.let(::File) + ?.canonicalFile + check( + activeCompositePath?.isDirectory == true + && recordedCompositePath == + activeCompositePath + ) { + "Local-composite source path changed" + } + val activeComposite = + gitWorkspaceFingerprint(activeCompositePath) + check( + !activeComposite.dirty + && activeComposite.commit == + first["composite.commit"] + && activeComposite.statusSha256 == + first["composite.gitStatusSha256"] + && activeComposite.workspaceSha256 == + first["composite.workspaceSha256"] + && activeComposite.pathCount.toString() == + first["composite.pathCount"] + ) { + "Local-composite source state changed" + } + } + for (artifactName in artifactNames) { + val key = "artifact.$artifactName.sha256" + val firstHash = first[key] + val secondHash = second[key] + check( + firstHash != null && + firstHash.matches(Regex("[0-9a-f]{64}")) + ) { + "First $artifactName hash is unavailable" + } + check(firstHash == secondHash) { + "$artifactName differs across clean builds: " + + "$firstHash != $secondHash" + } + values["artifact.$artifactName.sha256"] = + firstHash + values["artifact.$artifactName.byteIdentical"] = + "true" + } + writeEvidence( + independentCleanBuildEvidence.asFile, + values + ) + } +} + +val binaryApiEvidence = + layout.buildDirectory.file( + "reports/bex-release/binary-api.properties" + ) +val binaryApiManifest = + layout.buildDirectory.file( + "reports/bex-release/public-api.txt" + ) +val requiredBinaryApi = + layout.projectDirectory.file( + "src/test/resources/hosted-release/" + + "required-public-api.txt" + ) +val generateBinaryApiManifest by tasks.registering(JavaExec::class) { + group = "verification" + description = + "Generates a deterministic descriptor-level public/protected API manifest from the packaged JAR." + dependsOn(tasks.testClasses, mainJar) + classpath = sourceSets.test.get().runtimeClasspath + mainClass.set( + "blue.bex.conformance.BexBinaryApiManifestMain" + ) + javaLauncher.set( + javaToolchains.launcherFor { + languageVersion.set(JavaLanguageVersion.of(8)) + } + ) + doFirst { + setArgs( + listOf( + mainJar.get().archiveFile.get().asFile.absolutePath, + binaryApiManifest.get().asFile.absolutePath + ) + ) + } + inputs.file(mainJar.flatMap { it.archiveFile }) + outputs.file(binaryApiManifest) + outputs.upToDateWhen { false } +} +val binaryApiCheck by tasks.registering(Test::class) { + group = "verification" + description = + "Runs the BEX 2.0 API-surface checks against the packaged binary JAR." + dependsOn( + tasks.testClasses, + mainJar, + generateBinaryApiManifest + ) + testClassesDirs = sourceSets.test.get().output.classesDirs + classpath = + sourceSets.test.get().output + + files(mainJar.flatMap { it.archiveFile }) + + configurations.testRuntimeClasspath.get() + useJUnitPlatform() + include("**/Bex20ApiSurfaceTest.class") + javaLauncher.set( + javaToolchains.launcherFor { + languageVersion.set(JavaLanguageVersion.of(8)) + } + ) + reports.junitXml.required.set(true) + reports.html.required.set(true) + inputs.file(requiredBinaryApi) + outputs.file(binaryApiEvidence) + outputs.upToDateWhen { false } + doFirst { + binaryApiEvidence.get().asFile.delete() + } + doLast { + val artifact = mainJar.get().archiveFile.get().asFile + val manifest = binaryApiManifest.get().asFile + val required = requiredBinaryApi.asFile + check(manifest.isFile) { + "Binary API manifest is missing: $manifest" + } + check(required.isFile) { + "Required binary API signature set is missing: $required" + } + val actualSignatures = manifest.readLines() + .map { it.trimEnd() } + val requiredSignatures = required.readLines() + .map { it.trimEnd() } + check(actualSignatures == requiredSignatures) { + val firstDifference = + (0 until maxOf( + actualSignatures.size, + requiredSignatures.size + )).firstOrNull { index -> + actualSignatures.getOrNull(index) != + requiredSignatures.getOrNull(index) + } + "Packaged JAR public/protected API differs from the exact " + + "first-public BEX 2.0 baseline at line " + + "${firstDifference?.plus(1) ?: 1}:\n" + + "expected=" + + requiredSignatures.getOrNull(firstDifference ?: 0) + + "\nactual=" + + actualSignatures.getOrNull(firstDifference ?: 0) + } + writeEvidence( + binaryApiEvidence.get().asFile, + mapOf( + "artifact.path" to + artifact.relativeTo(projectDir).invariantSeparatorsPath, + "artifact.sha256" to sha256(artifact), + "manifest.path" to + manifest.relativeTo(projectDir).invariantSeparatorsPath, + "manifest.sha256" to sha256(manifest), + "manifest.schema" to + "blue-bex-binary-api-manifest/1.0", + "required.path" to + required.relativeTo(projectDir).invariantSeparatorsPath, + "required.sha256" to sha256(required), + "required.signatureCount" to + requiredSignatures.size.toString(), + "required.missingCount" to "0", + "required.unexpectedCount" to "0", + "required.comparison" to "exact-match", + "status" to "passed", + "testClass" to "blue.bex.api.Bex20ApiSurfaceTest" + ) + ) + } +} + +val java8BytecodeEvidence = + layout.buildDirectory.file( + "reports/bex-release/java8-bytecode.properties" + ) +val java8BytecodeCheck by tasks.registering { + group = "verification" + description = + "Verifies that every class in the packaged main JAR is Java 8 bytecode." + dependsOn(mainJar) + inputs.file(mainJar.flatMap { it.archiveFile }) + outputs.file(java8BytecodeEvidence) + outputs.upToDateWhen { false } + doFirst { + java8BytecodeEvidence.get().asFile.delete() + } + doLast { + val artifact = mainJar.get().archiveFile.get().asFile + val expectedMagic = "cafebabe" + val expectedMajor = 52 + val observedMajors = sortedSetOf() + val classCount = + ZipFile(artifact).use { archive -> + val classEntries = + archive.entries().asSequence() + .filter { + !it.isDirectory && + it.name.endsWith(".class") + } + .sortedBy { it.name } + .toList() + check(classEntries.isNotEmpty()) { + "Packaged main JAR contains no class entries: $artifact" + } + classEntries.forEach { entry -> + val header = ByteArray(8) + val bytesRead = + archive.getInputStream(entry).buffered().use { input -> + var offset = 0 + while (offset < header.size) { + val read = + input.read( + header, + offset, + header.size - offset + ) + if (read < 0) { + break + } + offset += read + } + offset + } + check(bytesRead == header.size) { + "Truncated class header in ${entry.name}: " + + "$bytesRead bytes" + } + val magic = + header.take(4).joinToString("") { + "%02x".format(it.toInt() and 0xff) + } + check(magic == expectedMagic) { + "Invalid class magic in ${entry.name}: $magic" + } + val major = + ((header[6].toInt() and 0xff) shl 8) or + (header[7].toInt() and 0xff) + observedMajors.add(major) + check(major == expectedMajor) { + "Non-Java-8 bytecode in ${entry.name}: " + + "major $major (expected $expectedMajor)" + } + } + classEntries.size + } + writeEvidence( + java8BytecodeEvidence.get().asFile, + mapOf( + "artifact.path" to + artifact.relativeTo(projectDir).invariantSeparatorsPath, + "artifact.sha256" to sha256(artifact), + "classCount" to classCount.toString(), + "expected.magic" to expectedMagic.uppercase(), + "expected.major" to expectedMajor.toString(), + "observed.magic" to expectedMagic.uppercase(), + "observed.major" to observedMajors.joinToString(","), + "schema" to + "blue-bex-java8-bytecode-evidence/1.0", + "status" to "passed" + ) + ) + } +} + +val benchmarkCompilationEvidence = + layout.buildDirectory.file( + "reports/bex-release/benchmark-compilation.properties" + ) +val benchmarkCompilationCheck by tasks.registering { + group = "verification" + description = + "Verifies that the compile-only local benchmark builds under Java 8; it does not run timing." + dependsOn(tasks.testClasses) + val benchmarkSource = + layout.projectDirectory.file( + "src/test/java/blue/bex/BexLocalBenchmarkTest.java" + ) + val benchmarkClass = + layout.buildDirectory.file( + "classes/java/test/blue/bex/BexLocalBenchmarkTest.class" + ) + inputs.file(benchmarkSource) + inputs.file(benchmarkClass) + outputs.file(benchmarkCompilationEvidence) + outputs.upToDateWhen { false } + doFirst { + benchmarkCompilationEvidence.get().asFile.delete() + } + doLast { + val source = benchmarkSource.asFile + val compiled = benchmarkClass.get().asFile + check(source.isFile) { + "Benchmark source is missing: $source" + } + check(compiled.isFile) { + "Benchmark did not compile to: $compiled" + } + writeEvidence( + benchmarkCompilationEvidence.get().asFile, + mapOf( + "class.path" to + compiled.relativeTo(projectDir).invariantSeparatorsPath, + "class.sha256" to sha256(compiled), + "source.path" to + source.relativeTo(projectDir).invariantSeparatorsPath, + "source.sha256" to sha256(source), + "status" to "passed", + "timingExecuted" to "false" + ) + ) + } +} + +val dependencyResolutionEvidence = + layout.buildDirectory.file( + "reports/bex-release/dependency-resolution.properties" + ) +val writeDependencyResolutionEvidence by tasks.registering { + group = "verification" + description = + "Resolves blue-language-java and verifies standalone artifacts against recorded Maven Central provenance." + outputs.file(dependencyResolutionEvidence) + outputs.upToDateWhen { false } + doFirst { + dependencyResolutionEvidence.get().asFile.delete() + } + doLast { + val matches = + configurations.compileClasspath.get() + .resolvedConfiguration + .resolvedArtifacts + .filter { + it.moduleVersion.id.group == "blue.language" && + it.name == "blue-language-java" && + it.extension == "jar" + } + check(matches.size == 1) { + "Expected exactly one blue-language-java compile artifact, found " + + matches.joinToString { it.file.absolutePath } + } + val artifact = matches.single() + val component = artifact.id.componentIdentifier + val compositeDirectory = + blueLanguageCompositePath + ?.let { file(it).canonicalFile } + val artifactHash = sha256(artifact.file) + val provenanceStatus: String + val moduleVersionCacheAcceptance: String + if (blueLanguageDependencyMode == "standalone-published") { + check( + publishedBlueLanguageCoordinate == + blueLanguageDeclaredCoordinate + ) { + "Recorded Maven Central coordinate differs from the " + + "declared dependency: $publishedBlueLanguageCoordinate" + } + check( + publishedBlueLanguageRepository == + "https://repo1.maven.org/maven2" + ) { + "Unrecognized recorded Maven Central provenance: " + + publishedBlueLanguageRepository + } + check(artifactHash == publishedBlueLanguageSha256) { + "Resolved standalone artifact does not match the recorded " + + "Maven Central SHA-256: $artifactHash != " + + publishedBlueLanguageSha256 + } + provenanceStatus = + "verified-against-recorded-maven-central-hash" + // This is deliberately narrower than claiming that the entire + // Gradle cache was clean. Project configuration captures whether + // this exact Blue Language module/version directory was absent; + // resolution above then verifies the resulting JAR against the + // source-controlled Maven Central hash. + moduleVersionCacheAcceptance = + if (blueLanguageModuleVersionCacheInitiallyAbsent) { + "passed" + } else { + "not-executed" + } + } else { + provenanceStatus = "not-applicable-local-composite" + moduleVersionCacheAcceptance = "not-executed" + } + writeEvidence( + dependencyResolutionEvidence.get().asFile, + mapOf( + "schema" to + "blue-bex-dependency-resolution-evidence/1.0", + "status" to "resolved", + "mode" to blueLanguageDependencyMode, + "declared.coordinate" to + blueLanguageDeclaredCoordinate, + "effective.component" to component.displayName, + "effective.group" to artifact.moduleVersion.id.group, + "effective.name" to artifact.name, + "effective.version" to + artifact.moduleVersion.id.version, + "artifact.path" to artifact.file.canonicalPath, + "artifact.bytes" to artifact.file.length().toString(), + "artifact.sha256" to artifactHash, + "composite.path" to + (compositeDirectory?.path ?: ""), + "repository.policy" to "maven-central-only", + "provenance.status" to provenanceStatus, + "provenance.recorded.repository" to + publishedBlueLanguageRepository, + "provenance.recorded.coordinate" to + publishedBlueLanguageCoordinate, + "provenance.recorded.sha256" to + publishedBlueLanguageSha256, + "provenance.networkFetchObservation" to + "not-exposed-by-gradle-resolution-api", + "cache.blueLanguageModuleVersionPath" to + blueLanguageModuleVersionCache.canonicalPath, + "cache.blueLanguageModuleVersionInitiallyAbsent" to + blueLanguageModuleVersionCacheInitiallyAbsent.toString(), + "cache.acceptance" to moduleVersionCacheAcceptance, + "cache.acceptanceScope" to + "standalone-published-blue-language-module-version-cache" + ) + ) + } +} + +val writeBexConformanceReport by tasks.registering(JavaExec::class) { + group = "verification" + description = "Writes truthful BEX 2.0 test, coverage, identity, and artifact evidence." + dependsOn( + tasks.testClasses, + mainJar, + sourcesJarTask, + verifyDeterministicArchives, + binaryApiCheck, + java8BytecodeCheck, + benchmarkCompilationCheck, + writeDependencyResolutionEvidence + ) + classpath = sourceSets.test.get().runtimeClasspath + mainClass.set("blue.bex.conformance.BexConformanceReportMain") + javaLauncher.set( + javaToolchains.launcherFor { + languageVersion.set(JavaLanguageVersion.of(8)) + } + ) + doFirst { + args( + project.layout.projectDirectory.asFile.absolutePath, + project.layout.buildDirectory.get().asFile.absolutePath, + gradle.gradleVersion, + project.version.toString(), + blueLanguageDependencyMode, + blueLanguageDeclaredCoordinate, + project.layout.projectDirectory + .dir(".gradle/bex-hosted-release") + .asFile + .absolutePath, + blueLanguageCompositePath + ?.let { file(it).canonicalPath } + .orEmpty() + ) + } + outputs.file( + layout.buildDirectory.file( + "reports/bex-conformance/report.json" + ) + ) + outputs.file( + layout.buildDirectory.file( + "reports/bex-conformance/report.md" + ) + ) + outputs.file( + layout.buildDirectory.file( + "reports/bex-conformance/release-readiness.properties" + ) + ) + outputs.upToDateWhen { false } +} +writeBexConformanceReport { + mustRunAfter(tasks.test) +} + +tasks.test { + finalizedBy(writeBexConformanceReport) +} + +tasks.register("bexConformanceReport") { + group = "verification" + description = "Runs all tests and produces the machine-readable BEX 2.0 conformance report." + dependsOn(tasks.test) +} + +val bexReleaseEvidence by tasks.registering { + group = "verification" + description = + "Runs tests, conformance, same-tree and independent-clean archive gates, binary API, Java 8 bytecode, benchmark compilation, and writes release evidence." + dependsOn(tasks.test, writeBexConformanceReport) + doLast { + val readiness = + layout.buildDirectory.file( + "reports/bex-conformance/release-readiness.properties" + ).get().asFile + check(readiness.isFile) { + "Hosted release readiness evidence was not generated" + } + val values = + readiness.readLines() + .filter { it.contains("=") } + .associate { + val separator = it.indexOf('=') + it.substring(0, separator) to + it.substring(separator + 1) + } + check(values["releaseReady"] == "true") { + "Hosted release evidence is incomplete: " + + (values["reason"] + ?: "see build/reports/bex-conformance/report.md") + } + } +} + +tasks.check { + dependsOn( + verifyDeterministicArchives, + binaryApiCheck, + java8BytecodeCheck, + benchmarkCompilationCheck + ) +} + val genResourcesDir = layout.buildDirectory.dir("generated-resources") val generateBuildProperties by tasks.registering { val buildPropertiesFile = genResourcesDir.map { it.file("blue/bex/build.properties") } + val sourceDateEpoch = + System.getenv("SOURCE_DATE_EPOCH")?.toLongOrNull() ?: 0L + val reproducibleBuildTimestamp = + Instant.ofEpochSecond(sourceDateEpoch).toString() + inputs.property("buildTimestamp", reproducibleBuildTimestamp) outputs.file(buildPropertiesFile) doLast { val file = buildPropertiesFile.get().asFile @@ -71,7 +1787,7 @@ val generateBuildProperties by tasks.registering { file.writeText( """ blue-bex-java.build.version=${project.version} - blue-bex-java.build.timestamp=${SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").format(Date())} + blue-bex-java.build.timestamp=$reproducibleBuildTimestamp """.trimIndent() ) } @@ -130,6 +1846,29 @@ publishing { } } +tasks.withType< + org.gradle.api.publish.maven.tasks.PublishToMavenRepository +>().configureEach { + dependsOn(bexReleaseEvidence) +} +tasks.withType< + org.gradle.api.publish.maven.tasks.PublishToMavenLocal +>().configureEach { + dependsOn(bexReleaseEvidence) +} +tasks.matching { + it.name in setOf( + "jreleaserAnnounce", + "jreleaserDeploy", + "jreleaserFullRelease", + "jreleaserPublish", + "jreleaserRelease", + "jreleaserUpload" + ) +}.configureEach { + dependsOn(bexReleaseEvidence) +} + if (System.getenv("CI") != null) { jreleaser { signing { diff --git a/docs/BEX_CONFORMANCE.md b/docs/BEX_CONFORMANCE.md new file mode 100644 index 0000000..129a780 --- /dev/null +++ b/docs/BEX_CONFORMANCE.md @@ -0,0 +1,149 @@ +# Blue BEX 2.0 conformance + +The executable BEX 2.0 release package is copied unchanged under: + +```text +src/test/resources/conformance/bex/ +``` + +`BexConformancePackageIntegrityTest` verifies the closed fixture schema, the +complete 147-file inventory, every LF-normalized byte length and SHA-256 +digest, all three package identities, the 60-vector reverse map, direct +coverage for all 86 operators, all 30 gas counters, and the exact runtime +registry files and BlueIds. + +`BexConformanceFixtureTest` executes all 105 manifest-declared behavior +fixtures. Explicit variants and complete `expected.cases` run independently. +The harness has no disabled-test, assumption, or skip path. Fixture +`additionalCase` metadata is not executable, as required by `HARNESS.md`. + +`BexGasMicrofixtureTest` executes all 30 named-counter microfixtures directly +against the public meter. Each test verifies the namespace, counter, sequence, +quantity, manifest weight, subtotal, reason, and trace-derived total. + +The JSON and Markdown reports also publish concrete exhaustion traces from +`BexPrimitiveExhaustionEvidenceTest`. Each example includes the exact +namespace, rejected counter, quantity, weight, admitted gas, effective budget, +rejected-charge absence, and zero later work. The numeric expectations are +source-controlled in +`src/test/resources/hosted-release/gas-exhaustion-trace-examples.properties`; +an example is marked passing only when its exact dynamic JUnit selector ran +and passed. + +The published baseline reconciliations are explicit: + +- the manifest count of 60 vectors and 105 behavior fixtures is authoritative; +- BEX-S-07 is a runtime uninitialized-binding failure; +- BEX-C-09 rejects recursion with `recursive-call-graph` before runtime; +- BEX-E-14's `result.identityB` expected value is a projection reference; +- `$findEntry` requires the canonical `index` in addition to the fixture's + `key`/`val` subset; +- BEX-G-09's `canonical-merge-sort` value is algorithm evidence backed by the + exact comparison trace. + +Run the complete test and evidence workflow with: + +```text +./gradlew bexConformanceReport \ + -PblueLanguageCompositePath=../blue-language-java +``` + +The composite path is explicit. Omitting it selects +`standalone-published`, which resolves the declared Blue Language coordinate +from Maven Central only. Dependency resolution never consults `mavenLocal`; +the resolved standalone JAR must match the coordinate, repository provenance, +and SHA-256 recorded in +`src/test/resources/hosted-release/published-api-inspection.properties`. + +The test task always finalizes by writing: + +```text +build/reports/bex-conformance/report.json +build/reports/bex-conformance/report.md +``` + +The report is deterministic for a fixed source state, test result set, and +artifacts. It reports the current commit and dirty-worktree flag, Java and +Gradle versions, fixture/registry/gas identities, actual JUnit XML counts, +operator and counter matrices, cache and representation matrices, recursion +and finite-loop evidence, and SHA-256 hashes only for current-version +artifacts. Failed, skipped, or unexecuted evidence remains visibly so; a +declaration is never reported as an execution. + +Normative-vector passing totals are derived from the status of every mapped +behavior or gas fixture in `vector-coverage.yaml`. The report does not turn a +passing inventory-integrity test into a hardcoded `60/60` execution claim. + +`verifyDeterministicArchives` is an archive-packaging determinism gate. It +repackages the same compiled main output and source inputs and independently +regenerates Javadoc content before byte comparison. It does not claim a +second clean compilation. + +`writeCleanBuildArtifactHashes` is intentionally stricter: it runs only from a +completely clean checkout and records the commit, dependency mode, version, +and main, sources, Javadoc, and source-release hashes. Run it in two clean +checkouts of the same commit with the non-snapshot CI version and one source +epoch. Every `GRADLE_USER_HOME` below must be a distinct fresh empty directory. +Then compare the two property files with: + +```bash +export CI=true +export SOURCE_DATE_EPOCH="$(git show -s --format=%ct HEAD)" + +(cd /first/clean/blue-bex-java && \ + GRADLE_USER_HOME=/tmp/blue-bex-gradle-one \ + ./gradlew --no-daemon clean test writeCleanBuildArtifactHashes) + +(cd /second/clean/blue-bex-java && \ + GRADLE_USER_HOME=/tmp/blue-bex-gradle-two \ + ./gradlew --no-daemon clean test writeCleanBuildArtifactHashes) + +./gradlew verifyIndependentCleanBuildReproducibility \ + -PcleanBuildEvidenceOne=/first/clean/blue-bex-java/build/reports/bex-release/clean-build-artifacts.properties \ + -PcleanBuildEvidenceTwo=/second/clean/blue-bex-java/build/reports/bex-release/clean-build-artifacts.properties +``` + +Both evidence producers must use the same dependency mode. The publication +pair uses standalone-published mode. To prove local-composite packaging +separately, run another two-clean-checkout pair with the same explicit +`-PblueLanguageCompositePath=/absolute/path/to/clean/blue-language-java` +argument on both builds; never compare one build from each mode. + +The combined evidence is commit-bound and stale evidence fails closed. The +conformance report also requires its own four artifacts to match the hashes +from both clean builds. After this comparison exists, record both modes and +make the final decision in the reporting checkout: + +```bash +export CI=true +export SOURCE_DATE_EPOCH="$(git show -s --format=%ct HEAD)" + +GRADLE_USER_HOME=/tmp/blue-bex-standalone-mode \ + ./gradlew --no-daemon clean test +GRADLE_USER_HOME=/tmp/blue-bex-local-mode \ + ./gradlew --no-daemon clean test \ + -PblueLanguageCompositePath=/absolute/path/to/clean/blue-language-java +GRADLE_USER_HOME=/tmp/blue-bex-final-standalone \ + ./gradlew --no-daemon clean bexReleaseEvidence +``` + +The release and RC workflows perform that complete sequence before any +publication command and archive `build/reports`, `build/distributions`, test +results, and persistent mode evidence. + +`binaryApiCheck` writes +`build/reports/bex-release/public-api.txt`, a deterministic +public/protected descriptor manifest of the packaged JAR, then fails closed +unless it exactly equals the source-controlled first-public BEX 2.0 baseline +in `src/test/resources/hosted-release/required-public-api.txt`. Missing, +changed, reordered, or unexpected public/protected signatures all fail. The +generated manifest hash is identity evidence; exact line equality is the API +compatibility claim. + +A module-specific cache acceptance is reported separately for the exact +`blue.language:blue-language-java:3.1.0-rc.19` Gradle module-version path. +Standalone acceptance passes only when that exact path was absent at project +configuration and the subsequently resolved JAR matches the recorded Maven +Central hash. It does not claim that the entire Gradle cache was empty or that +a network fetch was directly observed. Cached local-composite runs remain +`not-executed` for this acceptance. diff --git a/docs/FIXTURES.md b/docs/FIXTURES.md index b6f1c03..95378e5 100644 --- a/docs/FIXTURES.md +++ b/docs/FIXTURES.md @@ -1,187 +1,192 @@ -# BEX Rich Fixture Format +# Blue BEX 2.0 Fixture Format -Rich fixtures are portable YAML test cases for BEX implementations. They live +The normative fixtures use the closed `blue-bex-fixture/2.0` schema and live under: ```text -src/test/resources/rich-fixtures/ +src/test/resources/conformance/bex/fixtures/ ``` -The Java runner rejects unknown fixture fields so typos do not silently weaken a -conformance test. - -## Root Fields - -Allowed root fields: - -| Field | Required | Meaning | -| --- | --- | --- | -| `fixtureId` | yes | Stable fixture identifier. | -| `title` | yes | Human-readable fixture title. | -| `targetStatus` | no | Informational status used while migrating fixtures. | -| `tags` | no | List of grouping tags. | -| `context` | no | Execution context data. | -| `blueDefinitions` | no | Fixture-local BlueId provider definitions. | -| `gasSchedule` | no | Per-fixture gas schedule overrides. | -| `programSource` | yes | Blue YAML source for the BEX program. | -| `expectation` | yes | Expected outcome and assertions. | - -Example: - -```yaml -fixtureId: BEX-EXAMPLE-001 -title: Const returns declared value -tags: - - constants -programSource: | - type: Blue/BEX Program - constants: - amount: 400 - expr: - $const: amount -expectation: - outcome: success - resultSimple: 400 -``` - -`tags` must be a list of non-empty text values. `targetStatus`, when present, -must be one of: +The authoritative files are: ```text -current-pass -current-compile-error -current-runtime-error -current-output-conversion-error -current-parse-error -current-parse-error-or-output-conversion-error -current-gas-property +fixture-schema.yaml +HARNESS.md +manifest.yaml +operator-coverage.yaml +projection-catalog.yaml +vector-coverage.yaml ``` -## Context - -Allowed `context` fields: - -| Field | Meaning | -| --- | --- | -| `documentScope` | Current document scope path. Defaults to `/`. | -| `rootDocumentSource` | Blue YAML for the canonical/resolved root document. Defaults to `{}`. | -| `eventSource` | Blue YAML for the event binding. Defaults to `{}`. | -| `currentContractSource` | Blue YAML for the current contract binding. Defaults to `{}`. | -| `stepsBinding` | Map of step names to simple step-result values. | -| `gasLimit` | Execution gas limit. Defaults to `1000000`. | -| `bindings` | Additional host bindings as simple YAML values. | +Unknown fixture fields, context bindings, operators, assertion projections, or +intrinsic types fail closed. No normative fixture may be skipped. -Document pointers are resolved using `documentScope`. Value-local pointers such -as `$event`, `$currentContract`, `$steps`, `$binding`, `$pointerGet`, and -`$pointerSet` are resolved inside the selected value. +## Root Shape -## Blue Definitions - -Use `blueDefinitions` when a fixture references custom BlueIds: +Every fixture requires: ```yaml -blueDefinitions: - HotelOrderType: | - status: - type: Text +schema: blue-bex-fixture/2.0 +id: example-id +vectors: +- BEX-E-01 +category: e +program: + expr: 1 +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + result: 1 ``` -The fixture runner exposes each key as a Blue provider entry. This keeps -fixtures portable and avoids Java-only hardcoded provider behavior. +The closed categories are `c`, `e`, `g`, `gas`, `h`, `operator`, `r`, and `s`. +`vectors` binds the fixture to one or more normative specification vectors. -## Gas Schedule +## Context -Allowed `gasSchedule` override fields: +The context may supply exact values for: ```text -expressionBase -statementBase -documentRead -eventRead -stepsRead -currentContractRead -varRead -resultValueRead -pointerGetBase -pointerSetBase -objectSetBase -appendChangeBase -appendEventBase -forEachItem -functionCall +rootDocument +event +processingEvent +currentContract +steps +bindings ``` -Any omitted field uses the default schedule documented in -[GAS.md](GAS.md). +It may also supply: -## Outcomes +- `documentScope`, the current Contracts scope; +- `provider`, verified exact BlueId entries; +- `parentRemainingGas`, the live host budget; +- `gasLimit`, a BEX-local limit that may only reduce that budget; +- `directCounterFixture`, used only by exact named-counter microfixtures. -Allowed `expectation.outcome` values: +Context values can be inline or reference-backed. Representation, cache state, +and provider segmentation must not change result or gas. -| Outcome | Meaning | -| --- | --- | -| `success` | Program compiles and executes successfully. | -| `compile-error` | Program parses but BEX compilation fails. | -| `runtime-error` | Program compiles but execution fails. | -| `parse-error` | Blue YAML parsing fails. | -| `output-conversion-error` | Execution succeeds, but converting the output to a Blue node/frozen node fails. | -| `parse-error-or-output-conversion-error` | Either parse or output conversion failure is acceptable for strict Blue authoring edge cases. | -| `gas-property` | Fixture asserts a named gas property rather than exact output. | +## Expected Results -For `success`, allowed expectation fields are: +The closed expected-result fields include: ```text -outcome -resultSimple -changeset +compileStatus +result +changes events -gasUsed +errorClass +gasTrace +totalGas +assertions +variants +cases +additionalCase +reason ``` -For `compile-error`, `runtime-error`, `parse-error`, -`output-conversion-error`, and `parse-error-or-output-conversion-error`, allowed -fields are: +`expected.cases` contains complete executable subcases. Metadata such as +`additionalCase` is not executable. + +Assertions use a path from `projection-catalog.yaml` and one of: ```text -outcome -errorContains +equals +notEquals +absent +present +contains +notContains +lessThan +greaterThan +sameAcrossVariants +all +none ``` -For `gas-property`, allowed fields are: +A missing or unknown projection is a harness error unless the fixture +explicitly asserts `absent`. -```text -outcome -property -``` +## Representation Variants + +Each variant is an explicit independent transformation. Supported axes include: -## Exact Gas +- root form: inline, reference, eager, lazy, or materialized; +- cache: warm or cold; +- provider batching: batched or unbatched; +- exact raw root JSON; +- internal delivery classification. -Success fixtures may assert exact gas: +`sameAcrossVariants` compares the semantic result and requested canonical trace +projections. Variant labels alone do not imply behavior. + +## Exact Named Gas + +Gas expectations use the ordered named ledger, never an opaque aggregate +accepted from the implementation: ```yaml -expectation: - outcome: success - gasUsed: 10 +expected: + gasTrace: + - sequence: 0 + counter: expressionEvaluated + quantity: 3 + weight: 1 + gas: 3 + totalGas: 3 ``` -Runtime gas exhaustion fixtures usually set a low context limit: +A gas microfixture supplies one `directCounterFixture`: ```yaml context: - gasLimit: 2 -expectation: - outcome: runtime-error - errorContains: BEX gas exhausted + directCounterFixture: + counter: expressionEvaluated + quantity: 3 ``` -## Manifest +The harness verifies namespace, counter name, sequence, quantity, manifest +weight, subtotal, reason, and the trace-derived total. The 30 microfixtures +cover every counter in the exact BEX 2.0 gas manifest. + +For exhaustion fixtures, `parentRemainingGas` is the host budget and +`gasLimit` is an optional lowering sub-limit. The failed charge must be absent, +the admitted trace prefix must remain exact, and buffered effects must not +commit. -The fixture suite has a machine-readable manifest at: +## Output and Error Phases + +The harness distinguishes compile, runtime, gas, representation, and Blue +boundary failures. Compile errors occur before runtime counters or effects. +Output admission validates Blue Language 1.0 before identity calculation, +including numeric-kind preservation and rejection of undefined list slots. + +## Package Integrity + +`manifest.yaml` inventories every fixture and support file with its normalized +byte length and SHA-256 digest. It binds the exact BEX runtime registry, gas +manifest, vector map, and operator map. + +The implementation-baseline identities are: ```text -src/test/resources/rich-fixtures/manifest.yaml +runtime registry: + sha256:23d282ec1c0bb016263922b1b49c369fdd537efdcf23e005eceeb888d7763fe1 +gas manifest: + sha256:41247c820d91a12fdfc17fd9e787a5d8d668d8acc5954fdcb131715bf9e6147d +fixture package: + sha256:f5f64a38152ef0e50ebb1b03caaa1b07fd556552eb071b940937079fc0234dfe ``` -The manifest records the suite name, version, fixture root, required -directories, gas-model document, fixture-format document, and fixture counts. -It is not itself executed as a fixture. +Run the complete package and generate machine-readable evidence with: + +```text +./gradlew bexConformanceReport \ + -PblueLanguageCompositePath=../blue-language-java +``` diff --git a/docs/GAS.md b/docs/GAS.md index bd306a0..bdc93f9 100644 --- a/docs/GAS.md +++ b/docs/GAS.md @@ -1,298 +1,228 @@ -# BEX Gas Model +# BEX 2.0 Gas Model -BEX gas is deterministic execution accounting. It is intended to make one -implementation's runtime behavior portable enough for conformance tests, not to -model every CPU or memory cost. +BEX 2.0 meters deterministic logical work. It does not meter serialized bytes, +recursive value size, cache state, or physical Blue representation. -Compilation does not consume gas. Every execution starts at `0`. Each runtime -charge adds to the total. If `gasLimit >= 0` and the total becomes greater than -the limit, execution throws: +The normative sources are: ```text -BEX gas exhausted at gas units +specifications/blue-bex-specification-2.0.md +src/test/resources/conformance/bex/gas-manifest.yaml ``` -## Default Schedule - -| Field | Default | -| --- | ---: | -| `expressionBase` | 1 | -| `statementBase` | 1 | -| `documentRead` | 2 | -| `eventRead` | 1 | -| `stepsRead` | 1 | -| `currentContractRead` | 1 | -| `varRead` | 1 | -| `resultValueRead` | 2 | -| `pointerGetBase` | 1 | -| `pointerSetBase` | 3 | -| `objectSetBase` | 2 | -| `appendChangeBase` | 5 | -| `appendEventBase` | 5 | -| `forEachItem` | 1 | -| `functionCall` | 2 | - -Every function invocation charges `functionCall`. This includes the root program -function, so a trivial root expression costs at least `2 + expressionBase`. - -Every evaluated expression charges `expressionBase`. Every executed statement -charges `statementBase`. Source-path wrappers and other diagnostics wrappers do -not charge gas. - -## Reads - -Read operators charge their read cost in addition to `expressionBase`: - -| Operator | Cost | -| --- | --- | -| `$document` | `expressionBase + documentRead` | -| `$event` | `expressionBase + eventRead` | -| `$currentContract` | `expressionBase + currentContractRead` | -| `$steps` | `expressionBase + stepsRead` | -| `$binding` | `expressionBase + varRead` | -| `$var` | `expressionBase + varRead` | -| `$resultValue` | `expressionBase + resultValueRead` | - -Canonical and resolved document reads currently cost the same. - -Path-aware `$var` and `$const` object forms do not add a separate read charge -for static paths. If the `path` operand is dynamic, the path expression consumes -its normal expression gas before the value-local pointer read. - -`$kind` and `$isKind` charge their normal expression tree costs only: - -```text -$kind = expressionBase + gas for value expression -$isKind = expressionBase + gas for val expression -``` - -`$isKind.kind` is static authored data and does not consume expression gas. - -## Pointer And Object Updates - -`$pointerGet` charges: - -```text -expressionBase -+ gas for object expression -+ pointerGetBase -+ numberOfPathSegments -+ gas for default expression only if default is used -``` - -`$pointerSet` charges: +The schedule identifier is `blue-bex/gas/2.0`. The implementation-baseline +manifest identity is: ```text -expressionBase -+ gas for val expression, unless op is remove -+ gas for object expression -+ pointerSetBase -+ numberOfPathSegments -+ estimatedSize(val) +sha256:41247c820d91a12fdfc17fd9e787a5d8d668d8acc5954fdcb131715bf9e6147d ``` -For `remove`, `val` is not evaluated and `estimatedSize(undefined) = 0`. - -`$objectSet` charges: - -```text -expressionBase -+ gas for val expression -+ objectSetBase -+ estimatedSize(val) -+ gas for object expression -``` +## Closed Counter Vocabulary -Static keys and paths do not consume expression gas. Dynamic keys and paths do, -because their expressions are evaluated. +The portable BEX child ledger contains exactly these 30 counters: -## Append And Output +| Counter | Weight | Logical work | +| --- | ---: | --- | +| `expressionEvaluated` | 1 | One executed expression. | +| `statementExecuted` | 1 | One executed statement. | +| `functionCalled` | 2 | The root invocation or one user-function invocation. | +| `intrinsicCalled` | 5 | One registry-bound intrinsic invocation. | +| `documentRead` | 2 | One document read operation. | +| `eventRead` | 1 | One event read operation. | +| `processingEventRead` | 1 | One processing-event read operation. | +| `currentContractRead` | 1 | One current-contract read operation. | +| `stepsRead` | 1 | One steps read operation. | +| `bindingRead` | 1 | One host-binding read operation. | +| `variableRead` | 1 | One local-variable read operation. | +| `constantRead` | 1 | One program-constant read operation. | +| `resultValueRead` | 2 | One accumulated-result-overlay read. | +| `pointerSegmentRead` | 1 | One examined pointer segment. | +| `pointerSegmentWritten` | 1 | One traversed or created write segment. | +| `objectMemberRead` | 1 | One direct object-member read. | +| `listItemRead` | 1 | One direct list-position read. | +| `collectionItemVisited` | 1 | One input item evaluated by collection work. | +| `collectionItemProduced` | 1 | One result item produced by collection work. | +| `textBlockExamined` | 1 | One examined block of up to 64 Unicode code points. | +| `textBlockConstructed` | 1 | One constructed block of up to 64 Unicode code points. | +| `integerLimbOperation` | 1 | One canonical base-`2^32` limb-work unit. | +| `comparisonNodeVisited` | 1 | One semantic node occurrence compared or matched. | +| `sortComparison` | 1 | One canonical stable-merge-sort comparator call. | +| `patchAppended` | 5 | One validated patch appended. | +| `eventAppended` | 5 | One non-undefined event appended. | +| `transientObjectMemberProduced` | 1 | One retained transient object member. | +| `transientListItemProduced` | 1 | One transient list item. | +| `blueOutputBoundary` | 5 | One value admitted at a Blue output boundary. | +| `nodeIdentityRequested` | 5 | One explicit `$nodeBlueId` request. | -`$appendChange` charges: +`BexGasCounter` is the closed Java vocabulary. `BexGasSchedule` exposes the +manifest weights and supports deterministic overrides by these same names. -```text -statementBase -+ gas for val expression if op is add or replace -+ appendChangeBase -+ estimatedSize(val) -``` +## Live Parent-Bounded Ledger -For `remove`, `val` is not evaluated and size is `0`. +Before execution, the host supplies a child ledger bounded by its exact +remaining budget. Every charge is admitted before the associated work. A +charge that would exceed the effective budget is absent from the trace and the +work does not begin. -`$appendChanges` charges: +A BEX-local `gasLimit` may only lower the available parent budget: ```text -statementBase -+ gas for list expression -+ for each patch: appendChangeBase + estimatedSize(val) +effectiveBudget = min(parentRemainingGas, localGasLimit) ``` -`$appendEvent` charges: +The child ledger is merged into the parent exactly once and in trace order. +Already admitted charges remain observable on deterministic failure. Buffered +patches, events, and output are discarded when execution fails or exhausts gas. +Transient provider unavailability suspends outside completed execution and +does not commit a child ledger. -```text -statementBase -+ gas for event expression -+ appendEventBase -+ estimatedSize(event) -``` +Portable evidence is the ordered named trace. `gasUsed()` and `totalGas()` are +convenience projections derived from that trace; no API accepts an opaque +aggregate gas integer as portable evidence. -`$appendEvents` charges: +## Evaluation and Lazy Work -```text -statementBase -+ gas for list expression -+ for each event: appendEventBase + estimatedSize(event) -``` +Every executed expression charges `expressionEvaluated`. Every executed +statement charges `statementExecuted`. The root program and each called user +function charge `functionCalled`. -## Control Flow +Skipped work charges nothing. This includes: -`$if` charges `statementBase`, then the condition expression, then only the -selected branch. +- unselected branches; +- operands skipped by lazy boolean/coalescing operators; +- collection items after a short circuit; +- `val` for a `remove` patch; +- unused lazy expressions. -`$forEach` charges `statementBase`, the input expression, `forEachItem` for each -iterated item, then the body statements for each iteration. +A statically compiled constant is not reconstructed on each read. Its read +still charges `constantRead`, in addition to the expression charge. -`$returnIf` charges `statementBase`, then the condition expression. Its `expr` -operand is evaluated only when the condition is truthy. +## Reads and Pointers -`$failIf` charges `statementBase`, then the condition expression. Its `message` -operand is evaluated only when the condition is truthy. +Context reads charge their corresponding named read counter. Each examined +pointer segment charges `pointerSegmentRead`, and the semantic member or +position examined also charges `objectMemberRead` or `listItemRead`. -`$let.vars` charges `statementBase`. In unordered form, all binding expressions -are evaluated before any slot is assigned. In ordered form, each binding -expression is evaluated and assigned in the explicit order. +`$pointerSet` charges `pointerSegmentWritten` for every traversed or created +segment. It charges transient production only for structure it actually +creates. The assigned value is never recursively sized, cloned, or rehashed. -Collection query bindings are restored after `$map`, `$filter`, `$flatMap`, -`$some`, `$find`, `$findEntry`, and `$reduce` finish. Capturing and restoring -slots is deterministic bookkeeping and does not add gas beyond the expression -and item charges below. +BEX owns the access it requests. The same logical member read is not charged +again as Contracts semantic work; Blue validation and identity establishment +remain host semantic work. -`$and`, `$or`, `$coalesce`, `$some`, `$find`, `$findEntry`, and `$includes` -short-circuit. Unevaluated operands or collection items consume no gas. +## Text and Numeric Work -Collection expressions `$map`, `$filter`, `$flatMap`, `$reduce`, `$some`, -`$find`, and `$findEntry` charge: +Text work uses Unicode code points, not UTF-16 code units. A block contains up +to 64 code points: ```text -expressionBase -+ gas for input expression -+ forEachItem for each evaluated item -+ gas for the query/body expression for each evaluated item +fullScan(t) = ceil(codePointLength(t) / 64) +construction(t) = ceil(constructedCodePointLength(t) / 64) ``` -`$reduce` also charges the initializer expression once after the input -expression. - -`$objectFromEntries` charges: - -```text -expressionBase -+ gas for entries expression -+ forEachItem for each input entry -``` +Comparisons charge only the blocks actually read through the first difference +or the end of the shorter operand. -`$includes` charges: +Integer work uses an unsigned base-`2^32` magnitude with a separate sign. Let +`L(x)` be at least one and otherwise the magnitude limb count: -```text -expressionBase -+ gas for list expression -+ gas for val expression -+ forEachItem for each compared list item -``` +| Operation | `integerLimbOperation` quantity | +| --- | ---: | +| equality or ordering | `L(a) + L(b)` | +| addition or subtraction | `max(L(a), L(b)) + 1` | +| multiplication | `L(a) * L(b)` | +| division or remainder | `L(a) * L(b)` | -`$hasKey` charges: +Exact decimal operations use the same formula over unscaled Integer magnitudes +and add one operation for scale alignment. -```text -expressionBase -+ gas for object expression -+ gas for key expression only when key is dynamic -``` +## Collections, Equality, and Sorting -`$intrinsic` charges: +Collection operators charge `collectionItemVisited` once per evaluated input +item. Output-producing operators additionally charge +`collectionItemProduced` once per produced item. Constructing a new object or +list charges the corresponding transient production counter. A single logical +iteration is not double-counted. -```text -expressionBase -+ gas for each evaluated payload field expression -+ gas charged explicitly by the registered intrinsic processor -``` +Deep equality and pattern matching charge `comparisonNodeVisited` once per +semantic node occurrence. Known exact Node BlueIds may conclude equality after +one visited comparison node. Text and numeric content add their corresponding +block or limb work. -`$intrinsic.type` is static authored Blue data and does not consume expression -gas. Unsupported intrinsic BlueIds fail during compilation before execution -starts. Payload fields that evaluate to `undefined` are omitted after their -normal expression gas has been charged. +Sorting uses the canonical trace of a stable bottom-up merge sort: initial run +width one, left-to-right merges, doubled width after each pass, and left +selection on equality. Every comparator call charges `sortComparison`, +`comparisonNodeVisited`, and any scalar-content work. -Function calls charge the caller expression or statement normally, then the -called function invocation charges `functionCall`. Argument expressions are -charged before entering the callee. +## Patches, Events, Output, and Identity -## Size Estimator +`$appendChange` charges `patchAppended` once after validation and required +operand evaluation. `$appendChanges` applies the same rule per entry. +`$appendEvent` charges `eventAppended` once after evaluating a non-undefined +event, and `$appendEvents` applies it per event. -`estimatedSize(value)` is: +Every value crossing a Blue boundary charges `blueOutputBoundary` once. +Existing exact nodes retain their exact identity and incur no recursive +construction or size charge. Transient values pay the logical construction, +validation, and host identity work they actually require. -| Value | Size | -| --- | ---: | -| `undefined` | 0 | -| `null` | 0 | -| scalar | `max(1, length(value.asText()))` | -| list | `list.size + sum(estimatedSize(item))` | -| object | `numberOfKeys + sum(length(key)) + sum(estimatedSize(valueForKey))` | +`$nodeBlueId` charges `nodeIdentityRequested`. For a transient operand it also +crosses the Blue boundary and performs Contracts semantic identity +establishment. That identity work is merged once. -Examples: +There is deliberately no `estimatedSize` counter or replacement based on +serialized payload bytes. Passing a large exact value through a variable, +function, patch, event, or output does not scan it. Content is charged only +when it is inspected, compared, constructed, iterated, sorted, validated, or +identified. -```text -estimatedSize("x") = 1 -estimatedSize("") = 1 -estimatedSize(null) = 0 -estimatedSize("hello") = 5 -estimatedSize(12345) = 5 -estimatedSize(true) = 4 -estimatedSize(["a", "bb"]) = 2 + 1 + 2 = 5 -estimatedSize({ a: "x", bb: "yy" }) = 2 + 1 + 1 + 2 + 2 = 8 -``` +## Intrinsics -Frozen values may be cached by BlueId and runtime values may be cached by -identity, but caching does not change gas used. It only affects metrics and -performance. +`$intrinsic` charges `intrinsicCalled` plus normal payload-expression work. +Each intrinsic registration binds: -## Known Limit +- an exact intrinsic registry identity; +- a disjoint namespace; +- a closed name-to-weight map; +- its deterministic processor. -Large values returned by pure expressions are not directly size-charged unless -they are later appended or inserted through charged output/update operators. -For example, `$concat`, `$join`, `$split`, `$keys`, `$entries`, `$merge`, -`$listConcat`, object literals, and list literals pay expression and operand gas -but not `estimatedSize(result)`. +The processor calls `BexIntrinsicInvocation.charge(counter, quantity, reason)`. +Unknown counter names fail. An intrinsic cannot return arbitrary aggregate gas +or hide unnamed portable work. -This is the current specified model: simple execution and output accounting. +Hosted execution opens each statically required intrinsic namespace as its own +runtime-session child. Intrinsic counters are never flattened into `bex`, and +`/` is reserved as the physical namespace separator. -## Conformance Fixtures +## Trace and Conformance -Exact gas conformance fixtures live under: +Each admitted `BexGasCharge` records: ```text -src/test/resources/rich-fixtures/gas/ +sequence +namespace +counter +quantity +weight +gas +sourcePath? +operator? +reason ``` -Success fixtures may assert: - -```yaml -expectation: - outcome: success - gasUsed: 10 -``` +Sequence starts at zero, and `gas` is exactly `quantity * weight`. The ledger +total is the sum of the ordered trace. -Fixtures may set execution limits: +The exact counter microfixtures live in: -```yaml -context: - gasLimit: 2 -expectation: - outcome: runtime-error - errorContains: BEX gas exhausted +```text +src/test/resources/conformance/bex/fixtures/gas-micro/ ``` -Fixtures may override individual schedule fields: +Run the complete BEX 2.0 fixture, integrity, and report workflow with: -```yaml -gasSchedule: - expressionBase: 10 +```text +./gradlew bexConformanceReport \ + -PblueLanguageCompositePath=../blue-language-java ``` diff --git a/settings.gradle.kts b/settings.gradle.kts index f2468bd..2ad4e2f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -3,3 +3,30 @@ plugins { } rootProject.name = "blue-bex-java" + +val blueLanguageCompositePath = + providers.gradleProperty("blueLanguageCompositePath") + .orNull + ?.trim() + ?.takeIf { it.isNotEmpty() } + +if (blueLanguageCompositePath != null) { + val compositeDirectory = file(blueLanguageCompositePath) + require(compositeDirectory.isDirectory) { + "blueLanguageCompositePath is not a directory: " + + compositeDirectory.absolutePath + } + require( + file("${compositeDirectory.path}/settings.gradle.kts").isFile || + file("${compositeDirectory.path}/settings.gradle").isFile + ) { + "blueLanguageCompositePath is not a Gradle build: " + + compositeDirectory.absolutePath + } + includeBuild(compositeDirectory) { + dependencySubstitution { + substitute(module("blue.language:blue-language-java")) + .using(project(":")) + } + } +} diff --git a/specifications/blue-bex-specification-2.0.md b/specifications/blue-bex-specification-2.0.md new file mode 100644 index 0000000..62ee856 --- /dev/null +++ b/specifications/blue-bex-specification-2.0.md @@ -0,0 +1,2248 @@ +# Blue BEX Specification 2.0 + +> **Status.** Final Implementation Baseline. Operator semantics, counter ownership, counter names, formulas, and trace ordering are frozen for implementation; numerical weights and portable limits remain provisional pending calibration. Final public publication freezes the calibrated manifest and regenerates dependent fixture identities. + +> **Scope.** This document defines Blue BEX: a deterministic expression and statement language encoded as Blue-compatible data. It specifies the program model, compilation, values, expressions, statements, functions, pointers, result accumulation, Blue host boundary, exact gas ledger, errors, fixtures, and conformance. It does not redefine Blue Language or Contracts processing. + +BEX programs are Blue data. BEX execution is computation above Blue content. A BEX runtime integrated with Contracts 1.0 receives identity-preserving read-only Blue values and contributes one exact child ledger to the Contracts shared meter. + +## Conventions + +The key words **MUST**, **MUST NOT**, **REQUIRED**, **SHOULD**, **SHOULD NOT**, **MAY**, and **OPTIONAL** are normative requirement levels. + +Sections marked **normative** define required behavior. Sections marked **informative** explain intent or implementation guidance. + +The term **Language** means Blue Language 1.0. The term **Contracts** means Blue Contracts and Processor 1.0. + +--- + +## 0. Overview + +Blue BEX, short for **Blue Expression Objects**, is a deterministic scripting language written as Blue-compatible object trees. + +A BEX program contains one of: + +- a root expression under `expr`; +- a statement sequence under `do`; +- an `entry` function name; +- reusable `constants` and `functions`. + +BEX has no ambient side effects. It does not mutate the input Root, perform network I/O, read clocks, use randomness, or execute arbitrary host callbacks. It computes a result containing: + +```text +BexExecutionResult { + value + changeset + events + runtimeLedger + diagnostic? +} +``` + +The Contracts host decides whether to admit the value, apply patches, emit events, or reject the result. + +A typical pipeline is: + +```text +BEX Source Blue node + -> compile immutable program + -> execute against identity-preserving context + -> return value, changes, events, and exact child ledger + -> Contracts validates and merges the result exactly once +``` + +### 0.1 Operator shape + +An expression operator is an object with exactly one field whose key begins with `$`: + +```yaml +$add: [1, 2] +``` + +An object with more than one field is a literal/computed object even if a key begins with `$`: + +```yaml +$foo: 1 +bar: 2 +``` + +A statement object contains exactly one statement operator field and no siblings. + +### 0.2 BEX and Blue + +BEX operators are not Language operators. BEX may read Blue nodes, compare or match values, and produce Blue-compatible values, but it does not change BlueId rules. + +A BEX implementation MUST preserve these boundaries: + +- executable BEX is forbidden inside static Language fields such as `type`, `itemType`, `keyType`, `valueType`, `blue`, and `schema`; +- output claiming to be Blue content must pass the Blue output boundary; +- host Blue values are immutable and identity-preserving; +- a verified pure reference and its materialization are indistinguishable to portable BEX operators; +- exact identity is exposed only by `$nodeBlueId`; +- provider, cache, storage, and materialization state are not BEX values; +- all runtime work enters one named live-bounded ledger. + +### 0.3 Existing exact values and transient values + +BEX distinguishes two implementation-level categories without exposing them as ordinary application kinds: + +```text +Exact Blue value: + an admitted Blue node with a known exact Node BlueId and optional materialization. + +Transient BEX value: + a scalar, list, object, null, or undefined produced by BEX and not yet admitted as a Blue node. +``` + +Portable operators observe semantic kind and content, not the category. Passing an exact Blue value preserves its Node BlueId and does not recursively clone or size it. A transient aggregate pays construction work as it is produced. Crossing an identity/output boundary pays Blue normalization and identity work through the Contracts semantic ledger. + +--- + +## 1. Scope, Versioning, and Conformance + +### 1.1 Goal + +Blue BEX 2.0 defines: + +- deterministic expression and statement execution; +- immutable identity-preserving Blue context values; +- representation-blind access, equality, matching, iteration, and truthiness; +- explicit `$processingEvent` and `$nodeBlueId` operations; +- deterministic functions, constants, collection operations, pointers, patches, and events; +- fail-closed registered intrinsics; +- strict Blue output admission; +- one exact named gas schedule shared with Contracts; +- machine-readable conformance vectors and error classes. + +### 1.2 Out of scope + +BEX does not define: + +- Blue parsing, BlueId, resolution, or provider transport; +- Contracts routing, checkpoints, lifecycle, or patch application; +- host authorization or persistence; +- clocks, randomness, I/O, or nondeterministic intrinsics; +- concurrent execution semantics. + +### 1.3 Version selection + +This document defines **Blue BEX 2.0**. BEX 2.0 is an incompatible runtime generation selected by the exact runtime-type BlueId, such as `Compute 2.0`. A platform may continue to recognize older runtime types under their own exact semantics, but a runtime type claiming BEX 2.0 conformance MUST implement this specification. + +A BEX program does not carry a required `bexVersion` field. The exact executable runtime-type BlueId, such as the canonical type registered as `Compute 2.0`, selects this specification, its intrinsics, and its gas schedule. + +An incompatible change to operator recognition, evaluation order, values, equality, pointer semantics, output admission, or gas requires a new BEX version and runtime-type BlueId. + +### 1.4 Conformance + +A conforming Blue BEX 2.0 implementation MUST implement the complete specification: + +- compilation and static validation; +- execution and all required operators; +- exact context semantics; +- strict host boundary; +- exact runtime counters and weights; +- live gas-limit enforcement; +- deterministic diagnostics; +- the BEX 2.0 conformance suite. + +A compiler-only component may describe itself as a BEX 2.0 compiler, but not as a conforming BEX 2.0 runtime. + +The implementation-baseline BEX runtime registry package identity is: + +```text +sha256:23d282ec1c0bb016263922b1b49c369fdd537efdcf23e005eceeb888d7763fe1 +``` + +The machine-readable `blue-bex/gas/2.0` manifest is normative for all portable runtime counters, weights, formulas, and forbidden metering shortcuts. Its implementation-baseline package identity is: + +```text +sha256:41247c820d91a12fdfc17fd9e787a5d8d668d8acc5954fdcb131715bf9e6147d +``` + +### 1.5 Dependencies + +Portable BEX 2.0 uses Blue Language 1.0 for node semantics and output admission. When hosted by Contracts 1.0: + +- `$document` is the current Root view; +- `$event` is the current channelized payload; +- `$processingEvent` is the original external `PROCESS` event; +- `$currentContract` is the frozen executing contract; +- runtime counters merge into the Contracts meter exactly once. + +--- + +## 2. BEX Source Documents and Program Model + +### 2.1 BEX source as Blue data (normative) + +A BEX source is a Blue-compatible node. BEX does not use a separate textual grammar. YAML and JSON authoring follow the host's Blue parser rules. + +The root program node MAY contain: + +| Field | Shape | Meaning | +|---|---|---| +| `constants` | plain object | Named compile-time constants. | +| `functions` | plain object | Named user-defined functions. | +| `expr` | any BEX expression | Root expression program. | +| `do` | list of statements | Root statement program. | +| `entry` | string | Name of the function to invoke as the root program. | + +Other ordinary fields are not program-control fields. An exact runtime type may define host bindings and intrinsics, but it does not change the BEX program-selection rules. Programs commonly include Blue metadata such as `name` or `type`, but those fields do not by themselves affect BEX execution unless read or returned as data. + +### 2.2 Program selection (normative) + +The root executable is selected as follows: + +1. If an explicit host API entry name is supplied, that function is the root executable. +2. Otherwise, if the program node contains an `entry` field, its string value is the root executable name. +3. Otherwise, if the program node contains `expr`, the root executable is that expression. +4. Otherwise, the root executable is the statement list under `do`. +5. If no executable content is present, the root statement program is empty and returns the default result value described in §7.9. + +An entry function MUST exist and MUST declare no arguments. If an entry function declares arguments, compilation MUST fail. + +### 2.3 Definition node plus program node (normative) + +An implementation MAY accept a separate **definition node** and **program node**. When both are supplied: + +- constants from the definition node are loaded first; +- constants from the program node are loaded second and replace same-named definition constants; +- functions from the definition node are loaded first; +- functions from the program node are loaded second and replace same-named definition functions. + +Replacement is by name. Implementations MUST make replacement deterministic. + +### 2.4 Plain name containers (normative) + +The following BEX containers are **plain name containers**: + +- `constants`; +- `functions`; +- function `args`; +- `$call.args`. + +A plain name container MUST be an ordinary object map and MUST NOT use Blue language keys, BEX list-control keys, or Blue wrapper keys as user-defined names. + +The reserved user-name set is: + +```text +name, description, +type, itemType, keyType, valueType, +value, items, +blueId, blue, +schema, constraints, mergePolicy, +properties, contracts, +$previous, $pos, $replace, $empty +``` + +A plain name container MUST NOT be a scalar node, list node, pure reference, Blue wrapper node, or list-control node. + +### 2.5 Static Blue definition fields (normative) + +BEX expressions MUST NOT appear inside static Blue definition fields: + +```text +type, itemType, keyType, valueType, blue, schema +``` + +This rejection applies recursively inside those fields. For example, a computed `type`, a computed `blueId` inside `type`, or a BEX operator embedded in `schema` MUST be rejected at compile time. + +The same static rule applies to `$intrinsic.type`: the intrinsic operation BlueId MUST be knowable at compile time. + +The purpose is to keep Blue type definitions, schema declarations, and intrinsic operation identity static Blue content, rather than executable BEX content. + +### 2.6 Literal escape (normative) + +`$literal` returns its body as literal data and prevents nested BEX operator interpretation, except that the compiler MUST still reject BEX expressions in static Blue definition fields before accepting the literal. + +Example: + +```yaml +$literal: + $unknownOperator: kept as data +``` + +Invalid because the nested expression is inside a static Blue type field: + +```yaml +$literal: + type: + $const: SomeType +``` + +### 2.7 Operator recognition (normative) + +An expression object is a BEX operator if and only if it has exactly one field and that field name begins with `$`. + +- If the sole operator name is unknown, compilation MUST fail. +- If an object has more than one field, it is not an expression operator solely by virtue of dollar-prefixed fields. +- Lists, scalars, and objects without an operator shape are literals unless they contain nested BEX expressions in ordinary expression positions. + +A statement object MUST have exactly one statement operator field. Unknown statement operators or sibling fields MUST fail compilation. + +Null or empty statement items MUST fail compilation. + +--- + +## 3. BEX Values + +### 3.1 Semantic value kinds (normative) + +BEX exposes exactly these semantic value kinds: + +| Kind | Meaning | +|---|---| +| `undefined` | Internal absence. It is not a Blue value and is invalid as root output. | +| `null` | Explicit null-like BEX value. | +| `text` | Exact Unicode text. | +| `integer` | Arbitrary-precision mathematical integer. | +| `double` | Exact decimal execution value used at the finite Blue `Double` boundary. | +| `boolean` | `true` or `false`. | +| `object` | String-keyed map of BEX values. | +| `list` | Ordered sequence of BEX values. | + +An implementation may internally represent a value as an exact Blue node, a pure reference, an immutable cursor, a frozen snapshot, a transient aggregate, or an overlay. These are **not** additional BEX kinds and MUST NOT be observable through `$kind`, `$isKind`, truthiness, equality, matching, iteration, key enumeration, size, pointer access, or serialization. + +### 3.2 Exact Blue values and transient values (normative) + +An **exact Blue value** retains an established Node BlueId and may be materialized only as far as the program demands. A verified pure reference and its verified materialization are the same exact Blue value. + +A **transient value** is constructed by BEX and does not yet have to possess a Blue Node BlueId. It becomes exact only when it crosses the Blue output boundary or when `$nodeBlueId` explicitly requests identity. + +Rules: + +1. Passing an exact Blue value through a variable, function, list, object, patch, event, or return value MUST preserve its exact identity without recursively cloning, serializing, sizing, hashing, or materializing it. +2. Access to an exact Blue value MAY demand the direct node or selected descendants. A limit, unavailable reference, or missing evidence MUST NOT be interpreted as a missing field or non-match. +3. Hidden cache state and current materialization MUST NOT change result, failure class, counter trace, or gas once the same required evidence is available. +4. BEX code may observe exact identity only through `$nodeBlueId`. +5. Reconstructing content equal to an already stored global node is still transient construction and pays construction and identity-boundary work. Cache presence cannot make that work free. + +### 3.3 Undefined (normative) + +`undefined` represents semantic absence within BEX. + +- Reading a semantically absent object key returns `undefined`. +- Reading an out-of-range list index returns `undefined`. +- `$pointerGet` returns `undefined` for an absent path unless a default is supplied. +- Object construction omits fields whose evaluated value is `undefined`. +- Lists MUST NOT contain `undefined` items. +- Converting root `undefined` to a Blue node MUST fail. +- Incomplete access, provider unavailability, or a limit is an error/suspension outcome, not `undefined`. + +### 3.4 Null (normative) + +`null` is a value, not absence. + +- `null` is falsy. +- `$exists(null)` is true. +- BEX `null` converts to an empty Blue object at the strict Blue output boundary. +- `null` is distinct from `undefined` and from an empty object during BEX execution. + +### 3.5 Scalars and the Blue numeric boundary (normative) + +BEX scalar values are exact Text, arbitrary-precision Integer, exact decimal execution values, and Boolean. + +A Blue `Integer` becomes a BEX Integer exactly. A Blue `Double` becomes the exact decimal value represented by the shortest round-tripping decimal rendering of its finite IEEE 754 binary64 value. Source token spelling is never observable. + +BEX numeric equality is computation equality: Integer `1` and decimal `1.0` compare equal. This is not Blue scalar identity. `$nodeBlueId` is the operation for exact Blue identity. + +Conversion back to Blue is deterministic: + +- a BEX Integer becomes Blue `Integer`; +- every BEX decimal, including a mathematically integral decimal such as `1.0`, becomes Blue `Double` by round-to-nearest, ties-to-even binary64 conversion; +- decimal provenance is preserved at the Blue boundary, so BEX Integer `1` and BEX decimal `1.0` may compare numerically equal while producing different Blue scalar identities; +- overflow, NaN, or infinity fails; +- an explicit Blue-shaped scalar must satisfy Blue Language 1.0. + +### 3.6 Objects (normative) + +BEX objects map Text keys to BEX values. + +- A semantically absent key reads as `undefined`. +- Keys are exposed in lexicographic Unicode code-point order for `$keys`, `$entries`, object iteration, deterministic conversion, sorting inputs, and diagnostics. +- Object construction omits `undefined` fields and preserves `null` fields. +- Exact Blue objects may remain collapsed behind their Node BlueIds until a direct member is demanded. +- Key enumeration requires the complete direct key set. Inability to establish that set is not an empty object. + +Object equality compares key set and values, not insertion order or physical representation. + +### 3.7 Lists (normative) + +BEX lists are ordered sequences. + +- Indexing is zero-based. +- An index must be a non-negative Integer. +- An out-of-range index reads as `undefined` only after length or absence is established. +- Lists MUST NOT contain `undefined`. +- Order and multiplicity are preserved. +- Exact Blue lists may retain exact element identities without materializing element bodies. + +### 3.8 Truthiness (normative) + +| Value | Truthy? | +|---|---| +| `undefined` | false | +| `null` | false | +| Boolean | its value | +| Text | true iff non-empty | +| Integer or decimal | true, including zero | +| object | true iff it has at least one semantic key | +| list | true iff it has at least one item | + +Truthiness of an exact Blue object or list is determined from semantic direct structure, not whether the value is currently a pure reference or materialized. `$empty(x)` is `not truthy(x)`. + +### 3.9 Equality and identity (normative) + +`$eq` and `$ne` use BEX semantic equality: + +- `undefined` equals only `undefined`; +- `null` equals only `null`; +- Boolean and Text compare by exact value; +- numeric values compare numerically; +- lists compare length and elements in order; +- objects compare semantic key set and values. + +When both operands are exact Blue values with known Node BlueIds, an implementation MAY conclude equality or inequality from those identities where the semantic relation permits it. It MUST report the canonical counter trace defined in §12, independent of the physical shortcut. Otherwise equality visits only the semantic nodes required by the recursive comparison. + +`$eq` is not a substitute for BlueId equality. `$nodeBlueId` returns exact Blue identity and establishes identity for a transient value when necessary. + +### 3.10 Conversions (normative) + +| Operator | Conversion | +|---|---| +| `$text` | `undefined` and `null` become `""`; scalars use §3.10.1; non-scalars fail. | +| `$integer` | Exact Integer conversion; fractional decimals and invalid Text fail. | +| `$number` | Decimal conversion from Integer, decimal, or numeric Text. | +| `$boolean` | `undefined`/`null` become false; Boolean stays; Text `true`/`false` map directly; otherwise truthiness. | +| `$object` | `undefined`/`null` become `{}`; objects pass through; otherwise fail. | +| `$list` | `undefined`/`null` become `[]`; lists pass through; otherwise fail. | + +### 3.10.1 Scalar text representation (normative) + +All Text-producing and Text-consuming scalar operations use one locale-independent representation: + +- Booleans: `true` or `false`; +- Integers: canonical decimal text; +- decimals: canonical `BigDecimal.toString()`-equivalent rendering with no locale dependence; +- Text: unchanged, with no Unicode normalization. + +Text work is metered in Unicode code-point blocks under §12, never in UTF-16 units and never by recursive value size. + +## 4. Compilation + +### 4.1 Compile-time failures (normative) + +Compilation MUST fail for: + +- unknown expression operators; +- unknown statement operators; +- malformed statement objects; +- null or empty statement items; +- unknown constants referenced by `$const`; +- unknown variables referenced by `$var`, including object-form `$var.name`; +- unknown functions referenced by `$call` or `entry`; +- missing function call arguments; +- extra function call arguments; +- reserved names in user-defined name containers; +- BEX expressions inside static Blue definition fields; +- dynamic or computed patterns for function arguments and `$is.pattern`; +- recursive function calls, whether direct or indirect; +- invalid operator body shapes that are statically known; +- unsupported `$intrinsic.type` BlueIds for the active processor registry; +- invalid `$isKind.kind` values; +- duplicate binding names within a single collection query or `$reduce` binding set; +- `$let.order` that omits, duplicates, or references names outside `$let.vars`. + +Compile-time failures MUST expose an error class. They MUST include the operator name when the failing operator is known. They MUST include a source path within the BEX source document when the source path is known. They MUST NOT fabricate a path or operator name if unavailable. + +### 4.2 Root compilation (normative) + +A compiled program contains a root function. The root function either evaluates a root expression, executes root statements, or invokes an entry function. + +Every execution invokes the root function and therefore charges function-call gas (§12). + +### 4.3 Function compilation (normative) + +A user function definition MAY contain: + +| Field | Shape | Meaning | +|---|---|---| +| `args` | plain object | Required argument patterns by name. | +| `expr` | expression | Expression function body. | +| `do` | list of statements | Statement function body. | + +If `expr` is present, the function is an expression function. Otherwise, the function is a statement function using `do`. If `do` is absent or empty, the statement function has an empty statement list and returns the default result value (§7.9). + +All declared arguments are required. Extra call arguments are forbidden. Function argument names are matched by name, not by position. + +Function argument patterns are static Blue nodes. The compiler MUST reject BEX expressions inside argument patterns. + +### 4.4 Recursion (normative) + +BEX functions MUST NOT be recursive. The compiler MUST reject direct or indirect cycles in the static call graph. + +Calls hidden inside `$literal` are not executable calls and MUST NOT contribute to the call graph. Calls inside static `$is.pattern` content are invalid because static patterns cannot contain BEX expressions. + +### 4.5 Local variables (normative) + +Local variables are function-frame slots. + +- `$let` declares or assigns a local variable in the current function frame. +- `$set` assigns an existing local variable and MUST fail compilation if the variable has not been declared in the current function's compile scope. +- `$let.vars` may declare or assign multiple local variables with either parallel or explicit sequential evaluation (§7.3). +- Function calls execute in a separate frame. +- Caller local variables MUST NOT leak into callee frames. +- Callee variables MUST NOT leak back into caller frames, except through the returned value, changeset, or events. + +Nested statement blocks do not create separate lexical scopes under BEX 2.0. Duplicate loop binding names within a single `$forEach` statement MUST be rejected. + +Collection query binding slots (`item`, optional `key`, optional `index`, and `$reduce.acc`) are temporary for the expression evaluation. If they reuse an outer local variable name, the previous slot value MUST be restored after the collection expression completes or fails. + +### 4.6 Static versus dynamic operands (normative) + +Many operators accept either static scalar shorthand or dynamic expressions. Implementations MUST distinguish: + +- a static omitted path, which may intentionally mean a default root path; +- a dynamic expression that evaluates to `undefined` or `null`, which MUST fail when a path, key, operation, binding name, or step name is required. + +Dynamic text operands that evaluate to `undefined` or `null` MUST fail for: + +```text +$get.key, $objectSet.key, $hasKey.key, $binding.name, $steps.step, +$appendChange.op, $pointerSet.op +``` + +Dynamic pointer operands that evaluate to `undefined` or `null` MUST fail. + +--- + +## 5. Execution Context and Blue Views + +### 5.1 Execution context (normative) + +A BEX execution context may provide: + +| Context value | Meaning | +|---|---| +| `documentScope` | Scope-relative pointer base. | +| `rootDocument` | Current exact Root view. | +| resolved document access | Demand-limited resolved access when requested. | +| `event` | Current channelized payload. | +| `processingEvent` | Original external event passed to `PROCESS`; equal to `event` outside nested/internal delivery. | +| `currentContract` | Frozen effective executing contract snapshot. | +| `steps` | Named prior workflow-step values. | +| `bindings` | Named host bindings. | +| shared gas meter | Live-bounded child meter supplied by the host. | +| registered intrinsics | Deterministic operations selected by exact runtime type BlueId. | + +Missing optional context values read as `undefined`, except that an operation requiring a missing mandatory host binding fails. + +### 5.2 Document access (normative) + +`$document` reads the current Root view. Static or dynamic pointers are relative to `documentScope` unless absolute. + +```yaml +$document: /status +``` + +A demand-limited resolved read is requested with: + +```yaml +$document: + path: /status + view: resolved +``` + +Only `view: resolved` selects resolved access. The host MUST either establish the requested semantic result, report deterministic invalidity, or suspend/fail because required evidence is unavailable. It MUST NOT substitute unresolved, empty, or absent content. + +### 5.3 Event and causal-event access (normative) + +`$event` reads the current delivery payload. `$processingEvent` reads the original external event that caused the complete Contracts invocation. + +For an external Channel delivery: + +```text +$event == $processingEvent +``` + +For a Document Update, Triggered Event, Lifecycle Event, or Embedded Event delivery, `$event` is the immediate payload while `$processingEvent` remains the original external event. + +### 5.4 Scope-relative pointers (normative) + +- A missing or empty static document path selects `documentScope`. +- An absolute JSON Pointer selects from Root. +- A relative pointer appends its segments to `documentScope`. +- Value-local pointer operators always begin at the evaluated value rather than Root. + +### 5.5 Identity-preserving host values (normative) + +Host Blue values are immutable exact-value carriers. The host may implement them as frozen nodes, cursors, handles, persistent nodes, or another representation, but BEX MUST observe only semantic value and explicit identity. + +The host MUST NOT: + +- expose a pure-reference wrapper as an object with a semantic `blueId` child; +- clone or recursively materialize exact values merely to pass them into BEX; +- let hidden cache state alter results or gas; +- convert incomplete access into absence; +- mutate a host value during execution. + +A BEX execution may construct transient overlays and aggregates. Such values remain separate from host state until returned as effects or output. + +## 6. Expressions + +### 6.1 Expression evaluation (normative) + +Each expression evaluation MUST: + +1. admit one `expressionEvaluated` charge; +2. append that counter to the child ledger; +3. evaluate according to the operator or literal semantics; +4. return a BEX value or fail with a runtime error. + +Source wrappers and compile-time structures do not themselves consume gas unless they execute as expressions. + +### 6.1.1 Operand evaluation order (normative) + +Unless an operator explicitly short-circuits or defines lazy behavior, evaluated operands MUST be evaluated in a deterministic order. + +- List operand sequences are evaluated left to right by list index. +- List literal items are evaluated left to right by list index. +- Object literal fields are evaluated in BEX object key exposure order, not host map iteration order. +- `$call.args` expressions are evaluated in lexicographic argument-name order, not source map order. + +The following operands remain lazy or selected-only according to their operator semantics: + +```text +$and, $or, $coalesce, $choose, $if, +$listGet.default, $pointerGet.default, +$appendChange.val for remove, $pointerSet.val for remove, +$returnIf.expr when cond is false, $failIf.message when cond is false, +$some/$find/$findEntry/$includes items after a short-circuit match +``` + +Skipped lazy operands produce no side effects and consume no gas. + +### 6.2 Literal expressions (normative) + +Scalars, lists, and non-operator objects are expressions. + +The explicit literal helper operators are: + +| Operator | Semantics | +|---|---| +| `$null` | Return BEX `null`. | +| `$emptyObject` | Return an empty object. | +| `$emptyList` | Return an empty list. | + +- A scalar literal evaluates to the corresponding scalar BEX value, or `null` for a null literal. +- A list literal evaluates each item and returns a list. If any item evaluates to `undefined`, list construction MUST fail. +- An object literal evaluates ordinary fields in BEX object key exposure order and returns an object. Fields whose values evaluate to `undefined` are omitted. +- Blue language metadata fields may be preserved as Blue output fields when the object is later converted to a Blue node. + +### 6.3 Read expressions (normative) + +| Operator | Body | Semantics | +|---|---|---| +| `$document` | pointer or `{path, view}` | Read canonical or resolved document view at a document pointer. | +| `$binding` | `name/path` or `{name, path}` | Read named host binding at value-local path. | +| `$event` | pointer | Read current delivery payload at value-local path. | +| `$processingEvent` | pointer | Read the original external `PROCESS` event at value-local path. | +| `$steps` | `step.path` or `{step, path}` | Read named prior step value at value-local path. | +| `$currentContract` | pointer | Read current contract value at value-local path. | +| `$var` | name or `{name, path?}` | Read local variable slot, optionally at a value-local path. | +| `$const` | name or `{name, path?}` | Read compile-time constant, optionally at a value-local path. | +| `$get` | `{object, key}` | Read object key; missing or non-object reads as `undefined`. | +| `$changeset` | ignored | Return accumulated changeset as a BEX value. | +| `$events` | ignored | Return accumulated events as a BEX value. | +| `$resultValue` | pointer | Read document value after accumulated changes have been overlaid. | + +`$binding` short form splits at the first `/`. The text before the first slash is the binding name. The slash and following text are the path. If no slash appears, the path is `/`. + +`$steps` short form splits at the first `.`. The text before the first dot is the step name. The dot and following text are converted to a path beginning with `/`. If no dot appears, the path is `/`. + +`$var` and `$const` object forms have a static `name` and optional value-local `path`: + +```yaml +$var: + name: request + path: /summary +``` + +```yaml +$const: + name: Policy/minimumAmount + path: /amount +``` + +The `name` operand is static and MUST NOT be computed. Dynamic variable names and dynamic constant names are not part of BEX 2.0. The `path` operand MAY be static text or a dynamic expression that evaluates to pointer text. A missing path target returns `undefined`. + +There is no BEX 2.0 shorthand that splits `$var: request/summary` or `$const: Policy/minimumAmount`. Variable and constant names may contain `/`; object-form `path` exists to avoid changing the meaning of such programs. + +All read operators are representation-blind. In particular, `$exists: {$document: /x/blueId}` does not reveal the internal pure-reference wrapper for `/x`; it reads a logical child named `blueId` only when that child is actual content. Exact identity is obtained through `$nodeBlueId`. + +### 6.4 Type and conversion expressions (normative) + +| Operator | Semantics | +|---|---| +| `$unwrap` | Repeatedly reads `value` while the current value is an object with a defined `value` field. | +| `$is` | Blue type/shape match of `node` against static `pattern`. | +| `$kind` | Return the visible BEX runtime kind of a value. | +| `$isKind` | Return whether a value's visible BEX runtime kind is in a static kind set. | +| `$text` | Convert to text (§3.10). | +| `$integer` | Convert to exact integer (§3.10). | +| `$number` | Convert to decimal number (§3.10). | +| `$boolean` | Convert to boolean (§3.10). | +| `$object` | Convert `undefined`/`null` to `{}` or pass through object; otherwise fail. | +| `$list` | Convert `undefined`/`null` to `[]` or pass through list; otherwise fail. | +| `$nodeBlueId` | Return the exact Node BlueId of a Blue value; establish identity for a transient value. | + +`$is` body MUST contain `node` and static `pattern` operands. The pattern MUST NOT contain BEX expressions. + +`$is` evaluates `node` first. If evaluating `$is.node` itself fails, that runtime failure propagates and is not converted to `false`. + +If `$is.node` evaluates successfully but the resulting value is `undefined`, `$is` returns `false`. If conversion of the successfully evaluated value to a Blue node fails, `$is` returns `false`. If the Blue type matcher returns no match, `$is` returns `false`. + +If the static pattern is malformed or contains BEX expressions, compilation fails. Host, provider, or type-resolution failures required to evaluate the pattern deterministically are errors according to the Blue Language 1.0 matcher semantics; implementations MUST NOT silently treat unavailable type definitions as a successful non-match. Function argument matching uses the same type matcher boundary (§8.3). + +`$kind` returns one of: + +```text +undefined, null, text, integer, double, boolean, object, list +``` + +`$kind` exposes semantic BEX kind only. A pure reference, cursor, frozen node, materialized node, and equivalent transient value report the same kind once the semantic kind is established. Incomplete access is not `object`, `undefined`, or `false`; it is an error/suspension outcome. + +`$isKind` has body `{val, kind}`. The `kind` operand is static authored data and MUST be either a single kind text value or a list of kind text values. Unknown kind names MUST fail compilation. The `val` operand is evaluated at runtime. + +Example: + +```yaml +$isKind: + val: + $event: /message/request/amount + kind: [integer, double] +``` + +### 6.5 String expressions (normative) + +| Operator | Body | Semantics | +|---|---|---| +| `$concat` | list of operands | Convert each operand to text and concatenate. | +| `$pointerJoin` | list of segment operands | Convert each segment to text, JSON-Pointer-escape it, and join as an absolute pointer. No segments returns `/`. | +| `$join` | `{list, separator}` | Join list items converted to text using separator text. | +| `$split` | `{text, separator, limit?}` | Split text by non-empty separator. `limit` is optional; `-1` means no limit. | +| `$startsWith` | two operands | True if the first text starts with the second text. | +| `$sliceAfter` | two operands | If first text starts with second text, return the suffix after that prefix; otherwise return empty text. | + +`$split.separator` MUST NOT be empty. A missing `$split.limit` is equivalent to `-1`. + +`$split.limit` semantics are: + +- `-1` means no limit and preserves trailing empty parts; +- `n > 0` returns at most `n` parts; +- for `n > 0`, the first `n - 1` separator occurrences split normally, and the final part contains the remaining suffix, including separators that were not consumed; +- `1` returns the whole input as a single-item list; +- `0` or any value less than `-1` MUST fail; +- if the separator does not occur, the result is a one-item list containing the input text. + +Examples: + +```yaml +{ $split: { text: "a,b,c", separator: ",", limit: 2 } } +# => ["a", "b,c"] + +{ $split: { text: "a,", separator: "," } } +# => ["a", ""] + +{ $split: { text: "a,", separator: ",", limit: 1 } } +# => ["a,"] +``` + +`$pointerJoin` MUST escape `~` as `~0` and `/` as `~1`. + +### 6.6 Logic and comparison expressions (normative) + +| Operator | Semantics | +|---|---| +| `$eq` | BEX equality of exactly two operands. | +| `$ne` | Negation of `$eq`. | +| `$gt`, `$gte`, `$lt`, `$lte` | Numeric comparison of exactly two operands using decimal numeric conversion. | +| `$and` | Left-to-right truthy conjunction with short-circuit. Empty operand list returns `true`. | +| `$or` | Left-to-right truthy disjunction with short-circuit. Empty operand list returns `false`. | +| `$not` | Truthy negation. | +| `$truthy` | Return truthiness as boolean. | +| `$empty` | Return inverse truthiness as boolean. | +| `$isEmpty` | Alias for `$empty`. | +| `$exists` | Return false only for `undefined`; true otherwise. | +| `$coalesce` | Return the first truthy operand; if none is truthy, return `undefined`. | +| `$default` | Alias for `$coalesce`. | + +`$and`, `$or`, and `$coalesce` MUST NOT evaluate operands after their result is determined. Unevaluated operands consume no gas and produce no errors. + +`$isEmpty` is an exact alias for `$empty`. It exists to avoid ambiguity with Blue empty-placeholder authoring syntax when an empty check appears in a list operand. + +### 6.7 Numeric expressions (normative) + +| Operator | Semantics | +|---|---| +| `$add` | Exact integer addition. | +| `$subtract` | Exact integer subtraction. | +| `$multiply` | Exact integer multiplication. | +| `$divide` | Exact integer division. | + +Numeric arithmetic operators use exact integer conversion for all operands. Non-integer numeric values and invalid integer text MUST fail. Division by zero MUST fail. Division with a non-zero remainder MUST fail. + +A one-operand numeric expression returns that operand converted to integer. + +### 6.8 Object and list expressions (normative) + +| Operator | Body | Semantics | +|---|---|---| +| `$keys` | expression | Return sorted object keys as a list; non-objects return `[]`. | +| `$entries` | expression | Return sorted object entries as `{key, val}` objects. | +| `$size` | expression | List length, object field count, scalar `1`, or `0` for `undefined`/`null`. | +| `$listGet` | `{list, index, default?}` | Read list index; use default only when missing. | +| `$listConcat` | list of list operands | Concatenate lists. All operands MUST be lists. | +| `$merge` | list of object operands | Shallow merge objects left to right; later keys win. | +| `$objectSet` | `{object, key, val}` | Set or remove object key. Undefined `val` removes the key. | +| `$pointerGet` | `{object, path, default?}` | Read a value-local JSON Pointer; evaluate default only when missing. | +| `$pointerSet` | `{object, path, op?, val?}` | Set or remove a value-local JSON Pointer. | +| `$map` | `{in, item, key?, index?, expr}` | Project each list item or sorted object value to a list. | +| `$filter` | `{in, item, key?, index?, where}` | Keep matching list items or object fields. | +| `$flatMap` | `{in, item, key?, index?, expr}` | Project each item to a list and concatenate. | +| `$reduce` | `{in, acc, init, item, key?, index?, expr}` | Fold a list or sorted object values into an accumulator. | +| `$some` | `{in, item, key?, index?, where}` | Return true on the first truthy match; otherwise false. | +| `$find` | `{in, item, key?, index?, where}` | Return the first matching item/value, or `undefined`. | +| `$findEntry` | `{in, item, key?, index?, where}` | Return the first matching entry object, or `undefined`. | +| `$includes` | `{list, val}` | Return whether a list contains `val` by BEX equality. | +| `$hasKey` | `{object, key}` | Return whether an object has a non-undefined value for `key`. | +| `$objectFromEntries` | list expression | Build an object from `{key, val}` entries. | + +`$objectSet` treats an `undefined` or `null` object operand as `{}`. It MUST fail for scalar base values. + +`$pointerSet.op` defaults to `set`. The only valid operations are `set` and `remove`. `remove` MUST NOT evaluate its `val` operand. `set` MUST evaluate `val`. + +When `$pointerSet` needs to create missing intermediate containers, it creates objects. It MUST fail if an existing intermediate value is scalar or otherwise incompatible with traversal. + +`$size` is a collection/cardinality helper. For scalar text it returns `1`, not text length. A dedicated text code-point length operator is a candidate for a later BEX revision. + +#### 6.8.1 Collection query expressions (normative) + +Collection query expressions accept either a list or an object in `in`. Non-list and non-object inputs MUST fail unless a specific operator says otherwise. + +For list input: + +- iteration order is list index order; +- `item` receives the list item; +- `index`, when present, receives the zero-based integer index; +- `key`, when present, receives `undefined`. + +For object input: + +- iteration order is lexicographic Unicode code-point order of object keys; +- `item` receives the field value; +- `key`, when present, receives the object key text; +- `index`, when present, receives the zero-based ordinal in sorted-key order. + +The binding names `item`, optional `key`, optional `index`, and `$reduce.acc` within one collection expression MUST be distinct when present. Collection expression bindings MUST restore any previous slot values after the expression completes or fails. + +`$map` evaluates `expr` for each item and returns a list of results. Object input still returns a list, in sorted-key order. + +`$filter` evaluates `where` for each item. For list input it returns a filtered list preserving item order. For object input it returns a filtered object preserving BEX object key exposure semantics. + +`$flatMap` evaluates `expr` for each item. Each result MUST be a list. The returned value is a concatenated list of all projected lists. + +`$reduce` evaluates `init` once after evaluating `in`, assigns it to `acc`, then evaluates `expr` once per item. After each iteration, the expression result becomes the next accumulator value. The final accumulator is returned. For empty input, the `init` value is returned. + +`$some`, `$find`, `$findEntry`, and `$includes` are short-circuiting. Items after a determined result MUST NOT be evaluated and consume no gas. + +`$some` returns true for the first truthy `where`, otherwise false. + +`$find` returns the first item/value whose `where` is truthy, otherwise `undefined`. + +For list input, `$findEntry` returns: + +```yaml +val: +index: +``` + +For object input, `$findEntry` returns: + +```yaml +key: +val: +index: +``` + +`$includes.list` MUST evaluate to a list. It evaluates `val` once, then compares each list item using BEX equality. It returns true on the first equal item, otherwise false. + +`$hasKey.object` returns false for non-object inputs. For object inputs it returns true when reading `key` returns a non-`undefined` value. The `key` operand is a static or dynamic text operand and follows dynamic text failure rules (§4.6). + +`$objectFromEntries` requires a list of object entries. Each entry MUST be an object. Each entry key is read from `key`; `undefined` or `null` keys MUST fail. Keys are converted to text. Each entry value is read from `val`; an `undefined` value removes/omits that key from the output object. Duplicate keys use the last non-undefined value unless a later undefined value removes the key. + +### 6.9 Result helper expressions (normative) + +`$changeset` returns the accumulated changeset as a list of patch objects. + +`$events` returns the accumulated event list. + +`$resultValue` reads the input document after applying all accumulated patches in order through the result overlay model (§10). It charges `resultValueRead` in addition to `expressionEvaluated`. + +### 6.10 Control expressions (normative) + +| Operator | Body | Semantics | +|---|---|---| +| `$choose` | `{cond, then, else?}` | Evaluate `cond`; if truthy evaluate `then`, otherwise evaluate `else` or return `undefined`. | +| `$call` | `{function, args}` | Invoke a user-defined function with named arguments. | +| `$intrinsic` | object with static `type` and payload fields | Invoke a registered host intrinsic processor by the BlueId of `type`. | +| `$literal` | any | Return literal body without nested BEX interpretation, subject to §2.6. | +| `$null`, `$emptyObject`, `$emptyList` | ignored | Return explicit null, empty object, or empty list. | + +`$call` evaluates argument expressions, validates each argument against its declared static pattern, and then invokes the callee in a new frame. Argument validation failure is a runtime error. + +`$call` may also appear as a statement; statement-form `$call` invokes the function for effects and discards its return value (§7.2). + +### 6.11 `$intrinsic` (normative) + +`$intrinsic` is BEX's explicit host capability boundary. It invokes a host-registered processor keyed by the BlueId of the static `type` field. + +Example: + +```yaml +$intrinsic: + type: + blueId: CommonCryptoEd25519Verify + publicKey: + $const: trustedSignerPublicKey + message: + $event: /message/canonicalBytes + signature: + $event: /message/signature +``` + +Rules: + +- `$intrinsic` body MUST be an object. +- `type` is REQUIRED and MUST be static authored Blue data. BEX expressions inside `type` MUST fail compilation. +- The implementation resolves or computes the BlueId of `type` under Blue Language 1.0. +- The resolved BlueId MUST have a registered intrinsic processor for the exact active BEX runtime type. Otherwise compilation MUST fail. +- Payload fields are the ordinary object properties beside `type`. The `type` field itself is not passed as a payload field; processors receive it separately as static type data. +- Payload field expressions are evaluated normally in deterministic object key exposure order. Fields evaluating to `undefined` are omitted. +- A processor returns one BEX value. A host API that permits a null implementation return MUST normalize it to BEX `undefined` or fail deterministically. +- A processor is responsible for returning a deterministic named child ledger for its own work (§12.13). + +`$intrinsic` MUST NOT be used for arbitrary non-deterministic host calls in a portable BEX 2.0 execution. Standard intrinsics such as signature verification MUST define their exact input bytes, failure behavior, and conformance vectors outside the BEX program text. + +--- + +## 7. Statements + +### 7.1 Statement execution (normative) + +Each statement execution MUST: + +1. admit one `statementExecuted` charge; +2. append the charge to the child ledger; +3. execute according to the statement operator; +4. either proceed to the next statement, return, or fail. + +A statement list executes in order until it completes, returns, or fails. + +### 7.2 Statement operators (normative) + +| Operator | Body | Semantics | +|---|---|---| +| `$let` | `{name, expr}` or `{vars, order?}` | Declare or assign one or more local variables. | +| `$set` | `{name, expr}` | Assign existing local variable. | +| `$if` | `{cond, then?, else?}` | Execute selected statement list. | +| `$forEach` | `{in, item, key?, index?, do}` | Iterate list or object. | +| `$appendChange` | `{op, path, val?}` | Append one document patch. | +| `$appendChanges` | expression | Append a list of patch entries. | +| `$appendEvent` | expression | Append one event value. | +| `$appendEvents` | expression | Append a list of event values. | +| `$call` | `{function, args}` | Invoke function for effects and discard return value. | +| `$return` | expression or empty | Return from current function. | +| `$returnIf` | `{cond, expr?}` | Return from current function when `cond` is truthy. | +| `$fail` | expression or `{message}` | Throw a runtime failure with message text. | +| `$failIf` | `{cond, message}` | Throw a runtime failure when `cond` is truthy. | + +### 7.3 `$let` and `$set` (normative) + +Single-bind `$let` evaluates `expr` and assigns it to `name` in the current frame. Reusing an existing name assigns the existing slot. + +Multi-bind `$let` uses `vars`: + +```yaml +$let: + vars: + a: 1 + b: 2 +``` + +Without `order`, `$let.vars` is parallel: all binding expressions are evaluated against the frame as it existed before the `$let`, then all variables are assigned. Implementations MAY sort variable names to make evaluation deterministic, but sorted evaluation MUST NOT create intra-batch dependencies. + +With `order`, `$let.vars` is sequential: + +```yaml +$let: + order: [request, summary] + vars: + request: + $event: /message/request + summary: + $var: + name: request + path: /summary +``` + +`order` MUST list every key in `vars` exactly once. Later ordered bindings may read earlier ordered bindings. + +`$set` evaluates `expr` and assigns an existing slot. Compilation MUST fail if the name is not known in the current function compile scope. + +### 7.4 `$if` (normative) + +`$if` evaluates `cond`. If truthy, it executes `then`; otherwise it executes `else`. Only the selected branch executes and consumes gas. + +A missing `then` or `else` branch is an empty statement list. + +### 7.5 `$forEach` (normative) + +`$forEach.in` MUST evaluate to a list or object. + +For a list: + +- `item` receives each item; +- `index`, when provided, receives the zero-based integer index; +- `key`, when provided, receives `undefined`. + +For an object: + +- keys are visited in lexicographic order over Unicode code points of the unescaped key text; +- if `key` is provided, `key` receives the key text and `item` receives the field value; +- if `key` is not provided, `item` receives an object of the form `{ key: , val: }`; +- `index`, when provided, receives `undefined`. + +The loop body executes for each visited element. Each visited input pays exactly one `collectionItemVisited` charge plus the evaluated body. There is no additional generic loop-size charge and no charge for items not reached after a function return or failure. + +The names `item`, `key`, and `index` within one `$forEach` statement MUST be distinct when present. + +BEX 2.0 iteration has no loop-local `break` or loop-local `continue`. `$return` exits the current function, not merely the loop. `$fail` aborts execution. + +### 7.6 `$appendChange` (normative) + +`$appendChange` appends one patch entry to the changeset accumulator. + +Valid operations are: + +```text +add, replace, remove +``` + +Rules: + +- `op` is a required text operand. +- `path` is a document pointer operand and is resolved relative to the document scope when not absolute. +- `add` and `replace` require a non-`undefined` `val`. +- `remove` MUST NOT evaluate `val`. +- The appended patch preserves author order. Duplicate paths are allowed and are not coalesced. + +### 7.7 `$appendChanges` (normative) + +`$appendChanges` evaluates its body as a list of patch entries. Each entry is validated as if supplied to `$appendChange`. + +Invalid entries, bad operations, missing values for `add`/`replace`, or non-list inputs MUST fail. Successfully appended entries preserve list order. + +### 7.8 `$appendEvent` and `$appendEvents` (normative) + +`$appendEvent` evaluates its body and appends the resulting value to the event accumulator. The event value MUST NOT be `undefined`. Events need not be objects. + +`$appendEvents` evaluates its body as a list and appends each item in order. Each item MUST NOT be `undefined`. + +### 7.9 `$return` (normative) + +`$return` returns from the current function. + +If `$return` has an expression body, that expression is the returned value. If `$return` is empty or null, the default result value is returned. + +At the root statement list, `$return` returns from the root function and therefore exits the whole BEX program. Statements after that root `$return` MUST NOT execute. + +The default result value is an object with: + +```yaml +changeset: +events: +``` + +If a statement function completes without an explicit `$return`, it returns the same default result value. + +### 7.10 `$returnIf` and `$failIf` (normative) + +`$returnIf` evaluates `cond`. If truthy, it evaluates optional `expr` and returns from the current function with that value. If `expr` is absent, it returns the default result value. If `cond` is falsy, `expr` MUST NOT be evaluated. + +The return payload field is named `expr`. `value` is not a valid `$returnIf` payload field because `value` is a Blue scalar-wrapper field in authored Blue data. + +`$failIf` evaluates `cond`. If truthy, it evaluates `message`, converts it to text, and raises a runtime error. If `cond` is falsy, `message` MUST NOT be evaluated. + +### 7.11 `$fail` (normative) + +`$fail` raises a runtime error. If the body is an object with a `message` field, the `message` operand supplies the error message. Otherwise, the body expression supplies the message. The message is converted to text. + +--- + +## 8. Functions, Constants, and Static Patterns + +### 8.1 Constants (normative) + +`constants` is a plain name container. A constant value is compiled as static literal content. `$const` references a named constant. + +A `$const` reference to an unknown constant MUST fail compilation. + +Constants are immutable during execution. + +### 8.2 Functions (normative) + +`functions` is a plain name container. A function name maps to a function definition. + +Function names are static. `$call.function` MUST resolve to a known function at compile time. Dynamic function dispatch is not part of BEX 2.0. + +### 8.3 Function arguments (normative) + +Function `args` is a plain name container whose values are static Blue type or shape patterns. + +A call MUST supply exactly the declared argument names: + +- missing declared argument: compile error; +- extra argument: compile error; +- unknown function: compile error; +- invalid reserved argument name: compile error. + +At runtime, after each argument expression is evaluated, the value MUST match the declared Blue pattern if the pattern is non-empty. Mismatch is a runtime error. + +Function argument pattern matching uses the same Blue type matcher boundary as `$is` (§6.4). If argument expression evaluation itself fails, that failure propagates. If an argument evaluates successfully but conversion or type matching returns non-match, argument validation fails as a runtime error. Host, provider, and type-resolution failures follow Blue Language 1.0 matcher semantics. + +### 8.4 Static `$is.pattern` (normative) + +`$is.pattern` is a static Blue node. It MUST NOT contain BEX expressions. + +`$is.node` is an expression and MAY contain BEX. + +### 8.5 Empty patterns (normative) + +An empty or null Blue pattern matches any non-`undefined` value. `undefined` does not match a pattern. + +### 8.6 Pattern labels (normative) + +Blue `name` and `description` semantics are inherited from the Blue type matcher. They are matcher-neutral when Blue Language type matching treats them as matcher-neutral. + +--- + +## 9. Pointers + +### 9.1 JSON Pointer syntax (normative) + +BEX pointer strings use JSON Pointer syntax with `/`-separated segments. Implementations MUST support `~0` for `~` and `~1` for `/` in pointer segments. + +A pointer that does not begin with `/` may be interpreted relative to a scope depending on pointer kind. + +### 9.2 Pointer kinds (normative) + +BEX distinguishes document pointers from value-local pointers. + +| Pointer kind | Used by | Scope | +|---|---|---| +| Document pointer | `$document`, `$resultValue`, `$appendChange.path`, `$appendChanges` entry path | Relative to current document scope unless absolute. | +| Value-local pointer | `$event`, `$currentContract`, `$steps.path`, `$binding.path`, `$pointerGet.path`, `$pointerSet.path` | Relative to the root of the operand value. | + +### 9.3 Static pointer defaults (normative) + +A missing or empty static pointer MAY intentionally mean the root/default path. + +- Static document path omitted or empty: current document scope. +- Static value path omitted or empty: `/`. + +### 9.4 Dynamic pointer failures (normative) + +A dynamic pointer expression that evaluates to `undefined` or `null` MUST fail. It MUST NOT be silently interpreted as `/`. + +Dynamic pointer text is canonicalized as JSON Pointer text. Relative dynamic document pointers are resolved against document scope. Relative dynamic value pointers are made value-local by prefixing `/`. + +### 9.5 `$pointerJoin` (normative) + +`$pointerJoin` builds an absolute JSON Pointer from unescaped segment values. + +```yaml +$pointerJoin: [rooms, "12/34", "a~b"] +# => /rooms/12~134/a~0b +``` + +No segments returns `/`. + +--- + +## 10. Changesets, Events, and Result Overlay + +### 10.1 Changeset entries (normative) + +A changeset is an ordered list of patch entries. + +A patch entry has: + +| Field | Meaning | +|---|---| +| `op` | `add`, `replace`, or `remove`. | +| `path` | Absolute document pointer after scope resolution. | +| `val` | Patch value for `add` and `replace`; absent for `remove`. | + +Patch entries are accumulated. BEX does not itself persist or apply them to the host document. + +### 10.2 Patch order and duplicates (normative) + +Patch entries MUST remain in append order. Duplicate paths MUST be preserved. Implementations MUST NOT coalesce, reorder, or discard patches in the accumulator. + +### 10.3 Events (normative) + +Events are ordered BEX values. Events need not be Blue objects unless the host imposes such a requirement. BEX MUST preserve event append order. + +### 10.4 `$resultValue` overlay (normative) + +`$resultValue` reads from a transient overlay formed by applying accumulated patches, in order, to the canonical document view. + +The overlay is a BEX execution view. It does not mutate the host document. + +Rules: + +- A later patch to the same path is visible to later `$resultValue` reads. +- A read of a parent object reflects descendant patches. +- A parent replacement followed by a child replacement is applied in order. +- Removing a child makes that child read as `undefined`. +- Adding to a missing parent creates intermediate object containers for overlay purposes. +- List index replacement is supported. +- List index removal is non-shifting in the overlay model; later indexes retain their positions. +- Root replacement replaces the overlay root. +- Root removal makes the overlay root `undefined`. + +### 10.4.1 Sparse list overlay views (normative) + +A list-index remove patch in `$resultValue` creates a sparse overlay slot at that index. + +- Direct reads of the removed index return `undefined`. +- Later indexes retain their original positions. +- `$size` of such an overlay list returns the overlay list's positional extent, including removed slots. +- A sparse overlay list is a BEX overlay view, not a dense BEX list literal. +- Converting a sparse overlay list to a Blue list MUST fail if any indexed slot in `0..size-1` is `undefined`, because Blue/BEX output lists cannot contain `undefined`. +- Programs that need portable Blue output after a non-shifting removal MUST return specific paths, construct a dense list explicitly, or leave the change in the changeset for the host to apply. + +### 10.5 Result value output (normative) + +The final `value` returned by a program is independent of the changeset and event accumulators unless the program explicitly returns `$changeset`, `$events`, `$resultValue`, or the default result value. + +--- + +## 11. Blue Output and Identity Boundaries + +### 11.1 Purpose (normative) + +BEX may manipulate semantic values without immediately creating Blue nodes. A **Blue output boundary** occurs when a value becomes: + +- the root return value requested as Blue output; +- a patch value; +- an emitted event; +- an intrinsic input that requires an exact Blue node; +- the operand of `$nodeBlueId` when it is not already exact. + +### 11.2 Existing exact values (normative) + +An existing exact Blue value crosses the boundary by identity. The host MUST preserve its Node BlueId and MUST NOT recursively reconstruct, serialize, size, hash, or materialize it. The boundary charges only the BEX boundary counter plus any Contracts operation that receives it. + +### 11.3 Transient conversion (normative) + +A transient value is converted recursively to a valid Blue Language 1.0 node: + +- `undefined` root fails; +- an object omits `undefined` fields; +- a list containing `undefined` fails; +- `null` becomes an empty Blue object; +- Text, Integer, decimal, and Boolean use the deterministic scalar rules; +- object and list members are converted in canonical BEX order; +- the resulting node must satisfy Blue Language syntax, payload-kind, reserved-field, list-control, and schema rules. + +BEX charges runtime construction and `blueOutputBoundary`; the host charges Blue semantic identity establishment exactly once under Contracts 1.0. These ledgers MUST NOT both charge the same runtime member production or the same semantic identity step. + +### 11.4 `$nodeBlueId` (normative) + +`$nodeBlueId` evaluates one operand. + +- If the operand is an exact Blue value, it returns that value's Node BlueId as Text without transitive expansion. +- If the operand is transient, it performs the Blue output conversion, establishes its exact Node BlueId, and returns that Text. +- `undefined` fails. +- The operation never exposes whether an exact value was originally inline or a pure reference. + +### 11.5 Pure references and payload kinds (normative) + +A Blue object exactly `{blueId: X}` is a pure reference. A converted output containing `blueId` with any sibling field fails. + +A node MUST NOT mix scalar `value`, list `items`, and ordinary object payload fields. `properties` is not a Blue wrapper and remains an ordinary reserved-invalid key. + +### 11.6 Schema and reserved fields (normative) + +Converted output may use only Blue Language 1.0 schema keywords. `constraints`, `allowMultiple`, `options`, or any unsupported schema key fails. Computed language fields such as `type`, `itemType`, `keyType`, `valueType`, `schema`, `mergePolicy`, and `contracts` retain their Blue language meaning and are validated accordingly. + +### 11.7 Preprocessing and list controls (normative) + +Computed output containing `blue` fails. BEX output is runtime Blue content, not an authored preprocessing source document. + +Computed output MUST NOT contain `$previous`, `$pos`, or `$replace` as Blue list controls. `$empty: true` is allowed only in its exact Blue Language placeholder shape. + +### 11.8 Failure atomicity (normative) + +Output-boundary failure produces no patch, event, returned Blue node, or identity result from that boundary. The child ledger up to the failing charge remains available to the host, while Contracts 1.0 determines invocation rollback. + +## 12. Canonical Gas Accounting + +### 12.1 Governing rule (normative) + +BEX meters logical computation, not representation size. + +```text +existing exact node carried: + no recursive size charge + +content inspected, compared, constructed, iterated, sorted, or converted: + charge the exact canonical work below +``` + +There is no recursive `estimatedSize`, serialized-payload charge, UTF-16-length charge, cache-dependent discount, or opaque implementation-selected `gasConsumed` in portable BEX 2.0. + +### 12.2 Shared child ledger (normative) + +When hosted by Contracts 1.0, BEX receives the exact remaining process budget. Before every unit of work it MUST admit the corresponding named counter charge. If the next charge does not fit, execution stops before that work and returns the canonical admitted trace prefix. + +The BEX child ledger is merged into the Contracts ledger exactly once in original order. A BEX-local gas limit may only reduce the remaining budget. It cannot replenish gas. A portable runtime MUST return named counters and MUST NOT both debit the parent meter and return the same debit as an opaque integer. + +### 12.3 Counter weights (normative) + +| Counter | Weight | +|---|---:| +| `expressionEvaluated` | 1 | +| `statementExecuted` | 1 | +| `functionCalled` | 2 | +| `intrinsicCalled` | 5 | +| `documentRead` | 2 | +| `eventRead` | 1 | +| `processingEventRead` | 1 | +| `currentContractRead` | 1 | +| `stepsRead` | 1 | +| `bindingRead` | 1 | +| `variableRead` | 1 | +| `constantRead` | 1 | +| `resultValueRead` | 2 | +| `pointerSegmentRead` | 1 | +| `pointerSegmentWritten` | 1 | +| `objectMemberRead` | 1 | +| `listItemRead` | 1 | +| `collectionItemVisited` | 1 | +| `collectionItemProduced` | 1 | +| `textBlockExamined` | 1 | +| `textBlockConstructed` | 1 | +| `integerLimbOperation` | 1 | +| `comparisonNodeVisited` | 1 | +| `sortComparison` | 1 | +| `patchAppended` | 5 | +| `eventAppended` | 5 | +| `transientObjectMemberProduced` | 1 | +| `transientListItemProduced` | 1 | +| `blueOutputBoundary` | 5 | +| `nodeIdentityRequested` | 5 | + +The manifest and fixture package bind the current provisional weights. Final BEX 2.0 publication freezes the calibrated values and regenerates every dependent identity. + +### 12.4 Universal evaluation charges (normative) + +Every executed expression charges `expressionEvaluated` once. Every executed statement charges `statementExecuted` once. A root program and each user function invocation charge `functionCalled` once. + +A statically compiled constant literal is not reconstructed on each read. Reading it charges the expression and `constantRead`; dynamic aggregates produced by expression evaluation pay production counters. + +Skipped lazy operands, unselected branches, and post-short-circuit collection items charge nothing. + +### 12.5 Read and pointer charges (normative) + +A context read charges its named read counter. Traversing a value-local or document pointer charges `pointerSegmentRead` per examined segment. The direct member or list position examined additionally charges `objectMemberRead` or `listItemRead`. + +The same logical access MUST NOT be charged both as a BEX runtime read and again as a Contracts semantic read. When BEX requests the access, the BEX child ledger owns the access counters; Blue identity admission and validation remain Contracts semantic work. + +`$pointerSet` charges `pointerSegmentWritten` per traversed or created segment and transient member/item production for newly created transient structure. It never recursively sizes the assigned value. + +### 12.6 Text work (normative) + +One text block contains up to 64 Unicode code points. + +A full scan of Text `t` charges: + +```text +textBlockExamined += ceil(codePointLength(t) / 64) +``` + +Newly constructed Text charges `textBlockConstructed` by the same formula. Concatenation, join, split, prefix tests, slicing, pointer escaping, Text conversion, and Text comparison charge only blocks actually examined or constructed according to their specified algorithm. + +For lexicographic comparison, each operand charges blocks containing code points read through the first difference or the end of the shorter operand. + +### 12.7 Integer and decimal work (normative) + +Integers use canonical unsigned base-`2^32` magnitude and separate sign. Let `L(x)` be at least 1 and otherwise the limb count. + +| Operation | `integerLimbOperation` quantity | +|---|---:| +| equality or ordering | `L(a) + L(b)` | +| addition/subtraction | `max(L(a), L(b)) + 1` | +| multiplication | `L(a) * L(b)` | +| division/remainder | `L(a) * L(b)` | + +Exact decimal operations use the same formulas over the unscaled Integer magnitudes plus one operation for scale alignment. This is a portable work formula, not a required host algorithm. + +### 12.8 Objects, lists, and collection queries (normative) + +- Reading a direct object member or list item charges its read counter. +- Enumerating an object charges `collectionItemVisited` per key in canonical key order, in addition to the direct read needed for a value when that value is read. +- Enumerating a list charges `collectionItemVisited` per visited position. +- Dynamic object construction charges `transientObjectMemberProduced` per retained field. +- Dynamic list construction charges `transientListItemProduced` per item. +- `$map`, `$filter`, `$flatMap`, `$reduce`, `$some`, `$find`, `$findEntry`, `$includes`, and `$forEach` charge one `collectionItemVisited` per evaluated input item. They do not add a second generic visit charge for the same iteration. +- Output-producing collection operators charge `collectionItemProduced` for each produced result item in addition to the dynamic aggregate production counter when a new container is built. +- Short-circuit operators stop all later item charges. + +`$keys` and `$entries` visit each semantic key once. Physical reference wrappers do not add keys or visits. + +### 12.9 Equality, matching, and type work (normative) + +Semantic equality and deep matching charge `comparisonNodeVisited` once per compared semantic node occurrence. + +- Known exact Node BlueIds may conclude exact-node identity after one visited comparison node. +- Text and numeric comparisons add §12.6 or §12.7 work. +- Objects visit keys in canonical order and stop at the first difference. +- Lists visit positions in order and stop at the first difference. +- `$is` additionally incurs the host's Contracts/Language type and validation counters; BEX MUST NOT duplicate them. + +### 12.10 Canonical sorting (normative) + +Whenever BEX sorts, the canonical trace is calculated as stable bottom-up merge sort: + +1. runs begin at width 1; +2. adjacent runs merge left-to-right; +3. width doubles after each pass; +4. equal elements select the left element; +5. every comparator call charges `sortComparison`, `comparisonNodeVisited`, and scalar content work. + +A physical implementation may use another algorithm but MUST report this canonical trace. + +### 12.11 Changes, events, and updates (normative) + +`$appendChange` charges `patchAppended` once after validating the operation and evaluating required operands. `remove` does not evaluate `val`. `$appendChanges` applies the same rule per entry. + +`$appendEvent` charges `eventAppended` once after evaluating a non-`undefined` event. `$appendEvents` applies it per event. + +Existing exact values are appended by identity. Newly constructed values already paid construction and later pay `blueOutputBoundary`; no recursive boundary-size term exists. + +`$objectSet` and `$pointerSet` charge operation expressions, key/path work, and transient members actually produced. They do not recursively size or rehash the inserted value. + +### 12.12 Blue output and identity (normative) + +Every transient value crossing a Blue boundary charges `blueOutputBoundary` once. Exact Blue values also charge the boundary counter but no recursive construction. + +`$nodeBlueId` charges `nodeIdentityRequested`. For a transient operand it also invokes the Blue output boundary and Contracts semantic identity establishment. The same identity work MUST be merged once, not repeated in BEX. + +### 12.13 Intrinsics (normative) + +`$intrinsic` charges `intrinsicCalled`, evaluated payload work, and a registered named child ledger from the exact intrinsic type. An intrinsic has no authority to return an arbitrary portable gas integer or to hide unnamed work. + +### 12.14 Exhaustion and failure (normative) + +Charges are admitted before work. The charge that would exceed the child budget is absent. Buffered BEX effects are discarded on runtime error or exhaustion. The admitted child trace is returned to Contracts, which applies its whole-invocation rollback rules. + +Transient provider unavailability suspends outside completed execution and commits no BEX child ledger. Deterministic invalid evidence retains the admitted trace and fails. + +### 12.15 Conformance trace (normative) + +The fixture harness records ordered entries: + +```text +BexGasCharge { + sequence + counter + quantity + weight + gas + sourcePath? + operator? + reason +} +``` + +`sequence` begins at zero. The sum of `quantity * weight` is the BEX child total. Every counter and operator family has a machine-readable microfixture. + +## 13. Errors and Diagnostics + +### 13.1 Error classes (normative) + +BEX distinguishes at least these error classes for fixtures and host APIs: + +| Class | Meaning | +|---|---| +| compile error | Source is syntactically or statically invalid as BEX. | +| runtime error | Execution fails after successful compilation. | +| parse error | Source text cannot be parsed as Blue/YAML/JSON input. | +| output-conversion error | A BEX result cannot be converted to a Blue node. | +| gas exhaustion | Runtime failure caused by gas limit. | + +A fixture implementation MUST classify errors deterministically. + +### 13.1.1 Diagnostic fields (normative) + +Fixture and host diagnostics MUST expose at least: + +- error class; +- message; +- source path when known; +- operator name when known; +- function name or call-frame path when known; +- pointer or path operand when the failure is pointer-related and safely reportable. + +Exact message text is not normative unless a fixture asserts `errorContains`. + +### 13.2 Runtime failures (normative) + +Runtime failure MUST stop execution. Accumulated changes and events are not committed by BEX. The host decides whether failed partial accumulators are observable for diagnostics. + +Runtime failures include: + +- invalid dynamic pointer, key, op, binding name, or step name; +- invalid type conversion; +- non-list input to list-only operators; +- non-object input to object-only operators where conversion is not defined; +- non-exact integer arithmetic; +- division by zero; +- function argument mismatch; +- invalid patch or event append; +- `$fail`; +- `$failIf` when its condition is truthy; +- invalid collection input or invalid collection result shape; +- invalid `$objectFromEntries` entries or keys; +- unsupported or failing intrinsic processor invocation. + +### 13.3 Metrics (normative) + +An execution implementation SHOULD expose deterministic metrics. The Java-derived metric categories include: + +- expression evaluations; +- statement executions; +- function calls; +- document reads; +- result-value reads; +- patch appends; +- event appends. + +Metrics are diagnostic. The canonical gas ledger and conformance-fixture expectations are normative; additional host metrics are not. + +--- + +## 14. Determinism and Security + +### 14.1 Determinism (normative) + +For a fixed: + +- BEX source; +- optional definition node; +- execution context; +- immutable document views; +- bindings, event, steps, and current contract values; +- gas schedule and gas limit; +- Blue Language 1.0 type matcher; +- registered intrinsic processor set and deterministic intrinsic processor behavior; + +BEX execution MUST be deterministic. + +BEX execution is synchronous from the program's point of view. Hosts MAY run BEX inside asynchronous runtimes, but BEX exposes no async primitives and no BEX operator may observe scheduling, timing, or interleaving. + +### 14.2 No implicit authority (normative) + +Except for explicitly registered `$intrinsic` processors, BEX has no implicit authority to: + +- mutate host documents; +- apply changesets; +- emit events externally; +- read clocks or randomness; +- access files, networks, databases, or environment variables; +- execute Blue contracts. + +All host data visible to BEX MUST be explicitly supplied through the execution context or through an explicitly registered intrinsic processor. A portable intrinsic processor MUST define deterministic semantics, failure behavior, and gas accounting for fixed inputs. + +### 14.3 Host validation (normative) + +A host MUST validate BEX output under its own authorization and content rules before applying changes or emitting events. + +BEX patch accumulation is not an authorization decision. + +### 14.4 Resource limits (normative) + +Hosts SHOULD enforce gas limits and MAY impose additional deterministic limits on: + +- source size; +- compile depth; +- expression nesting; +- statement count; +- list and object sizes; +- output size; +- pointer depth. + +If a limit affects execution, failure MUST be deterministic. + +--- + +## 15. Operator Reference Summary + +This section is normative unless otherwise marked. + +### 15.1 Expression operators + +```text +Reading: + $document, $binding, $event, $processingEvent, $steps, $currentContract, + $var, $const, $get, $changeset, $events, $resultValue + +Type, identity, and conversion: + $unwrap, $is, $kind, $isKind, $nodeBlueId, + $text, $integer, $number, $boolean, $object, $list + +Strings: + $concat, $pointerJoin, $join, $split, $startsWith, $sliceAfter + +Logic and comparison: + $eq, $ne, $gt, $gte, $lt, $lte, + $and, $or, $not, $truthy, $empty, $isEmpty, + $exists, $coalesce, $default + +Numeric: + $add, $subtract, $multiply, $divide + +Objects and lists: + $keys, $entries, $size, $listGet, $listConcat, + $merge, $objectSet, $pointerGet, $pointerSet, + $map, $filter, $flatMap, $reduce, + $some, $find, $findEntry, + $includes, $hasKey, $objectFromEntries + +Control and host boundary: + $choose, $call, $intrinsic, $literal, + $null, $emptyObject, $emptyList +``` + +### 15.2 Statement operators + +```text +$let, $set, $if, $forEach, +$appendChange, $appendChanges, +$appendEvent, $appendEvents, +$call, $return, $returnIf, $fail, $failIf +``` + +### 15.3 Operand naming guidance (informative) + +Operator bodies SHOULD use BEX operand names rather than Blue wrapper names where possible. For example: + +```yaml +# Preferred +$join: + list: [a, b] + separator: "," + +# Avoid using Blue payload keys as BEX operand names +# unless the operator explicitly defines them. +``` + +BEX operator operands commonly use: + +```text +node, list, input, pattern, object, key, path, val, +cond, then, else, name, expr, where, in, item, index, +acc, init, order, vars, op, args, function, message +``` + +### 15.4 Future standard library candidates (informative) + +The following names are candidates for later BEX revisions and are not normative BEX 2.0 operators: + +```text +$length, $slice, $findIndex, $findKey, $every, +$pick, $omit, $entriesMap, +$range, $min, $max, $sort, $match, $break, $continue +``` + +In BEX 2.0, `$size` is a collection/cardinality helper. For scalar text it returns `1`, not text length. A dedicated text code-point length operator is deferred. + +--- + +## 16. Machine-Readable Conformance Fixtures + +### 16.1 Fixture root (normative) + +A BEX fixture is a YAML object with: + +```yaml +id: BEX-... +category: compiler | execution | statement | boundary | gas | representation +program: ... +context: ... +expected: ... +``` + +`expected` may contain result value, changes, events, error class, exact ordered gas trace, total gas, semantic demands, and equivalent representation variants. + +### 16.2 Required context fields (normative) + +The harness can provide exact Blue values for: + +```text +rootDocument +event +processingEvent +currentContract +steps +bindings +documentScope +registered intrinsics +``` + +Exact values may be supplied inline or by pure reference with provider content. Equivalent variants MUST produce the same result and canonical counter trace. + +### 16.3 Required fixture assertions (normative) + +The fixture package MUST include: + +- all compiler and execution vectors in §17; +- an exact microfixture for every gas counter; +- paired inline/reference and eager/lazy variants for every representation-sensitive operator family; +- `$processingEvent` under external and internal delivery; +- `$nodeBlueId` for exact and transient values; +- no-observable-reference-wrapper cases; +- constructed-versus-carried large values; +- short-circuit and exhaustion trace prefixes; +- output-boundary validation; +- runtime-ledger merge exactly once. + +### 16.4 Manifest and package identity (normative) + +The release publishes a manifest containing: + +```text +specificationVersion +fixture schema version +fixture files and SHA-256 digests +vector coverage +counter coverage +gas schedule identity +fixture package SHA-256 +``` + +A conforming BEX 2.0 implementation MUST report the exact runtime-registry, gas-manifest, and fixture-package identities it implements and passes. + +The implementation-baseline fixture package is bound to the exact BEX runtime registry and `blue-bex/gas/2.0` manifest. It covers 57 normative vectors with 102 behavior fixtures, including direct executable coverage of every normative operator, and 30 gas-counter microfixtures. Its identity is: + +```text +sha256:f5f64a38152ef0e50ebb1b03caaa1b07fd556552eb071b940937079fc0234dfe +``` + +## 17. Conformance Vectors + +Every vector is behavior-defining and has at least one machine-readable fixture. + +### 17.1 Compiler vectors + +- **BEX-C-01.** Exactly one `$` key denotes an expression operator; multiple keys denote an object expression. +- **BEX-C-02.** Unknown expression and statement operators fail compilation. +- **BEX-C-03.** `$literal` preserves nested unknown operators but cannot place expressions in static Blue fields. +- **BEX-C-04.** Unknown constants/functions, missing/extra arguments, recursive calls, and entry functions with arguments fail. +- **BEX-C-05.** Reserved names in plain name containers fail. +- **BEX-C-06.** Dynamic `$is.pattern` and `$intrinsic.type` fail. +- **BEX-C-07.** `$set` of an undeclared local and invalid `$let.order` fail. +- **BEX-C-08.** Compiler diagnostics include class, source path, and operator when known. +- **BEX-C-09.** Direct or mutual function recursion is rejected from the complete static call graph before execution. + +### 17.2 Semantic execution vectors + +- **BEX-E-01.** `$document` uses document-scope-relative and absolute pointers correctly. +- **BEX-E-02.** `$event`, `$processingEvent`, `$currentContract`, `$steps`, `$binding`, `$var`, and `$const` use value-local pointers. +- **BEX-E-03.** Dynamic null/undefined pointers and dynamic null/undefined key/name operands fail. +- **BEX-E-04.** `$pointerJoin` escapes `~` and `/`. +- **BEX-E-05.** `$exists` is false only for semantic `undefined`; incomplete access is not absence. +- **BEX-E-06.** Numeric zero is truthy; empty object/list and null are falsy. +- **BEX-E-07.** `$and`, `$or`, `$coalesce`, `$choose`, `$if`, and collection search short-circuit. +- **BEX-E-08.** Numeric conversions, exact division, Text rendering, and finite Double conversion are deterministic. +- **BEX-E-09.** Object keys are exposed in Unicode code-point order independent of host maps and locale. +- **BEX-E-10.** `$kind` and `$isKind` report semantic kinds, never reference/cursor classes. +- **BEX-E-11.** Collection queries iterate in canonical order, restore bindings, and preserve short-circuiting. +- **BEX-E-12.** `$objectSet`/`$pointerSet` use value-local semantics and create only permitted transient intermediates. +- **BEX-E-13.** Function frames isolate locals and validate static argument patterns. +- **BEX-E-14.** BEX numeric equality remains distinct from Blue identity. + +### 17.3 Statements and accumulators + +- **BEX-S-01.** `$if` executes only one branch; `$forEach` binds item/key/index deterministically. +- **BEX-S-02.** `$appendChange(s)` validates operations, requires `val` for add/replace, and does not evaluate remove `val`. +- **BEX-S-03.** Duplicate patch paths are preserved in append order. +- **BEX-S-04.** `$appendEvent(s)` preserves order and multiplicity and rejects `undefined`. +- **BEX-S-05.** `$resultValue` applies accumulated patches in order, including parent/child replacement and sparse non-shifting list removal. +- **BEX-S-06.** `$return`, `$returnIf`, `$fail`, and `$failIf` have deterministic lazy exit behavior. +- **BEX-S-07.** Parallel and ordered `$let` semantics are distinct and deterministic. + +### 17.4 Representation and identity vectors + +- **BEX-R-01.** Inline, pure-reference, eagerly materialized, and lazily materialized forms of the same exact value produce the same result and gas. +- **BEX-R-02.** `$kind`, `$exists`, `$keys`, `$entries`, `$size`, truthiness, equality, matching, iteration, and pointer reads do not reveal reference state. +- **BEX-R-03.** A pure reference wrapper does not create a semantic child named `blueId`. +- **BEX-R-04.** `$nodeBlueId` returns exact identity without transitive expansion. +- **BEX-R-05.** `$nodeBlueId` on a transient value establishes one exact identity and merges semantic identity work once. +- **BEX-R-06.** Passing an existing large exact node through patches, events, constants, functions, and output has no recursive size charge. +- **BEX-R-07.** Constructing or scanning a large value pays member/Text/numeric work. +- **BEX-R-08.** Warm/cold cache, provider batching, and physical segmentation do not change result or gas. +- **BEX-R-09.** A final cyclic-set member is an exact opaque value: `$nodeBlueId` returns `MASTER#index` without provider demand or independent member hashing; structural reads require cyclic-set proof. + +### 17.5 Host-boundary vectors + +- **BEX-H-01.** Root `undefined`, list `undefined`, mixed `blueId`, mixed payload kinds, `properties`, invalid schema keys, computed `blue`, and unsupported list controls fail conversion. +- **BEX-H-02.** Existing exact nodes cross output by identity; transient aggregates convert deterministically. +- **BEX-H-03.** Computed Blue language fields retain their reserved meaning. +- **BEX-H-04.** Sparse overlay lists cannot cross as ordinary Blue lists. +- **BEX-H-05.** Blue Double input is source-token independent and output rounding is deterministic. +- **BEX-H-06.** `$processingEvent` remains the original external event in Document Update, Triggered, Lifecycle, and Embedded deliveries. + +### 17.6 Gas vectors + +- **BEX-G-01.** Every named counter has an exact microfixture and a weight bound by the current gas-manifest identity; final publication freezes the calibrated weight. +- **BEX-G-02.** Charges are admitted before work; the failing charge is absent on exhaustion. +- **BEX-G-03.** Skipped operands and post-short-circuit items produce no charges. +- **BEX-G-04.** Pointer reads/writes charge exact segment and member/item work. +- **BEX-G-05.** Text uses 64-code-point blocks, never UTF-16 units. +- **BEX-G-06.** Integer operations use the portable limb formulas. +- **BEX-G-07.** Collection visits and produced items are charged exactly once per semantic occurrence. +- **BEX-G-08.** Equality/matching charges comparison nodes and examined scalar work. +- **BEX-G-09.** Sorting reports canonical stable merge-sort comparisons. +- **BEX-G-10.** `$pointerSet`, `$objectSet`, `$appendChange(s)`, and `$appendEvent(s)` contain no recursive `estimatedSize` term. +- **BEX-G-11.** Exact values and transient values differ only by actual construction/identity-boundary work, not by serialization shape. +- **BEX-G-12.** The BEX child ledger is live-bounded and merged into Contracts exactly once. +- **BEX-G-13.** A BEX-local gas limit can reduce but never replenish the parent budget. +- **BEX-G-14.** Intrinsics return named deterministic child counters rather than opaque gas. +- **BEX-G-15.** A large bounded iteration is stopped by live gas admission at an exact trace prefix; buffered changes and events are discarded. + +## 18. Worked Examples + +### 18.1 Simple expression + +```yaml +expr: + $add: + - 2 + - 3 +``` + +The program returns integer `5`. + +### 18.2 Statement program with changes and events + +```yaml +do: + - $appendChange: + op: replace + path: /status + val: CONFIRMED + - $appendEvent: + type: StatusChanged + status: CONFIRMED + - $return: + changeset: + $changeset: true + events: + $events: true +``` + +The program accumulates one patch and one event, then returns them as data. + +### 18.3 Constants + +```yaml +constants: + threshold: 400 +expr: + $gte: + - $document: /amount + - $const: threshold +``` + +A missing `threshold` constant would be a compile-time error. + +### 18.4 Function with static argument pattern + +```yaml +functions: + isLarge: + args: + amount: + type: Integer + expr: + $gte: + - $var: amount + - 400 +expr: + $call: + function: isLarge + args: + amount: + $document: /amount +``` + +The argument is evaluated and then matched against the static Blue pattern before the function body runs. + +### 18.5 Reading host bindings + +```yaml +expr: + $binding: actor/id +``` + +This reads binding `actor` at value-local path `/id`. + +Equivalent object form: + +```yaml +expr: + $binding: + name: actor + path: /id +``` + +### 18.6 Dynamic pointer construction + +```yaml +expr: + $document: + $pointerJoin: + - reservations + - $event: /reservationId + - status +``` + +If `reservationId` contains `/` or `~`, `$pointerJoin` escapes it correctly. + +### 18.7 `$resultValue` + +```yaml +do: + - $appendChange: + op: replace + path: /status + val: CONFIRMED + - $return: + statusAfterPatch: + $resultValue: /status +``` + +The returned `statusAfterPatch` is `CONFIRMED`, even though the host document has not been mutated by BEX execution. + +### 18.8 `$resultValue` non-shifting list removal + +```yaml +context: + rootDocumentSource: + items: [A, B, C, D] +programSource: + do: + - $appendChange: + op: remove + path: /items/1 + - $return: + removedExists: + $exists: + $resultValue: /items/1 + stillAt2: + $resultValue: /items/2 + positionalSize: + $size: + $resultValue: /items +``` + +`removedExists` is `false`. `/items/2` still reads `C`. `positionalSize` is `4`. The overlay does not shift later indexes. Returning the whole sparse overlay list directly would fail Blue output conversion under the host boundary. + +### 18.9 `$resultValue` parent and child replacement + +```yaml +context: + rootDocumentSource: + order: + status: DRAFT + total: 10 +programSource: + do: + - $appendChange: + op: replace + path: /order + val: + status: CONFIRMED + - $appendChange: + op: replace + path: /order/total + val: 20 + - $return: + orderAfter: + $resultValue: /order +``` + +The returned `orderAfter` is: + +```yaml +status: CONFIRMED +total: 20 +``` + +Patches are applied in append order, so the child replacement is applied after the parent replacement. + +### 18.10 Guard-style return + +```yaml +do: + - $returnIf: + cond: + $not: + $isKind: + val: + $event: /message/request/summary + kind: text + expr: + changeset: [] + events: + - type: Conversation/Proposed Change Invalid + reason: summary is missing + - $return: + accepted: true +``` + +If the summary is missing or not text, the root function returns the invalid result and the later `$return` is not executed. + +### 18.11 Collection projection + +```yaml +expr: + $map: + in: + $event: /message/request/changeset + item: patch + expr: + op: + $var: + name: patch + path: /op + path: + $var: + name: patch + path: /path +``` + +The expression projects each requested patch into a normalized patch-like object without mutable accumulator boilerplate. + +### 18.12 Intrinsic signature verification + +```yaml +expr: + $intrinsic: + type: + blueId: CommonCryptoEd25519Verify + publicKey: + $const: trustedSignerPublicKey + message: + $event: /message/canonicalBytes + signature: + $event: /message/signature +``` + +The host must have a processor registered for the BlueId of `CommonCryptoEd25519Verify`. The intrinsic definition must specify the exact message bytes being verified. + +### 18.13 Literal escape + +```yaml +expr: + $literal: + $call: + function: notExecuted + args: {} +``` + +The result is an object containing the `$call` key as data. No function is invoked. + +--- + +## Appendix A — BEX Value and Blue Node Boundary + +This appendix is informative. + +BEX values are optimized for execution, not identity. Blue nodes are optimized for content semantics and BlueId. A host should not assume that every BEX object is already a valid Blue node. The conversion boundary is deliberately strict so that invalid Blue output fails before it is stored or hashed. + +Examples of invalid output: + +```yaml +# Invalid: mixed pure reference and content +blueId: X +name: Something +``` + +```yaml +# Invalid: Blue has no properties wrapper +properties: + a: 1 +``` + +```yaml +# Invalid: mixed payload kinds +value: 1 +items: [2] +``` + +--- + +## Appendix B — Gas Calculation Examples + +This appendix is informative. Exact expected traces are machine-readable fixtures. + +A trivial root expression: + +```yaml +expr: 1 +``` + +charges: + +```text +functionCalled 1 × 2 +expressionEvaluated 1 × 1 +-------------------------------- +total 3 +``` + +A short-circuit expression: + +```yaml +expr: + $and: + - false + - $fail: should not run +``` + +charges the root function, `$and`, and the first operand. The skipped `$fail` expression and message produce no charges. + +Text construction uses 64-code-point blocks: + +```text +"" -> 0 text blocks +"hello" -> 1 text block +64 code points -> 1 text block +65 code points -> 2 text blocks +"😀" -> 1 code-point block +``` + +Passing an exact node containing a large Text value does not charge those blocks. Scanning or constructing that Text does. + +Appending an existing exact event charges the evaluated expression, `eventAppended`, and the later Blue output boundary. It does not recursively size the event. Constructing a new object event additionally charges one transient-object-member counter per produced field and Text/numeric work actually performed. + +## Appendix C — Common Implementer Mistakes + +This appendix is informative. + +### C.1 Do not treat every `$` key as an operator + +Only an expression object with exactly one `$` key is an expression operator. Multi-field dollar objects are data. + +### C.2 Do not allow executable BEX in Blue type fields + +`type`, `itemType`, `keyType`, `valueType`, `blue`, and `schema` are static Blue definition positions. + +### C.3 Do not use Blue reserved keys as user argument names + +Function argument names and call argument names live in plain name containers. Reserved Blue keys such as `value`, `items`, `type`, and `schema` are invalid there. + +### C.4 Do not apply changesets automatically + +BEX accumulates patches. The host applies or rejects them. + +### C.5 Do not delete or reorder patches + +Patch order and duplicate patch paths are meaningful. + +### C.6 Do not interpret dynamic null pointers as root + +Omitted static paths can mean root/default. Dynamic `null` or `undefined` paths are errors. + +### C.7 Do not forget root function gas + +Every execution invokes the root function and charges `functionCalled`. + +### C.8 Do not expose mutable host nodes + +Host nodes visible to BEX must be immutable for the duration of execution or snapshotted. + +### C.9 Do not treat BEX numeric equality as Blue identity + +BEX `1` and `1.0` compare equal as execution numbers. Blue scalar identity still distinguishes Blue `Integer` from Blue `Double`. + +### C.10 Do not expose object keys in host map iteration order + +BEX object key exposure order is lexicographic by Unicode code point, independent of host map insertion or iteration order. + +### C.11 Do not swallow runtime failures inside `$is.node` + +If evaluating `$is.node` fails, that runtime failure propagates. Only successful non-conversion or non-match returns `false`. + +### C.12 Do not treat `$size` as text length + +`$size` is a collection/cardinality helper. For scalar text it returns `1`. + +### C.13 Do not implement `$binding` with host-defined gas under the fixture harness + +A `$binding` read charges `expressionEvaluated`, `bindingRead`, and demanded pointer/member work. + +### C.14 Do not rely on host operand evaluation order + +BEX defines operand evaluation order. Host map order and source parser map order are not execution semantics. + +### C.15 Do not assume `$split` uses regex semantics or drops trailing empty parts + +`$split` uses literal separators. Omitted or `-1` limit preserves trailing empty parts. + +### C.16 Do not materialize sparse `$resultValue` list overlays as dense Blue lists + +Non-shifting list removals create sparse overlay slots. Converting such an overlay list to Blue output fails while a removed slot is `undefined`. + +### C.17 Do not emit `blue` under the strict host boundary + +Computed `blue` is invalid portable BEX output. + +### C.18 Do not use UTF-16 or recursive size gas + +BEX 2.0 meters Text in Unicode code-point blocks and meters construction/inspection work. It never recursively estimates value size. + +### C.19 Do not emit Java compatibility schema keys + +BEX 2.0 output uses only the schema vocabulary of Blue Language 1.0. + +### C.20 Do not use collection queries as leaking local scopes + +`$map`, `$filter`, `$flatMap`, `$reduce`, `$some`, `$find`, and `$findEntry` temporarily bind item/key/index/acc slots. Implementations MUST restore previous local values after the expression. + +### C.21 Do not split `$var` or `$const` names on `/` + +Path-aware reads use object form with `name` and `path`. The scalar form is a variable or constant name, even when it contains `/`. + +### C.22 Do not implement `$intrinsic` as arbitrary host callback dispatch + +`$intrinsic.type` is static Blue data. The operation is allowed only when the active registry has a processor for the resolved BlueId. Unsupported BlueIds fail at compile time. + +--- + +*End of Blue BEX Specification 2.0.* diff --git a/src/main/java/blue/bex/api/BexEngine.java b/src/main/java/blue/bex/api/BexEngine.java index 0720cf1..631c51c 100644 --- a/src/main/java/blue/bex/api/BexEngine.java +++ b/src/main/java/blue/bex/api/BexEngine.java @@ -12,6 +12,9 @@ import blue.bex.result.BexMetrics; import blue.bex.runtime.BexRuntime; import blue.language.Blue; +import blue.language.registry.BlueCoreTypeRegistry; + +import java.util.Map; /** * Public entry point for compiling and executing selected BEX programs. @@ -84,8 +87,9 @@ private BexExecutionResult execute(BexCompiledProgram program, BexExecutionConte return new BexExecutionResult(result.value(), result.changeset(), result.events(), - result.gasUsed(), - metrics); + result.gasLedger(), + metrics, + result.output()); } public BexExecutionResult compileAndExecute(BexProgramSource source, BexExecutionContext context) { @@ -97,7 +101,18 @@ public BexExecutionResult compileAndExecute(BexProgramSource source, BexExecutio } private BexCompiledProgramKey key(BexProgramSource source) { - return BexCompiledProgramKey.from(source); + return BexCompiledProgramKey.from(source, compileEnvironmentIdentity()); + } + + private String compileEnvironmentIdentity() { + return BexCompiledProgramKey.COMPILER_IDENTITY + + "|runtimeRegistry=" + + BexCompiledProgramKey.BEX_RUNTIME_REGISTRY_IDENTITY + + "|gasManifest=" + gasSchedule.manifestIdentity() + + "|gasWeights=" + gasSchedule.counterWeights() + + "|languageRegistry=" + + BlueCoreTypeRegistry.INSTANCE.packageIdentity() + + "|intrinsics=" + intrinsics.identity(); } private void validateIntrinsicSupport(BexCompiledProgram program) { @@ -140,13 +155,24 @@ public Builder intrinsics(BexIntrinsicRegistry intrinsics) { return this; } - public Builder intrinsic(String blueId, BexIntrinsicProcessor processor) { - this.intrinsics = this.intrinsics.with(blueId, processor); + public Builder intrinsic(String blueId, + String registryIdentity, + Map counterWeights, + BexIntrinsicProcessor processor) { + this.intrinsics = this.intrinsics.with( + blueId, registryIdentity, counterWeights, processor); return this; } - public Builder intrinsic(Class typeClass, BexIntrinsicProcessor processor) { - this.intrinsics = this.intrinsics.with(typeClass, processor); + public Builder intrinsic(Class typeClass, + String registryIdentity, + Map counterWeights, + BexIntrinsicProcessor processor) { + this.intrinsics = this.intrinsics.with( + typeClass, + registryIdentity, + counterWeights, + processor); return this; } diff --git a/src/main/java/blue/bex/api/BexExecutionContext.java b/src/main/java/blue/bex/api/BexExecutionContext.java index 557650f..4346370 100644 --- a/src/main/java/blue/bex/api/BexExecutionContext.java +++ b/src/main/java/blue/bex/api/BexExecutionContext.java @@ -1,7 +1,13 @@ package blue.bex.api; +import blue.bex.output.BexSemanticIdentityBoundary; +import blue.bex.output.ProcessorExecutionContextBexSemanticIdentityBoundary; +import blue.bex.gas.BexGasMeter; +import blue.bex.gas.BexGasCounter; import blue.bex.value.BexValue; import blue.bex.value.BexValues; +import blue.language.processor.ProcessorExecutionContext; +import blue.language.snapshot.FrozenNode; import java.util.ArrayDeque; import java.util.Collections; @@ -24,16 +30,32 @@ public final class BexExecutionContext { private final BexDocumentView document; private final Map bindingSlots; private final BexValue event; + private final BexValue processingEvent; private final BexValue currentContract; private final BexStepResults steps; + private final long parentRemainingGas; private final long gasLimit; + private final BexGasLedgerHost gasLedgerHost; + private final BexSemanticIdentityBoundary semanticIdentityBoundary; private final ResolutionCoordinator resolutionCoordinator; private volatile Map materializedBindings; private BexExecutionContext(Builder builder) { this.document = builder.document; this.steps = builder.steps != null ? builder.steps : BexStepResults.empty(); + this.parentRemainingGas = builder.parentRemainingGas; this.gasLimit = builder.gasLimit; + this.gasLedgerHost = builder.gasLedgerHost; + if (builder.semanticIdentityBoundary == null + && builder.gasLedgerHost != null) { + throw new IllegalArgumentException( + "Hosted BEX execution requires an explicit " + + "semantic identity boundary"); + } + this.semanticIdentityBoundary = + builder.semanticIdentityBoundary != null + ? builder.semanticIdentityBoundary + : BexSemanticIdentityBoundary.STANDALONE; if (document == null) { throw new IllegalArgumentException("document is required"); } @@ -47,6 +69,7 @@ private BexExecutionContext(Builder builder) { } this.bindingSlots = Collections.unmodifiableMap(copy); this.event = bindingFrom(copy, "event"); + this.processingEvent = bindingFrom(copy, "processingEvent"); this.currentContract = bindingFrom(copy, "currentContract"); } @@ -62,6 +85,14 @@ public BexValue event() { return event; } + /** + * Original external Processing Event, distinct from the current channel + * event used by triggered/lifecycle/embedded delivery. + */ + public BexValue processingEvent() { + return processingEvent; + } + public BexValue currentContract() { return currentContract; } @@ -107,10 +138,29 @@ public long gasLimit() { return gasLimit; } + /** + * Standalone parent budget. Hosted execution obtains the exact value from + * the live child ledger instead. + */ + public long parentRemainingGas() { + return parentRemainingGas; + } + + public BexGasLedgerHost gasLedgerHost() { + return gasLedgerHost; + } + + public BexSemanticIdentityBoundary semanticIdentityBoundary() { + return semanticIdentityBoundary; + } + public static final class Builder { private BexDocumentView document; private BexStepResults steps; - private long gasLimit = 100_000L; + private long parentRemainingGas = Long.MAX_VALUE; + private long gasLimit = BexGasMeter.NO_LOCAL_LIMIT; + private BexGasLedgerHost gasLedgerHost; + private BexSemanticIdentityBoundary semanticIdentityBoundary; private final LinkedHashMap bindings = new LinkedHashMap<>(); public Builder document(BexDocumentView document) { @@ -118,10 +168,56 @@ public Builder document(BexDocumentView document) { return this; } + /** + * Configures the standard Contracts 1.0 host views, live gas bridge, + * and invocation-owned semantic identity boundary under the default + * physical runtime namespace {@code bex}. + * + *

Use {@link #processorExecutionContext(ProcessorExecutionContext, + * String)} when one host invocation executes more than one BEX + * program.

+ */ + public Builder processorExecutionContext(ProcessorExecutionContext context) { + return processorExecutionContext( + context, BexGasCounter.NAMESPACE); + } + + /** + * Configures hosted execution under an explicit deterministic physical + * runtime namespace. Distinct BEX executions in one processor work + * session must use distinct namespaces so they share one live parent + * budget without producing ambiguous traces. + */ + public Builder processorExecutionContext( + ProcessorExecutionContext context, + String runtimeNamespace) { + Objects.requireNonNull(context, "context"); + document(new ProcessorExecutionContextBexDocumentView(context)); + gasLedgerHost(new ProcessorExecutionContextBexGasLedgerHost( + context, runtimeNamespace)); + semanticIdentityBoundary( + new ProcessorExecutionContextBexSemanticIdentityBoundary( + context)); + event(BexValues.nodeSnapshot(context.event())); + FrozenNode processEvent = context.frozenProcessEvent(); + processingEvent(processEvent != null + ? BexValues.frozen(processEvent) + : BexValues.undefined()); + FrozenNode contract = context.frozenContractNode(); + currentContract(contract != null + ? BexValues.frozen(contract) + : BexValues.undefined()); + return this; + } + public Builder event(BexValue event) { return binding("event", event); } + public Builder processingEvent(BexValue processingEvent) { + return binding("processingEvent", processingEvent); + } + public Builder currentContract(BexValue currentContract) { return binding("currentContract", currentContract); } @@ -176,18 +272,34 @@ public Builder bindings(Map values) { return this; } - /** - * @deprecated current scope belongs to the configured - * {@link BexDocumentView}. This method is retained as a no-op for - * source compatibility. - */ - @Deprecated - public Builder currentScopePath(String currentScopePath) { + public Builder gasLimit(long gasLimit) { + if (gasLimit < BexGasMeter.NO_LOCAL_LIMIT) { + throw new IllegalArgumentException( + "gasLimit must be non-negative or NO_LOCAL_LIMIT"); + } + this.gasLimit = gasLimit; return this; } - public Builder gasLimit(long gasLimit) { - this.gasLimit = gasLimit; + public Builder parentRemainingGas(long parentRemainingGas) { + if (parentRemainingGas < 0L) { + throw new IllegalArgumentException( + "parentRemainingGas must be non-negative"); + } + this.parentRemainingGas = parentRemainingGas; + return this; + } + + public Builder gasLedgerHost(BexGasLedgerHost gasLedgerHost) { + this.gasLedgerHost = gasLedgerHost; + return this; + } + + public Builder semanticIdentityBoundary( + BexSemanticIdentityBoundary semanticIdentityBoundary) { + this.semanticIdentityBoundary = semanticIdentityBoundary != null + ? semanticIdentityBoundary + : null; return this; } @@ -204,6 +316,7 @@ private static void validateBindingName(String name) { private static boolean isStandardBindingName(String name) { return "event".equals(name) + || "processingEvent".equals(name) || "currentContract".equals(name) || "steps".equals(name); } diff --git a/src/main/java/blue/bex/api/BexGasLedgerHost.java b/src/main/java/blue/bex/api/BexGasLedgerHost.java new file mode 100644 index 0000000..a1c55bf --- /dev/null +++ b/src/main/java/blue/bex/api/BexGasLedgerHost.java @@ -0,0 +1,85 @@ +package blue.bex.api; + +import blue.bex.gas.BexGasLimitExceededException; +import blue.language.processor.GasMeter; +import blue.language.processor.GasLimitExceededException; + +import java.util.Map; +import java.util.Objects; + +/** + * Parent-runtime bridge for one live BEX named child ledger. + * + *

The ordinary {@link #submit(GasMeter.ChildGasLedger)} callback is the + * success path only. Failure callbacks are separate because a generic + * processor work session owns final retention or discard, while older + * detached hosts merged deterministic prefixes directly.

+ */ +public interface BexGasLedgerHost { + GasMeter.ChildGasLedger open(String namespace, Map counterWeights); + + void submit(GasMeter.ChildGasLedger ledger); + + /** + * Whether this host can own separate live child ledgers for BEX and each + * registered intrinsic namespace. + * + *

The default requires physical separation. A compatibility host may + * return {@code false} only when no intrinsic namespace is registered; + * hosted execution never flattens intrinsic counters into {@code bex}.

+ */ + default boolean separatesRuntimeNamespaces() { + return true; + } + + /** + * Reports deterministic failure after the ledger admitted a prefix. + * + *

Every host must classify this path explicitly. Session-backed + * adapters leave the ledger staged because the enclosing processor + * failure path retains every admitted prefix exactly once.

+ */ + void failedDeterministically(GasMeter.ChildGasLedger ledger); + + /** + * Reports transient evidence unavailability. + * + *

Every host must classify this path explicitly. A session-backed host + * leaves the reservation staged so its enclosing suspension can discard + * it.

+ */ + void evidenceUnavailable(GasMeter.ChildGasLedger ledger); + + /** + * Maps a BEX-local sub-limit rejection after every admitted ledger prefix + * has been reported through {@link #failedDeterministically}. + * + *

A detached host retains the exact BEX exception. A processor adapter + * can expose its stable gas-limit category without inventing a structured + * host rejection that its live session did not produce.

+ */ + default RuntimeException localGasLimitExceeded( + BexGasLimitExceededException exhaustion, + RuntimeException originalFailure) { + Objects.requireNonNull(exhaustion, "exhaustion"); + return Objects.requireNonNull( + originalFailure, "originalFailure"); + } + + /** + * Returns a host-recorded rejected charge through the canonical host + * exhaustion path. + * + *

BEX reports every opened ledger through + * {@link #failedDeterministically(GasMeter.ChildGasLedger)} first, so the + * detached-host default only needs to rethrow the exact exception. + * Session-backed hosts delegate to their owning runtime work session, + * which validates that the same exception was recorded live.

+ */ + default void propagateGasExhaustion( + GasMeter.ChildGasLedger ledger, + GasLimitExceededException exhaustion) { + Objects.requireNonNull(ledger, "ledger"); + throw Objects.requireNonNull(exhaustion, "exhaustion"); + } +} diff --git a/src/main/java/blue/bex/api/BexIntrinsicInvocation.java b/src/main/java/blue/bex/api/BexIntrinsicInvocation.java index 594f937..5059b51 100644 --- a/src/main/java/blue/bex/api/BexIntrinsicInvocation.java +++ b/src/main/java/blue/bex/api/BexIntrinsicInvocation.java @@ -1,5 +1,6 @@ package blue.bex.api; +import blue.bex.output.BexAdmittedValue; import blue.bex.value.BexValue; import blue.bex.value.BexValues; @@ -7,47 +8,58 @@ import java.util.LinkedHashMap; import java.util.Map; import java.util.Objects; -import java.util.function.LongConsumer; import java.util.function.LongSupplier; +import java.util.function.Function; /** - * Evaluated request passed to a host intrinsic processor. + * Evaluated request passed to one exact registry-bound intrinsic processor. */ public final class BexIntrinsicInvocation { + @FunctionalInterface + interface NamedGasCharger { + void charge(String counterName, long quantity, String reason); + } + private final String blueId; private final BexValue type; private final Map fields; - private final LongConsumer gasCharger; + private final String gasNamespace; + private final Map namedCounterWeights; + private final NamedGasCharger gasCharger; private final LongSupplier gasUsed; + private final Function exactAdmission; - public BexIntrinsicInvocation(String blueId, - BexValue type, - Map fields, - LongConsumer gasCharger, - LongSupplier gasUsed) { + BexIntrinsicInvocation(String blueId, + BexValue type, + Map fields, + String gasNamespace, + Map namedCounterWeights, + NamedGasCharger gasCharger, + LongSupplier gasUsed, + Function exactAdmission) { this.blueId = Objects.requireNonNull(blueId, "blueId"); this.type = type != null ? type : BexValues.undefined(); - this.fields = Collections.unmodifiableMap(new LinkedHashMap<>(fields != null - ? fields - : Collections.emptyMap())); - this.gasCharger = gasCharger != null ? gasCharger : amount -> { }; + this.fields = Collections.unmodifiableMap(new LinkedHashMap<>( + fields != null + ? fields + : Collections.emptyMap())); + this.gasNamespace = Objects.requireNonNull(gasNamespace, "gasNamespace"); + this.namedCounterWeights = Collections.unmodifiableMap( + new LinkedHashMap<>(namedCounterWeights)); + this.gasCharger = Objects.requireNonNull(gasCharger, "gasCharger"); this.gasUsed = gasUsed != null ? gasUsed : () -> 0L; + this.exactAdmission = + Objects.requireNonNull(exactAdmission, "exactAdmission"); } public String blueId() { return blueId; } - /** - * Static Blue type value from {@code $intrinsic.type}. - */ public BexValue type() { return type; } - /** - * Evaluated payload fields, excluding {@code type}. - */ public Map fields() { return fields; } @@ -57,13 +69,45 @@ public BexValue field(String name) { return value != null ? value : BexValues.undefined(); } + public String gasNamespace() { + return gasNamespace; + } + + /** + * Exact registry-declared counter vocabulary and weights. + */ + public Map namedCounterWeights() { + return namedCounterWeights; + } + + public void charge(String counterName, long quantity) { + charge(counterName, quantity, "intrinsic-work"); + } + + public void charge(String counterName, long quantity, String reason) { + if (!namedCounterWeights.containsKey(counterName)) { + throw new IllegalArgumentException( + "Unknown intrinsic gas counter " + gasNamespace + "." + counterName); + } + gasCharger.charge(counterName, quantity, reason); + } + /** - * Charge deterministic gas from the intrinsic implementation. + * Explicitly admits a payload field when this intrinsic's declared + * semantics require an exact Blue node. */ - public void chargeGas(long amount) { - gasCharger.accept(amount); + public BexAdmittedValue exactField(String name) { + BexValue value = field(name); + if (value.isUndefined()) { + throw new IllegalArgumentException( + "Required exact intrinsic field is missing: " + name); + } + return exactAdmission.apply(value); } + /** + * Diagnostic total of the shared BEX ledger at this instant. + */ public long gasUsed() { return gasUsed.getAsLong(); } diff --git a/src/main/java/blue/bex/api/BexIntrinsicRegistry.java b/src/main/java/blue/bex/api/BexIntrinsicRegistry.java index 3a9a3a6..433e990 100644 --- a/src/main/java/blue/bex/api/BexIntrinsicRegistry.java +++ b/src/main/java/blue/bex/api/BexIntrinsicRegistry.java @@ -1,6 +1,10 @@ package blue.bex.api; import blue.bex.BexException; +import blue.bex.gas.BexGasMeter; +import blue.bex.output.BexOutputAdmission; +import blue.bex.output.BexOutputKind; +import blue.bex.value.BexUnicodeOrder; import blue.bex.value.BexValue; import blue.bex.value.BexValues; import blue.language.utils.BlueIdResolver; @@ -10,19 +14,77 @@ import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.function.LongConsumer; -import java.util.function.LongSupplier; /** - * Registry of host intrinsic processors keyed by BlueId. + * Exact intrinsic registry keyed by static Blue type identity. */ public final class BexIntrinsicRegistry { - private static final BexIntrinsicRegistry EMPTY = new BexIntrinsicRegistry(Collections.emptyMap()); + private static final BexIntrinsicRegistry EMPTY = + new BexIntrinsicRegistry(Collections.emptyMap()); - private final Map processors; + private final Map registrations; + private final Map registeredNamedWeights; + private final Map> + registeredNamespaceWeights; + private final String identity; - private BexIntrinsicRegistry(Map processors) { - this.processors = Collections.unmodifiableMap(new LinkedHashMap<>(processors)); + private BexIntrinsicRegistry(Map registrations) { + this.registrations = Collections.unmodifiableMap( + new LinkedHashMap<>(registrations)); + LinkedHashMap weights = new LinkedHashMap<>(); + LinkedHashMap> + weightsByNamespace = new LinkedHashMap<>(); + StringBuilder identityBuilder = + new StringBuilder("blue-bex/intrinsics/2.0;"); + identityBuilder.append(registrations.size()).append(';'); + for (String blueId : BexUnicodeOrder.sortedCopy(registrations.keySet())) { + Registration registration = registrations.get(blueId); + appendIdentityToken(identityBuilder, blueId); + appendIdentityToken( + identityBuilder, registration.registryIdentity); + appendIdentityToken( + identityBuilder, registration.namespace); + identityBuilder.append( + registration.counterWeights.size()).append(';'); + for (Map.Entry counter + : registration.counterWeights.entrySet()) { + appendIdentityToken( + identityBuilder, counter.getKey()); + identityBuilder.append( + counter.getValue()).append(';'); + } + for (Map.Entry counter + : registration.counterWeights.entrySet()) { + LinkedHashMap namespaceWeights = + weightsByNamespace.get(registration.namespace); + if (namespaceWeights == null) { + namespaceWeights = new LinkedHashMap<>(); + weightsByNamespace.put( + registration.namespace, namespaceWeights); + } + String qualified = BexGasMeter.qualifiedCounterName( + registration.namespace, counter.getKey()); + if (weights.put(qualified, counter.getValue()) != null + || namespaceWeights.put( + counter.getKey(), counter.getValue()) != null) { + throw new IllegalArgumentException( + "Duplicate registered intrinsic counter " + qualified); + } + } + } + this.identity = identityBuilder.toString(); + this.registeredNamedWeights = Collections.unmodifiableMap(weights); + LinkedHashMap> immutableNamespaces = + new LinkedHashMap<>(); + for (Map.Entry> entry + : weightsByNamespace.entrySet()) { + immutableNamespaces.put( + entry.getKey(), + Collections.unmodifiableMap( + new LinkedHashMap<>(entry.getValue()))); + } + this.registeredNamespaceWeights = + Collections.unmodifiableMap(immutableNamespaces); } public static BexIntrinsicRegistry empty() { @@ -33,73 +95,279 @@ public static Builder builder() { return new Builder(); } - public BexIntrinsicRegistry with(String blueId, BexIntrinsicProcessor processor) { - Builder builder = builder(); - for (Map.Entry entry : processors.entrySet()) { - builder.register(entry.getKey(), entry.getValue()); - } - builder.register(blueId, processor); - return builder.build(); + public BexIntrinsicRegistry with(String blueId, + String registryIdentity, + Map counterWeights, + BexIntrinsicProcessor processor) { + return toBuilder() + .register(blueId, registryIdentity, counterWeights, processor) + .build(); } - public BexIntrinsicRegistry with(Class typeClass, BexIntrinsicProcessor processor) { - Builder builder = builder(); - for (Map.Entry entry : processors.entrySet()) { - builder.register(entry.getKey(), entry.getValue()); - } - builder.register(typeClass, processor); - return builder.build(); + public BexIntrinsicRegistry with(Class typeClass, + String registryIdentity, + Map counterWeights, + BexIntrinsicProcessor processor) { + return toBuilder() + .register( + typeClass, + registryIdentity, + counterWeights, + processor) + .build(); } public boolean supports(String blueId) { - return processors.containsKey(blueId); + return registrations.containsKey(blueId); } public Set supportedBlueIds() { - return processors.keySet(); + return registrations.keySet(); + } + + public String identity() { + return identity; + } + + /** + * Namespace-qualified weights to register in the live parent child ledger. + */ + public Map registeredNamedWeights() { + return registeredNamedWeights; + } + + /** + * Exact immutable counter catalogs grouped by their intrinsic runtime + * namespace. Hosted BEX uses these catalogs to open separate physical + * child ledgers instead of flattening intrinsic counters into {@code bex}. + */ + public Map> registeredNamespaceWeights() { + return registeredNamespaceWeights; + } + + /** + * Namespace-qualified weights needed by one statically compiled program. + */ + public Map registeredNamedWeights( + Set requiredBlueIds) { + LinkedHashMap selected = + new LinkedHashMap<>(); + for (String blueId : BexUnicodeOrder.sortedCopy( + requiredBlueIds != null + ? requiredBlueIds + : Collections.emptySet())) { + Registration registration = requiredRegistration(blueId); + for (Map.Entry counter + : registration.counterWeights.entrySet()) { + selected.put( + BexGasMeter.qualifiedCounterName( + registration.namespace, + counter.getKey()), + counter.getValue()); + } + } + return Collections.unmodifiableMap(selected); + } + + /** + * Physical runtime catalogs needed by one statically compiled program. + */ + public Map> registeredNamespaceWeights( + Set requiredBlueIds) { + LinkedHashMap> selected = + new LinkedHashMap<>(); + for (String blueId : BexUnicodeOrder.sortedCopy( + requiredBlueIds != null + ? requiredBlueIds + : Collections.emptySet())) { + Registration registration = requiredRegistration(blueId); + LinkedHashMap namespace = + selected.get(registration.namespace); + if (namespace == null) { + namespace = new LinkedHashMap<>(); + selected.put(registration.namespace, namespace); + } + namespace.putAll(registration.counterWeights); + } + LinkedHashMap> immutable = + new LinkedHashMap<>(); + for (Map.Entry> entry + : selected.entrySet()) { + immutable.put( + entry.getKey(), + Collections.unmodifiableMap( + new LinkedHashMap<>(entry.getValue()))); + } + return Collections.unmodifiableMap(immutable); } public BexValue invoke(String blueId, BexValue type, Map fields, - LongConsumer gasCharger, - LongSupplier gasUsed) { - BexIntrinsicProcessor processor = processors.get(blueId); - if (processor == null) { + BexGasMeter gas, + BexOutputAdmission outputAdmission) { + Registration registration = registrations.get(blueId); + if (registration == null) { throw new BexException("Unsupported intrinsic BlueId: " + blueId); } - BexValue value = processor.execute(new BexIntrinsicInvocation(blueId, type, fields, gasCharger, gasUsed)); + BexIntrinsicInvocation invocation = new BexIntrinsicInvocation( + blueId, + type, + fields, + registration.namespace, + registration.counterWeights, + (counter, quantity, reason) -> gas.chargeNamed( + registration.namespace, + counter, + quantity, + (String) null, + "$intrinsic", + reason), + gas::used, + value -> outputAdmission.admit( + value, BexOutputKind.INTRINSIC_INPUT)); + BexValue value = registration.processor.execute(invocation); return value != null ? value : BexValues.undefined(); } + private Builder toBuilder() { + Builder builder = builder(); + builder.registrations.putAll(registrations); + return builder; + } + + private Registration requiredRegistration(String blueId) { + Registration registration = registrations.get(blueId); + if (registration == null) { + throw new BexException( + "Unsupported intrinsic BlueId: " + blueId); + } + return registration; + } + + private static String namespaceFor(String blueId) { + return "intrinsic-" + blueId; + } + + private static void appendIdentityToken( + StringBuilder destination, + String value) { + destination.append(value.length()) + .append(':') + .append(value) + .append(';'); + } + + private static final class Registration { + private final String registryIdentity; + private final String namespace; + private final Map counterWeights; + private final BexIntrinsicProcessor processor; + + private Registration(String registryIdentity, + String namespace, + Map counterWeights, + BexIntrinsicProcessor processor) { + this.registryIdentity = registryIdentity; + this.namespace = namespace; + this.counterWeights = counterWeights; + this.processor = processor; + } + } + public static final class Builder { - private final LinkedHashMap processors = new LinkedHashMap<>(); + private final LinkedHashMap registrations = + new LinkedHashMap<>(); + + public Builder register(String blueId, + String registryIdentity, + Map counterWeights, + BexIntrinsicProcessor processor) { + return register( + blueId, + registryIdentity, + namespaceFor(blueId), + counterWeights, + processor); + } - public Builder register(String blueId, BexIntrinsicProcessor processor) { + public Builder register(String blueId, + String registryIdentity, + String namespace, + Map counterWeights, + BexIntrinsicProcessor processor) { if (blueId == null || blueId.trim().isEmpty()) { throw new IllegalArgumentException("intrinsic blueId is required"); } - processors.put(blueId, Objects.requireNonNull(processor, "processor")); + if (registryIdentity == null || registryIdentity.trim().isEmpty()) { + throw new IllegalArgumentException( + "intrinsic registry identity is required"); + } + if (namespace == null || namespace.trim().isEmpty() + || "bex".equals(namespace)) { + throw new IllegalArgumentException( + "intrinsic gas namespace must be non-empty and disjoint"); + } + if (namespace.indexOf('/') >= 0) { + throw new IllegalArgumentException( + "intrinsic gas namespace must not contain the reserved '/' separator"); + } + if (registrations.containsKey(blueId)) { + throw new IllegalArgumentException( + "intrinsic blueId is already registered: " + blueId); + } + Objects.requireNonNull( + counterWeights, "intrinsic named counter weights"); + if (counterWeights.isEmpty()) { + throw new IllegalArgumentException( + "intrinsic named counter weights must not be empty"); + } + LinkedHashMap weights = new LinkedHashMap<>(); + for (String counter + : BexUnicodeOrder.sortedCopy(counterWeights.keySet())) { + Long weight = counterWeights.get(counter); + if (counter == null || counter.trim().isEmpty() + || weight == null || weight <= 0L) { + throw new IllegalArgumentException( + "Intrinsic named gas counters require positive weights"); + } + weights.put(counter, weight); + } + registrations.put( + blueId, + new Registration( + registryIdentity, + namespace, + Collections.unmodifiableMap(weights), + Objects.requireNonNull(processor, "processor"))); return this; } - public Builder register(Class typeClass, BexIntrinsicProcessor processor) { + public Builder register(Class typeClass, + String registryIdentity, + Map counterWeights, + BexIntrinsicProcessor processor) { if (typeClass == null) { - throw new IllegalArgumentException("intrinsic type class is required"); + throw new IllegalArgumentException( + "intrinsic type class is required"); } String blueId = BlueIdResolver.resolveBlueId(typeClass); if (blueId == null || blueId.trim().isEmpty()) { - throw new IllegalArgumentException("intrinsic type class must have a resolvable @TypeBlueId: " - + typeClass.getName()); + throw new IllegalArgumentException( + "intrinsic type class must have a resolvable @TypeBlueId: " + + typeClass.getName()); } - return register(blueId, processor); + return register( + blueId, + registryIdentity, + counterWeights, + processor); } public BexIntrinsicRegistry build() { - if (processors.isEmpty()) { - return EMPTY; - } - return new BexIntrinsicRegistry(processors); + return registrations.isEmpty() + ? EMPTY + : new BexIntrinsicRegistry(registrations); } } } diff --git a/src/main/java/blue/bex/api/FrozenBexDocumentView.java b/src/main/java/blue/bex/api/FrozenBexDocumentView.java index 030d7d7..4d60fff 100644 --- a/src/main/java/blue/bex/api/FrozenBexDocumentView.java +++ b/src/main/java/blue/bex/api/FrozenBexDocumentView.java @@ -42,12 +42,12 @@ public String resolvePointer(String authoredPointer) { @Override public BexValue canonicalAt(String absolutePointer) { - return read(canonicalRoot, absolutePointer); + return readExact(absolutePointer); } @Override public BexValue resolvedAt(String absolutePointer) { - return read(resolvedRoot, absolutePointer); + return readExact(absolutePointer); } @Override @@ -55,12 +55,14 @@ public String currentScopePath() { return currentScopePath; } - private BexValue read(FrozenNode root, String pointer) { + private BexValue readExact(String pointer) { List segments = JsonPointer.split(pointer); - FrozenNode selected = root.at(JsonPointer.toPointer(segments)); - if (selected != null) { - return BexValues.frozen(selected); + String canonicalPointer = JsonPointer.toPointer(segments); + FrozenNode canonical = canonicalRoot.at(canonicalPointer); + FrozenNode resolved = resolvedRoot.at(canonicalPointer); + if (canonical != null || resolved != null) { + return BexValues.exact(canonical, resolved); } - return BexValues.frozen(root).at(segments); + return BexValues.exact(canonicalRoot, resolvedRoot).at(segments); } } diff --git a/src/main/java/blue/bex/api/ProcessorExecutionContextBexDocumentView.java b/src/main/java/blue/bex/api/ProcessorExecutionContextBexDocumentView.java index f7724bb..348d1ec 100644 --- a/src/main/java/blue/bex/api/ProcessorExecutionContextBexDocumentView.java +++ b/src/main/java/blue/bex/api/ProcessorExecutionContextBexDocumentView.java @@ -2,7 +2,7 @@ import blue.bex.value.BexValue; import blue.bex.value.BexValues; -import blue.language.model.Node; +import blue.language.snapshot.FrozenNode; import blue.language.processor.ProcessorExecutionContext; import blue.language.utils.JsonPointer; @@ -25,12 +25,12 @@ public String resolvePointer(String authoredPointer) { @Override public BexValue canonicalAt(String absolutePointer) { - return documentAt(absolutePointer); + return exactAt(absolutePointer); } @Override public BexValue resolvedAt(String absolutePointer) { - return documentAt(absolutePointer); + return exactAt(absolutePointer); } @Override @@ -39,8 +39,11 @@ public String currentScopePath() { return pointer != null ? JsonPointer.canonicalize(pointer) : "/"; } - private BexValue documentAt(String absolutePointer) { - Node selected = context.documentAt(absolutePointer); - return selected != null ? BexValues.nodeSnapshot(selected) : BexValues.undefined(); + private BexValue exactAt(String absolutePointer) { + FrozenNode canonical = context.canonicalFrozenAt(absolutePointer); + FrozenNode resolved = context.resolvedFrozenAt(absolutePointer); + return canonical != null || resolved != null + ? BexValues.exact(canonical, resolved) + : BexValues.undefined(); } } diff --git a/src/main/java/blue/bex/api/ProcessorExecutionContextBexGasLedgerHost.java b/src/main/java/blue/bex/api/ProcessorExecutionContextBexGasLedgerHost.java new file mode 100644 index 0000000..0383f00 --- /dev/null +++ b/src/main/java/blue/bex/api/ProcessorExecutionContextBexGasLedgerHost.java @@ -0,0 +1,136 @@ +package blue.bex.api; + +import blue.bex.gas.BexGasCounter; +import blue.bex.gas.BexGasLimitExceededException; +import blue.language.processor.GasLimitExceededException; +import blue.language.processor.GasMeter; +import blue.language.processor.ProcessorExecutionContext; +import blue.language.processor.ProcessorErrorCategory; +import blue.language.processor.ProcessorFailureException; +import blue.language.processor.RuntimeWorkSession; + +import java.util.Map; +import java.util.Objects; + +/** + * Contracts 1.0 host adapter for BEX's parent-bounded runtime ledger. + * + *

The adapter opens the logical BEX ledger under one deterministic + * physical runtime namespace. Callers executing more than one BEX program in + * a host invocation must provide distinct physical namespaces; all such + * ledgers remain owned by the same {@link RuntimeWorkSession} and therefore + * share its live parent budget.

+ */ +public final class ProcessorExecutionContextBexGasLedgerHost implements BexGasLedgerHost { + private final RuntimeWorkSession session; + private final String runtimeNamespace; + + public ProcessorExecutionContextBexGasLedgerHost(ProcessorExecutionContext context) { + this(context, BexGasCounter.NAMESPACE); + } + + public ProcessorExecutionContextBexGasLedgerHost( + ProcessorExecutionContext context, + String runtimeNamespace) { + this(Objects.requireNonNull(context, "context").runtimeWorkSession(), + runtimeNamespace); + } + + public ProcessorExecutionContextBexGasLedgerHost( + RuntimeWorkSession session, + String runtimeNamespace) { + this.session = Objects.requireNonNull(session, "session"); + this.runtimeNamespace = + requireRuntimeNamespace(runtimeNamespace); + } + + @Override + public GasMeter.ChildGasLedger open(String namespace, + Map counterWeights) { + String logicalNamespace = + requireRuntimeNamespace(namespace); + return session.openLedger( + physicalNamespace(logicalNamespace), + counterWeights); + } + + @Override + public void submit(GasMeter.ChildGasLedger ledger) { + session.submit(ledger); + } + + @Override + public boolean separatesRuntimeNamespaces() { + return true; + } + + /** + * The enclosing processor failure owns prefix retention. BEX must leave + * this ledger staged and unsubmitted. + */ + @Override + public void failedDeterministically(GasMeter.ChildGasLedger ledger) { + Objects.requireNonNull(ledger, "ledger"); + } + + /** + * The enclosing processor suspension owns reservation discard. BEX must + * leave this ledger staged and unsubmitted. + */ + @Override + public void evidenceUnavailable(GasMeter.ChildGasLedger ledger) { + Objects.requireNonNull(ledger, "ledger"); + } + + @Override + public RuntimeException localGasLimitExceeded( + BexGasLimitExceededException exhaustion, + RuntimeException originalFailure) { + BexGasLimitExceededException exact = + Objects.requireNonNull(exhaustion, "exhaustion"); + Objects.requireNonNull( + originalFailure, "originalFailure"); + return new ProcessorFailureException( + ProcessorErrorCategory.GasLimitExceeded, + exact.getMessage(), + exact); + } + + @Override + public void propagateGasExhaustion( + GasMeter.ChildGasLedger ledger, + GasLimitExceededException exhaustion) { + Objects.requireNonNull(ledger, "ledger"); + session.propagateGasExhaustion( + Objects.requireNonNull(exhaustion, "exhaustion")); + } + + public String runtimeNamespace() { + return runtimeNamespace; + } + + /** + * Returns the deterministic physical session namespace for one logical + * portable ledger. The primary BEX ledger uses the configured namespace + * verbatim; intrinsic ledgers are separate children below it. + */ + public String physicalNamespace(String logicalNamespace) { + String exactLogical = + requireRuntimeNamespace(logicalNamespace); + return BexGasCounter.NAMESPACE.equals(exactLogical) + ? runtimeNamespace + : runtimeNamespace + "/" + exactLogical; + } + + private static String requireRuntimeNamespace(String value) { + if (value == null || value.trim().isEmpty()) { + throw new IllegalArgumentException( + "runtimeNamespace is required"); + } + if (value.indexOf('/') >= 0) { + throw new IllegalArgumentException( + "runtimeNamespace must not contain the reserved '/' separator"); + } + return value; + } +} diff --git a/src/main/java/blue/bex/compile/BexCompiledProgram.java b/src/main/java/blue/bex/compile/BexCompiledProgram.java index 819799e..12da1c7 100644 --- a/src/main/java/blue/bex/compile/BexCompiledProgram.java +++ b/src/main/java/blue/bex/compile/BexCompiledProgram.java @@ -1,6 +1,7 @@ package blue.bex.compile; import blue.bex.BexException; +import blue.bex.gas.BexGasCounter; import blue.bex.runtime.BexRuntime; import blue.bex.runtime.CompiledFrame; import blue.bex.runtime.CompiledStatement; @@ -120,15 +121,22 @@ public BexValue invokeRoot(BexRuntime runtime) { } public BexValue invokePrepared(BexRuntime runtime, CompiledFrame parent, int[] slots, BexValue[] values) { + runtime.gas().charge(BexGasCounter.FUNCTION_CALLED); runtime.metrics().incrementFunctionCalls(); - runtime.gas().charge(runtime.gas().schedule().functionCall); + if (parent == null) { + runtime.metrics().incrementCompiledExecutions(); + } CompiledFrame frame = new CompiledFrame(runtime, frameSize, parent); for (int i = 0; i < slots.length; i++) { if (slots[i] >= 0) { BexValue value = values[i]; ArgSpec arg = slots[i] < argBySlot.length ? argBySlot[slots[i]] : null; if (arg != null && arg.typed() - && !runtime.typeMatcher().matches(value, arg.pattern())) { + && !runtime.typeMatcher().matches( + value, + arg.pattern(), + runtime.gas(), + parent != null ? parent.sourcePath() : null)) { throw new BexException("Function " + name + " argument " + arg.name() + " does not match declared Blue pattern at " diff --git a/src/main/java/blue/bex/compile/BexCompiledProgramKey.java b/src/main/java/blue/bex/compile/BexCompiledProgramKey.java index fe1861b..9128a7c 100644 --- a/src/main/java/blue/bex/compile/BexCompiledProgramKey.java +++ b/src/main/java/blue/bex/compile/BexCompiledProgramKey.java @@ -8,33 +8,56 @@ * Cache key for selected compiled BEX programs. */ public final class BexCompiledProgramKey { + public static final String COMPILER_IDENTITY = "blue-bex-java/compiler/2.0"; + public static final String BEX_RUNTIME_REGISTRY_IDENTITY = + "sha256:23d282ec1c0bb016263922b1b49c369fdd537efdcf23e005eceeb888d7763fe1"; + private final BexProgramSource.Kind kind; private final String programIdentity; private final String definitionIdentity; private final String entryName; + private final String compileEnvironmentIdentity; public BexCompiledProgramKey(String programIdentity, String definitionIdentity, String entryName) { - this(BexProgramSource.Kind.FULL_PROGRAM, programIdentity, definitionIdentity, entryName); + this(BexProgramSource.Kind.FULL_PROGRAM, programIdentity, definitionIdentity, entryName, + COMPILER_IDENTITY); } public BexCompiledProgramKey(BexProgramSource.Kind kind, String programIdentity, String definitionIdentity, String entryName) { + this(kind, programIdentity, definitionIdentity, entryName, COMPILER_IDENTITY); + } + + public BexCompiledProgramKey(BexProgramSource.Kind kind, + String programIdentity, + String definitionIdentity, + String entryName, + String compileEnvironmentIdentity) { this.kind = Objects.requireNonNull(kind, "kind"); this.programIdentity = Objects.requireNonNull(programIdentity, "programIdentity"); this.definitionIdentity = definitionIdentity != null ? definitionIdentity : "none"; this.entryName = entryName != null ? entryName : ""; + this.compileEnvironmentIdentity = Objects.requireNonNull( + compileEnvironmentIdentity, "compileEnvironmentIdentity"); } public static BexCompiledProgramKey from(BexProgramSource source) { + return from(source, COMPILER_IDENTITY); + } + + public static BexCompiledProgramKey from(BexProgramSource source, + String compileEnvironmentIdentity) { return new BexCompiledProgramKey(source.kind(), BexNodeIdentity.stable(source.programNode()), source.definitionNode().map(BexNodeIdentity::stable).orElse("none"), - source.entry().orElse(null)); + source.entry().orElse(null), + compileEnvironmentIdentity); } public BexProgramSource.Kind kind() { return kind; } public String programIdentity() { return programIdentity; } public String definitionIdentity() { return definitionIdentity; } public String entryName() { return entryName; } + public String compileEnvironmentIdentity() { return compileEnvironmentIdentity; } @Override public boolean equals(Object other) { @@ -48,11 +71,13 @@ public boolean equals(Object other) { return kind == that.kind && Objects.equals(programIdentity, that.programIdentity) && Objects.equals(definitionIdentity, that.definitionIdentity) - && Objects.equals(entryName, that.entryName); + && Objects.equals(entryName, that.entryName) + && Objects.equals(compileEnvironmentIdentity, that.compileEnvironmentIdentity); } @Override public int hashCode() { - return Objects.hash(kind, programIdentity, definitionIdentity, entryName); + return Objects.hash(kind, programIdentity, definitionIdentity, entryName, + compileEnvironmentIdentity); } } diff --git a/src/main/java/blue/bex/compile/BexCompiler.java b/src/main/java/blue/bex/compile/BexCompiler.java index d3d0cba..6ce569f 100644 --- a/src/main/java/blue/bex/compile/BexCompiler.java +++ b/src/main/java/blue/bex/compile/BexCompiler.java @@ -7,6 +7,7 @@ import blue.bex.runtime.CompiledExpression; import blue.bex.runtime.CompiledStatement; import blue.bex.value.BexValue; +import blue.bex.value.BexUnicodeOrder; import blue.bex.value.BexValues; import blue.bex.result.BexMetrics; import blue.language.model.Node; @@ -22,6 +23,11 @@ import java.util.Map; import java.util.Set; +import static blue.language.utils.Properties.BOOLEAN_TYPE_BLUE_ID; +import static blue.language.utils.Properties.DOUBLE_TYPE_BLUE_ID; +import static blue.language.utils.Properties.INTEGER_TYPE_BLUE_ID; +import static blue.language.utils.Properties.TEXT_TYPE_BLUE_ID; + /** * Compiler from frozen BEX Blue data to specialized runtime objects. */ @@ -46,6 +52,7 @@ public BexCompiler(BexMetrics metrics, BexIntrinsicRegistry intrinsics) { } public BexCompiledProgram compile(blue.bex.api.BexProgramSource source) { + requiredIntrinsicBlueIds.clear(); FrozenNode step = source.programNode(); FrozenNode definition = source.definitionNode().orElse(null); if (source.isExpression()) { @@ -67,28 +74,38 @@ public BexCompiledProgram compile(blue.bex.api.BexProgramSource source) { return new BexCompiledProgram(root, Collections.emptyMap(), constants, scope.frameSize(), BexNodeIdentity.safeBlueId(step), requiredIntrinsicBlueIds); } + requireProgramNode(step, "program"); + if (definition != null) { + requireProgramNode(definition, "definition"); + } Map loadedConstants = new LinkedHashMap<>(); - loadConstants(loadedConstants, prop(definition, "constants")); - loadConstants(loadedConstants, prop(step, "constants")); + loadConstants(loadedConstants, explicitProp(definition, "constants")); + loadConstants(loadedConstants, explicitProp(step, "constants")); constants = Collections.unmodifiableMap(new LinkedHashMap<>(loadedConstants)); Map functionNodes = new LinkedHashMap<>(); - loadFunctions(functionNodes, prop(definition, "functions")); - loadFunctions(functionNodes, prop(step, "functions")); + loadFunctions(functionNodes, explicitProp(definition, "functions")); + loadFunctions(functionNodes, explicitProp(step, "functions")); rejectRecursion(functionNodes); functionSignatures = compileFunctionSignatures(functionNodes); Map compiledFunctions = new LinkedHashMap<>(); - for (String name : functionNodes.keySet()) { + for (String name : BexUnicodeOrder.sortedCopy(functionNodes.keySet())) { compiledFunctions.put(name, compileFunction(name, functionNodes.get(name), functionSignatures.get(name))); } - FrozenNode stepExpr = meaningful(prop(step, "expr")); - String entryName = source.entry().orElse(text(meaningful(prop(step, "entry")))); + boolean hasStepEntry = hasExplicitProperty(step, "entry"); + boolean hasStepExpr = hasExplicitProperty(step, "expr"); + boolean hasStepDo = hasExplicitProperty(step, "do"); + String entryName = source.entry().isPresent() + ? requiredNonEmptyText(scalarNode(source.entry().get()), "entry") + : hasStepEntry + ? requiredNonEmptyText(explicitProp(step, "entry"), "entry") + : null; BexCompiledProgram.CompiledFunction root; int rootFrameSize = 0; - if (entryName != null && !entryName.isEmpty()) { + if (entryName != null) { BexCompiledProgram.CompiledFunction entry = compiledFunctions.get(entryName); if (entry == null) { throw new BexException("Unknown entry function: " + entryName); @@ -99,19 +116,25 @@ public BexCompiledProgram compile(blue.bex.api.BexProgramSource source) { root = new BexCompiledProgram.CompiledFunction("$root", Collections.emptyList(), Collections.singletonList(sourceStatement("$root", "/entry", "$return", new ReturnStatement(sourceExpr("$root", "/entry/$call", "$call", - new CallExpr(entryName, new int[0], new CompiledExpression[0]))))), + new CallExpr(entryName, new int[0], new CompiledExpression[0]))))), null, 0); - } else if (stepExpr != null) { + validateUnselectedRoot(step, hasStepExpr, hasStepDo); + } else if (hasStepExpr) { currentFunction = "$root"; CompileScope scope = new CompileScope(); - CompiledExpression expression = compileExpr(stepExpr, scope, "/expr"); + CompiledExpression expression = compileExpr(explicitProp(step, "expr"), scope, "/expr"); rootFrameSize = scope.frameSize(); root = new BexCompiledProgram.CompiledFunction("$root", Collections.emptyList(), Collections.emptyList(), expression, rootFrameSize); + if (hasStepDo) { + validateRootStatements(explicitProp(step, "do")); + } } else { CompileScope scope = new CompileScope(); currentFunction = "$root"; - List statements = compileStatements(meaningful(prop(step, "do")), scope, "/do"); + List statements = hasStepDo + ? compileStatements(explicitProp(step, "do"), scope, "/do") + : Collections.emptyList(); rootFrameSize = scope.frameSize(); root = new BexCompiledProgram.CompiledFunction("$root", Collections.emptyList(), statements, null, rootFrameSize); } @@ -123,6 +146,29 @@ public BexCompiledProgram compile(blue.bex.api.BexProgramSource source) { private BexCompiledProgram.CompiledFunction compileFunction(String name, FrozenNode functionNode, FunctionSignature signature) { String previousFunction = currentFunction; currentFunction = name; + try { + CompileScope scope = functionScope(name, signature); + String basePointer = "/functions/" + escape(name); + boolean hasExpression = hasExplicitProperty(functionNode, "expr"); + boolean hasStatements = hasExplicitProperty(functionNode, "do"); + CompiledExpression expression = hasExpression + ? compileExpr(explicitProp(functionNode, "expr"), scope, basePointer + "/expr") + : null; + List statements = !hasExpression && hasStatements + ? compileStatements(explicitProp(functionNode, "do"), scope, basePointer + "/do") + : Collections.emptyList(); + if (hasExpression && hasStatements) { + CompileScope validationScope = functionScope(name, signature); + compileStatements(explicitProp(functionNode, "do"), validationScope, basePointer + "/do"); + } + return new BexCompiledProgram.CompiledFunction(name, signature.args(), + statements, expression, scope.frameSize()); + } finally { + currentFunction = previousFunction; + } + } + + private CompileScope functionScope(String name, FunctionSignature signature) { CompileScope scope = new CompileScope(); for (BexCompiledProgram.ArgSpec arg : signature.args()) { int slot = scope.declareOrGetSlot(arg.name()); @@ -130,25 +176,34 @@ private BexCompiledProgram.CompiledFunction compileFunction(String name, FrozenN throw new BexException("Internal function arg slot mismatch for " + name + "." + arg.name()); } } - FrozenNode functionExpr = meaningful(prop(functionNode, "expr")); - CompiledExpression expression = functionExpr != null ? compileExpr(functionExpr, scope, "/functions/" + escape(name) + "/expr") : null; - List statements = expression == null - ? compileStatements(meaningful(prop(functionNode, "do")), scope, "/functions/" + escape(name) + "/do") - : Collections.emptyList(); - currentFunction = previousFunction; - return new BexCompiledProgram.CompiledFunction(name, signature.args(), - statements, expression, scope.frameSize()); + return scope; + } + + private void validateUnselectedRoot(FrozenNode step, boolean hasExpression, boolean hasStatements) { + if (hasExpression) { + currentFunction = "$root"; + compileExpr(explicitProp(step, "expr"), new CompileScope(), "/expr"); + } + if (hasStatements) { + validateRootStatements(explicitProp(step, "do")); + } + } + + private void validateRootStatements(FrozenNode statements) { + currentFunction = "$root"; + compileStatements(statements, new CompileScope(), "/do"); } private Map compileFunctionSignatures(Map functionNodes) { Map signatures = new LinkedHashMap<>(); - for (Map.Entry entry : functionNodes.entrySet()) { - signatures.put(entry.getKey(), compileFunctionSignature(entry.getKey(), entry.getValue())); + for (String name : BexUnicodeOrder.sortedCopy(functionNodes.keySet())) { + signatures.put(name, compileFunctionSignature(name, functionNodes.get(name))); } return signatures; } private FunctionSignature compileFunctionSignature(String name, FrozenNode functionNode) { + requireObjectBody(functionNode, "Function " + name, "args", "expr", "do"); List names = new ArrayList<>(); FrozenNode argsNode = prop(functionNode, "args"); if (argsNode != null) { @@ -159,7 +214,7 @@ private FunctionSignature compileFunctionSignature(String name, FrozenNode funct } } else { names.addAll(argsNode.getProperties().keySet()); - Collections.sort(names); + Collections.sort(names, BexUnicodeOrder.CODE_POINT_COMPARATOR); } } List args = new ArrayList<>(); @@ -180,8 +235,10 @@ private void loadConstants(Map constants, FrozenNode node) { if (node == null || node.getProperties() == null) { return; } - for (Map.Entry entry : node.getProperties().entrySet()) { - constants.put(entry.getKey(), BexValues.frozen(entry.getValue())); + for (String name : BexUnicodeOrder.sortedCopy(node.getProperties().keySet())) { + FrozenNode value = node.getProperties().get(name); + rejectBexInStaticBlueDefinitionFields(value, "/constants/" + escape(name)); + constants.put(name, BexValues.frozen(value)); } } @@ -190,40 +247,84 @@ private void loadFunctions(Map functions, FrozenNode node) { if (node == null || node.getProperties() == null) { return; } - functions.putAll(node.getProperties()); + for (String name : BexUnicodeOrder.sortedCopy(node.getProperties().keySet())) { + functions.put(name, node.getProperties().get(name)); + } } private void rejectRecursion(Map functions) { - Map> calls = new LinkedHashMap<>(); - for (String name : functions.keySet()) { - Set targets = new LinkedHashSet<>(); + Map> calls = new LinkedHashMap<>(); + for (String name : BexUnicodeOrder.sortedCopy(functions.keySet())) { + List targets = new ArrayList<>(); FrozenNode function = functions.get(name); - collectCalls(meaningful(prop(function, "expr")), targets); - collectCalls(meaningful(prop(function, "do")), targets); + String base = "/functions/" + escape(name); + if (hasExplicitProperty(function, "expr")) { + collectCalls(explicitProp(function, "expr"), name, base + "/expr", targets); + } + if (hasExplicitProperty(function, "do")) { + collectCalls(explicitProp(function, "do"), name, base + "/do", targets); + } calls.put(name, targets); } + Map states = new LinkedHashMap<>(); for (String name : calls.keySet()) { - detectCycle(name, name, calls, new ArrayDeque()); + states.put(name, VisitState.UNVISITED); + } + ArrayDeque stack = new ArrayDeque<>(); + for (String name : calls.keySet()) { + if (states.get(name) == VisitState.UNVISITED) { + detectCycle(name, calls, states, stack); + } } } - private void detectCycle(String root, String current, Map> calls, ArrayDeque stack) { - if (stack.contains(current)) { - throw new BexException("Recursive BEX function call rejected: " + current); + private void detectCycle(String current, + Map> calls, + Map states, + ArrayDeque stack) { + states.put(current, VisitState.VISITING); + stack.addLast(current); + List edges = new ArrayList<>(calls.get(current)); + Collections.sort(edges, (left, right) -> { + int byTarget = BexUnicodeOrder.compareCodePoints(left.target, right.target); + return byTarget != 0 + ? byTarget + : BexUnicodeOrder.compareCodePoints(left.sourcePath.pointer(), right.sourcePath.pointer()); + }); + for (CallSite edge : edges) { + if (!calls.containsKey(edge.target)) { + continue; + } + VisitState state = states.get(edge.target); + if (state == VisitState.VISITING) { + throw BexException.at(edge.sourcePath, + "Compile error reason=recursive-call-graph: recursive BEX function cycle " + + cycleText(stack, edge.target)); + } + if (state == VisitState.UNVISITED) { + detectCycle(edge.target, calls, states, stack); + } } - stack.push(current); - for (String next : calls.get(current)) { - if (root.equals(next)) { - throw new BexException("Recursive BEX function call rejected: " + root); + stack.removeLast(); + states.put(current, VisitState.VISITED); + } + + private String cycleText(ArrayDeque stack, String target) { + List cycle = new ArrayList<>(); + boolean append = false; + for (String name : stack) { + if (name.equals(target)) { + append = true; } - if (calls.containsKey(next)) { - detectCycle(root, next, calls, stack); + if (append) { + cycle.add(name); } } - stack.pop(); + cycle.add(target); + return String.join(" -> ", cycle); } - private void collectCalls(FrozenNode node, Set calls) { + private void collectCalls(FrozenNode node, String functionName, String pointer, List calls) { if (node == null) { return; } @@ -231,24 +332,39 @@ private void collectCalls(FrozenNode node, Set calls) { return; } if (isOperator(node, "$is")) { - collectCalls(prop(onlyValue(node), "node"), calls); + collectCalls(prop(onlyValue(node), "node"), functionName, + pointer + "/$is/node", calls); + return; + } + if (isOperator(node, "$fail")) { + FrozenNode body = onlyValue(node); + boolean messageWrapper = body != null + && body.getProperties() != null + && hasExplicitProperty(body, "message"); + collectCalls(messageWrapper ? explicitProp(body, "message") : body, + functionName, + messageWrapper ? pointer + "/$fail/message" : pointer + "/$fail", + calls); return; } if (isOperator(node, "$call")) { FrozenNode body = onlyValue(node); String function = text(prop(body, "function")); if (function != null) { - calls.add(function); + calls.add(new CallSite(function, + BexSourcePath.of(functionName, pointer + "/$call", "$call"))); } } if (node.getProperties() != null) { - for (FrozenNode child : node.getProperties().values()) { - collectCalls(child, calls); + for (String key : BexUnicodeOrder.sortedCopy(node.getProperties().keySet())) { + collectCalls(node.getProperties().get(key), functionName, + pointer + "/" + escape(key), calls); } } if (node.getItems() != null) { - for (FrozenNode child : node.getItems()) { - collectCalls(child, calls); + for (int i = 0; i < node.getItems().size(); i++) { + collectCalls(node.getItems().get(i), functionName, + pointer + "/" + i, calls); } } } @@ -258,7 +374,8 @@ private List compileStatements(FrozenNode node, CompileScope return Collections.emptyList(); } if (node.getItems() == null) { - throw new BexException("Statement body must be a list"); + throw BexException.at(BexSourcePath.of(currentFunction, pointer, null), + "Compile error: statement body must be a list"); } List statements = new ArrayList<>(); for (int i = 0; i < node.getItems().size(); i++) { @@ -270,10 +387,12 @@ private List compileStatements(FrozenNode node, CompileScope private CompiledStatement compileStatement(FrozenNode statement, CompileScope scope, String pointer) { if (isEmptyStatement(statement, pointer)) { - return sourceStatement(currentFunction, pointer, "$return", new ReturnStatement(null)); + throw BexException.at(BexSourcePath.of(currentFunction, pointer, null), + "Compile error: null or empty statement item"); } if (statement.getProperties() == null) { - throw new BexException("Statement must be an operator object at " + pointer); + throw BexException.at(BexSourcePath.of(currentFunction, pointer, null), + "Compile error: statement must be an operator object"); } int count = 0; String op = null; @@ -285,12 +404,21 @@ private CompiledStatement compileStatement(FrozenNode statement, CompileScope sc body = entry.getValue(); } } - if (count != 1 || statement.getProperties().size() != 1) { - throw new BexException("Statement must have exactly one $ operator at " + pointer); + if (count != 1 + || statement.getProperties().size() != 1 + || authoredFieldNames(statement).size() != 1) { + throw BexException.at(BexSourcePath.of(currentFunction, pointer, null), + "Compile error: statement must have exactly one $ operator"); } String bodyPointer = pointer + "/" + escape(op); - CompiledStatement compiled; - if ("$let".equals(op)) { + BexSourcePath sourcePath = BexSourcePath.of(currentFunction, bodyPointer, op); + try { + if ("$empty".equals(op)) { + throw new BexException("Compile error: $empty placeholder is not a BEX statement"); + } + validateStatementBody(op, body); + CompiledStatement compiled; + if ("$let".equals(op)) { if (prop(body, "vars") != null) { compiled = compileMultiLet(body, scope, bodyPointer); } else { @@ -311,10 +439,12 @@ private CompiledStatement compileStatement(FrozenNode statement, CompileScope sc String keyName = prop(body, "key") != null ? requiredText(prop(body, "key"), "$forEach.key") : null; String indexName = prop(body, "index") != null ? requiredText(prop(body, "index"), "$forEach.index") : null; validateDistinctForEachBindings(itemName, keyName, indexName); + CompiledExpression input = compileExpr(required(prop(body, "in"), "$forEach.in"), + scope, bodyPointer + "/in"); int slot = scope.declareOrGetSlot(itemName); int keySlot = keyName != null ? scope.declareOrGetSlot(keyName) : -1; int indexSlot = indexName != null ? scope.declareOrGetSlot(indexName) : -1; - compiled = new ForEachStatement(compileExpr(required(prop(body, "in"), "$forEach.in"), scope, bodyPointer + "/in"), + compiled = new ForEachStatement(input, slot, keySlot, indexSlot, compileStatements(prop(body, "do"), scope, bodyPointer + "/do")); } else if ("$appendChange".equals(op)) { compiled = new AppendChangeStatement(textOrExpr(required(prop(body, "op"), "$appendChange.op"), scope, null, bodyPointer + "/op"), @@ -340,10 +470,11 @@ private CompiledStatement compileStatement(FrozenNode statement, CompileScope sc } compiled = new ReturnIfStatement( compileExpr(required(prop(body, "cond"), "$returnIf.cond"), scope, bodyPointer + "/cond"), - prop(body, "expr") != null ? compileExpr(prop(body, "expr"), scope, bodyPointer + "/expr") : null); + hasExplicitProperty(body, "expr") + ? compileExpr(explicitProp(body, "expr"), scope, bodyPointer + "/expr") + : null); } else if ("$fail".equals(op)) { - FrozenNode message = body != null && body.getProperties() != null ? prop(body, "message") : body; - compiled = new FailStatement(compileExpr(message, scope, bodyPointer + "/message")); + compiled = new FailStatement(failMessageExpr(body, scope, bodyPointer)); } else if ("$failIf".equals(op)) { compiled = new FailIfStatement( compileExpr(required(prop(body, "cond"), "$failIf.cond"), scope, bodyPointer + "/cond"), @@ -351,7 +482,10 @@ private CompiledStatement compileStatement(FrozenNode statement, CompileScope sc } else { throw new BexException("Unknown statement operator: " + op); } - return sourceStatement(currentFunction, bodyPointer, op, compiled); + return new SourceStatement(sourcePath, compiled); + } catch (BexException ex) { + throw ex.withSourcePath(sourcePath); + } } private CompiledStatement compileMultiLet(FrozenNode body, CompileScope scope, String pointer) { @@ -368,7 +502,7 @@ private CompiledStatement compileMultiLet(FrozenNode body, CompileScope scope, S if (sequential) { names = orderedLetNames(prop(body, "order"), varsNode, pointer + "/order"); } else { - Collections.sort(names); + Collections.sort(names, BexUnicodeOrder.CODE_POINT_COMPARATOR); } int[] slots = new int[names.size()]; @@ -381,13 +515,13 @@ private CompiledStatement compileMultiLet(FrozenNode body, CompileScope scope, S slots[i] = scope.declareOrGetSlot(name); } } else { + for (int i = 0; i < names.size(); i++) { + slots[i] = scope.declareOrGetSlot(names.get(i)); + } for (String name : names) { expressions.add(compileExpr(varsNode.getProperties().get(name), scope, pointer + "/vars/" + escape(name))); } - for (int i = 0; i < names.size(); i++) { - slots[i] = scope.declareOrGetSlot(names.get(i)); - } } return new MultiLetStatement(slots, expressions.toArray(new CompiledExpression[0]), sequential); } @@ -419,35 +553,7 @@ private List orderedLetNames(FrozenNode orderNode, FrozenNode varsNode, } private boolean isEmptyStatement(FrozenNode statement, String pointer) { - if (statement == null || statement.isEmptyNode()) { - return true; - } - if (statement.getProperties() == null || !statement.getProperties().containsKey("$empty")) { - return false; - } - if (statement.getProperties().size() == 1 - && !hasLanguageFields(statement) - && statement.getItems() == null - && (statement.getValue() == null || Boolean.TRUE.equals(statement.getValue())) - && statement.getPreviousBlueId() == null - && statement.getPosition() == null - && isEmptyMarkerValue(statement.getProperties().get("$empty"))) { - return true; - } - throw new BexException("Statement $empty placeholder must be exactly $empty: true at " + pointer); - } - - private boolean isEmptyMarkerValue(FrozenNode node) { - return node != null && (node.isEmptyNode() || isTrueScalar(node)); - } - - private boolean isTrueScalar(FrozenNode node) { - return node != null - && Boolean.TRUE.equals(node.getValue()) - && node.getProperties() == null - && node.getItems() == null - && node.getPreviousBlueId() == null - && node.getPosition() == null; + return statement == null || statement.isEmptyNode(); } private CompiledExpression compileExpr(FrozenNode node, CompileScope scope, String pointer) { @@ -455,21 +561,23 @@ private CompiledExpression compileExpr(FrozenNode node, CompileScope scope, Stri return sourceExpr(currentFunction, pointer, null, new LiteralExpr(BexValues.nullValue())); } rejectBexInStaticBlueDefinitionFields(node, pointer); - if (!containsCache.containsBex(node, metrics)) { - return sourceExpr(currentFunction, pointer, null, new LiteralExpr(BexValues.frozen(node))); - } - if (node.getProperties() != null && node.getProperties().size() == 1) { + if (isExpressionOperatorShape(node)) { String op = node.getProperties().keySet().iterator().next(); FrozenNode body = node.getProperties().values().iterator().next(); - if (op.startsWith("$")) { - BexSourcePath sourcePath = BexSourcePath.of(currentFunction, pointer + "/" + escape(op), op); - try { - return new SourceExpr(sourcePath, compileOperator(op, body, scope, pointer + "/" + escape(op))); - } catch (BexException ex) { - throw ex.withSourcePath(sourcePath); - } + BexSourcePath sourcePath = BexSourcePath.of(currentFunction, pointer + "/" + escape(op), op); + try { + return new SourceExpr(sourcePath, compileOperator(op, body, scope, pointer + "/" + escape(op))); + } catch (BexException ex) { + throw ex.withSourcePath(sourcePath); } } + if (node.isEmptyNode()) { + return sourceExpr(currentFunction, pointer, null, new LiteralExpr(BexValues.nullValue())); + } + if (isScalarNode(node)) { + return sourceExpr(currentFunction, pointer, null, + new TransientLiteralExpr(node.getValue())); + } if (node.getItems() != null && !hasLanguageFields(node)) { List items = new ArrayList<>(); for (int i = 0; i < node.getItems().size(); i++) { @@ -488,16 +596,19 @@ private CompiledExpression compileExpr(FrozenNode node, CompileScope scope, Stri fields.put("items", new ListExpr(items)); } if (node.getProperties() != null) { - for (Map.Entry entry : node.getProperties().entrySet()) { - fields.put(entry.getKey(), compileExpr(entry.getValue(), scope, pointer + "/" + escape(entry.getKey()))); + for (String key : BexUnicodeOrder.sortedCopy(node.getProperties().keySet())) { + fields.put(key, compileExpr(node.getProperties().get(key), scope, + pointer + "/" + escape(key))); } } return sourceExpr(currentFunction, pointer, null, new ObjectExpr(fields)); } - return sourceExpr(currentFunction, pointer, null, new LiteralExpr(BexValues.frozen(node))); + return sourceExpr(currentFunction, pointer, null, + new TransientLiteralExpr(node.getValue())); } private CompiledExpression compileOperator(String op, FrozenNode body, CompileScope scope, String pointer) { + validateExpressionBody(op, body); if ("$literal".equals(op)) return new LiteralExpr(BexValues.frozen(body)); if ("$null".equals(op)) return new LiteralExpr(BexValues.nullValue()); if ("$emptyObject".equals(op)) return new LiteralExpr(BexValues.map(Collections.emptyMap())); @@ -505,6 +616,7 @@ private CompiledExpression compileOperator(String op, FrozenNode body, CompileSc if ("$document".equals(op)) return documentExpr(body, scope, pointer); if ("$binding".equals(op)) return bindingExpr(body, scope, pointer); if ("$event".equals(op)) return contextPointerExpr(body, scope, ContextKind.EVENT, pointer); + if ("$processingEvent".equals(op)) return contextPointerExpr(body, scope, ContextKind.PROCESSING_EVENT, pointer); if ("$steps".equals(op)) return stepsExpr(body, scope, pointer); if ("$currentContract".equals(op)) return contextPointerExpr(body, scope, ContextKind.CURRENT_CONTRACT, pointer); if ("$var".equals(op)) return varExpr(body, scope, pointer); @@ -547,6 +659,7 @@ private CompiledExpression compileOperator(String op, FrozenNode body, CompileSc if ("$exists".equals(op)) return new UnaryExpr(compileExpr(body, scope, pointer), UnaryOp.EXISTS); if ("$kind".equals(op)) return new KindExpr(compileExpr(body, scope, pointer)); if ("$isKind".equals(op)) return new IsKindExpr(compileExpr(required(prop(body, "val"), "$isKind.val"), scope, pointer + "/val"), kindSet(required(prop(body, "kind"), "$isKind.kind"))); + if ("$nodeBlueId".equals(op)) return new NodeBlueIdExpr(compileExpr(body, scope, pointer)); if ("$coalesce".equals(op)) return new CoalesceExpr(compileExprList(body, scope, pointer)); if ("$default".equals(op)) return new CoalesceExpr(compileExprList(body, scope, pointer)); if ("$add".equals(op)) return new NumericExpr(compileExprList(body, scope, pointer), NumericOp.ADD); @@ -575,11 +688,258 @@ private CompiledExpression compileOperator(String op, FrozenNode body, CompileSc textOrExpr(required(prop(body, "key"), "$hasKey.key"), scope, null, pointer + "/key")); if ("$objectFromEntries".equals(op)) return new ObjectFromEntriesExpr(compileExpr(body, scope, pointer)); if ("$intrinsic".equals(op)) return intrinsicExpr(body, scope, pointer); + if ("$fail".equals(op)) return new FailExpr(failMessageExpr(body, scope, pointer)); if ("$choose".equals(op)) return new ChooseExpr(compileExpr(required(prop(body, "cond"), "$choose.cond"), scope, pointer + "/cond"), compileExpr(required(prop(body, "then"), "$choose.then"), scope, pointer + "/then"), prop(body, "else") != null ? compileExpr(prop(body, "else"), scope, pointer + "/else") : new LiteralExpr(BexValues.undefined())); if ("$call".equals(op)) return compileCall(body, scope, pointer); throw new BexException("Unknown expression operator: " + op); } + private CompiledExpression failMessageExpr(FrozenNode body, + CompileScope scope, + String pointer) { + boolean messageWrapper = body != null + && body.getProperties() != null + && hasExplicitProperty(body, "message"); + return compileExpr(messageWrapper ? explicitProp(body, "message") : body, + scope, + messageWrapper ? pointer + "/message" : pointer); + } + + private void validateExpressionBody(String op, FrozenNode body) { + if ("$concat".equals(op) + || "$pointerJoin".equals(op) + || "$listConcat".equals(op) + || "$merge".equals(op) + || "$and".equals(op) + || "$or".equals(op) + || "$coalesce".equals(op) + || "$default".equals(op)) { + requireListBody(body, op); + return; + } + if ("$eq".equals(op) + || "$ne".equals(op) + || "$gt".equals(op) + || "$gte".equals(op) + || "$lt".equals(op) + || "$lte".equals(op) + || "$startsWith".equals(op) + || "$sliceAfter".equals(op)) { + requireListArity(body, op, 2); + return; + } + if ("$add".equals(op) + || "$subtract".equals(op) + || "$multiply".equals(op) + || "$divide".equals(op)) { + requireListBody(body, op); + if (body.getItems().isEmpty()) { + throw new BexException(op + " requires at least one operand"); + } + return; + } + if ("$get".equals(op)) { + requireObjectBody(body, op, "object", "key"); + } else if ("$is".equals(op)) { + requireObjectBody(body, op, "node", "pattern"); + } else if ("$isKind".equals(op)) { + requireObjectBody(body, op, "val", "kind"); + } else if ("$join".equals(op)) { + requireObjectBody(body, op, "list", "separator"); + } else if ("$split".equals(op)) { + requireObjectBody(body, op, "text", "separator", "limit"); + } else if ("$listGet".equals(op)) { + requireObjectBody(body, op, "list", "index", "default"); + } else if ("$objectSet".equals(op)) { + requireObjectBody(body, op, "object", "key", "val"); + } else if ("$pointerGet".equals(op)) { + requireObjectBody(body, op, "object", "path", "default"); + } else if ("$pointerSet".equals(op)) { + requireObjectBody(body, op, "object", "path", "op", "val"); + } else if ("$map".equals(op) || "$flatMap".equals(op)) { + requireObjectBody(body, op, "in", "item", "key", "index", "expr"); + } else if ("$filter".equals(op) + || "$some".equals(op) + || "$find".equals(op) + || "$findEntry".equals(op)) { + requireObjectBody(body, op, "in", "item", "key", "index", "where"); + } else if ("$reduce".equals(op)) { + requireObjectBody(body, op, "in", "acc", "init", "item", "key", "index", "expr"); + } else if ("$includes".equals(op)) { + requireObjectBody(body, op, "list", "val"); + } else if ("$hasKey".equals(op)) { + requireObjectBody(body, op, "object", "key"); + } else if ("$choose".equals(op)) { + requireObjectBody(body, op, "cond", "then", "else"); + } else if ("$call".equals(op)) { + requireObjectBody(body, op, "function", "args"); + } else if ("$intrinsic".equals(op)) { + requireObjectNode(body, op); + } else if ("$binding".equals(op)) { + if (!isScalarBody(body)) { + requireObjectBody(body, op, "name", "path"); + } + } else if ("$steps".equals(op)) { + if (!isScalarBody(body)) { + requireObjectBody(body, op, "step", "path"); + } + } else if ("$var".equals(op) || "$const".equals(op)) { + if (!isScalarBody(body)) { + requireObjectBody(body, op, "name", "path"); + } + } else if ("$document".equals(op) + && body != null + && hasAuthoredField(body, "path") + && !isExpressionOperatorShape(body)) { + requireObjectBody(body, op, "path", "view"); + } + } + + private void validateStatementBody(String op, FrozenNode body) { + if ("$let".equals(op)) { + requireObjectBody(body, op, "name", "expr", "vars", "order"); + boolean multi = hasAuthoredField(body, "vars"); + if (multi && (hasAuthoredField(body, "name") || hasAuthoredField(body, "expr"))) { + throw new BexException("$let must use either name/expr or vars/order form"); + } + if (!multi && (!hasAuthoredField(body, "name") || !hasAuthoredField(body, "expr"))) { + throw new BexException("$let single-binding form requires name and expr"); + } + } else if ("$set".equals(op)) { + requireObjectBody(body, op, "name", "expr"); + } else if ("$if".equals(op)) { + requireObjectBody(body, op, "cond", "then", "else"); + } else if ("$forEach".equals(op)) { + requireObjectBody(body, op, "in", "item", "key", "index", "do"); + if (!hasAuthoredField(body, "do")) { + throw new BexException("$forEach.do is required"); + } + } else if ("$appendChange".equals(op)) { + requireObjectBody(body, op, "op", "path", "val"); + } else if ("$call".equals(op)) { + requireObjectBody(body, op, "function", "args"); + } else if ("$returnIf".equals(op)) { + requireObjectBody(body, op, "cond", "expr"); + } else if ("$failIf".equals(op)) { + requireObjectBody(body, op, "cond", "message"); + } + } + + private void requireListBody(FrozenNode body, String op) { + if (body == null || body.getItems() == null || hasNonListPayload(body)) { + throw new BexException(op + " expects a list body"); + } + } + + private void requireListArity(FrozenNode body, String op, int expected) { + requireListBody(body, op); + if (body.getItems().size() != expected) { + throw new BexException(op + " expects exactly " + expected + " operands"); + } + } + + private void requireObjectBody(FrozenNode body, String op, String... allowedFields) { + requireObjectNode(body, op); + Set allowed = new LinkedHashSet<>(); + Collections.addAll(allowed, allowedFields); + for (String field : authoredFieldNames(body)) { + if (!allowed.contains(field)) { + throw new BexException(op + " has unknown body field: " + field); + } + } + } + + private void requireObjectNode(FrozenNode body, String op) { + if (body == null + || body.getItems() != null + || body.getValue() != null + || body.getReferenceBlueId() != null + || body.getPreviousBlueId() != null + || body.getPosition() != null) { + throw new BexException(op + " expects an object body"); + } + } + + private void requireProgramNode(FrozenNode node, String label) { + if (node == null + || node.getValue() != null + || node.getItems() != null + || node.getReferenceBlueId() != null + || node.getPreviousBlueId() != null + || node.getPosition() != null) { + throw new BexException("BEX " + label + " must be an object node"); + } + } + + private boolean hasNonListPayload(FrozenNode node) { + return node.getValue() != null + || node.getProperties() != null + || hasLanguageFields(node) + || node.getPreviousBlueId() != null + || node.getPosition() != null; + } + + private boolean isScalarBody(FrozenNode body) { + return isScalarNode(body); + } + + /** + * Blue preprocessing adds an exact core-type reference to an authored + * scalar. That inferred metadata is part of the scalar representation, not + * a BEX object literal field. Keep the exception deliberately narrow: + * computed or additional Blue metadata must still compile as an object. + */ + private boolean isScalarNode(FrozenNode node) { + if (node == null + || node.getValue() == null + || node.getItems() != null + || node.getProperties() != null + || node.getName() != null + || node.getDescription() != null + || node.getItemType() != null + || node.getKeyType() != null + || node.getValueType() != null + || node.getReferenceBlueId() != null + || node.getBlue() != null + || node.getContracts() != null + || node.getSchema() != null + || node.getMergePolicy() != null + || node.getPreviousBlueId() != null + || node.getPosition() != null) { + return false; + } + FrozenNode type = node.getType(); + if (type == null) { + return true; + } + String expectedTypeBlueId = scalarTypeBlueId(node.getValue()); + return expectedTypeBlueId != null + && type.isReferenceOnly() + && expectedTypeBlueId.equals(type.getReferenceBlueId()); + } + + private String scalarTypeBlueId(Object value) { + if (value instanceof String) { + return TEXT_TYPE_BLUE_ID; + } + if (value instanceof Boolean) { + return BOOLEAN_TYPE_BLUE_ID; + } + if (value instanceof java.math.BigDecimal + || value instanceof Float + || value instanceof Double) { + return DOUBLE_TYPE_BLUE_ID; + } + if (value instanceof java.math.BigInteger + || value instanceof Byte + || value instanceof Short + || value instanceof Integer + || value instanceof Long) { + return INTEGER_TYPE_BLUE_ID; + } + return null; + } + private CompiledExpression intrinsicExpr(FrozenNode body, CompileScope scope, String pointer) { if (body == null || body.getValue() != null || body.getItems() != null) { throw new BexException("$intrinsic expects an object body"); @@ -598,28 +958,27 @@ private CompiledExpression intrinsicExpr(FrozenNode body, CompileScope scope, St Map fields = new LinkedHashMap<>(); if (body.getProperties() != null) { - for (Map.Entry entry : body.getProperties().entrySet()) { - if ("type".equals(entry.getKey())) { + for (String fieldName : BexUnicodeOrder.sortedCopy(body.getProperties().keySet())) { + if ("type".equals(fieldName)) { continue; } - fields.put(entry.getKey(), compileExpr(entry.getValue(), scope, pointer + "/" + escape(entry.getKey()))); + fields.put(fieldName, compileExpr(body.getProperties().get(fieldName), scope, + pointer + "/" + escape(fieldName))); } } return new IntrinsicExpr(blueId, typeValue, fields); } private String intrinsicTypeBlueId(FrozenNode typeNode) { - String blueId = BexNodeIdentity.safeBlueId(typeNode); - if (blueId != null && !blueId.isEmpty()) { - return blueId; + if (hasExplicitProperty(typeNode, "blueId")) { + return text(explicitProp(typeNode, "blueId")); } if (typeNode.getReferenceBlueId() != null && !typeNode.getReferenceBlueId().isEmpty()) { return typeNode.getReferenceBlueId(); } - FrozenNode blueIdProperty = prop(typeNode, "blueId"); - String text = text(blueIdProperty); - if (text != null && !text.isEmpty()) { - return text; + String blueId = BexNodeIdentity.safeBlueId(typeNode); + if (blueId != null && !blueId.isEmpty()) { + return blueId; } return text(typeNode); } @@ -680,12 +1039,17 @@ private CompiledExpression collectionExpr(FrozenNode body, CompileScope scope, S String keyName = prop(body, "key") != null ? requiredText(prop(body, "key"), operator + ".key") : null; String indexName = prop(body, "index") != null ? requiredText(prop(body, "index"), operator + ".index") : null; validateDistinctCollectionBindings(operator, itemName, keyName, indexName); - int itemSlot = scope.declareOrGetSlot(itemName); - int keySlot = keyName != null ? scope.declareOrGetSlot(keyName) : -1; - int indexSlot = indexName != null ? scope.declareOrGetSlot(indexName) : -1; - CompiledExpression expr = compileExpr(required(prop(body, bodyField), operator + "." + bodyField), - scope, pointer + "/" + bodyField); - return new CollectionQueryExpr(input, itemSlot, keySlot, indexSlot, expr, op); + CompileScope.Visibility visibility = scope.captureVisibility(); + try { + int itemSlot = scope.declareOrGetSlot(itemName); + int keySlot = keyName != null ? scope.declareOrGetSlot(keyName) : -1; + int indexSlot = indexName != null ? scope.declareOrGetSlot(indexName) : -1; + CompiledExpression expr = compileExpr(required(prop(body, bodyField), operator + "." + bodyField), + scope, pointer + "/" + bodyField); + return new CollectionQueryExpr(input, itemSlot, keySlot, indexSlot, expr, op); + } finally { + scope.restoreVisibility(visibility); + } } private CompiledExpression reduceExpr(FrozenNode body, CompileScope scope, String pointer) { @@ -699,12 +1063,17 @@ private CompiledExpression reduceExpr(FrozenNode body, CompileScope scope, Strin throw new BexException("$reduce.acc must use a different binding name"); } CompiledExpression init = compileExpr(required(prop(body, "init"), "$reduce.init"), scope, pointer + "/init"); - int accSlot = scope.declareOrGetSlot(accName); - int itemSlot = scope.declareOrGetSlot(itemName); - int keySlot = keyName != null ? scope.declareOrGetSlot(keyName) : -1; - int indexSlot = indexName != null ? scope.declareOrGetSlot(indexName) : -1; - CompiledExpression expr = compileExpr(required(prop(body, "expr"), "$reduce.expr"), scope, pointer + "/expr"); - return new ReduceExpr(input, accSlot, init, itemSlot, keySlot, indexSlot, expr); + CompileScope.Visibility visibility = scope.captureVisibility(); + try { + int accSlot = scope.declareOrGetSlot(accName); + int itemSlot = scope.declareOrGetSlot(itemName); + int keySlot = keyName != null ? scope.declareOrGetSlot(keyName) : -1; + int indexSlot = indexName != null ? scope.declareOrGetSlot(indexName) : -1; + CompiledExpression expr = compileExpr(required(prop(body, "expr"), "$reduce.expr"), scope, pointer + "/expr"); + return new ReduceExpr(input, accSlot, init, itemSlot, keySlot, indexSlot, expr); + } finally { + scope.restoreVisibility(visibility); + } } private String collectionOperatorName(CollectionOp op) { @@ -788,7 +1157,7 @@ private CompiledExpression stepsExpr(FrozenNode body, CompileScope scope, String String selector = String.valueOf(body.getValue()); int dot = selector.indexOf('.'); String step = dot >= 0 ? selector.substring(0, dot) : selector; - String path = dot >= 0 ? "/" + selector.substring(dot + 1).replace('.', '/') : "/"; + String path = dot >= 0 ? "/" + selector.substring(dot + 1) : "/"; return new StepsExpr(new StaticTextExpr(step), StaticValuePointerOperand.of(path)); } return new StepsExpr(textOrExpr(required(prop(body, "step"), "$steps.step"), scope, null, pointer + "/step"), @@ -812,15 +1181,15 @@ private CallExpr compileCall(FrozenNode body, CompileScope scope, String pointer throw new BexException("$call.args must be an object at " + pointer + "/args"); } } else { - for (Map.Entry entry : argsNode.getProperties().entrySet()) { - String argName = entry.getKey(); + for (String argName : BexUnicodeOrder.sortedCopy(argsNode.getProperties().keySet())) { BexCompiledProgram.ArgSpec arg = signature.arg(argName); if (arg == null) { throw new BexException("Unknown argument " + argName + " for function " + function); } providedArgs.add(argName); targetSlots.add(arg.slot()); - argExpressions.add(compileExpr(entry.getValue(), scope, pointer + "/args/" + escape(argName))); + argExpressions.add(compileExpr(argsNode.getProperties().get(argName), scope, + pointer + "/args/" + escape(argName))); } } } @@ -919,12 +1288,44 @@ private FrozenNode prop(FrozenNode node, String key) { return null; } + private FrozenNode explicitProp(FrozenNode node, String key) { + return node != null && node.getProperties() != null + ? node.getProperties().get(key) + : null; + } + private boolean hasExplicitProperty(FrozenNode node, String key) { return node != null && node.getProperties() != null && node.getProperties().containsKey(key); } - private FrozenNode meaningful(FrozenNode node) { - return node == null || node.isEmptyNode() ? null : node; + private boolean hasAuthoredField(FrozenNode node, String key) { + return authoredFieldNames(node).contains(key); + } + + private Set authoredFieldNames(FrozenNode node) { + Set fields = new LinkedHashSet<>(); + if (node == null) { + return fields; + } + if (node.getProperties() != null) { + fields.addAll(node.getProperties().keySet()); + } + if (node.getName() != null) fields.add("name"); + if (node.getDescription() != null) fields.add("description"); + if (node.getType() != null) fields.add("type"); + if (node.getItemType() != null) fields.add("itemType"); + if (node.getKeyType() != null) fields.add("keyType"); + if (node.getValueType() != null) fields.add("valueType"); + if (node.getValue() != null) fields.add("value"); + if (node.getItems() != null) fields.add("items"); + if (node.getReferenceBlueId() != null) fields.add("blueId"); + if (node.getBlue() != null) fields.add("blue"); + if (node.getSchema() != null) fields.add("schema"); + if (node.getMergePolicy() != null) fields.add("mergePolicy"); + if (node.getContracts() != null) fields.add("contracts"); + if (node.getPreviousBlueId() != null) fields.add("$previous"); + if (node.getPosition() != null) fields.add("$pos"); + return fields; } private FrozenNode required(FrozenNode node, String label) { @@ -942,15 +1343,33 @@ private String requiredText(FrozenNode node, String label) { return value; } + private String requiredNonEmptyText(FrozenNode node, String label) { + String value = requiredText(node, label); + if (value.isEmpty()) { + throw new BexException("Required text field is empty: " + label); + } + return value; + } + private String text(FrozenNode node) { - return node != null && node.getValue() != null ? String.valueOf(node.getValue()) : null; + return node != null && node.getValue() instanceof String + ? (String) node.getValue() + : null; + } + + private boolean isExpressionOperatorShape(FrozenNode node) { + if (node == null + || node.getProperties() == null + || node.getProperties().size() != 1 + || authoredFieldNames(node).size() != 1) { + return false; + } + String key = node.getProperties().keySet().iterator().next(); + return key.startsWith("$"); } private boolean isOperator(FrozenNode node, String op) { - return node != null - && node.getProperties() != null - && node.getProperties().size() == 1 - && node.getProperties().containsKey(op); + return isExpressionOperatorShape(node) && node.getProperties().containsKey(op); } private FrozenNode onlyValue(FrozenNode node) { @@ -959,10 +1378,10 @@ private FrozenNode onlyValue(FrozenNode node) { private void addMetadataFields(Map fields, FrozenNode node, CompileScope scope, String pointer) { if (node.getName() != null) { - fields.put("name", new LiteralExpr(BexValues.scalar(node.getName()))); + fields.put("name", new TransientLiteralExpr(node.getName())); } if (node.getDescription() != null) { - fields.put("description", new LiteralExpr(BexValues.scalar(node.getDescription()))); + fields.put("description", new TransientLiteralExpr(node.getDescription())); } if (node.getType() != null) { fields.put("type", compileExpr(node.getType(), scope, pointer + "/type")); @@ -977,10 +1396,10 @@ private void addMetadataFields(Map fields, FrozenNod fields.put("valueType", compileExpr(node.getValueType(), scope, pointer + "/valueType")); } if (node.getValue() != null) { - fields.put("value", new LiteralExpr(BexValues.scalar(node.getValue()))); + fields.put("value", new TransientLiteralExpr(node.getValue())); } if (node.getReferenceBlueId() != null) { - fields.put("blueId", new LiteralExpr(BexValues.scalar(node.getReferenceBlueId()))); + fields.put("blueId", new TransientLiteralExpr(node.getReferenceBlueId())); } if (node.getBlue() != null) { fields.put("blue", compileExpr(node.getBlue(), scope, pointer + "/blue")); @@ -992,7 +1411,7 @@ private void addMetadataFields(Map fields, FrozenNod fields.put("schema", new LiteralExpr(BexValues.nodeSnapshot(new blue.language.model.Node().schema(node.getSchema())))); } if (node.getMergePolicy() != null) { - fields.put("mergePolicy", new LiteralExpr(BexValues.scalar(node.getMergePolicy()))); + fields.put("mergePolicy", new TransientLiteralExpr(node.getMergePolicy())); } } @@ -1162,7 +1581,9 @@ private static Set reservedBlueKeys() { "properties", "contracts", "$previous", - "$pos"); + "$pos", + "$replace", + "$empty"); return Collections.unmodifiableSet(keys); } @@ -1188,4 +1609,20 @@ private BexCompiledProgram.ArgSpec arg(String name) { } } + private enum VisitState { + UNVISITED, + VISITING, + VISITED + } + + private static final class CallSite { + private final String target; + private final BexSourcePath sourcePath; + + private CallSite(String target, BexSourcePath sourcePath) { + this.target = target; + this.sourcePath = sourcePath; + } + } + } diff --git a/src/main/java/blue/bex/compile/BexContainsCache.java b/src/main/java/blue/bex/compile/BexContainsCache.java index 64ec67d..6187053 100644 --- a/src/main/java/blue/bex/compile/BexContainsCache.java +++ b/src/main/java/blue/bex/compile/BexContainsCache.java @@ -65,11 +65,9 @@ private boolean scan(FrozenNode node) { return true; } if (node.getProperties() != null) { - if (node.getProperties().size() == 1) { + if (isExactOperatorShape(node)) { String key = node.getProperties().keySet().iterator().next(); - if (key.startsWith("$")) { - return true; - } + return key.startsWith("$"); } for (FrozenNode child : node.getProperties().values()) { if (scan(child)) { @@ -86,4 +84,24 @@ private boolean scan(FrozenNode node) { } return false; } + + private boolean isExactOperatorShape(FrozenNode node) { + return node.getProperties() != null + && node.getProperties().size() == 1 + && node.getName() == null + && node.getDescription() == null + && node.getType() == null + && node.getItemType() == null + && node.getKeyType() == null + && node.getValueType() == null + && node.getValue() == null + && node.getItems() == null + && node.getContracts() == null + && node.getReferenceBlueId() == null + && node.getSchema() == null + && node.getMergePolicy() == null + && node.getPreviousBlueId() == null + && node.getPosition() == null + && node.getBlue() == null; + } } diff --git a/src/main/java/blue/bex/compile/BexExpressions.java b/src/main/java/blue/bex/compile/BexExpressions.java index 85fec08..79d8fed 100644 --- a/src/main/java/blue/bex/compile/BexExpressions.java +++ b/src/main/java/blue/bex/compile/BexExpressions.java @@ -2,21 +2,1365 @@ import blue.bex.BexException; import blue.bex.BexSourcePath; +import blue.bex.gas.BexGasCounter; import blue.bex.runtime.CompiledExpression; import blue.bex.runtime.CompiledFrame; import blue.bex.value.BexValue; +import blue.bex.value.BexUnicodeOrder; import blue.bex.value.BexValues; +import java.math.BigDecimal; +import java.math.BigInteger; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +final class BexGasWork { + private static final int TEXT_BLOCK_CODE_POINTS = 64; + private static final BigInteger TEN = BigInteger.TEN; + + private BexGasWork() { + } + + static void charge(CompiledFrame frame, BexGasCounter counter) { + charge(frame, counter, 1L); + } + + static void charge(CompiledFrame frame, BexGasCounter counter, long quantity) { + if (quantity <= 0L) { + return; + } + BexSourcePath source = frame.sourcePath(); + frame.runtime().gas().charge(counter, + quantity, + source, + source != null ? source.operator() : null, + counter.canonicalName()); + } + + static long textBlocks(String text) { + return textBlocksForCodePoints(textCodePoints(text)); + } + + static long textCodePoints(String text) { + return text == null || text.isEmpty() + ? 0L + : text.codePointCount(0, text.length()); + } + + static long textBlocksForCodePoints(long codePoints) { + if (codePoints < 0L) { + throw new IllegalArgumentException( + "Text code-point count must be non-negative"); + } + return codePoints == 0L + ? 0L + : 1L + (codePoints - 1L) / TEXT_BLOCK_CODE_POINTS; + } + + /** + * Converts one scalar to canonical text while admitting every full-scan + * block before the conversion or scan which consumes that block. + */ + static MeteredText fullText( + CompiledFrame frame, + BexValue value) { + return fullText(frame, value, false); + } + + /** + * `$text` additionally constructs a new BEX Text value. Each output + * block's examination and construction units are admitted before the + * scalar formatter computes that block. + */ + static MeteredText constructedText( + CompiledFrame frame, + BexValue value) { + return fullText(frame, value, true); + } + + private static MeteredText fullText( + CompiledFrame frame, + BexValue value, + boolean constructed) { + BexValue exactValue = + value != null ? value : BexValues.undefined(); + if (exactValue.isUndefined() || exactValue.isNull()) { + return new MeteredText("", 0L, 0L); + } + if (!exactValue.isScalar()) { + throw new BexException("Value cannot be converted to text"); + } + + Object raw = exactValue.toSimple(); + if (raw instanceof String) { + TextScan scan = chargeTextScan( + frame, + BexGasCounter.TEXT_BLOCK_EXAMINED, + (String) raw, + 0, + ((String) raw).length()); + if (constructed) { + charge( + frame, + BexGasCounter.TEXT_BLOCK_CONSTRUCTED, + textBlocksForCodePoints(scan.codePoints)); + } + return new MeteredText( + (String) raw, + scan.codePoints, + scan.pointerEscapeExpansions); + } + + ScalarTextCursor cursor = scalarTextCursor(raw); + StringBuilder text = null; + long codePoints = 0L; + while (cursor.hasNext()) { + charge( + frame, + BexGasCounter.TEXT_BLOCK_EXAMINED); + if (constructed) { + charge( + frame, + BexGasCounter.TEXT_BLOCK_CONSTRUCTED); + } + String block = cursor.nextBlock( + TEXT_BLOCK_CODE_POINTS); + if (block.isEmpty()) { + throw new BexException( + "Scalar text conversion made no progress"); + } + if (text == null) { + text = new StringBuilder(); + } + text.append(block); + codePoints += block.codePointCount( + 0, block.length()); + } + return new MeteredText( + text != null ? text.toString() : "", + codePoints, + 0L); + } + + /** + * Charges one construction unit before scanning each output block and + * creates the requested substring only after its final unit is admitted. + */ + static String constructSubstring( + CompiledFrame frame, + String text, + int start, + int end) { + chargeSubstringConstruction( + frame, text, start, end); + return text.substring(start, end); + } + + static void chargeSubstringConstruction( + CompiledFrame frame, + String text, + int start, + int end) { + chargeTextScan( + frame, + BexGasCounter.TEXT_BLOCK_CONSTRUCTED, + text, + start, + end); + } + + static void chargeTextConstruction( + CompiledFrame frame, + String text) { + chargeSubstringConstruction( + frame, text, 0, text.length()); + } + + /** + * Canonical Unicode code-point comparison with block-pair admission before + * each compared block is read. + */ + static int compareText( + CompiledFrame frame, + String left, + String right) { + int leftOffset = 0; + int rightOffset = 0; + while (leftOffset < left.length() && rightOffset < right.length()) { + charge( + frame, + BexGasCounter.TEXT_BLOCK_EXAMINED, + 2L); + int inBlock = 0; + while (inBlock < TEXT_BLOCK_CODE_POINTS + && leftOffset < left.length() + && rightOffset < right.length()) { + int leftCodePoint = codePointAt( + left, leftOffset, left.length()); + int rightCodePoint = codePointAt( + right, rightOffset, right.length()); + leftOffset += charCountAt( + left, leftOffset, left.length()); + rightOffset += charCountAt( + right, rightOffset, right.length()); + if (leftCodePoint != rightCodePoint) { + return Integer.compare( + leftCodePoint, rightCodePoint); + } + inBlock++; + } + } + return Integer.compare( + left.length() - leftOffset, + right.length() - rightOffset); + } + + /** + * Scalar Text equality with each pair of source blocks admitted before it + * is read. This deliberately avoids eagerly materializing formatted + * numeric scalars, even though equality currently calls it only for Text. + */ + static boolean equalText( + CompiledFrame frame, + BexValue left, + BexValue right) { + ScalarTextCursor leftCursor = + scalarTextCursor(left); + ScalarTextCursor rightCursor = + scalarTextCursor(right); + while (leftCursor.hasNext() + && rightCursor.hasNext()) { + charge( + frame, + BexGasCounter.TEXT_BLOCK_EXAMINED, + 2L); + String leftBlock = leftCursor.nextBlock( + TEXT_BLOCK_CODE_POINTS); + String rightBlock = rightCursor.nextBlock( + TEXT_BLOCK_CODE_POINTS); + if (!leftBlock.equals(rightBlock)) { + return false; + } + } + return !leftCursor.hasNext() + && !rightCursor.hasNext(); + } + + /** + * Lazily converts and compares prefix blocks. Non-Text scalar formatting + * is deferred until after the exact block-pair charge which consumes it. + */ + static PrefixResult comparePrefix( + CompiledFrame frame, + BexValue text, + BexValue prefix) { + ScalarTextCursor textCursor = + scalarTextCursor(text); + ScalarTextCursor prefixCursor = + scalarTextCursor(prefix); + while (prefixCursor.hasNext()) { + if (!textCursor.hasNext()) { + return new PrefixResult( + false, textCursor); + } + charge( + frame, + BexGasCounter.TEXT_BLOCK_EXAMINED, + 2L); + String prefixBlock = prefixCursor.nextBlock( + TEXT_BLOCK_CODE_POINTS); + int comparedCodePoints = prefixBlock.codePointCount( + 0, prefixBlock.length()); + String textBlock = textCursor.nextBlock( + comparedCodePoints); + if (!textBlock.equals(prefixBlock)) { + return new PrefixResult( + false, textCursor); + } + } + return new PrefixResult( + true, textCursor); + } + + static long integerLimbs(BigInteger value) { + if (value == null) { + return 1L; + } + int bits = value.abs().bitLength(); + return Math.max(1L, (bits + 31L) / 32L); + } + + /** + * Exact Integer conversion with admission before parsing or decimal + * conversion work. + */ + static BigInteger convertInteger( + CompiledFrame frame, + BexValue value) { + Object raw = numericScalar(value, "integer"); + if (raw instanceof BigInteger) { + BigInteger integer = (BigInteger) raw; + charge( + frame, + BexGasCounter.INTEGER_LIMB_OPERATION, + integerLimbs(integer)); + return integer; + } + if (isIntegralPrimitive(raw)) { + long primitive = ((Number) raw).longValue(); + long limbs = primitive == Long.MIN_VALUE + || primitive > 0xFFFFFFFFL + || primitive < -0xFFFFFFFFL + ? 2L + : 1L; + charge( + frame, + BexGasCounter.INTEGER_LIMB_OPERATION, + limbs); + return BigInteger.valueOf(primitive); + } + if (raw instanceof String) { + return parseIntegerText( + frame, (String) raw); + } + if (raw instanceof BigDecimal) { + return exactDecimalInteger( + frame, (BigDecimal) raw); + } + if (raw instanceof Float || raw instanceof Double) { + charge( + frame, + BexGasCounter.INTEGER_LIMB_OPERATION); + BigDecimal decimal; + try { + decimal = BigDecimal.valueOf( + ((Number) raw).doubleValue()); + } catch (NumberFormatException ex) { + throw new BexException( + "Value cannot be converted to integer"); + } + charge( + frame, + BexGasCounter.INTEGER_LIMB_OPERATION, + integerLimbs(decimal.unscaledValue())); + BigInteger integer; + try { + integer = decimal.toBigIntegerExact(); + } catch (ArithmeticException ex) { + throw new BexException( + "Value cannot be converted to integer"); + } + return integer; + } + throw new BexException( + "Value cannot be converted to integer"); + } + + /** + * Reads an already-integer operand without adding a conversion charge, + * while routing Text and decimal coercion through the metered conversion + * path before arithmetic begins. + */ + static BigInteger integerOperand( + CompiledFrame frame, + BexValue value) { + Object raw = numericScalar(value, "integer"); + if (raw instanceof BigInteger) { + return (BigInteger) raw; + } + if (isIntegralPrimitive(raw)) { + return BigInteger.valueOf( + ((Number) raw).longValue()); + } + return convertInteger(frame, value); + } + + /** + * Decimal conversion with one scale-alignment unit and every unscaled + * magnitude limb admitted before parsing or allocation. + */ + static BigDecimal convertNumber( + CompiledFrame frame, + BexValue value) { + Object raw = numericScalar(value, "number"); + if (raw instanceof String) { + charge( + frame, + BexGasCounter.INTEGER_LIMB_OPERATION, + 2L); + return parseDecimalText( + frame, (String) raw, 1L); + } + if (raw instanceof BigDecimal) { + BigDecimal decimal = (BigDecimal) raw; + charge( + frame, + BexGasCounter.INTEGER_LIMB_OPERATION, + integerLimbs(decimal.unscaledValue()) + 1L); + return decimal; + } + if (raw instanceof BigInteger) { + BigInteger integer = (BigInteger) raw; + charge( + frame, + BexGasCounter.INTEGER_LIMB_OPERATION, + integerLimbs(integer) + 1L); + return new BigDecimal(integer); + } + if (isIntegralPrimitive(raw)) { + BigInteger integer = BigInteger.valueOf( + ((Number) raw).longValue()); + charge( + frame, + BexGasCounter.INTEGER_LIMB_OPERATION, + integerLimbs(integer) + 1L); + return new BigDecimal(integer); + } + if (raw instanceof Float || raw instanceof Double) { + charge( + frame, + BexGasCounter.INTEGER_LIMB_OPERATION, + 2L); + BigDecimal decimal; + try { + decimal = BigDecimal.valueOf( + ((Number) raw).doubleValue()); + } catch (NumberFormatException ex) { + throw new BexException( + "Value cannot be converted to number"); + } + charge( + frame, + BexGasCounter.INTEGER_LIMB_OPERATION, + integerLimbs(decimal.unscaledValue()) - 1L); + return decimal; + } + throw new BexException( + "Value cannot be converted to number"); + } + + /** + * Numeric equality/ordering conversion and comparison. The guaranteed + * first limb of each operand (plus the canonical decimal alignment unit) + * is admitted before any Text parse or Integer-to-decimal allocation. + */ + static int compareNumbers( + CompiledFrame frame, + BexValue left, + BexValue right, + boolean decimalAlignment) { + Object leftRaw = numericScalar(left, "number"); + Object rightRaw = numericScalar(right, "number"); + long base = 2L; + if (decimalAlignment + && (isDecimalRaw(leftRaw) + || isDecimalRaw(rightRaw))) { + base++; + } + charge( + frame, + BexGasCounter.INTEGER_LIMB_OPERATION, + base); + BigDecimal leftNumber = comparisonNumber( + frame, leftRaw); + BigDecimal rightNumber = comparisonNumber( + frame, rightRaw); + return leftNumber.compareTo(rightNumber); + } + + private static BigDecimal comparisonNumber( + CompiledFrame frame, + Object raw) { + if (raw instanceof BigDecimal) { + BigDecimal decimal = (BigDecimal) raw; + charge( + frame, + BexGasCounter.INTEGER_LIMB_OPERATION, + integerLimbs(decimal.unscaledValue()) - 1L); + return decimal; + } + if (raw instanceof BigInteger) { + BigInteger integer = (BigInteger) raw; + charge( + frame, + BexGasCounter.INTEGER_LIMB_OPERATION, + integerLimbs(integer) - 1L); + return new BigDecimal(integer); + } + if (isIntegralPrimitive(raw)) { + BigInteger integer = BigInteger.valueOf( + ((Number) raw).longValue()); + charge( + frame, + BexGasCounter.INTEGER_LIMB_OPERATION, + integerLimbs(integer) - 1L); + return new BigDecimal(integer); + } + if (raw instanceof String) { + return parseDecimalText( + frame, (String) raw, 1L); + } + if (raw instanceof Float || raw instanceof Double) { + BigDecimal decimal; + try { + decimal = BigDecimal.valueOf( + ((Number) raw).doubleValue()); + } catch (NumberFormatException ex) { + throw new BexException( + "Value cannot be converted to number"); + } + charge( + frame, + BexGasCounter.INTEGER_LIMB_OPERATION, + integerLimbs(decimal.unscaledValue()) - 1L); + return decimal; + } + throw new BexException( + "Value cannot be converted to number"); + } + + private static BigInteger parseIntegerText( + CompiledFrame frame, + String text) { + charge( + frame, + BexGasCounter.INTEGER_LIMB_OPERATION); + if (text == null || text.isEmpty()) { + throw new BexException( + "Value cannot be converted to integer"); + } + int offset = 0; + boolean negative = false; + if (text.charAt(0) == '-') { + negative = true; + offset = 1; + } + if (offset == text.length()) { + throw new BexException( + "Value cannot be converted to integer"); + } + + IncrementalMagnitude magnitude = + new IncrementalMagnitude(frame, 1L); + while (offset < text.length()) { + char character = text.charAt(offset++); + if (character < '0' || character > '9') { + throw new BexException( + "Value cannot be converted to integer"); + } + magnitude.append(character - '0'); + } + BigInteger integer = magnitude.value(); + return negative ? integer.negate() : integer; + } + + private static BigInteger exactDecimalInteger( + CompiledFrame frame, + BigDecimal decimal) { + int scale = decimal.scale(); + BigInteger unscaled = decimal.unscaledValue(); + if (scale <= 0) { + long admittedMagnitudeLimbs = + integerLimbs(unscaled); + charge( + frame, + BexGasCounter.INTEGER_LIMB_OPERATION, + admittedMagnitudeLimbs + 1L); + boolean negative = unscaled.signum() < 0; + IncrementalMagnitude magnitude = + new IncrementalMagnitude( + frame, + unscaled.abs(), + admittedMagnitudeLimbs); + for (long remaining = -(long) scale; + remaining > 0L; + remaining--) { + magnitude.append(0); + } + BigInteger integer = magnitude.value(); + return negative ? integer.negate() : integer; + } + + /* + * Exact scale reduction must inspect the unscaled magnitude. Admit + * every such limb before BigDecimal performs that work; charging only + * the eventual result limbs would allow a large fractional magnitude + * to be inspected before a later admission. + */ + charge( + frame, + BexGasCounter.INTEGER_LIMB_OPERATION, + integerLimbs(unscaled) + 1L); + BigInteger integer; + try { + integer = decimal.toBigIntegerExact(); + } catch (ArithmeticException ex) { + throw new BexException( + "Value cannot be converted to integer"); + } + return integer; + } + + private static BigDecimal parseDecimalText( + CompiledFrame frame, + String text, + long admittedLimbs) { + if (text == null || text.isEmpty()) { + throw new BexException( + "Value cannot be converted to number"); + } + int offset = 0; + boolean negative = false; + char first = text.charAt(offset); + if (first == '+' || first == '-') { + negative = first == '-'; + offset++; + } + + IncrementalMagnitude magnitude = + new IncrementalMagnitude( + frame, admittedLimbs); + boolean decimalPoint = false; + boolean digitSeen = false; + long fractionalDigits = 0L; + while (offset < text.length()) { + char character = text.charAt(offset); + if (character == 'e' || character == 'E') { + break; + } + if (character == '.') { + if (decimalPoint) { + throw new BexException( + "Value cannot be converted to number"); + } + decimalPoint = true; + offset++; + continue; + } + int digit = Character.digit(character, 10); + if (digit < 0) { + throw new BexException( + "Value cannot be converted to number"); + } + digitSeen = true; + magnitude.append(digit); + if (decimalPoint) { + fractionalDigits++; + } + offset++; + } + if (!digitSeen) { + throw new BexException( + "Value cannot be converted to number"); + } + + long exponent = 0L; + if (offset < text.length()) { + offset++; + boolean exponentNegative = false; + if (offset < text.length() + && (text.charAt(offset) == '+' + || text.charAt(offset) == '-')) { + exponentNegative = text.charAt(offset) == '-'; + offset++; + } + if (offset == text.length()) { + throw new BexException( + "Value cannot be converted to number"); + } + while (offset < text.length()) { + int digit = Character.digit( + text.charAt(offset++), 10); + if (digit < 0 + || exponent > (Long.MAX_VALUE - digit) / 10L) { + throw new BexException( + "Value cannot be converted to number"); + } + exponent = exponent * 10L + digit; + } + if (exponentNegative) { + exponent = -exponent; + } + } + + long scale; + try { + scale = Math.subtractExact( + fractionalDigits, exponent); + } catch (ArithmeticException overflow) { + throw new BexException( + "Value cannot be converted to number"); + } + if (scale < Integer.MIN_VALUE + || scale > Integer.MAX_VALUE) { + throw new BexException( + "Value cannot be converted to number"); + } + BigInteger unscaled = magnitude.value(); + if (negative) { + unscaled = unscaled.negate(); + } + return new BigDecimal(unscaled, (int) scale); + } + + private static Object numericScalar( + BexValue value, + String target) { + BexValue exactValue = + value != null ? value : BexValues.undefined(); + if (!exactValue.isScalar()) { + throw new BexException( + "Value cannot be converted to " + target); + } + return exactValue.toSimple(); + } + + private static ScalarTextCursor scalarTextCursor( + BexValue value) { + BexValue exactValue = + value != null ? value : BexValues.undefined(); + if (exactValue.isUndefined() || exactValue.isNull()) { + return new RawStringCursor(""); + } + if (!exactValue.isScalar()) { + throw new BexException( + "Value cannot be converted to text"); + } + return scalarTextCursor(exactValue.toSimple()); + } + + private static ScalarTextCursor scalarTextCursor( + Object raw) { + if (raw instanceof String) { + return new RawStringCursor((String) raw); + } + if (raw instanceof BigInteger) { + return new IntegerTextCursor( + (BigInteger) raw); + } + if (raw instanceof BigDecimal) { + return new DecimalTextCursor( + (BigDecimal) raw); + } + if (isIntegralPrimitive(raw) + || raw instanceof Float + || raw instanceof Double + || raw instanceof Boolean) { + return new FixedScalarTextCursor(raw); + } + throw new BexException( + "Value cannot be converted to text"); + } + + private static boolean isIntegralPrimitive(Object raw) { + return raw instanceof Byte + || raw instanceof Short + || raw instanceof Integer + || raw instanceof Long; + } + + private static boolean isDecimalRaw(Object raw) { + return raw instanceof BigDecimal + || raw instanceof Float + || raw instanceof Double; + } + + private static int decimalDigits(BigInteger magnitude) { + if (magnitude.signum() == 0) { + return 1; + } + int bitLength = magnitude.bitLength(); + int estimate = Math.max( + 1, + (int) Math.floor( + (bitLength - 1) + * 0.3010299956639812d) + + 1); + BigInteger lower = TEN.pow(estimate - 1); + while (magnitude.compareTo(lower) < 0) { + estimate--; + lower = lower.divide(TEN); + } + BigInteger upper = lower.multiply(TEN); + while (magnitude.compareTo(upper) >= 0) { + estimate++; + lower = upper; + upper = upper.multiply(TEN); + } + return estimate; + } + + private static int decimalDigits(long value) { + long remaining = value < 0L ? -value : value; + int digits = 1; + while (remaining >= 10L) { + remaining /= 10L; + digits++; + } + return digits; + } + + private static TextScan chargeTextScan( + CompiledFrame frame, + BexGasCounter counter, + String text, + int start, + int end) { + int offset = start; + long codePoints = 0L; + long pointerEscapeExpansions = 0L; + while (offset < end) { + charge(frame, counter); + int inBlock = 0; + while (inBlock < TEXT_BLOCK_CODE_POINTS + && offset < end) { + char character = text.charAt(offset); + if (character == '~' || character == '/') { + pointerEscapeExpansions++; + } + offset += charCountAt(text, offset, end); + codePoints++; + inBlock++; + } + } + return new TextScan( + codePoints, + pointerEscapeExpansions); + } + + private static int codePointAt( + String text, + int offset, + int end) { + char first = text.charAt(offset); + if (Character.isHighSurrogate(first) + && offset + 1 < end) { + char second = text.charAt(offset + 1); + if (Character.isLowSurrogate(second)) { + return Character.toCodePoint( + first, second); + } + } + return first; + } + + private static int charCountAt( + String text, + int offset, + int end) { + char first = text.charAt(offset); + return Character.isHighSurrogate(first) + && offset + 1 < end + && Character.isLowSurrogate( + text.charAt(offset + 1)) + ? 2 + : 1; + } + + static final class MeteredText { + private final String text; + private final long codePoints; + private final long pointerEscapeExpansions; + + private MeteredText( + String text, + long codePoints, + long pointerEscapeExpansions) { + this.text = text; + this.codePoints = codePoints; + this.pointerEscapeExpansions = + pointerEscapeExpansions; + } + + String text() { + return text; + } + + long codePoints() { + return codePoints; + } + + long pointerEscapeExpansions() { + return pointerEscapeExpansions; + } + } + + private static final class TextScan { + private final long codePoints; + private final long pointerEscapeExpansions; + + private TextScan( + long codePoints, + long pointerEscapeExpansions) { + this.codePoints = codePoints; + this.pointerEscapeExpansions = + pointerEscapeExpansions; + } + } + + static final class PrefixResult { + private final boolean matched; + private final ScalarTextCursor text; + + private PrefixResult( + boolean matched, + ScalarTextCursor text) { + this.matched = matched; + this.text = text; + } + + boolean matched() { + return matched; + } + + String constructSuffix( + CompiledFrame frame) { + if (!matched) { + return ""; + } + StringBuilder suffix = null; + while (text.hasNext()) { + charge( + frame, + BexGasCounter.TEXT_BLOCK_CONSTRUCTED); + String block = text.nextBlock( + TEXT_BLOCK_CODE_POINTS); + if (suffix == null) { + suffix = new StringBuilder(); + } + suffix.append(block); + } + return suffix != null + ? suffix.toString() + : ""; + } + } + + private interface ScalarTextCursor { + boolean hasNext(); + + String nextBlock(int maxCodePoints); + } + + private static final class RawStringCursor + implements ScalarTextCursor { + private final String text; + private int offset; + + private RawStringCursor(String text) { + this.text = text; + } + + @Override + public boolean hasNext() { + return offset < text.length(); + } + + @Override + public String nextBlock(int maxCodePoints) { + int start = offset; + int consumed = 0; + while (consumed < maxCodePoints + && offset < text.length()) { + offset += charCountAt( + text, offset, text.length()); + consumed++; + } + return text.substring(start, offset); + } + } + + private static final class FixedScalarTextCursor + implements ScalarTextCursor { + private final Object raw; + private String text; + private int offset; + + private FixedScalarTextCursor(Object raw) { + this.raw = raw; + } + + @Override + public boolean hasNext() { + return text == null || offset < text.length(); + } + + @Override + public String nextBlock(int maxCodePoints) { + if (text == null) { + text = String.valueOf(raw); + } + int end = Math.min( + text.length(), + offset + maxCodePoints); + String block = text.substring(offset, end); + offset = end; + return block; + } + } + + private static final class IntegerTextCursor + implements ScalarTextCursor { + private final boolean negative; + private final DecimalDigitsCursor digits; + private boolean signPending; + + private IntegerTextCursor(BigInteger integer) { + this.negative = integer.signum() < 0; + this.signPending = negative; + this.digits = new DecimalDigitsCursor(integer); + } + + @Override + public boolean hasNext() { + return signPending || digits.hasNext(); + } + + @Override + public String nextBlock(int maxCodePoints) { + StringBuilder block = + new StringBuilder(maxCodePoints); + if (signPending && block.length() < maxCodePoints) { + block.append('-'); + signPending = false; + } + if (block.length() < maxCodePoints + && digits.hasNext()) { + block.append(digits.nextDigits( + maxCodePoints - block.length())); + } + return block.toString(); + } + } + + private static final class DecimalTextCursor + implements ScalarTextCursor { + private static final int LITERAL = 0; + private static final int DIGITS = 1; + private static final int ZEROS = 2; + private static final int EXPONENT = 3; + + private final BigDecimal decimal; + private final DecimalDigitsCursor digits; + private List parts; + private int partIndex; + + private DecimalTextCursor(BigDecimal decimal) { + this.decimal = decimal; + this.digits = new DecimalDigitsCursor( + decimal.unscaledValue()); + } + + @Override + public boolean hasNext() { + if (parts == null) { + return true; + } + skipEmptyParts(); + return partIndex < parts.size(); + } + + @Override + public String nextBlock(int maxCodePoints) { + if (parts == null) { + initialize(); + } + StringBuilder block = + new StringBuilder(maxCodePoints); + while (block.length() < maxCodePoints) { + skipEmptyParts(); + if (partIndex >= parts.size()) { + break; + } + TextPart part = parts.get(partIndex); + int capacity = + maxCodePoints - block.length(); + if (part.kind == DIGITS) { + int take = (int) Math.min( + part.remaining, + (long) capacity); + block.append( + digits.nextDigits(take)); + part.remaining -= take; + } else if (part.kind == ZEROS) { + int take = (int) Math.min( + part.remaining, + (long) capacity); + for (int index = 0; + index < take; + index++) { + block.append('0'); + } + part.remaining -= take; + } else { + if (part.text == null) { + part.text = part.kind == EXPONENT + ? Long.toString(part.exponent) + : ""; + } + int take = Math.min( + capacity, + part.text.length() + - part.textOffset); + block.append( + part.text, + part.textOffset, + part.textOffset + take); + part.textOffset += take; + part.remaining -= take; + } + } + return block.toString(); + } + + private void initialize() { + parts = new ArrayList<>(); + if (decimal.signum() < 0) { + parts.add(TextPart.literal("-")); + } + long precision = digits.digitCount(); + long scale = decimal.scale(); + long adjustedExponent = + -scale + precision - 1L; + if (scale >= 0L + && adjustedExponent >= -6L) { + if (scale == 0L) { + parts.add(TextPart.digits( + precision)); + } else if (scale < precision) { + parts.add(TextPart.digits( + precision - scale)); + parts.add(TextPart.literal(".")); + parts.add(TextPart.digits(scale)); + } else { + parts.add(TextPart.literal("0.")); + parts.add(TextPart.zeros( + scale - precision)); + parts.add(TextPart.digits( + precision)); + } + return; + } + + parts.add(TextPart.digits(1L)); + if (precision > 1L) { + parts.add(TextPart.literal(".")); + parts.add(TextPart.digits( + precision - 1L)); + } + parts.add(TextPart.literal( + adjustedExponent >= 0L + ? "E+" + : "E-")); + parts.add(TextPart.exponent( + Math.abs(adjustedExponent))); + } + + private void skipEmptyParts() { + while (partIndex < parts.size() + && parts.get(partIndex).remaining == 0L) { + partIndex++; + } + } + } + + private static final class DecimalDigitsCursor { + private final BigInteger signed; + private BigInteger magnitude; + private BigInteger emittedPrefix = BigInteger.ZERO; + private int totalDigits; + private int emittedDigits; + private boolean initialized; + + private DecimalDigitsCursor(BigInteger signed) { + this.signed = signed; + } + + private boolean hasNext() { + return !initialized || emittedDigits < totalDigits; + } + + private int digitCount() { + initialize(); + return totalDigits; + } + + private String nextDigits(int maximum) { + if (maximum <= 0) { + throw new IllegalArgumentException( + "Decimal digit block must be positive"); + } + initialize(); + int take = Math.min( + maximum, totalDigits - emittedDigits); + int after = totalDigits + - emittedDigits + - take; + + /* + * Extract only the prefix ending at this admitted output block. + * In particular, do not use divideAndRemainder here: its remainder + * materializes every lower digit even though a later text-block + * charge may still reject before those digits are examined. + * + * Recomputing the progressively longer quotient from the original + * magnitude is deliberate. Work for a lower block begins only + * when nextDigits is called for that block, after its caller has + * admitted the corresponding text counter. + */ + BigInteger divisor = TEN.pow(after); + BigInteger prefixThroughBlock = + magnitude.divide(divisor); + BigInteger blockMagnitude = + prefixThroughBlock.subtract( + emittedPrefix.multiply( + TEN.pow(take))); + String digits = blockMagnitude.toString(); + emittedPrefix = prefixThroughBlock; + emittedDigits += take; + if (digits.length() == take) { + return digits; + } + StringBuilder padded = + new StringBuilder(take); + for (int index = digits.length(); + index < take; + index++) { + padded.append('0'); + } + padded.append(digits); + return padded.toString(); + } + + private void initialize() { + if (initialized) { + return; + } + magnitude = signed.signum() < 0 + ? signed.negate() + : signed; + totalDigits = decimalDigits( + magnitude); + initialized = true; + } + } + + private static final class TextPart { + private final int kind; + private long remaining; + private String text; + private int textOffset; + private long exponent; + + private TextPart( + int kind, + long remaining, + String text, + long exponent) { + this.kind = kind; + this.remaining = remaining; + this.text = text; + this.exponent = exponent; + } + + private static TextPart literal(String value) { + return new TextPart( + DecimalTextCursor.LITERAL, + value.length(), + value, + 0L); + } + + private static TextPart digits(long count) { + return new TextPart( + DecimalTextCursor.DIGITS, + count, + null, + 0L); + } + + private static TextPart zeros(long count) { + return new TextPart( + DecimalTextCursor.ZEROS, + count, + null, + 0L); + } + + private static TextPart exponent(long value) { + return new TextPart( + DecimalTextCursor.EXPONENT, + decimalDigits(value), + null, + value); + } + } + + private static final class IncrementalMagnitude { + private final CompiledFrame frame; + private BigInteger value; + private long admittedLimbs; + + private IncrementalMagnitude( + CompiledFrame frame, + long admittedLimbs) { + this(frame, BigInteger.ZERO, admittedLimbs); + } + + private IncrementalMagnitude( + CompiledFrame frame, + BigInteger value, + long admittedLimbs) { + this.frame = frame; + this.value = value; + this.admittedLimbs = admittedLimbs; + } + + private void append(int digit) { + if (wouldGrow(digit)) { + charge( + frame, + BexGasCounter.INTEGER_LIMB_OPERATION); + admittedLimbs++; + } + value = value.multiply(TEN) + .add(BigInteger.valueOf(digit)); + } + + private boolean wouldGrow(int digit) { + long admittedBits = admittedLimbs * 32L; + if (admittedBits > Integer.MAX_VALUE) { + throw new BexException( + "Integer magnitude exceeds supported range"); + } + if ((long) value.bitLength() + 4L + <= admittedBits) { + return false; + } + BigInteger boundary = BigInteger.ONE.shiftLeft( + (int) admittedBits); + BigInteger largestWithoutGrowth = + boundary.subtract( + BigInteger.ONE + .add(BigInteger.valueOf(digit))) + .divide(TEN); + return value.compareTo( + largestWithoutGrowth) > 0; + } + + private BigInteger value() { + return value; + } + } +} + abstract class Expr implements CompiledExpression { @Override public final BexValue eval(CompiledFrame frame) { + BexGasWork.charge(frame, BexGasCounter.EXPRESSION_EVALUATED); frame.runtime().metrics().incrementExpressionEvaluations(); - frame.runtime().gas().charge(frame.runtime().gas().schedule().expressionBase); try { return doEval(frame); } catch (BexException ex) { @@ -69,6 +1413,23 @@ protected BexValue doEval(CompiledFrame frame) { } } +final class TransientLiteralExpr extends Expr { + private final Object scalar; + + TransientLiteralExpr(Object scalar) { + this.scalar = scalar; + } + + @Override + protected BexValue doEval(CompiledFrame frame) { + if (scalar instanceof String) { + BexGasWork.chargeTextConstruction( + frame, (String) scalar); + } + return BexValues.scalar(scalar); + } +} + final class DocumentExpr extends Expr { private final PointerOperand pointer; private final boolean resolved; @@ -80,12 +1441,12 @@ final class DocumentExpr extends Expr { @Override protected BexValue doEval(CompiledFrame frame) { - String absolute = pointer.absolute(frame); - return frame.readDocument(absolute, pointer.segments(frame), resolved); + ResolvedPointer resolvedPointer = pointer.resolve(frame); + return frame.readDocument(resolvedPointer.absolute(), resolvedPointer.segments(), resolved); } } -enum ContextKind { EVENT, CURRENT_CONTRACT } +enum ContextKind { EVENT, PROCESSING_EVENT, CURRENT_CONTRACT } final class ContextPointerExpr extends Expr { private final PointerOperand pointer; @@ -99,7 +1460,13 @@ final class ContextPointerExpr extends Expr { @Override protected BexValue doEval(CompiledFrame frame) { List segments = pointer.segments(frame); - return kind == ContextKind.EVENT ? frame.readEvent(segments) : frame.readCurrentContract(segments); + if (kind == ContextKind.EVENT) { + return frame.readEvent(segments); + } + if (kind == ContextKind.PROCESSING_EVENT) { + return frame.readProcessingEvent(segments); + } + return frame.readCurrentContract(segments); } } @@ -114,7 +1481,11 @@ final class StepsExpr extends Expr { @Override protected BexValue doEval(CompiledFrame frame) { - return frame.runtime().readSteps(step.get(frame), pointer.segments(frame)); + String stepName = step.get(frame); + if (stepName.isEmpty()) { + throw new BexException("$steps.step cannot be empty"); + } + return frame.runtime().readSteps(stepName, pointer.segments(frame)); } } @@ -129,7 +1500,11 @@ final class BindingExpr extends Expr { @Override protected BexValue doEval(CompiledFrame frame) { - return frame.readBinding(name.get(frame), pointer.segments(frame)); + String bindingName = name.get(frame); + if (bindingName.isEmpty()) { + throw new BexException("$binding.name cannot be empty"); + } + return frame.readBinding(bindingName, pointer.segments(frame)); } } @@ -148,9 +1523,11 @@ final class VarExpr extends Expr { @Override protected BexValue doEval(CompiledFrame frame) { - frame.runtime().gas().charge(frame.runtime().gas().schedule().varRead); - BexValue value = frame.get(slot); - return pointer != null ? value.at(pointer.segments(frame)) : value; + BexGasWork.charge(frame, BexGasCounter.VARIABLE_READ); + BexValue value = frame.getRequired(slot); + return pointer != null + ? frame.runtime().readValuePointer(value, pointer.segments(frame)) + : value; } } @@ -169,8 +1546,11 @@ final class ConstExpr extends Expr { @Override protected BexValue doEval(CompiledFrame frame) { + BexGasWork.charge(frame, BexGasCounter.CONSTANT_READ); BexValue value = frame.runtime().program().constant(name); - return pointer != null ? value.at(pointer.segments(frame)) : value; + return pointer != null + ? frame.runtime().readValuePointer(value, pointer.segments(frame)) + : value; } } @@ -185,7 +1565,12 @@ final class GetExpr extends Expr { @Override protected BexValue doEval(CompiledFrame frame) { - return object.eval(frame).get(key.get(frame)); + BexValue value = object.eval(frame); + String evaluatedKey = key.get(frame); + if (value.isObject()) { + BexGasWork.charge(frame, BexGasCounter.OBJECT_MEMBER_READ); + } + return value.get(evaluatedKey); } } @@ -193,7 +1578,10 @@ final class ObjectExpr extends Expr { private final Map fields; ObjectExpr(Map fields) { - this.fields = fields; + this.fields = new LinkedHashMap<>(); + for (String key : BexUnicodeOrder.sortedCopy(fields.keySet())) { + this.fields.put(key, fields.get(key)); + } } @Override @@ -202,6 +1590,7 @@ protected BexValue doEval(CompiledFrame frame) { for (Map.Entry entry : fields.entrySet()) { BexValue value = entry.getValue().eval(frame); if (!value.isUndefined()) { + BexGasWork.charge(frame, BexGasCounter.TRANSIENT_OBJECT_MEMBER_PRODUCED); out.put(entry.getKey(), value); } } @@ -220,7 +1609,13 @@ final class ListExpr extends Expr { protected BexValue doEval(CompiledFrame frame) { List out = new ArrayList<>(); for (CompiledExpression item : items) { - out.add(item.eval(frame)); + BexValue value = item.eval(frame); + if (value.isUndefined()) { + throw new BexException( + "Undefined cannot appear in a Blue list"); + } + BexGasWork.charge(frame, BexGasCounter.TRANSIENT_LIST_ITEM_PRODUCED); + out.add(value); } return BexValues.list(out); } @@ -234,7 +1629,10 @@ final class IntrinsicExpr extends Expr { IntrinsicExpr(String blueId, BexValue type, Map fields) { this.blueId = blueId; this.type = type; - this.fields = fields; + this.fields = new LinkedHashMap<>(); + for (String key : BexUnicodeOrder.sortedCopy(fields.keySet())) { + this.fields.put(key, fields.get(key)); + } } @Override @@ -246,6 +1644,33 @@ protected BexValue doEval(CompiledFrame frame) { values.put(entry.getKey(), value); } } + BexGasWork.charge(frame, BexGasCounter.INTRINSIC_CALLED); return frame.runtime().invokeIntrinsic(blueId, type, values); } } + +final class FailExpr extends Expr { + private final CompiledExpression message; + + FailExpr(CompiledExpression message) { + this.message = message; + } + + @Override + protected BexValue doEval(CompiledFrame frame) { + throw new BexException(message.eval(frame).asText()); + } +} + +final class NodeBlueIdExpr extends Expr { + private final CompiledExpression expression; + + NodeBlueIdExpr(CompiledExpression expression) { + this.expression = expression; + } + + @Override + protected BexValue doEval(CompiledFrame frame) { + return frame.runtime().nodeBlueId(expression.eval(frame)); + } +} diff --git a/src/main/java/blue/bex/compile/BexNodeFingerprint.java b/src/main/java/blue/bex/compile/BexNodeFingerprint.java index 6ed4bae..47c16c8 100644 --- a/src/main/java/blue/bex/compile/BexNodeFingerprint.java +++ b/src/main/java/blue/bex/compile/BexNodeFingerprint.java @@ -3,6 +3,7 @@ import blue.language.snapshot.FrozenNode; import blue.language.model.Node; import blue.language.model.Schema; +import blue.bex.value.BexUnicodeOrder; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; @@ -83,7 +84,7 @@ private static void updateNode(MessageDigest digest, FrozenNode node) { if (node.getProperties() != null) { update(digest, "properties{"); List keys = new ArrayList<>(node.getProperties().keySet()); - Collections.sort(keys); + Collections.sort(keys, BexUnicodeOrder.CODE_POINT_COMPARATOR); for (String key : keys) { updateField(digest, "key", key); updateNode(digest, node.getProperties().get(key)); diff --git a/src/main/java/blue/bex/compile/BexOperands.java b/src/main/java/blue/bex/compile/BexOperands.java index b77a0c1..7146f7c 100644 --- a/src/main/java/blue/bex/compile/BexOperands.java +++ b/src/main/java/blue/bex/compile/BexOperands.java @@ -13,9 +13,43 @@ interface TextOperand { } interface PointerOperand { - String authored(CompiledFrame frame); - String absolute(CompiledFrame frame); - List segments(CompiledFrame frame); + ResolvedPointer resolve(CompiledFrame frame); + + default String authored(CompiledFrame frame) { + return resolve(frame).authored(); + } + + default String absolute(CompiledFrame frame) { + return resolve(frame).absolute(); + } + + default List segments(CompiledFrame frame) { + return resolve(frame).segments(); + } +} + +final class ResolvedPointer { + private final String authored; + private final String absolute; + private final List segments; + + ResolvedPointer(String authored, String absolute, List segments) { + this.authored = authored; + this.absolute = absolute; + this.segments = segments; + } + + String authored() { + return authored; + } + + String absolute() { + return absolute; + } + + List segments() { + return segments; + } } final class StaticTextExpr implements TextOperand { @@ -66,21 +100,13 @@ static StaticPointerOperand absolute(String pointer) { } @Override - public String authored(CompiledFrame frame) { - return authored; - } - - @Override - public String absolute(CompiledFrame frame) { - return absolute ? pointer.text() : frame.runtime().resolvePointer(authored); - } - - @Override - public List segments(CompiledFrame frame) { + public ResolvedPointer resolve(CompiledFrame frame) { if (absolute) { - return pointer.segments(); + return new ResolvedPointer(authored, pointer.text(), pointer.segments()); } - return frame.runtime().parseDynamicPointer(absolute(frame)); + String resolved = frame.runtime().resolvePointer(authored); + return new ResolvedPointer(authored, resolved, + frame.runtime().parseDynamicPointer(resolved)); } } @@ -92,18 +118,11 @@ final class DynamicPointerOperand implements PointerOperand { } @Override - public String authored(CompiledFrame frame) { - return PointerOperands.pointerText(expr.eval(frame)); - } - - @Override - public String absolute(CompiledFrame frame) { - return frame.runtime().resolvePointer(authored(frame)); - } - - @Override - public List segments(CompiledFrame frame) { - return frame.runtime().parseDynamicPointer(absolute(frame)); + public ResolvedPointer resolve(CompiledFrame frame) { + String authored = PointerOperands.pointerText(expr.eval(frame)); + String absolute = frame.runtime().resolvePointer(authored); + return new ResolvedPointer(authored, absolute, + frame.runtime().parseDynamicPointer(absolute)); } } @@ -119,18 +138,8 @@ static StaticValuePointerOperand of(String authored) { } @Override - public String authored(CompiledFrame frame) { - return pointer.text(); - } - - @Override - public String absolute(CompiledFrame frame) { - return pointer.text(); - } - - @Override - public List segments(CompiledFrame frame) { - return pointer.segments(); + public ResolvedPointer resolve(CompiledFrame frame) { + return new ResolvedPointer(pointer.text(), pointer.text(), pointer.segments()); } static String normalize(String authored) { @@ -149,18 +158,12 @@ final class DynamicValuePointerOperand implements PointerOperand { } @Override - public String authored(CompiledFrame frame) { - return StaticValuePointerOperand.normalize(PointerOperands.pointerText(expr.eval(frame))); - } - - @Override - public String absolute(CompiledFrame frame) { - return frame.runtime().canonicalPointer(authored(frame)); - } - - @Override - public List segments(CompiledFrame frame) { - return frame.runtime().parseDynamicPointer(absolute(frame)); + public ResolvedPointer resolve(CompiledFrame frame) { + String authored = StaticValuePointerOperand.normalize( + PointerOperands.pointerText(expr.eval(frame))); + String absolute = frame.runtime().canonicalPointer(authored); + return new ResolvedPointer(authored, absolute, + frame.runtime().parseDynamicPointer(absolute)); } } diff --git a/src/main/java/blue/bex/compile/BexStatements.java b/src/main/java/blue/bex/compile/BexStatements.java index 8e88d37..c3fb5fe 100644 --- a/src/main/java/blue/bex/compile/BexStatements.java +++ b/src/main/java/blue/bex/compile/BexStatements.java @@ -2,6 +2,7 @@ import blue.bex.BexException; import blue.bex.BexSourcePath; +import blue.bex.gas.BexGasCounter; import blue.bex.result.BexPatchEntry; import blue.bex.runtime.CompiledExpression; import blue.bex.runtime.CompiledFrame; @@ -18,8 +19,8 @@ abstract class Stmt implements CompiledStatement { @Override public final Control exec(CompiledFrame frame) { + BexGasWork.charge(frame, BexGasCounter.STATEMENT_EXECUTED); frame.runtime().metrics().incrementStatementExecutions(); - frame.runtime().gas().charge(frame.runtime().gas().schedule().statementBase); try { return doExec(frame); } catch (BexException ex) { @@ -152,13 +153,15 @@ protected Control doExec(CompiledFrame frame) { BexValue value = input.eval(frame); if (value.isObject()) { for (String key : value.keys()) { + BexGasWork.charge(frame, BexGasCounter.COLLECTION_ITEM_VISITED); + BexGasWork.charge(frame, BexGasCounter.OBJECT_MEMBER_READ); frame.runtime().metrics().incrementLoopIterations(); - frame.runtime().gas().charge(frame.runtime().gas().schedule().forEachItem); if (keySlot >= 0) { frame.set(keySlot, BexValues.scalar(key)); frame.set(itemSlot, value.get(key)); } else { Map entry = new LinkedHashMap<>(); + BexGasWork.charge(frame, BexGasCounter.TRANSIENT_OBJECT_MEMBER_PRODUCED, 2L); entry.put("key", BexValues.scalar(key)); entry.put("val", value.get(key)); frame.set(itemSlot, BexValues.map(entry)); @@ -172,8 +175,9 @@ protected Control doExec(CompiledFrame frame) { } } else if (value.isList()) { for (int i = 0; i < value.size(); i++) { + BexGasWork.charge(frame, BexGasCounter.COLLECTION_ITEM_VISITED); + BexGasWork.charge(frame, BexGasCounter.LIST_ITEM_READ); frame.runtime().metrics().incrementLoopIterations(); - frame.runtime().gas().charge(frame.runtime().gas().schedule().forEachItem); frame.set(itemSlot, value.get(String.valueOf(i))); if (indexSlot >= 0) { frame.set(indexSlot, BexValues.scalar(BigInteger.valueOf(i))); @@ -197,7 +201,7 @@ private BexStatementEffects() { } static void appendChange(CompiledFrame frame, BexPatchEntry entry) { - frame.runtime().gas().chargeValue(frame.runtime().gas().schedule().appendChangeBase, entry.val()); + BexGasWork.charge(frame, BexGasCounter.PATCH_APPENDED); frame.accumulator().appendChange(entry); } @@ -205,7 +209,7 @@ static void appendEvent(CompiledFrame frame, BexValue value) { if (value.isUndefined()) { throw new BexException("Undefined cannot be emitted as an event"); } - frame.runtime().gas().chargeValue(frame.runtime().gas().schedule().appendEventBase, value); + BexGasWork.charge(frame, BexGasCounter.EVENT_APPENDED); frame.accumulator().appendEvent(value); } } @@ -225,6 +229,7 @@ final class AppendChangeStatement extends Stmt { protected Control doExec(CompiledFrame frame) { String operation = op.get(frame); boolean requiresVal = BexPatchEntryParser.requiresValue(operation); + String authoredPath = pointer.authored(frame); BexValue value = BexValues.undefined(); if (requiresVal) { if (val == null) { @@ -233,7 +238,7 @@ protected Control doExec(CompiledFrame frame) { value = val.eval(frame); } BexStatementEffects.appendChange(frame, - BexPatchEntryParser.fromFields(frame, operation, pointer.authored(frame), val != null, value)); + BexPatchEntryParser.fromFields(frame, operation, authoredPath, val != null, value)); return Control.CONTINUE; } } @@ -250,6 +255,8 @@ protected Control doExec(CompiledFrame frame) { BexValue list = expr.eval(frame); if (!list.isList()) throw new BexException("$appendChanges requires a list"); for (int i = 0; i < list.size(); i++) { + BexGasWork.charge(frame, BexGasCounter.COLLECTION_ITEM_VISITED); + BexGasWork.charge(frame, BexGasCounter.LIST_ITEM_READ); BexStatementEffects.appendChange(frame, BexPatchEntryParser.fromValue(frame, list.get(String.valueOf(i)))); } @@ -283,6 +290,8 @@ protected Control doExec(CompiledFrame frame) { BexValue list = expr.eval(frame); if (!list.isList()) throw new BexException("$appendEvents requires a list"); for (int i = 0; i < list.size(); i++) { + BexGasWork.charge(frame, BexGasCounter.COLLECTION_ITEM_VISITED); + BexGasWork.charge(frame, BexGasCounter.LIST_ITEM_READ); BexStatementEffects.appendEvent(frame, list.get(String.valueOf(i))); } return Control.CONTINUE; diff --git a/src/main/java/blue/bex/compile/CollectionExpressions.java b/src/main/java/blue/bex/compile/CollectionExpressions.java index e9aa1d2..756f819 100644 --- a/src/main/java/blue/bex/compile/CollectionExpressions.java +++ b/src/main/java/blue/bex/compile/CollectionExpressions.java @@ -1,9 +1,11 @@ package blue.bex.compile; import blue.bex.BexException; +import blue.bex.gas.BexGasCounter; import blue.bex.runtime.CompiledExpression; import blue.bex.runtime.CompiledFrame; import blue.bex.value.BexValue; +import blue.bex.value.BexUnicodeOrder; import blue.bex.value.BexValues; import java.math.BigInteger; @@ -85,22 +87,25 @@ protected BexValue doEval(CompiledFrame frame) { private BexValue evalList(CompiledFrame frame, BexValue list) { List out = needsListOutput() ? new ArrayList() : null; for (int i = 0; i < list.size(); i++) { + BexGasWork.charge(frame, BexGasCounter.COLLECTION_ITEM_VISITED); + BexGasWork.charge(frame, BexGasCounter.LIST_ITEM_READ); frame.runtime().metrics().incrementLoopIterations(); - frame.runtime().gas().charge(frame.runtime().gas().schedule().forEachItem); BexValue item = list.get(String.valueOf(i)); bind(frame, item, BexValues.undefined(), BexValues.scalar(BigInteger.valueOf(i))); BexValue result = body.eval(frame); switch (op) { case MAP: + producedListItem(frame); out.add(result); break; case FILTER: if (BexValues.truthy(result)) { + producedListItem(frame); out.add(item); } break; case FLAT_MAP: - appendList(out, result); + appendList(frame, out, result); break; case SOME: if (BexValues.truthy(result)) { @@ -115,6 +120,8 @@ private BexValue evalList(CompiledFrame frame, BexValue list) { case FIND_ENTRY: if (BexValues.truthy(result)) { Map entry = new LinkedHashMap<>(); + BexGasWork.charge(frame, BexGasCounter.COLLECTION_ITEM_PRODUCED); + BexGasWork.charge(frame, BexGasCounter.TRANSIENT_OBJECT_MEMBER_PRODUCED, 2L); entry.put("val", item); entry.put("index", BexValues.scalar(BigInteger.valueOf(i))); return BexValues.map(entry); @@ -133,22 +140,26 @@ private BexValue evalObject(CompiledFrame frame, BexValue object) { List keys = object.keys(); for (int i = 0; i < keys.size(); i++) { String key = keys.get(i); + BexGasWork.charge(frame, BexGasCounter.COLLECTION_ITEM_VISITED); + BexGasWork.charge(frame, BexGasCounter.OBJECT_MEMBER_READ); frame.runtime().metrics().incrementLoopIterations(); - frame.runtime().gas().charge(frame.runtime().gas().schedule().forEachItem); BexValue item = object.get(key); bind(frame, item, BexValues.scalar(key), BexValues.scalar(BigInteger.valueOf(i))); BexValue result = body.eval(frame); switch (op) { case MAP: + producedListItem(frame); listOut.add(result); break; case FILTER: if (BexValues.truthy(result)) { + BexGasWork.charge(frame, BexGasCounter.COLLECTION_ITEM_PRODUCED); + BexGasWork.charge(frame, BexGasCounter.TRANSIENT_OBJECT_MEMBER_PRODUCED); objectOut.put(key, item); } break; case FLAT_MAP: - appendList(listOut, result); + appendList(frame, listOut, result); break; case SOME: if (BexValues.truthy(result)) { @@ -163,6 +174,8 @@ private BexValue evalObject(CompiledFrame frame, BexValue object) { case FIND_ENTRY: if (BexValues.truthy(result)) { Map entry = new LinkedHashMap<>(); + BexGasWork.charge(frame, BexGasCounter.COLLECTION_ITEM_PRODUCED); + BexGasWork.charge(frame, BexGasCounter.TRANSIENT_OBJECT_MEMBER_PRODUCED, 3L); entry.put("key", BexValues.scalar(key)); entry.put("val", item); entry.put("index", BexValues.scalar(BigInteger.valueOf(i))); @@ -209,15 +222,22 @@ private BexValue finishNoMatch(List out) { } } - private void appendList(List out, BexValue value) { + private void appendList(CompiledFrame frame, List out, BexValue value) { if (!value.isList()) { throw new BexException("$flatMap expr must return a list"); } for (int i = 0; i < value.size(); i++) { + BexGasWork.charge(frame, BexGasCounter.LIST_ITEM_READ); + producedListItem(frame); out.add(value.get(String.valueOf(i))); } } + private void producedListItem(CompiledFrame frame) { + BexGasWork.charge(frame, BexGasCounter.COLLECTION_ITEM_PRODUCED); + BexGasWork.charge(frame, BexGasCounter.TRANSIENT_LIST_ITEM_PRODUCED); + } + private String collectionName() { switch (op) { case MAP: @@ -272,8 +292,9 @@ protected BexValue doEval(CompiledFrame frame) { frame.set(accSlot, acc); if (collection.isList()) { for (int i = 0; i < collection.size(); i++) { + BexGasWork.charge(frame, BexGasCounter.COLLECTION_ITEM_VISITED); + BexGasWork.charge(frame, BexGasCounter.LIST_ITEM_READ); frame.runtime().metrics().incrementLoopIterations(); - frame.runtime().gas().charge(frame.runtime().gas().schedule().forEachItem); bind(frame, collection.get(String.valueOf(i)), BexValues.undefined(), BexValues.scalar(BigInteger.valueOf(i))); acc = expr.eval(frame); @@ -285,8 +306,9 @@ protected BexValue doEval(CompiledFrame frame) { List keys = collection.keys(); for (int i = 0; i < keys.size(); i++) { String key = keys.get(i); + BexGasWork.charge(frame, BexGasCounter.COLLECTION_ITEM_VISITED); + BexGasWork.charge(frame, BexGasCounter.OBJECT_MEMBER_READ); frame.runtime().metrics().incrementLoopIterations(); - frame.runtime().gas().charge(frame.runtime().gas().schedule().forEachItem); bind(frame, collection.get(key), BexValues.scalar(key), BexValues.scalar(BigInteger.valueOf(i))); acc = expr.eval(frame); frame.set(accSlot, acc); @@ -313,33 +335,43 @@ private void bind(CompiledFrame frame, BexValue item, BexValue key, BexValue ind final class SlotSnapshot { private final int[] slots; private final BexValue[] values; + private final boolean[] initialized; - private SlotSnapshot(int[] slots, BexValue[] values) { + private SlotSnapshot(int[] slots, BexValue[] values, boolean[] initialized) { this.slots = slots; this.values = values; + this.initialized = initialized; } static SlotSnapshot capture(CompiledFrame frame, int... candidates) { List slotList = new ArrayList<>(); List valueList = new ArrayList<>(); + List initializedList = new ArrayList<>(); for (int slot : candidates) { if (slot >= 0 && !slotList.contains(slot)) { slotList.add(slot); + initializedList.add(frame.isInitialized(slot)); valueList.add(frame.get(slot)); } } int[] slots = new int[slotList.size()]; BexValue[] values = new BexValue[valueList.size()]; + boolean[] initialized = new boolean[initializedList.size()]; for (int i = 0; i < slotList.size(); i++) { slots[i] = slotList.get(i); values[i] = valueList.get(i); + initialized[i] = initializedList.get(i); } - return new SlotSnapshot(slots, values); + return new SlotSnapshot(slots, values, initialized); } void restore(CompiledFrame frame) { for (int i = 0; i < slots.length; i++) { - frame.set(slots[i], values[i]); + if (initialized[i]) { + frame.set(slots[i], values[i]); + } else { + frame.clear(slots[i]); + } } } } @@ -361,9 +393,11 @@ protected BexValue doEval(CompiledFrame frame) { } BexValue val = valExpr.eval(frame); for (int i = 0; i < list.size(); i++) { + BexGasWork.charge(frame, BexGasCounter.COLLECTION_ITEM_VISITED); + BexGasWork.charge(frame, BexGasCounter.LIST_ITEM_READ); frame.runtime().metrics().incrementLoopIterations(); - frame.runtime().gas().charge(frame.runtime().gas().schedule().forEachItem); - if (BexValues.equal(list.get(String.valueOf(i)), val)) { + if (MeteredEquality.equal( + frame, list.get(String.valueOf(i)), val)) { return BexValues.scalar(true); } } @@ -386,7 +420,9 @@ protected BexValue doEval(CompiledFrame frame) { if (!object.isObject()) { return BexValues.scalar(false); } - return BexValues.scalar(!object.get(key.get(frame)).isUndefined()); + String evaluatedKey = key.get(frame); + BexGasWork.charge(frame, BexGasCounter.OBJECT_MEMBER_READ); + return BexValues.scalar(!object.get(evaluatedKey).isUndefined()); } } @@ -403,25 +439,48 @@ protected BexValue doEval(CompiledFrame frame) { if (!entries.isList()) { throw new BexException("$objectFromEntries input must be a list"); } - Map out = new LinkedHashMap<>(); + Map retained = new LinkedHashMap<>(); for (int i = 0; i < entries.size(); i++) { + BexGasWork.charge(frame, BexGasCounter.COLLECTION_ITEM_VISITED); + BexGasWork.charge(frame, BexGasCounter.LIST_ITEM_READ); frame.runtime().metrics().incrementLoopIterations(); - frame.runtime().gas().charge(frame.runtime().gas().schedule().forEachItem); BexValue entry = entries.get(String.valueOf(i)); if (!entry.isObject()) { throw new BexException("$objectFromEntries entries must be objects"); } + BexGasWork.charge(frame, BexGasCounter.OBJECT_MEMBER_READ); BexValue key = entry.get("key"); if (key.isUndefined() || key.isNull()) { throw new BexException("$objectFromEntries key cannot be null or undefined"); } + BexGasWork.MeteredText convertedKey = + BexGasWork.constructedText(frame, key); + String textKey = convertedKey.text(); + BexGasWork.charge(frame, BexGasCounter.OBJECT_MEMBER_READ); BexValue val = entry.get("val"); if (val.isUndefined()) { - out.remove(key.asText()); + retained.remove(textKey); } else { - out.put(key.asText(), val); + retained.put(textKey, val); } } + Map out = new LinkedHashMap<>(); + for (String key : BexUnicodeOrder.sortedCopy( + retained.keySet(), + (left, right) -> { + BexGasWork.charge( + frame, + BexGasCounter.SORT_COMPARISON); + BexGasWork.charge( + frame, + BexGasCounter.COMPARISON_NODE_VISITED); + return BexGasWork.compareText( + frame, left, right); + })) { + BexGasWork.charge(frame, BexGasCounter.COLLECTION_ITEM_PRODUCED); + BexGasWork.charge(frame, BexGasCounter.TRANSIENT_OBJECT_MEMBER_PRODUCED); + out.put(key, retained.get(key)); + } return BexValues.map(out); } } diff --git a/src/main/java/blue/bex/compile/LogicNumericExpressions.java b/src/main/java/blue/bex/compile/LogicNumericExpressions.java index 8bb0478..5ba97cc 100644 --- a/src/main/java/blue/bex/compile/LogicNumericExpressions.java +++ b/src/main/java/blue/bex/compile/LogicNumericExpressions.java @@ -1,6 +1,7 @@ package blue.bex.compile; import blue.bex.BexException; +import blue.bex.gas.BexGasCounter; import blue.bex.runtime.CompiledExpression; import blue.bex.runtime.CompiledFrame; import blue.bex.value.BexValue; @@ -11,6 +12,78 @@ enum CompareOp { EQ, NE, GT, GTE, LT, LTE } +final class MeteredEquality { + private MeteredEquality() { + } + + static boolean equal( + CompiledFrame frame, BexValue left, BexValue right) { + left = left != null ? left : BexValues.undefined(); + right = right != null ? right : BexValues.undefined(); + BexGasWork.charge( + frame, BexGasCounter.COMPARISON_NODE_VISITED); + + if (left.isUndefined() || right.isUndefined()) { + return left.isUndefined() && right.isUndefined(); + } + if (left.isNull() || right.isNull()) { + return left.isNull() && right.isNull(); + } + if (left.isExact() && right.isExact() + && left.exactBlueId().equals(right.exactBlueId())) { + return true; + } + + String leftKind = BexValues.kind(left); + String rightKind = BexValues.kind(right); + if (isNumeric(leftKind) && isNumeric(rightKind)) { + return BexGasWork.compareNumbers( + frame, left, right, true) == 0; + } + if (!leftKind.equals(rightKind)) { + return false; + } + if ("text".equals(leftKind)) { + return BexGasWork.equalText( + frame, left, right); + } + if ("boolean".equals(leftKind)) { + return left.asBoolean() == right.asBoolean(); + } + if ("list".equals(leftKind)) { + if (left.size() != right.size()) { + return false; + } + for (int index = 0; index < left.size(); index++) { + if (!equal(frame, + left.get(String.valueOf(index)), + right.get(String.valueOf(index)))) { + return false; + } + } + return true; + } + if ("object".equals(leftKind)) { + List leftKeys = left.keys(); + List rightKeys = right.keys(); + if (!leftKeys.equals(rightKeys)) { + return false; + } + for (String key : leftKeys) { + if (!equal(frame, left.get(key), right.get(key))) { + return false; + } + } + return true; + } + return false; + } + + private static boolean isNumeric(String kind) { + return "integer".equals(kind) || "double".equals(kind); + } +} + final class CompareExpr extends Expr { private final List expressions; private final CompareOp op; @@ -27,10 +100,13 @@ protected BexValue doEval(CompiledFrame frame) { BexValue b = expressions.get(1).eval(frame); boolean result; if (op == CompareOp.EQ || op == CompareOp.NE) { - result = BexValues.equal(a, b); + result = MeteredEquality.equal(frame, a, b); if (op == CompareOp.NE) result = !result; } else { - int compare = a.asNumber().compareTo(b.asNumber()); + BexGasWork.charge( + frame, BexGasCounter.COMPARISON_NODE_VISITED); + int compare = BexGasWork.compareNumbers( + frame, a, b, true); result = op == CompareOp.GT ? compare > 0 : op == CompareOp.GTE ? compare >= 0 : op == CompareOp.LT ? compare < 0 : compare <= 0; } return BexValues.scalar(result); @@ -101,22 +177,34 @@ final class NumericExpr extends Expr { @Override protected BexValue doEval(CompiledFrame frame) { if (expressions.isEmpty()) throw new BexException("Numeric operator needs operands"); - BigInteger result = expressions.get(0).eval(frame).asInteger(); + BigInteger result = BexGasWork.integerOperand( + frame, expressions.get(0).eval(frame)); if (op == NumericOp.ADD && expressions.size() == 1) return BexValues.scalar(result); for (int i = 1; i < expressions.size(); i++) { - BigInteger next = expressions.get(i).eval(frame).asInteger(); + BigInteger next = BexGasWork.integerOperand( + frame, expressions.get(i).eval(frame)); + long leftLimbs = BexGasWork.integerLimbs(result); + long rightLimbs = BexGasWork.integerLimbs(next); switch (op) { case ADD: + BexGasWork.charge(frame, BexGasCounter.INTEGER_LIMB_OPERATION, + Math.max(leftLimbs, rightLimbs) + 1L); result = result.add(next); break; case SUBTRACT: + BexGasWork.charge(frame, BexGasCounter.INTEGER_LIMB_OPERATION, + Math.max(leftLimbs, rightLimbs) + 1L); result = result.subtract(next); break; case MULTIPLY: + BexGasWork.charge(frame, BexGasCounter.INTEGER_LIMB_OPERATION, + leftLimbs * rightLimbs); result = result.multiply(next); break; case DIVIDE: if (BigInteger.ZERO.equals(next)) throw new BexException("Division by zero"); + BexGasWork.charge(frame, BexGasCounter.INTEGER_LIMB_OPERATION, + leftLimbs * rightLimbs); BigInteger[] div = result.divideAndRemainder(next); if (!BigInteger.ZERO.equals(div[1])) throw new BexException("Non-exact integer division"); result = div[0]; diff --git a/src/main/java/blue/bex/compile/ObjectResultExpressions.java b/src/main/java/blue/bex/compile/ObjectResultExpressions.java index c92dcc6..1e443e0 100644 --- a/src/main/java/blue/bex/compile/ObjectResultExpressions.java +++ b/src/main/java/blue/bex/compile/ObjectResultExpressions.java @@ -1,11 +1,14 @@ package blue.bex.compile; import blue.bex.BexException; +import blue.bex.gas.BexGasCounter; import blue.bex.runtime.CompiledExpression; import blue.bex.runtime.CompiledFrame; import blue.bex.value.BexValue; import blue.bex.value.BexValues; +import java.math.BigInteger; +import java.util.Collections; import java.util.List; final class ListGetExpr extends Expr { @@ -25,6 +28,7 @@ protected BexValue doEval(CompiledFrame frame) { if (!l.isList()) throw new BexException("$listGet list must be list"); int i = index.eval(frame).asInteger().intValueExact(); if (i < 0) throw new BexException("$listGet index must be non-negative"); + BexGasWork.charge(frame, BexGasCounter.LIST_ITEM_READ); BexValue value = l.get(String.valueOf(i)); return value.isUndefined() && defaultValue != null ? defaultValue.eval(frame) : value; } @@ -43,9 +47,16 @@ final class ObjectSetExpr extends Expr { @Override protected BexValue doEval(CompiledFrame frame) { + BexValue base = object.eval(frame); + String evaluatedKey = key.get(frame); BexValue val = value.eval(frame); - frame.runtime().gas().chargeValue(frame.runtime().gas().schedule().objectSetBase, val); - return BexValues.overlay(object.eval(frame), key.get(frame), val); + if (!base.isUndefined() && !base.isNull() && !base.isObject()) { + throw new BexException("$objectSet.object must be an object, null, or undefined"); + } + if (!val.isUndefined()) { + BexGasWork.charge(frame, BexGasCounter.TRANSIENT_OBJECT_MEMBER_PRODUCED); + } + return BexValues.overlay(base, evaluatedKey, val); } } @@ -62,11 +73,28 @@ final class PointerGetExpr extends Expr { @Override protected BexValue doEval(CompiledFrame frame) { + BexValue base = object.eval(frame); List segments = pointer.segments(frame); - frame.runtime().gas().charge(frame.runtime().gas().schedule().pointerGetBase + segments.size()); - BexValue value = object.eval(frame).at(segments); + BexValue value = readAt(frame, base, segments); return value.isUndefined() && defaultValue != null ? defaultValue.eval(frame) : value; } + + private BexValue readAt(CompiledFrame frame, BexValue base, List segments) { + BexValue current = base; + for (String segment : segments) { + BexGasWork.charge(frame, BexGasCounter.POINTER_SEGMENT_READ); + if (current.isObject()) { + BexGasWork.charge(frame, BexGasCounter.OBJECT_MEMBER_READ); + } else if (current.isList()) { + BexGasWork.charge(frame, BexGasCounter.LIST_ITEM_READ); + } + current = current.get(segment); + if (current.isUndefined()) { + return current; + } + } + return current; + } } final class PointerSetExpr extends Expr { @@ -84,30 +112,89 @@ final class PointerSetExpr extends Expr { @Override protected BexValue doEval(CompiledFrame frame) { + BexValue base = object.eval(frame); List segments = pointer.segments(frame); String operation = op.get(frame); if (!"set".equals(operation) && !"remove".equals(operation)) throw new BexException("Unsupported $pointerSet op: " + operation); + if ("set".equals(operation) && value == null) { + throw new BexException("$pointerSet val is required for set"); + } BexValue val = "remove".equals(operation) ? BexValues.undefined() : value.eval(frame); - BexValue base = object.eval(frame); - validatePointerSetBase(base, segments); - frame.runtime().gas().chargePointerValue(frame.runtime().gas().schedule().pointerSetBase, segments.size(), val); + chargeAndValidatePointerSet(frame, base, segments, val, operation); return BexValues.pointerSet(base, segments, val, operation); } - private void validatePointerSetBase(BexValue base, List segments) { + private void chargeAndValidatePointerSet(CompiledFrame frame, + BexValue base, + List segments, + BexValue val, + String operation) { BexValue current = base; - for (int i = 0; i + 1 < segments.size(); i++) { + for (int index = 0; index < segments.size(); index++) { + BexGasWork.charge(frame, BexGasCounter.POINTER_SEGMENT_WRITTEN); if (current.isUndefined() || current.isNull()) { - return; + current = BexValues.map( + Collections.emptyMap()); } if (!current.isObject() && !current.isList()) { throw new BexException("$pointerSet encountered incompatible intermediate scalar"); } - current = current.get(segments.get(i)); + + String segment = segments.get(index); + boolean terminal = index == segments.size() - 1; + boolean omittedLeaf = terminal + && ("remove".equals(operation) || val.isUndefined()); + if (current.isList()) { + int listIndex = + requireExistingListIndex(segment, current.size()); + if (terminal + && "set".equals(operation) + && val.isUndefined()) { + throw new BexException( + "$pointerSet cannot set a list item to undefined"); + } + if (!omittedLeaf) { + BexGasWork.charge( + frame, + BexGasCounter.TRANSIENT_LIST_ITEM_PRODUCED); + } + if (!terminal) { + current = current.get(String.valueOf(listIndex)); + } + } else { + if (!omittedLeaf) { + BexGasWork.charge( + frame, + BexGasCounter.TRANSIENT_OBJECT_MEMBER_PRODUCED); + } + if (!terminal) { + current = current.get(segment); + } + } + } + } + + private int requireExistingListIndex(String segment, int size) { + if (segment == null || segment.isEmpty()) { + throw new BexException( + "$pointerSet list segment must be a non-negative integer: " + + segment); + } + for (int index = 0; index < segment.length(); index++) { + char ch = segment.charAt(index); + if (ch < '0' || ch > '9') { + throw new BexException( + "$pointerSet list segment must be a non-negative integer: " + + segment); + } } - if (!current.isUndefined() && !current.isNull() && !current.isObject() && !current.isList() && segments.size() > 1) { - throw new BexException("$pointerSet encountered incompatible intermediate scalar"); + BigInteger parsed = new BigInteger(segment); + if (parsed.compareTo(BigInteger.valueOf(Integer.MAX_VALUE)) > 0 + || parsed.intValue() >= size) { + throw new BexException( + "$pointerSet list index is out of range: " + segment); } + return parsed.intValue(); } } @@ -151,6 +238,7 @@ final class ResultValueExpr extends Expr { @Override protected BexValue doEval(CompiledFrame frame) { - return frame.runtime().readResultValue(pointer.absolute(frame), pointer.segments(frame)); + ResolvedPointer resolved = pointer.resolve(frame); + return frame.runtime().readResultValue(resolved.absolute(), resolved.segments()); } } diff --git a/src/main/java/blue/bex/compile/TypeStringExpressions.java b/src/main/java/blue/bex/compile/TypeStringExpressions.java index 8b0b302..f6aa8bf 100644 --- a/src/main/java/blue/bex/compile/TypeStringExpressions.java +++ b/src/main/java/blue/bex/compile/TypeStringExpressions.java @@ -1,9 +1,11 @@ package blue.bex.compile; import blue.bex.BexException; +import blue.bex.gas.BexGasCounter; import blue.bex.runtime.CompiledExpression; import blue.bex.runtime.CompiledFrame; import blue.bex.value.BexValue; +import blue.bex.value.BexUnicodeOrder; import blue.bex.value.BexValues; import blue.language.snapshot.FrozenNode; @@ -13,7 +15,6 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.regex.Pattern; enum UnaryOp { UNWRAP, TEXT, INTEGER, NUMBER, BOOLEAN, OBJECT, LIST, TRUTHY, EMPTY, EXISTS, KEYS, ENTRIES, SIZE } @@ -31,16 +32,25 @@ protected BexValue doEval(CompiledFrame frame) { BexValue value = expression.eval(frame); switch (op) { case UNWRAP: - while (value.isObject() && !value.get("value").isUndefined()) { - value = value.get("value"); + while (value.isObject()) { + BexGasWork.charge(frame, BexGasCounter.OBJECT_MEMBER_READ); + BexValue next = value.get("value"); + if (next.isUndefined()) { + break; + } + value = next; } return value; case TEXT: - return BexValues.scalar(value.asText()); + BexGasWork.MeteredText text = + BexGasWork.constructedText(frame, value); + return BexValues.scalar(text.text()); case INTEGER: - return BexValues.scalar(value.asInteger()); + return BexValues.scalar( + BexGasWork.convertInteger(frame, value)); case NUMBER: - return BexValues.scalar(value.asNumber()); + return BexValues.scalar( + BexGasWork.convertNumber(frame, value)); case BOOLEAN: return BexValues.scalar(value.asBoolean()); case OBJECT: @@ -59,14 +69,26 @@ protected BexValue doEval(CompiledFrame frame) { return BexValues.scalar(!value.isUndefined()); case KEYS: List keys = new ArrayList<>(); - for (String key : value.keys()) keys.add(BexValues.scalar(key)); + for (String key : value.keys()) { + BexGasWork.charge(frame, BexGasCounter.COLLECTION_ITEM_VISITED); + BexGasWork.charge(frame, BexGasCounter.COLLECTION_ITEM_PRODUCED); + BexGasWork.charge(frame, BexGasCounter.TRANSIENT_LIST_ITEM_PRODUCED); + BexGasWork.chargeTextConstruction( + frame, key); + keys.add(BexValues.scalar(key)); + } return BexValues.list(keys); case ENTRIES: List entries = new ArrayList<>(); for (String key : value.keys()) { + BexGasWork.charge(frame, BexGasCounter.COLLECTION_ITEM_VISITED); + BexGasWork.charge(frame, BexGasCounter.OBJECT_MEMBER_READ); Map entry = new LinkedHashMap<>(); + BexGasWork.charge(frame, BexGasCounter.TRANSIENT_OBJECT_MEMBER_PRODUCED, 2L); entry.put("key", BexValues.scalar(key)); entry.put("val", value.get(key)); + BexGasWork.charge(frame, BexGasCounter.COLLECTION_ITEM_PRODUCED); + BexGasWork.charge(frame, BexGasCounter.TRANSIENT_LIST_ITEM_PRODUCED); entries.add(BexValues.map(entry)); } return BexValues.list(entries); @@ -90,7 +112,11 @@ final class IsExpr extends Expr { @Override protected BexValue doEval(CompiledFrame frame) { BexValue value = valueExpression.eval(frame); - return BexValues.scalar(frame.runtime().typeMatcher().matches(value, pattern)); + return BexValues.scalar(frame.runtime().typeMatcher().matches( + value, + pattern, + frame.runtime().gas(), + frame.sourcePath())); } } @@ -108,24 +134,79 @@ final class VariadicExpr extends Expr { @Override protected BexValue doEval(CompiledFrame frame) { if (op == VariadicOp.CONCAT) { + List items = + new ArrayList<>(expressions.size()); + TextConstructionShape resultShape = + new TextConstructionShape(); + for (CompiledExpression expression : expressions) { + BexGasWork.MeteredText item = + BexGasWork.fullText( + frame, + expression.eval(frame)); + resultShape.append(item); + items.add(item); + } + BexGasWork.charge(frame, BexGasCounter.TEXT_BLOCK_CONSTRUCTED, + BexGasWork.textBlocksForCodePoints( + resultShape.codePoints())); + + /* + * The aggregate construction charge is now admitted before any + * result buffer allocation, append, or toString work. + */ StringBuilder builder = new StringBuilder(); - for (CompiledExpression expression : expressions) builder.append(expression.eval(frame).asText()); - return BexValues.scalar(builder.toString()); + for (BexGasWork.MeteredText item : items) { + builder.append(item.text()); + } + String result = builder.toString(); + return BexValues.scalar(result); } if (op == VariadicOp.LIST_CONCAT) { List out = new ArrayList<>(); for (CompiledExpression expression : expressions) { BexValue list = expression.eval(frame); if (!list.isList()) throw new BexException("$listConcat operand must be list"); - for (int i = 0; i < list.size(); i++) out.add(list.get(String.valueOf(i))); + for (int i = 0; i < list.size(); i++) { + BexGasWork.charge(frame, BexGasCounter.COLLECTION_ITEM_VISITED); + BexGasWork.charge(frame, BexGasCounter.LIST_ITEM_READ); + BexGasWork.charge(frame, BexGasCounter.COLLECTION_ITEM_PRODUCED); + BexGasWork.charge(frame, BexGasCounter.TRANSIENT_LIST_ITEM_PRODUCED); + out.add(list.get(String.valueOf(i))); + } } return BexValues.list(out); } - Map out = new LinkedHashMap<>(); + Map retained = new LinkedHashMap<>(); for (CompiledExpression expression : expressions) { BexValue object = expression.eval(frame); if (!object.isObject()) throw new BexException("$merge operand must be object"); - for (String key : object.keys()) out.put(key, object.get(key)); + for (String key : object.keys()) { + BexGasWork.charge(frame, BexGasCounter.COLLECTION_ITEM_VISITED); + BexGasWork.charge(frame, BexGasCounter.OBJECT_MEMBER_READ); + retained.put(key, object.get(key)); + } + } + Map out = new LinkedHashMap<>(); + for (String key : BexUnicodeOrder.sortedCopy( + retained.keySet(), + new BexUnicodeOrder.Comparison() { + @Override + public int compare( + String left, + String right) { + BexGasWork.charge( + frame, + BexGasCounter.SORT_COMPARISON); + BexGasWork.charge( + frame, + BexGasCounter.COMPARISON_NODE_VISITED); + return BexGasWork.compareText( + frame, left, right); + } + })) { + BexGasWork.charge(frame, BexGasCounter.COLLECTION_ITEM_PRODUCED); + BexGasWork.charge(frame, BexGasCounter.TRANSIENT_OBJECT_MEMBER_PRODUCED); + out.put(key, retained.get(key)); } return BexValues.map(out); } @@ -144,13 +225,38 @@ final class JoinExpr extends Expr { protected BexValue doEval(CompiledFrame frame) { BexValue list = items.eval(frame); if (!list.isList()) throw new BexException("$join list must be a list"); - String sep = separator.eval(frame).asText(); - StringBuilder out = new StringBuilder(); + BexGasWork.MeteredText sep = BexGasWork.fullText( + frame, separator.eval(frame)); + List values = + new ArrayList<>(); + TextConstructionShape resultShape = + new TextConstructionShape(); for (int i = 0; i < list.size(); i++) { - if (i > 0) out.append(sep); - out.append(list.get(String.valueOf(i)).asText()); + BexGasWork.charge(frame, BexGasCounter.COLLECTION_ITEM_VISITED); + BexGasWork.charge(frame, BexGasCounter.LIST_ITEM_READ); + if (i > 0) { + resultShape.append(sep); + } + BexGasWork.MeteredText item = + BexGasWork.fullText( + frame, + list.get(String.valueOf(i))); + resultShape.append(item); + values.add(item); } - return BexValues.scalar(out.toString()); + BexGasWork.charge(frame, BexGasCounter.TEXT_BLOCK_CONSTRUCTED, + BexGasWork.textBlocksForCodePoints( + resultShape.codePoints())); + + StringBuilder out = new StringBuilder(); + for (int i = 0; i < values.size(); i++) { + if (i > 0) { + out.append(sep.text()); + } + out.append(values.get(i).text()); + } + String result = out.toString(); + return BexValues.scalar(result); } } @@ -164,22 +270,52 @@ final class PointerJoinExpr extends Expr { @Override protected BexValue doEval(CompiledFrame frame) { if (segments.isEmpty()) { + BexGasWork.charge(frame, BexGasCounter.TEXT_BLOCK_CONSTRUCTED, 1L); return BexValues.scalar("/"); } - StringBuilder out = new StringBuilder(); + List items = + new ArrayList<>(segments.size()); + long resultCodePoints = segments.size(); for (CompiledExpression expression : segments) { BexValue value = expression.eval(frame); if (value.isUndefined() || value.isNull()) { throw new BexException("$pointerJoin segment cannot be null or undefined"); } + BexGasWork.MeteredText segment = + BexGasWork.fullText(frame, value); + resultCodePoints = Math.addExact( + resultCodePoints, + Math.addExact( + segment.codePoints(), + segment.pointerEscapeExpansions())); + items.add(segment); + } + BexGasWork.charge(frame, BexGasCounter.TEXT_BLOCK_CONSTRUCTED, + BexGasWork.textBlocksForCodePoints( + resultCodePoints)); + + StringBuilder out = new StringBuilder(); + for (BexGasWork.MeteredText segment : items) { out.append('/'); - out.append(escapeSegment(value.asText())); + appendEscaped(out, segment.text()); } - return BexValues.scalar(out.toString()); + String result = out.toString(); + return BexValues.scalar(result); } - private String escapeSegment(String segment) { - return segment.replace("~", "~0").replace("/", "~1"); + private void appendEscaped( + StringBuilder destination, + String segment) { + for (int index = 0; index < segment.length(); index++) { + char character = segment.charAt(index); + if (character == '~') { + destination.append("~0"); + } else if (character == '/') { + destination.append("~1"); + } else { + destination.append(character); + } + } } } @@ -196,16 +332,47 @@ final class SplitExpr extends Expr { @Override protected BexValue doEval(CompiledFrame frame) { - String input = text.eval(frame).asText(); - String sep = separator.eval(frame).asText(); + String input = BexGasWork.fullText( + frame, text.eval(frame)).text(); + String sep = BexGasWork.fullText( + frame, separator.eval(frame)).text(); if (sep.isEmpty()) throw new BexException("$split separator must not be empty"); int max = limit != null ? limit.eval(frame).asInteger().intValueExact() : -1; if (max == 0 || max < -1) throw new BexException("$split limit must be positive"); - String[] parts = input.split(Pattern.quote(sep), max == -1 ? -1 : max); + List out = new ArrayList<>(); - for (String part : parts) out.add(BexValues.scalar(part)); + int start = 0; + int produced = 0; + while (max == -1 || produced < max - 1) { + int match = input.indexOf(sep, start); + if (match < 0) { + break; + } + addPart(frame, out, input, start, match); + produced++; + start = match + sep.length(); + } + addPart(frame, out, input, start, input.length()); return BexValues.list(out); } + + private void addPart( + CompiledFrame frame, + List out, + String input, + int start, + int end) { + BexGasWork.chargeSubstringConstruction( + frame, input, start, end); + BexGasWork.charge( + frame, + BexGasCounter.COLLECTION_ITEM_PRODUCED); + BexGasWork.charge( + frame, + BexGasCounter.TRANSIENT_LIST_ITEM_PRODUCED); + String part = input.substring(start, end); + out.add(BexValues.scalar(part)); + } } enum BinaryTextOp { STARTS_WITH, SLICE_AFTER } @@ -222,9 +389,42 @@ final class BinaryTextExpr extends Expr { @Override protected BexValue doEval(CompiledFrame frame) { if (expressions.size() != 2) throw new BexException("Text operator expects two operands"); - String text = expressions.get(0).eval(frame).asText(); - String prefix = expressions.get(1).eval(frame).asText(); - if (op == BinaryTextOp.STARTS_WITH) return BexValues.scalar(text.startsWith(prefix)); - return BexValues.scalar(text.startsWith(prefix) ? text.substring(prefix.length()) : ""); + BexGasWork.PrefixResult result = + BexGasWork.comparePrefix( + frame, + expressions.get(0).eval(frame), + expressions.get(1).eval(frame)); + if (op == BinaryTextOp.STARTS_WITH) { + return BexValues.scalar(result.matched()); + } + return BexValues.scalar( + result.constructSuffix(frame)); + } +} + +final class TextConstructionShape { + private long codePoints; + private boolean endsWithHighSurrogate; + + void append(BexGasWork.MeteredText value) { + String text = value.text(); + if (endsWithHighSurrogate + && !text.isEmpty() + && Character.isLowSurrogate( + text.charAt(0))) { + codePoints--; + } + codePoints = Math.addExact( + codePoints, value.codePoints()); + if (!text.isEmpty()) { + endsWithHighSurrogate = + Character.isHighSurrogate( + text.charAt( + text.length() - 1)); + } + } + + long codePoints() { + return codePoints; } } diff --git a/src/main/java/blue/bex/gas/BexGasCharge.java b/src/main/java/blue/bex/gas/BexGasCharge.java new file mode 100644 index 0000000..d6136cc --- /dev/null +++ b/src/main/java/blue/bex/gas/BexGasCharge.java @@ -0,0 +1,293 @@ +package blue.bex.gas; + +import java.util.Objects; + +/** + * One immutable, admitted entry in the canonical BEX child-ledger trace. + * + *

The portable BEX counters use namespace {@code bex}. Registry-bound + * intrinsic child counters retain their own namespace and name without + * expanding the closed {@link BexGasCounter} enum.

+ */ +public final class BexGasCharge { + private final long sequence; + private final String namespace; + private final BexGasCounter counter; + private final String counterName; + private final long quantity; + private final long weight; + private final long gas; + private final String sourcePath; + private final String operator; + private final String reason; + + public BexGasCharge(long sequence, + BexGasCounter counter, + long quantity, + long weight, + String sourcePath, + String operator, + String reason) { + this(sequence, + BexGasCounter.NAMESPACE, + Objects.requireNonNull(counter, "counter"), + counter.canonicalName(), + quantity, + weight, + multiplyExact(quantity, weight), + sourcePath, + operator, + reason); + } + + /** + * Creates a portable trace entry while validating the supplied derived gas + * value. This overload is useful when reconstructing fixture traces. + */ + public BexGasCharge(long sequence, + BexGasCounter counter, + long quantity, + long weight, + long gas, + String sourcePath, + String operator, + String reason) { + this(sequence, + BexGasCounter.NAMESPACE, + Objects.requireNonNull(counter, "counter"), + counter.canonicalName(), + quantity, + weight, + gas, + sourcePath, + operator, + reason); + } + + public BexGasCharge(long sequence, + String namespace, + String counterName, + long quantity, + long weight, + String sourcePath, + String operator, + String reason) { + this(sequence, + namespace, + portableCounter(namespace, counterName), + counterName, + quantity, + weight, + multiplyExact(quantity, weight), + sourcePath, + operator, + reason); + } + + public BexGasCharge(long sequence, + String namespace, + String counterName, + long quantity, + long weight, + long gas, + String sourcePath, + String operator, + String reason) { + this(sequence, + namespace, + portableCounter(namespace, counterName), + counterName, + quantity, + weight, + gas, + sourcePath, + operator, + reason); + } + + private BexGasCharge(long sequence, + String namespace, + BexGasCounter counter, + String counterName, + long quantity, + long weight, + long gas, + String sourcePath, + String operator, + String reason) { + if (sequence < 0L) { + throw new IllegalArgumentException("Gas sequence must be non-negative"); + } + if (quantity < 0L) { + throw new IllegalArgumentException("Gas quantity must be non-negative"); + } + if (weight < 0L) { + throw new IllegalArgumentException("Gas weight must be non-negative"); + } + long expectedGas = multiplyExact(quantity, weight); + if (gas != expectedGas) { + throw new IllegalArgumentException( + "Gas must equal quantity * weight: expected " + + expectedGas + " but was " + gas); + } + this.sequence = sequence; + this.namespace = requireName(namespace, "Gas namespace"); + this.counter = counter; + this.counterName = requireName(counterName, "Gas counter"); + this.quantity = quantity; + this.weight = weight; + this.gas = gas; + this.sourcePath = emptyToNull(sourcePath); + this.operator = emptyToNull(operator); + this.reason = requireReason(reason); + } + + public long sequence() { + return sequence; + } + + public String namespace() { + return namespace; + } + + /** + * Returns the portable BEX counter, or {@code null} for a registry-bound + * named child counter. + */ + public BexGasCounter counter() { + return counter; + } + + /** Alias for {@link #counter()}. */ + public BexGasCounter portableCounter() { + return counter; + } + + public String counterName() { + return counterName; + } + + /** + * Returns the key used for this entry in the enclosing host child ledger. + */ + public String qualifiedCounterName() { + return BexGasMeter.qualifiedCounterName(namespace, counterName); + } + + public long quantity() { + return quantity; + } + + public long weight() { + return weight; + } + + public long gas() { + return gas; + } + + /** Returns the optional canonical source path, or {@code null}. */ + public String sourcePath() { + return sourcePath; + } + + /** Returns the optional canonical operator name, or {@code null}. */ + public String operator() { + return operator; + } + + public String reason() { + return reason; + } + + private static String emptyToNull(String value) { + return value == null || value.isEmpty() ? null : value; + } + + private static String requireReason(String value) { + if (value == null || value.trim().isEmpty()) { + throw new IllegalArgumentException("Gas charge reason is required"); + } + return value; + } + + private static String requireName(String value, String label) { + if (value == null || value.trim().isEmpty()) { + throw new IllegalArgumentException(label + " is required"); + } + return value; + } + + private static BexGasCounter portableCounter(String namespace, + String counterName) { + if (!BexGasCounter.NAMESPACE.equals(namespace) || counterName == null) { + return null; + } + try { + return BexGasCounter.fromCanonicalName(counterName); + } catch (IllegalArgumentException notPortable) { + return null; + } + } + + private static long multiplyExact(long left, long right) { + if (left < 0L || right < 0L) { + throw new IllegalArgumentException( + "Gas quantity and weight must be non-negative"); + } + if (left != 0L && right > Long.MAX_VALUE / left) { + throw new IllegalArgumentException("Gas subtotal exceeds long range"); + } + return left * right; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof BexGasCharge)) { + return false; + } + BexGasCharge that = (BexGasCharge) other; + return sequence == that.sequence + && quantity == that.quantity + && weight == that.weight + && gas == that.gas + && counter == that.counter + && namespace.equals(that.namespace) + && counterName.equals(that.counterName) + && Objects.equals(sourcePath, that.sourcePath) + && Objects.equals(operator, that.operator) + && reason.equals(that.reason); + } + + @Override + public int hashCode() { + return Objects.hash(sequence, + namespace, + counter, + counterName, + quantity, + weight, + gas, + sourcePath, + operator, + reason); + } + + @Override + public String toString() { + return "BexGasCharge{" + + "sequence=" + sequence + + ", namespace='" + namespace + '\'' + + ", counter='" + counterName + '\'' + + ", quantity=" + quantity + + ", weight=" + weight + + ", gas=" + gas + + ", sourcePath='" + sourcePath + '\'' + + ", operator='" + operator + '\'' + + ", reason='" + reason + '\'' + + '}'; + } +} diff --git a/src/main/java/blue/bex/gas/BexGasCounter.java b/src/main/java/blue/bex/gas/BexGasCounter.java new file mode 100644 index 0000000..22f0d57 --- /dev/null +++ b/src/main/java/blue/bex/gas/BexGasCounter.java @@ -0,0 +1,133 @@ +package blue.bex.gas; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * The closed Blue BEX 2.0 portable gas-counter vocabulary. + * + *

Declaration order is manifest order. Portable weights and identities are + * loaded from the exact bound manifest rather than duplicated here.

+ */ +public enum BexGasCounter { + EXPRESSION_EVALUATED("expressionEvaluated"), + STATEMENT_EXECUTED("statementExecuted"), + FUNCTION_CALLED("functionCalled"), + INTRINSIC_CALLED("intrinsicCalled"), + DOCUMENT_READ("documentRead"), + EVENT_READ("eventRead"), + PROCESSING_EVENT_READ("processingEventRead"), + CURRENT_CONTRACT_READ("currentContractRead"), + STEPS_READ("stepsRead"), + BINDING_READ("bindingRead"), + VARIABLE_READ("variableRead"), + CONSTANT_READ("constantRead"), + RESULT_VALUE_READ("resultValueRead"), + POINTER_SEGMENT_READ("pointerSegmentRead"), + POINTER_SEGMENT_WRITTEN("pointerSegmentWritten"), + OBJECT_MEMBER_READ("objectMemberRead"), + LIST_ITEM_READ("listItemRead"), + COLLECTION_ITEM_VISITED("collectionItemVisited"), + COLLECTION_ITEM_PRODUCED("collectionItemProduced"), + TEXT_BLOCK_EXAMINED("textBlockExamined"), + TEXT_BLOCK_CONSTRUCTED("textBlockConstructed"), + INTEGER_LIMB_OPERATION("integerLimbOperation"), + COMPARISON_NODE_VISITED("comparisonNodeVisited"), + SORT_COMPARISON("sortComparison"), + PATCH_APPENDED("patchAppended"), + EVENT_APPENDED("eventAppended"), + TRANSIENT_OBJECT_MEMBER_PRODUCED("transientObjectMemberProduced"), + TRANSIENT_LIST_ITEM_PRODUCED("transientListItemProduced"), + BLUE_OUTPUT_BOUNDARY("blueOutputBoundary"), + NODE_IDENTITY_REQUESTED("nodeIdentityRequested"); + + private static final BexGasManifest DEFAULT_MANIFEST = + BexGasManifest.loadDefault(); + + /** Namespace used when a BEX ledger is attached to the shared host meter. */ + public static final String NAMESPACE = "bex"; + + /** Canonical schedule identifier from the BEX 2.0 gas manifest. */ + public static final String SCHEDULE_ID = + DEFAULT_MANIFEST.scheduleId(); + + /** Exact implementation-baseline BEX 2.0 gas-manifest package identity. */ + public static final String MANIFEST_IDENTITY = + DEFAULT_MANIFEST.packageIdentity(); + + private static final Map BY_CANONICAL_NAME; + private static final Map DEFAULT_WEIGHTS; + + static { + LinkedHashMap byName = new LinkedHashMap<>(); + LinkedHashMap weights = new LinkedHashMap<>(); + for (BexGasCounter counter : values()) { + byName.put(counter.canonicalName, counter); + Long weight = DEFAULT_MANIFEST.counterWeights() + .get(counter.canonicalName); + if (weight == null) { + throw new ExceptionInInitializerError( + "BEX gas manifest is missing " + + counter.canonicalName); + } + weights.put(counter.canonicalName, weight); + } + if (weights.size() + != DEFAULT_MANIFEST.counterWeights().size()) { + throw new ExceptionInInitializerError( + "BEX gas manifest contains unknown counters"); + } + BY_CANONICAL_NAME = Collections.unmodifiableMap(byName); + DEFAULT_WEIGHTS = Collections.unmodifiableMap(weights); + } + + private final String canonicalName; + + BexGasCounter(String canonicalName) { + this.canonicalName = canonicalName; + } + + /** + * Returns the exact lower-camel-case counter name used in fixtures and + * host-ledger entries. + */ + public String canonicalName() { + return canonicalName; + } + + /** Alias for {@link #canonicalName()}. */ + public String counterName() { + return canonicalName; + } + + public long defaultWeight() { + return DEFAULT_WEIGHTS.get(canonicalName); + } + + public static BexGasCounter fromCanonicalName(String name) { + BexGasCounter counter = BY_CANONICAL_NAME.get(name); + if (counter == null) { + throw new IllegalArgumentException("Unknown BEX gas counter: " + name); + } + return counter; + } + + /** Alias for {@link #fromCanonicalName(String)}. */ + public static BexGasCounter fromName(String name) { + return fromCanonicalName(name); + } + + /** + * Returns an immutable, manifest-ordered map suitable for creating a + * parent-bounded runtime child ledger. + */ + public static Map defaultWeights() { + return DEFAULT_WEIGHTS; + } + + @Override + public String toString() { + return canonicalName; + } +} diff --git a/src/main/java/blue/bex/gas/BexGasLedger.java b/src/main/java/blue/bex/gas/BexGasLedger.java new file mode 100644 index 0000000..0d1b3d4 --- /dev/null +++ b/src/main/java/blue/bex/gas/BexGasLedger.java @@ -0,0 +1,155 @@ +package blue.bex.gas; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * Immutable snapshot of an ordered BEX child gas ledger. + */ +public final class BexGasLedger { + private static final BexGasLedger EMPTY = + new BexGasLedger(Collections.emptyList()); + + private final List trace; + private final Map quantities; + private final Map namedQuantities; + private final long totalGas; + private final String scheduleId; + private final String manifestIdentity; + + public BexGasLedger(List trace) { + this(trace, + BexGasCounter.SCHEDULE_ID, + BexGasCounter.MANIFEST_IDENTITY); + } + + BexGasLedger(List trace, + String scheduleId, + String manifestIdentity) { + Objects.requireNonNull(trace, "trace"); + ArrayList copy = new ArrayList<>(trace.size()); + EnumMap quantityTotals = + new EnumMap<>(BexGasCounter.class); + LinkedHashMap namedQuantityTotals = + new LinkedHashMap<>(); + long gasTotal = 0L; + for (int index = 0; index < trace.size(); index++) { + BexGasCharge charge = + Objects.requireNonNull(trace.get(index), "trace entry"); + if (charge.sequence() != index) { + throw new IllegalArgumentException( + "Gas trace sequence must be contiguous from zero: " + + charge.sequence() + " at index " + index); + } + gasTotal = addExact(gasTotal, charge.gas()); + if (charge.portableCounter() != null) { + Long prior = quantityTotals.get(charge.portableCounter()); + quantityTotals.put(charge.portableCounter(), + addExact(prior != null ? prior : 0L, charge.quantity())); + } + String qualifiedName = charge.qualifiedCounterName(); + Long namedPrior = namedQuantityTotals.get(qualifiedName); + namedQuantityTotals.put(qualifiedName, + addExact(namedPrior != null ? namedPrior : 0L, + charge.quantity())); + copy.add(charge); + } + this.trace = Collections.unmodifiableList(copy); + this.quantities = Collections.unmodifiableMap(quantityTotals); + this.namedQuantities = + Collections.unmodifiableMap(namedQuantityTotals); + this.totalGas = gasTotal; + this.scheduleId = Objects.requireNonNull( + scheduleId, "scheduleId"); + this.manifestIdentity = Objects.requireNonNull( + manifestIdentity, "manifestIdentity"); + } + + public static BexGasLedger empty() { + return EMPTY; + } + + /** + * Returns the exact immutable trace. The entries themselves are immutable. + */ + public List trace() { + return trace; + } + + public long totalGas() { + return totalGas; + } + + /** Compatibility alias for {@link #totalGas()}. */ + public long gasUsed() { + return totalGas; + } + + public long quantity(BexGasCounter counter) { + Long quantity = quantities.get(Objects.requireNonNull(counter, "counter")); + return quantity != null ? quantity : 0L; + } + + public Map quantities() { + return quantities; + } + + public long quantity(String namespace, String counterName) { + Long quantity = namedQuantities.get( + BexGasMeter.qualifiedCounterName(namespace, counterName)); + return quantity != null ? quantity : 0L; + } + + /** + * Returns immutable totals keyed by their enclosing host-ledger counter + * names. Portable BEX keys are unqualified; registered child keys are + * namespace-qualified. + */ + public Map namedQuantities() { + return namedQuantities; + } + + public String scheduleId() { + return scheduleId; + } + + public String manifestIdentity() { + return manifestIdentity; + } + + private static long addExact(long left, long right) { + if (right > Long.MAX_VALUE - left) { + throw new IllegalArgumentException("Gas total exceeds long range"); + } + return left + right; + } + + @Override + public boolean equals(Object other) { + return this == other + || other instanceof BexGasLedger + && trace.equals(((BexGasLedger) other).trace) + && scheduleId.equals( + ((BexGasLedger) other).scheduleId) + && manifestIdentity.equals( + ((BexGasLedger) other).manifestIdentity); + } + + @Override + public int hashCode() { + return Objects.hash(trace, scheduleId, manifestIdentity); + } + + @Override + public String toString() { + return "BexGasLedger{scheduleId=" + scheduleId + + ", manifestIdentity=" + manifestIdentity + + ", totalGas=" + totalGas + + ", trace=" + trace + '}'; + } +} diff --git a/src/main/java/blue/bex/gas/BexGasLimitExceededException.java b/src/main/java/blue/bex/gas/BexGasLimitExceededException.java new file mode 100644 index 0000000..6afc0e8 --- /dev/null +++ b/src/main/java/blue/bex/gas/BexGasLimitExceededException.java @@ -0,0 +1,146 @@ +package blue.bex.gas; + +import blue.bex.BexException; +import blue.language.processor.GasLimitExceededException; + +/** + * Raised before work when the next named BEX charge cannot be admitted. + */ +public final class BexGasLimitExceededException extends BexException { + private static final long serialVersionUID = 1L; + + private final String namespace; + private final BexGasCounter counter; + private final String counterName; + private final long quantity; + private final long weight; + private final long admittedGas; + private final long effectiveBudget; + private final GasLimitExceededException hostGasLimitExceeded; + + BexGasLimitExceededException(BexGasCounter counter, + long quantity, + long weight, + long admittedGas, + long effectiveBudget) { + this(BexGasCounter.NAMESPACE, + counter, + counter.canonicalName(), + quantity, + weight, + admittedGas, + effectiveBudget, + null); + } + + BexGasLimitExceededException(String namespace, + String counterName, + long quantity, + long weight, + long admittedGas, + long effectiveBudget) { + this(namespace, + null, + counterName, + quantity, + weight, + admittedGas, + effectiveBudget, + null); + } + + BexGasLimitExceededException(BexGasCounter counter, + long quantity, + long weight, + long admittedGas, + long effectiveBudget, + GasLimitExceededException + hostGasLimitExceeded) { + this(BexGasCounter.NAMESPACE, + counter, + counter.canonicalName(), + quantity, + weight, + admittedGas, + effectiveBudget, + hostGasLimitExceeded); + } + + BexGasLimitExceededException(String namespace, + String counterName, + long quantity, + long weight, + long admittedGas, + long effectiveBudget, + GasLimitExceededException + hostGasLimitExceeded) { + this(namespace, + null, + counterName, + quantity, + weight, + admittedGas, + effectiveBudget, + hostGasLimitExceeded); + } + + private BexGasLimitExceededException(String namespace, + BexGasCounter counter, + String counterName, + long quantity, + long weight, + long admittedGas, + long effectiveBudget, + GasLimitExceededException + hostGasLimitExceeded) { + super("BEX gas exhausted before " + namespace + "." + counterName + + " at " + admittedGas + " of " + effectiveBudget + + " gas units", + hostGasLimitExceeded); + this.namespace = namespace; + this.counter = counter; + this.counterName = counterName; + this.quantity = quantity; + this.weight = weight; + this.admittedGas = admittedGas; + this.effectiveBudget = effectiveBudget; + this.hostGasLimitExceeded = hostGasLimitExceeded; + } + + public BexGasCounter counter() { + return counter; + } + + public String namespace() { + return namespace; + } + + public String counterName() { + return counterName; + } + + public long quantity() { + return quantity; + } + + public long weight() { + return weight; + } + + public long admittedGas() { + return admittedGas; + } + + public long effectiveBudget() { + return effectiveBudget; + } + + /** + * Returns the exact host rejection which caused this BEX failure, or + * {@code null} when the stricter BEX-local sub-limit rejected the charge + * before the host ledger was touched. + */ + public GasLimitExceededException hostGasLimitExceeded() { + return hostGasLimitExceeded; + } +} diff --git a/src/main/java/blue/bex/gas/BexGasManifest.java b/src/main/java/blue/bex/gas/BexGasManifest.java new file mode 100644 index 0000000..12ae449 --- /dev/null +++ b/src/main/java/blue/bex/gas/BexGasManifest.java @@ -0,0 +1,215 @@ +package blue.bex.gas; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Strict loader for the source-controlled BEX 2.0 portable gas manifest. + */ +final class BexGasManifest { + static final String RESOURCE = + "/blue/bex/gas/blue-bex-gas-2.0.yaml"; + static final String RESOURCE_SHA256 = + "1f689e0cf51b0f9afa6b18a640e0c755470921a7b0d66f62bfc2206679de640d"; + static final String PACKAGE_IDENTITY = + "sha256:41247c820d91a12fdfc17fd9e787a5d8d668d8acc5954fdcb131715bf9e6147d"; + + private final String scheduleId; + private final String packageIdentity; + private final Map counterWeights; + + private BexGasManifest( + String scheduleId, + String packageIdentity, + Map counterWeights) { + this.scheduleId = scheduleId; + this.packageIdentity = packageIdentity; + this.counterWeights = Collections.unmodifiableMap( + new LinkedHashMap(counterWeights)); + } + + static BexGasManifest loadDefault() { + InputStream resource = + BexGasManifest.class.getResourceAsStream(RESOURCE); + if (resource == null) { + throw new ExceptionInInitializerError( + "Missing BEX gas manifest " + RESOURCE); + } + try { + byte[] bytes = readAll(resource); + String observedHash = sha256(bytes); + if (!RESOURCE_SHA256.equals(observedHash)) { + throw new IllegalStateException( + "BEX gas manifest resource identity mismatch: " + + observedHash); + } + return parse(bytes); + } catch (IOException exception) { + throw new ExceptionInInitializerError(exception); + } finally { + try { + resource.close(); + } catch (IOException ignored) { + // The initialization failure, if any, is already authoritative. + } + } + } + + private static BexGasManifest parse(byte[] bytes) + throws IOException { + BufferedReader reader = new BufferedReader( + new InputStreamReader( + new ByteArrayInputStream(bytes), + StandardCharsets.UTF_8)); + String manifestType = null; + String schedule = null; + String packageIdentity = null; + Integer counterCount = null; + boolean counters = false; + Map weights = + new LinkedHashMap(); + String line; + while ((line = reader.readLine()) != null) { + if (line.trim().isEmpty() || line.trim().startsWith("#")) { + continue; + } + if ("counters:".equals(line)) { + counters = true; + continue; + } + if (counters && line.startsWith(" ")) { + String trimmed = line.trim(); + int separator = trimmed.indexOf(':'); + if (separator <= 0) { + throw invalid("Malformed counter entry: " + line); + } + String name = trimmed.substring(0, separator).trim(); + String rawWeight = + trimmed.substring(separator + 1).trim(); + long weight; + try { + weight = Long.parseLong(rawWeight); + } catch (NumberFormatException exception) { + throw invalid( + "Invalid gas weight for " + name); + } + if (weight <= 0L) { + throw invalid( + "Portable gas weight must be positive for " + + name); + } + if (weights.put(name, weight) != null) { + throw invalid( + "Duplicate gas counter " + name); + } + continue; + } + counters = false; + if (line.startsWith("manifestType:")) { + manifestType = scalar(line); + } else if (line.startsWith("schedule:")) { + schedule = scalar(line); + } else if (line.startsWith("counterCount:")) { + try { + counterCount = + Integer.valueOf(scalar(line)); + } catch (NumberFormatException exception) { + throw invalid("Invalid counterCount"); + } + } else if (line.startsWith("packageIdentity:")) { + packageIdentity = scalar(line); + } + } + if (!"blue-bex-gas-manifest".equals(manifestType)) { + throw invalid("Unexpected manifestType " + manifestType); + } + if (!PACKAGE_IDENTITY.equals(packageIdentity)) { + throw invalid( + "Unexpected gas manifest package identity " + + packageIdentity); + } + if (schedule == null || schedule.isEmpty()) { + throw invalid("Missing gas schedule identifier"); + } + if (counterCount == null + || counterCount.intValue() != weights.size()) { + throw invalid( + "counterCount does not match counter catalog"); + } + return new BexGasManifest( + schedule, + packageIdentity, + weights); + } + + private static String scalar(String line) { + int separator = line.indexOf(':'); + String value = line.substring(separator + 1).trim(); + if (value.length() >= 2 + && ((value.startsWith("'") + && value.endsWith("'")) + || (value.startsWith("\"") + && value.endsWith("\"")))) { + return value.substring(1, value.length() - 1); + } + return value; + } + + private static IllegalStateException invalid(String detail) { + return new IllegalStateException( + "Invalid BEX gas manifest: " + detail); + } + + private static byte[] readAll(InputStream input) + throws IOException { + ByteArrayOutputStream output = + new ByteArrayOutputStream(); + byte[] buffer = new byte[8192]; + int read; + while ((read = input.read(buffer)) >= 0) { + output.write(buffer, 0, read); + } + return output.toByteArray(); + } + + private static String sha256(byte[] bytes) { + try { + MessageDigest digest = + MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(bytes); + StringBuilder value = new StringBuilder(64); + for (byte item : hash) { + value.append(Character.forDigit( + (item >>> 4) & 0x0f, 16)); + value.append(Character.forDigit( + item & 0x0f, 16)); + } + return value.toString(); + } catch (NoSuchAlgorithmException exception) { + throw new IllegalStateException( + "SHA-256 is unavailable", exception); + } + } + + String scheduleId() { + return scheduleId; + } + + String packageIdentity() { + return packageIdentity; + } + + Map counterWeights() { + return counterWeights; + } +} diff --git a/src/main/java/blue/bex/gas/BexGasMeter.java b/src/main/java/blue/bex/gas/BexGasMeter.java index 67244c8..1189785 100644 --- a/src/main/java/blue/bex/gas/BexGasMeter.java +++ b/src/main/java/blue/bex/gas/BexGasMeter.java @@ -1,51 +1,857 @@ package blue.bex.gas; -import blue.bex.BexException; -import blue.bex.result.BexMetrics; -import blue.bex.value.BexValue; +import blue.bex.BexSourcePath; +import blue.language.processor.GasChargeContext; +import blue.language.processor.GasLimitExceededException; +import blue.language.processor.GasMeter; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.BiConsumer; +import java.util.function.Consumer; /** - * Deterministic gas meter. + * Deterministic, live-bounded BEX 2.0 named gas meter. + * + *

Every charge is checked and, when host-backed, admitted to the shared + * child ledger before the corresponding local trace entry is appended and + * before the caller performs its work. A charge which cannot fit is absent + * from both traces.

*/ public final class BexGasMeter { + /** Sentinel accepted by local-limit constructors for no local sub-limit. */ + public static final long NO_LOCAL_LIMIT = -1L; + private final BexGasSchedule schedule; - private final long limit; - private final BexSizeEstimator sizeEstimator; - private long used; + private final long parentRemainingGas; + private final long localLimit; + private final long effectiveBudget; + private final Map hostLedgers; + private final boolean qualifiedHostCounters; + private final Map registeredNamedWeights; + private final List trace = new ArrayList<>(); + private long totalGas; + private HostLedgerState hostLedgerState = HostLedgerState.OPEN; + + private enum HostLedgerState { + OPEN, + SUBMITTED, + FAILED, + UNAVAILABLE, + EXHAUSTED + } + + /** + * Creates a standalone meter bounded by the supplied parent budget. + */ + public BexGasMeter(BexGasSchedule schedule, long parentRemainingGas) { + this(schedule, + requireBudget(parentRemainingGas, "parentRemainingGas"), + NO_LOCAL_LIMIT, + Collections.emptyMap(), + false, + Collections.emptyMap()); + } + + /** + * Creates a standalone meter whose local BEX limit can only reduce the + * supplied parent budget. + */ + public BexGasMeter(BexGasSchedule schedule, + long parentRemainingGas, + long localLimit) { + this(schedule, + requireBudget(parentRemainingGas, "parentRemainingGas"), + requireLocalLimit(localLimit), + Collections.emptyMap(), + false, + Collections.emptyMap()); + } + + /** + * Creates a standalone meter with registry-bound named child counters. + */ + public BexGasMeter(BexGasSchedule schedule, + long parentRemainingGas, + long localLimit, + Map registeredNamedWeights) { + this(schedule, + requireBudget(parentRemainingGas, "parentRemainingGas"), + requireLocalLimit(localLimit), + Collections.emptyMap(), + false, + registeredNamedWeights); + } - public BexGasMeter(BexGasSchedule schedule, long limit, BexMetrics metrics) { - this.schedule = schedule; - this.limit = limit; - this.sizeEstimator = new BexSizeEstimator(metrics); + /** + * Creates a meter over a live parent-bounded host child ledger. + */ + public BexGasMeter(BexGasSchedule schedule, + GasMeter.ChildGasLedger hostLedger) { + this(schedule, hostLedger, NO_LOCAL_LIMIT); + } + + /** + * Creates a meter over a live parent-bounded host child ledger. The local + * limit may only reduce the child ledger's initial remaining budget. + */ + public BexGasMeter(BexGasSchedule schedule, + GasMeter.ChildGasLedger hostLedger, + long localLimit) { + this(schedule, + Objects.requireNonNull(hostLedger, "hostLedger").remainingGas(), + requireLocalLimit(localLimit), + singletonHostLedger(hostLedger), + true, + Collections.emptyMap()); + } + + /** + * Creates a hosted meter with one physical child ledger per logical + * runtime namespace. The map must contain {@code bex}; registered + * intrinsic namespaces use their own unqualified counter catalogs. + */ + public BexGasMeter( + BexGasSchedule schedule, + Map hostLedgers, + long localLimit, + Map registeredNamedWeights) { + this(schedule, + parentBudget(hostLedgers), + requireLocalLimit(localLimit), + hostLedgers, + false, + registeredNamedWeights); + } + + private BexGasMeter(BexGasSchedule schedule, + long parentRemainingGas, + long localLimit, + Map hostLedgers, + boolean qualifiedHostCounters, + Map registeredNamedWeights) { + this.schedule = Objects.requireNonNull(schedule, "schedule"); + this.parentRemainingGas = parentRemainingGas; + this.localLimit = localLimit; + this.effectiveBudget = localLimit == NO_LOCAL_LIMIT + ? parentRemainingGas + : Math.min(parentRemainingGas, localLimit); + this.hostLedgers = immutableHostLedgers(hostLedgers); + this.qualifiedHostCounters = qualifiedHostCounters; + this.registeredNamedWeights = + immutableRegisteredWeights(registeredNamedWeights); } public BexGasSchedule schedule() { return schedule; } + /** + * Produces the deterministic host-ledger key for a counter. Portable BEX + * counters remain unqualified; registry child counters are qualified by + * their exact namespace. + */ + public static String qualifiedCounterName(String namespace, + String counterName) { + String exactNamespace = requireName(namespace, "Gas namespace"); + String exactCounter = requireName(counterName, "Gas counter"); + return BexGasCounter.NAMESPACE.equals(exactNamespace) + ? exactCounter + : exactNamespace + "." + exactCounter; + } + + /** + * Builds a deterministic combined catalog for standalone inspection. + * Hosted intrinsic execution uses one physical ledger per namespace and + * never passes this combined map to a host. Registered map keys must + * already be produced by + * {@link #qualifiedCounterName(String, String)}. + */ + public static Map childLedgerWeights( + BexGasSchedule schedule, + Map registeredNamedWeights) { + LinkedHashMap combined = new LinkedHashMap<>( + Objects.requireNonNull(schedule, "schedule").counterWeights()); + Map registered = + immutableRegisteredWeights(registeredNamedWeights); + for (Map.Entry entry : registered.entrySet()) { + if (combined.containsKey(entry.getKey())) { + throw new IllegalArgumentException( + "Registered gas counter collides with BEX manifest counter: " + + entry.getKey()); + } + combined.put(entry.getKey(), entry.getValue()); + } + return Collections.unmodifiableMap(combined); + } + + public Map registeredNamedWeights() { + return registeredNamedWeights; + } + + public Map childLedgerWeights() { + return childLedgerWeights(schedule, registeredNamedWeights); + } + + /** + * Returns the exact parent budget observed when this child meter began. + */ + public long parentRemainingGas() { + return parentRemainingGas; + } + + /** + * Returns the configured local sub-limit, or {@link #NO_LOCAL_LIMIT}. + */ + public long localLimit() { + return localLimit; + } + + public long effectiveBudget() { + return effectiveBudget; + } + + public long totalGas() { + return totalGas; + } + + /** Compatibility alias for {@link #totalGas()}. */ public long used() { - return used; + return totalGas; + } + + public long remainingGas() { + return effectiveBudget - totalGas; + } + + /** Alias for {@link #remainingGas()}. */ + public long remaining() { + return remainingGas(); } - public void charge(long amount) { - if (amount <= 0) { + /** + * Returns an immutable snapshot of all successfully admitted charges. + */ + public List trace() { + return Collections.unmodifiableList(new ArrayList<>(trace)); + } + + /** + * Returns an immutable snapshot of the current admitted ledger. + */ + public BexGasLedger ledger() { + return new BexGasLedger( + trace, + schedule.scheduleId(), + schedule.manifestIdentity()); + } + + public boolean hasHostLedger() { + return !hostLedgers.isEmpty(); + } + + public boolean hostLedgerSubmitted() { + return hostLedgerState == HostLedgerState.SUBMITTED; + } + + public boolean hostLedgerFinalized() { + return hostLedgerState != HostLedgerState.OPEN; + } + + public void charge(BexGasCounter counter) { + charge(counter, 1L); + } + + public void charge(BexGasCounter counter, long quantity) { + BexGasCounter exactCounter = + Objects.requireNonNull(counter, "counter"); + charge(exactCounter, + quantity, + (String) null, + null, + exactCounter.canonicalName()); + } + + public void charge(BexGasCounter counter, + long quantity, + String reason) { + charge(counter, quantity, (String) null, null, reason); + } + + public void charge(BexGasCounter counter, + String sourcePath, + String operator, + String reason) { + charge(counter, 1L, sourcePath, operator, reason); + } + + public void charge(BexGasCounter counter, + BexSourcePath sourcePath, + String operator, + String reason) { + charge(counter, + 1L, + sourcePath != null ? sourcePath.toString() : null, + operator, + reason); + } + + public void charge(BexGasCounter counter, + long quantity, + BexSourcePath sourcePath, + String operator, + String reason) { + charge(counter, + quantity, + sourcePath != null ? sourcePath.toString() : null, + operator, + reason); + } + + public void charge(BexGasCounter counter, + long quantity, + String sourcePath, + String operator, + String reason) { + BexGasCounter exactCounter = + Objects.requireNonNull(counter, "counter"); + long weight = schedule.weight(exactCounter); + chargeAdmitted( + BexGasCounter.NAMESPACE, + exactCounter.canonicalName(), + exactCounter, + quantity, + weight, + sourcePath, + operator, + reason); + } + + public void chargeNamed(String namespace, + String counterName, + long quantity) { + chargeNamed(namespace, + counterName, + quantity, + (String) null, + null, + qualifiedCounterName(namespace, counterName)); + } + + public void chargeNamed(String namespace, + String counterName, + long quantity, + String reason) { + chargeNamed(namespace, + counterName, + quantity, + (String) null, + null, + reason); + } + + public void chargeNamed(String namespace, + String counterName, + long quantity, + String sourcePath, + String operator, + String reason) { + NamedWeight named = registeredWeight(namespace, counterName); + chargeAdmitted( + named.namespace, + named.counterName, + named.portableCounter, + quantity, + named.weight, + sourcePath, + operator, + reason); + } + + public void chargeNamed(String namespace, + String counterName, + long quantity, + long declaredWeight, + String sourcePath, + String operator, + String reason) { + NamedWeight named = registeredWeight(namespace, counterName); + if (declaredWeight != named.weight) { + throw new IllegalArgumentException( + "Registered gas weight mismatch for " + + qualifiedCounterName(namespace, counterName) + + ": expected " + named.weight + + " but was " + declaredWeight); + } + chargeAdmitted( + named.namespace, + named.counterName, + named.portableCounter, + quantity, + named.weight, + sourcePath, + operator, + reason); + } + + public void chargeNamed(String namespace, + String counterName, + long quantity, + BexSourcePath sourcePath, + String operator, + String reason) { + chargeNamed(namespace, + counterName, + quantity, + sourcePath != null ? sourcePath.toString() : null, + operator, + reason); + } + + private void chargeAdmitted(String namespace, + String counterName, + BexGasCounter portableCounter, + long quantity, + long weight, + String sourcePath, + String operator, + String reason) { + ensureOpen(); + if (quantity < 0L) { + throw new IllegalArgumentException( + "Gas quantity must be non-negative"); + } + if (quantity == 0L || weight == 0L) { return; } - used += amount; - if (limit >= 0 && used > limit) { - throw new BexException("BEX gas exhausted at " + used + " gas units"); + String exactReason = requireReason(reason); + long gas = multiplyExact(quantity, weight); + + /* + * A hosted meter prechecks only the optional BEX-local sub-limit. The + * processor-owned child ledger remains the sole authority for the live + * parent budget, including reservations consumed after this meter was + * opened. A standalone meter has no such owner and therefore checks + * the complete effective budget itself. + */ + long localAdmissionBudget = hostLedgers.isEmpty() + ? effectiveBudget + : localLimit; + if (localAdmissionBudget != NO_LOCAL_LIMIT + && gas > localAdmissionBudget - totalGas) { + throw exhausted( + namespace, + counterName, + portableCounter, + quantity, + weight); } + + GasMeter.ChildGasLedger hostLedger = + qualifiedHostCounters + ? hostLedgers.get(BexGasCounter.NAMESPACE) + : hostLedgers.get(namespace); + if (!hostLedgers.isEmpty() && hostLedger == null) { + throw new IllegalStateException( + "No live host child ledger for logical namespace " + + namespace); + } + if (hostLedger != null) { + try { + hostLedger.charge( + qualifiedHostCounters + ? qualifiedCounterName( + namespace, counterName) + : counterName, + quantity, + GasChargeContext.of( + emptyToNull(sourcePath), + null, + emptyToNull(operator), + exactReason)); + } catch (GasLimitExceededException exhausted) { + /* + * Retain the exact host rejection so the owning runtime work + * session can validate and propagate that same object. The + * BEX wrapper still exposes the portable logical namespace and + * preserves the invariant that the rejected entry is absent + * locally. + */ + throw exhausted( + namespace, + counterName, + portableCounter, + quantity, + weight, + exhausted); + } + } + + trace.add(new BexGasCharge( + trace.size(), + namespace, + counterName, + quantity, + weight, + gas, + sourcePath, + operator, + exactReason)); + totalGas += gas; + } + + /** + * Submits a successfully completed wrapped host child ledger exactly once. + * The final state is set before invoking the callback, so a + * throwing callback cannot cause an accidental second merge attempt. + */ + public void submitHostLedger( + Consumer submitter) { + finalizeHostLedger( + HostLedgerState.SUBMITTED, + Objects.requireNonNull(submitter, "submitter")); + } + + /** + * Finalizes the BEX side of a deterministic-failure callback. The host + * decides whether its contract merges immediately or leaves the ledger + * staged for enclosing-session finalization. + */ + public void failHostLedger( + Consumer failureHandler) { + finalizeHostLedger( + HostLedgerState.FAILED, + Objects.requireNonNull(failureHandler, "failureHandler")); + } + + /** + * Finalizes the BEX side of a transient-unavailability callback. + */ + public void unavailableHostLedger( + Consumer unavailableHandler) { + finalizeHostLedger( + HostLedgerState.UNAVAILABLE, + Objects.requireNonNull( + unavailableHandler, "unavailableHandler")); } - public long estimatedSize(BexValue value) { - return sizeEstimator.estimate(value); + /** + * Hands the exact recorded host rejection back to its owner. + */ + public void propagateHostGasExhaustion( + GasLimitExceededException exhaustion, + Consumer prefixHandler, + BiConsumer exhaustionHandler) { + requireOpenHostLedger(); + hostLedgerState = HostLedgerState.EXHAUSTED; + GasLimitExceededException exactExhaustion = + Objects.requireNonNull(exhaustion, "exhaustion"); + Consumer exactPrefixHandler = + Objects.requireNonNull( + prefixHandler, "prefixHandler"); + Throwable prefixFailure = null; + for (GasMeter.ChildGasLedger hostLedger + : hostLedgers.values()) { + try { + exactPrefixHandler.accept(hostLedger); + } catch (RuntimeException | Error failure) { + prefixFailure = retainFailure( + prefixFailure, failure); + } + } + try { + Objects.requireNonNull( + exhaustionHandler, "exhaustionHandler").accept( + rejectionLedger(exactExhaustion), + exactExhaustion); + } catch (RuntimeException | Error propagated) { + if (prefixFailure != null + && prefixFailure != propagated) { + propagated.addSuppressed(prefixFailure); + } + throw propagated; + } + rethrowFailure(prefixFailure); + } + + private void finalizeHostLedger( + HostLedgerState finalState, + Consumer callback) { + requireOpenHostLedger(); + hostLedgerState = finalState; + Throwable callbackFailure = null; + for (GasMeter.ChildGasLedger hostLedger + : hostLedgers.values()) { + try { + callback.accept(hostLedger); + } catch (RuntimeException | Error failure) { + callbackFailure = retainFailure( + callbackFailure, failure); + } + } + rethrowFailure(callbackFailure); + } + + private static Throwable retainFailure( + Throwable retained, + Throwable next) { + if (retained == null) { + return next; + } + if (retained != next) { + retained.addSuppressed(next); + } + return retained; + } + + private static void rethrowFailure(Throwable failure) { + if (failure instanceof RuntimeException) { + throw (RuntimeException) failure; + } + if (failure instanceof Error) { + throw (Error) failure; + } + } + + private void requireOpenHostLedger() { + if (hostLedgers.isEmpty()) { + throw new IllegalStateException( + "This BEX gas meter has no host child ledgers"); + } + if (hostLedgerState != HostLedgerState.OPEN) { + throw new IllegalStateException( + "BEX host child ledger was already finalized as " + + hostLedgerState); + } + } + + private GasMeter.ChildGasLedger rejectionLedger( + GasLimitExceededException exhaustion) { + for (GasMeter.ChildGasLedger ledger : hostLedgers.values()) { + if (ledger.namespace().equals(exhaustion.namespace())) { + return ledger; + } + } + /* + * Semantic admission shares the same work session but is not a BEX + * child ledger. The focused host callback receives the primary BEX + * ledger as an ownership token; the processor adapter validates the + * exact exception against the session itself. + */ + return hostLedgers.get(BexGasCounter.NAMESPACE); + } + + private void ensureOpen() { + if (hostLedgerState != HostLedgerState.OPEN) { + throw new IllegalStateException( + "Cannot charge a finalized BEX host child ledger"); + } + } + + private NamedWeight registeredWeight(String namespace, + String counterName) { + String exactNamespace = requireName(namespace, "Gas namespace"); + String exactCounterName = requireName(counterName, "Gas counter"); + if (BexGasCounter.NAMESPACE.equals(exactNamespace)) { + BexGasCounter portable = + BexGasCounter.fromCanonicalName(exactCounterName); + return new NamedWeight( + exactNamespace, + exactCounterName, + portable, + schedule.weight(portable)); + } + String qualified = + qualifiedCounterName(exactNamespace, exactCounterName); + Long weight = registeredNamedWeights.get(qualified); + if (weight == null) { + throw new IllegalArgumentException( + "Unregistered named gas counter: " + qualified); + } + return new NamedWeight( + exactNamespace, + exactCounterName, + null, + weight); } - public void chargeValue(long base, BexValue value) { - charge(base + estimatedSize(value)); + private BexGasLimitExceededException exhausted( + String namespace, + String counterName, + BexGasCounter portableCounter, + long quantity, + long weight) { + return exhausted( + namespace, + counterName, + portableCounter, + quantity, + weight, + null); } - public void chargePointerValue(long base, int pathSegments, BexValue value) { - charge(base + Math.max(0, pathSegments) + estimatedSize(value)); + private BexGasLimitExceededException exhausted( + String namespace, + String counterName, + BexGasCounter portableCounter, + long quantity, + long weight, + GasLimitExceededException hostGasLimitExceeded) { + if (portableCounter != null) { + return new BexGasLimitExceededException( + portableCounter, + quantity, + weight, + totalGas, + effectiveBudget, + hostGasLimitExceeded); + } + return new BexGasLimitExceededException( + namespace, + counterName, + quantity, + weight, + totalGas, + effectiveBudget, + hostGasLimitExceeded); + } + + private static long requireBudget(long value, String name) { + if (value < 0L) { + throw new IllegalArgumentException(name + " must be non-negative"); + } + return value; + } + + private static long requireLocalLimit(long value) { + if (value < NO_LOCAL_LIMIT) { + throw new IllegalArgumentException( + "localLimit must be non-negative or NO_LOCAL_LIMIT"); + } + return value; + } + + private static String emptyToNull(String value) { + return value == null || value.isEmpty() ? null : value; + } + + private static String requireReason(String value) { + if (value == null || value.trim().isEmpty()) { + throw new IllegalArgumentException("Gas charge reason is required"); + } + return value; + } + + private static String requireName(String value, String label) { + if (value == null || value.trim().isEmpty()) { + throw new IllegalArgumentException(label + " is required"); + } + return value; + } + + private static Map + singletonHostLedger(GasMeter.ChildGasLedger hostLedger) { + LinkedHashMap singleton = + new LinkedHashMap<>(); + singleton.put( + BexGasCounter.NAMESPACE, + Objects.requireNonNull(hostLedger, "hostLedger")); + return singleton; + } + + private static long parentBudget( + Map hostLedgers) { + Objects.requireNonNull(hostLedgers, "hostLedgers"); + GasMeter.ChildGasLedger bexLedger = + hostLedgers.get(BexGasCounter.NAMESPACE); + if (bexLedger == null) { + throw new IllegalArgumentException( + "Hosted BEX ledger map must contain logical namespace " + + BexGasCounter.NAMESPACE); + } + return bexLedger.remainingGas(); + } + + private static Map + immutableHostLedgers( + Map hostLedgers) { + if (hostLedgers == null || hostLedgers.isEmpty()) { + return Collections.emptyMap(); + } + if (!hostLedgers.containsKey(BexGasCounter.NAMESPACE)) { + throw new IllegalArgumentException( + "Hosted BEX ledger map must contain logical namespace " + + BexGasCounter.NAMESPACE); + } + LinkedHashMap copy = + new LinkedHashMap<>(); + IdentityHashMap identities = + new IdentityHashMap<>(); + for (Map.Entry entry + : hostLedgers.entrySet()) { + String namespace = + requireName(entry.getKey(), "Logical gas namespace"); + GasMeter.ChildGasLedger ledger = + Objects.requireNonNull( + entry.getValue(), "host child ledger"); + if (identities.put(ledger, Boolean.TRUE) != null) { + throw new IllegalArgumentException( + "Each logical runtime namespace requires a distinct " + + "host child ledger"); + } + copy.put(namespace, ledger); + } + return Collections.unmodifiableMap(copy); + } + + private static Map immutableRegisteredWeights( + Map registeredNamedWeights) { + if (registeredNamedWeights == null || registeredNamedWeights.isEmpty()) { + return Collections.emptyMap(); + } + LinkedHashMap copy = new LinkedHashMap<>(); + for (Map.Entry entry : + registeredNamedWeights.entrySet()) { + String counterName = + requireName(entry.getKey(), "Registered gas counter"); + Long weight = Objects.requireNonNull( + entry.getValue(), "Registered gas weight"); + if (weight <= 0L) { + throw new IllegalArgumentException( + "Registered gas weight must be positive"); + } + copy.put(counterName, weight); + } + return Collections.unmodifiableMap(copy); + } + + private static long multiplyExact(long left, long right) { + if (left != 0L && right > Long.MAX_VALUE / left) { + throw new IllegalArgumentException("Gas subtotal exceeds long range"); + } + return left * right; + } + + private static final class NamedWeight { + private final String namespace; + private final String counterName; + private final BexGasCounter portableCounter; + private final long weight; + + private NamedWeight(String namespace, + String counterName, + BexGasCounter portableCounter, + long weight) { + this.namespace = namespace; + this.counterName = counterName; + this.portableCounter = portableCounter; + this.weight = weight; + } } } diff --git a/src/main/java/blue/bex/gas/BexGasSchedule.java b/src/main/java/blue/bex/gas/BexGasSchedule.java index eedab92..21c3708 100644 --- a/src/main/java/blue/bex/gas/BexGasSchedule.java +++ b/src/main/java/blue/bex/gas/BexGasSchedule.java @@ -1,44 +1,126 @@ package blue.bex.gas; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Collections; +import java.util.EnumMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; + /** - * Deterministic gas cost schedule. + * Immutable deterministic BEX gas schedule backed by the complete BEX 2.0 + * counter vocabulary. * - *

The schedule assigns integer costs to compiled-expression operations. For a - * fixed program, context, and schedule, gas usage is deterministic.

+ *

The public lower-camel-case fields are convenient read-only projections + * of {@link #weight(BexGasCounter)}.

*/ public final class BexGasSchedule { - public final long expressionBase; - public final long statementBase; + public static final String SCHEDULE_ID = BexGasCounter.SCHEDULE_ID; + public static final String MANIFEST_IDENTITY = + BexGasCounter.MANIFEST_IDENTITY; + + public final long expressionEvaluated; + public final long statementExecuted; + public final long functionCalled; + public final long intrinsicCalled; public final long documentRead; public final long eventRead; - public final long stepsRead; + public final long processingEventRead; public final long currentContractRead; - public final long varRead; + public final long stepsRead; + public final long bindingRead; + public final long variableRead; + public final long constantRead; public final long resultValueRead; - public final long pointerGetBase; - public final long pointerSetBase; - public final long objectSetBase; - public final long appendChangeBase; - public final long appendEventBase; - public final long forEachItem; - public final long functionCall; + public final long pointerSegmentRead; + public final long pointerSegmentWritten; + public final long objectMemberRead; + public final long listItemRead; + public final long collectionItemVisited; + public final long collectionItemProduced; + public final long textBlockExamined; + public final long textBlockConstructed; + public final long integerLimbOperation; + public final long comparisonNodeVisited; + public final long sortComparison; + public final long patchAppended; + public final long eventAppended; + public final long transientObjectMemberProduced; + public final long transientListItemProduced; + public final long blueOutputBoundary; + public final long nodeIdentityRequested; + + private final Map weights; + private final Map counterWeights; + private final String scheduleId; + private final String manifestIdentity; private BexGasSchedule(Builder builder) { - this.expressionBase = builder.expressionBase; - this.statementBase = builder.statementBase; - this.documentRead = builder.documentRead; - this.eventRead = builder.eventRead; - this.stepsRead = builder.stepsRead; - this.currentContractRead = builder.currentContractRead; - this.varRead = builder.varRead; - this.resultValueRead = builder.resultValueRead; - this.pointerGetBase = builder.pointerGetBase; - this.pointerSetBase = builder.pointerSetBase; - this.objectSetBase = builder.objectSetBase; - this.appendChangeBase = builder.appendChangeBase; - this.appendEventBase = builder.appendEventBase; - this.forEachItem = builder.forEachItem; - this.functionCall = builder.functionCall; + EnumMap copy = + new EnumMap<>(BexGasCounter.class); + LinkedHashMap named = new LinkedHashMap<>(); + for (BexGasCounter counter : BexGasCounter.values()) { + Long weight = builder.weights.get(counter); + if (weight == null || weight <= 0L) { + throw new IllegalArgumentException( + "Missing or non-positive gas weight for " + + counter.canonicalName()); + } + copy.put(counter, weight); + named.put(counter.canonicalName(), weight); + } + this.weights = Collections.unmodifiableMap(copy); + this.counterWeights = Collections.unmodifiableMap(named); + boolean exactManifest = true; + for (BexGasCounter counter : BexGasCounter.values()) { + if (copy.get(counter).longValue() + != counter.defaultWeight()) { + exactManifest = false; + break; + } + } + this.manifestIdentity = exactManifest + ? MANIFEST_IDENTITY + : customIdentity(named); + this.scheduleId = exactManifest + ? SCHEDULE_ID + : "blue-bex-gas/custom@" + manifestIdentity; + + expressionEvaluated = weight(BexGasCounter.EXPRESSION_EVALUATED); + statementExecuted = weight(BexGasCounter.STATEMENT_EXECUTED); + functionCalled = weight(BexGasCounter.FUNCTION_CALLED); + intrinsicCalled = weight(BexGasCounter.INTRINSIC_CALLED); + documentRead = weight(BexGasCounter.DOCUMENT_READ); + eventRead = weight(BexGasCounter.EVENT_READ); + processingEventRead = weight(BexGasCounter.PROCESSING_EVENT_READ); + currentContractRead = weight(BexGasCounter.CURRENT_CONTRACT_READ); + stepsRead = weight(BexGasCounter.STEPS_READ); + bindingRead = weight(BexGasCounter.BINDING_READ); + variableRead = weight(BexGasCounter.VARIABLE_READ); + constantRead = weight(BexGasCounter.CONSTANT_READ); + resultValueRead = weight(BexGasCounter.RESULT_VALUE_READ); + pointerSegmentRead = weight(BexGasCounter.POINTER_SEGMENT_READ); + pointerSegmentWritten = weight(BexGasCounter.POINTER_SEGMENT_WRITTEN); + objectMemberRead = weight(BexGasCounter.OBJECT_MEMBER_READ); + listItemRead = weight(BexGasCounter.LIST_ITEM_READ); + collectionItemVisited = weight(BexGasCounter.COLLECTION_ITEM_VISITED); + collectionItemProduced = weight(BexGasCounter.COLLECTION_ITEM_PRODUCED); + textBlockExamined = weight(BexGasCounter.TEXT_BLOCK_EXAMINED); + textBlockConstructed = weight(BexGasCounter.TEXT_BLOCK_CONSTRUCTED); + integerLimbOperation = weight(BexGasCounter.INTEGER_LIMB_OPERATION); + comparisonNodeVisited = weight(BexGasCounter.COMPARISON_NODE_VISITED); + sortComparison = weight(BexGasCounter.SORT_COMPARISON); + patchAppended = weight(BexGasCounter.PATCH_APPENDED); + eventAppended = weight(BexGasCounter.EVENT_APPENDED); + transientObjectMemberProduced = + weight(BexGasCounter.TRANSIENT_OBJECT_MEMBER_PRODUCED); + transientListItemProduced = + weight(BexGasCounter.TRANSIENT_LIST_ITEM_PRODUCED); + blueOutputBoundary = weight(BexGasCounter.BLUE_OUTPUT_BOUNDARY); + nodeIdentityRequested = weight(BexGasCounter.NODE_IDENTITY_REQUESTED); + } public static BexGasSchedule defaults() { @@ -49,38 +131,219 @@ public static Builder builder() { return new Builder(); } + public Builder toBuilder() { + return new Builder(this); + } + + public long weight(BexGasCounter counter) { + Long weight = weights.get(Objects.requireNonNull(counter, "counter")); + if (weight == null) { + throw new IllegalArgumentException("Unknown BEX gas counter: " + counter); + } + return weight; + } + + public long weight(String counterName) { + return weight(BexGasCounter.fromCanonicalName(counterName)); + } + + public Map weights() { + return weights; + } + + /** + * Returns the exact manifest-ordered name-to-weight map expected by the + * shared host's runtime child-ledger factory. + */ + public Map counterWeights() { + return counterWeights; + } + + /** Alias for {@link #counterWeights()}. */ + public Map namedWeights() { + return counterWeights; + } + + public String scheduleId() { + return scheduleId; + } + + public String manifestIdentity() { + return manifestIdentity; + } + + private static String customIdentity(Map weights) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + for (Map.Entry entry : weights.entrySet()) { + digest.update(entry.getKey().getBytes(StandardCharsets.UTF_8)); + digest.update((byte) '='); + digest.update(String.valueOf(entry.getValue()) + .getBytes(StandardCharsets.UTF_8)); + digest.update((byte) '\n'); + } + StringBuilder hex = new StringBuilder(64); + for (byte value : digest.digest()) { + hex.append(Character.forDigit( + (value >>> 4) & 0x0f, 16)); + hex.append(Character.forDigit(value & 0x0f, 16)); + } + return "sha256:" + hex; + } catch (NoSuchAlgorithmException ex) { + throw new IllegalStateException( + "SHA-256 is unavailable", ex); + } + } + public static final class Builder { - private long expressionBase = 1; - private long statementBase = 1; - private long documentRead = 2; - private long eventRead = 1; - private long stepsRead = 1; - private long currentContractRead = 1; - private long varRead = 1; - private long resultValueRead = 2; - private long pointerGetBase = 1; - private long pointerSetBase = 3; - private long objectSetBase = 2; - private long appendChangeBase = 5; - private long appendEventBase = 5; - private long forEachItem = 1; - private long functionCall = 2; - - public Builder expressionBase(long value) { expressionBase = value; return this; } - public Builder statementBase(long value) { statementBase = value; return this; } - public Builder documentRead(long value) { documentRead = value; return this; } - public Builder eventRead(long value) { eventRead = value; return this; } - public Builder stepsRead(long value) { stepsRead = value; return this; } - public Builder currentContractRead(long value) { currentContractRead = value; return this; } - public Builder varRead(long value) { varRead = value; return this; } - public Builder resultValueRead(long value) { resultValueRead = value; return this; } - public Builder pointerGetBase(long value) { pointerGetBase = value; return this; } - public Builder pointerSetBase(long value) { pointerSetBase = value; return this; } - public Builder objectSetBase(long value) { objectSetBase = value; return this; } - public Builder appendChangeBase(long value) { appendChangeBase = value; return this; } - public Builder appendEventBase(long value) { appendEventBase = value; return this; } - public Builder forEachItem(long value) { forEachItem = value; return this; } - public Builder functionCall(long value) { functionCall = value; return this; } - public BexGasSchedule build() { return new BexGasSchedule(this); } + private final EnumMap weights = + new EnumMap<>(BexGasCounter.class); + + private Builder() { + for (BexGasCounter counter : BexGasCounter.values()) { + weights.put(counter, counter.defaultWeight()); + } + } + + private Builder(BexGasSchedule schedule) { + weights.putAll(schedule.weights); + } + + public Builder weight(BexGasCounter counter, long value) { + if (value <= 0L) { + throw new IllegalArgumentException( + "Portable gas weight must be positive"); + } + weights.put(Objects.requireNonNull(counter, "counter"), value); + return this; + } + + public Builder weight(String counterName, long value) { + return weight(BexGasCounter.fromCanonicalName(counterName), value); + } + + public Builder expressionEvaluated(long value) { + return weight(BexGasCounter.EXPRESSION_EVALUATED, value); + } + + public Builder statementExecuted(long value) { + return weight(BexGasCounter.STATEMENT_EXECUTED, value); + } + + public Builder functionCalled(long value) { + return weight(BexGasCounter.FUNCTION_CALLED, value); + } + + public Builder intrinsicCalled(long value) { + return weight(BexGasCounter.INTRINSIC_CALLED, value); + } + + public Builder documentRead(long value) { + return weight(BexGasCounter.DOCUMENT_READ, value); + } + + public Builder eventRead(long value) { + return weight(BexGasCounter.EVENT_READ, value); + } + + public Builder processingEventRead(long value) { + return weight(BexGasCounter.PROCESSING_EVENT_READ, value); + } + + public Builder currentContractRead(long value) { + return weight(BexGasCounter.CURRENT_CONTRACT_READ, value); + } + + public Builder stepsRead(long value) { + return weight(BexGasCounter.STEPS_READ, value); + } + + public Builder bindingRead(long value) { + return weight(BexGasCounter.BINDING_READ, value); + } + + public Builder variableRead(long value) { + return weight(BexGasCounter.VARIABLE_READ, value); + } + + public Builder constantRead(long value) { + return weight(BexGasCounter.CONSTANT_READ, value); + } + + public Builder resultValueRead(long value) { + return weight(BexGasCounter.RESULT_VALUE_READ, value); + } + + public Builder pointerSegmentRead(long value) { + return weight(BexGasCounter.POINTER_SEGMENT_READ, value); + } + + public Builder pointerSegmentWritten(long value) { + return weight(BexGasCounter.POINTER_SEGMENT_WRITTEN, value); + } + + public Builder objectMemberRead(long value) { + return weight(BexGasCounter.OBJECT_MEMBER_READ, value); + } + + public Builder listItemRead(long value) { + return weight(BexGasCounter.LIST_ITEM_READ, value); + } + + public Builder collectionItemVisited(long value) { + return weight(BexGasCounter.COLLECTION_ITEM_VISITED, value); + } + + public Builder collectionItemProduced(long value) { + return weight(BexGasCounter.COLLECTION_ITEM_PRODUCED, value); + } + + public Builder textBlockExamined(long value) { + return weight(BexGasCounter.TEXT_BLOCK_EXAMINED, value); + } + + public Builder textBlockConstructed(long value) { + return weight(BexGasCounter.TEXT_BLOCK_CONSTRUCTED, value); + } + + public Builder integerLimbOperation(long value) { + return weight(BexGasCounter.INTEGER_LIMB_OPERATION, value); + } + + public Builder comparisonNodeVisited(long value) { + return weight(BexGasCounter.COMPARISON_NODE_VISITED, value); + } + + public Builder sortComparison(long value) { + return weight(BexGasCounter.SORT_COMPARISON, value); + } + + public Builder patchAppended(long value) { + return weight(BexGasCounter.PATCH_APPENDED, value); + } + + public Builder eventAppended(long value) { + return weight(BexGasCounter.EVENT_APPENDED, value); + } + + public Builder transientObjectMemberProduced(long value) { + return weight(BexGasCounter.TRANSIENT_OBJECT_MEMBER_PRODUCED, value); + } + + public Builder transientListItemProduced(long value) { + return weight(BexGasCounter.TRANSIENT_LIST_ITEM_PRODUCED, value); + } + + public Builder blueOutputBoundary(long value) { + return weight(BexGasCounter.BLUE_OUTPUT_BOUNDARY, value); + } + + public Builder nodeIdentityRequested(long value) { + return weight(BexGasCounter.NODE_IDENTITY_REQUESTED, value); + } + + public BexGasSchedule build() { + return new BexGasSchedule(this); + } } } diff --git a/src/main/java/blue/bex/gas/BexSizeEstimator.java b/src/main/java/blue/bex/gas/BexSizeEstimator.java deleted file mode 100644 index c10119d..0000000 --- a/src/main/java/blue/bex/gas/BexSizeEstimator.java +++ /dev/null @@ -1,82 +0,0 @@ -package blue.bex.gas; - -import blue.bex.result.BexMetrics; -import blue.bex.value.BexValue; -import blue.bex.value.BexValues; - -import java.util.IdentityHashMap; -import java.util.LinkedHashMap; -import java.util.Map; - -/** - * Deterministic, cached size estimator used for gas accounting. - */ -public final class BexSizeEstimator { - private final IdentityHashMap identityCache = new IdentityHashMap<>(); - private final Map frozenBlueIdCache; - private final BexMetrics metrics; - - public BexSizeEstimator(BexMetrics metrics) { - this(metrics, 8192); - } - - BexSizeEstimator(BexMetrics metrics, final int frozenCacheCapacity) { - this.metrics = metrics; - this.frozenBlueIdCache = new LinkedHashMap(16, 0.75f, true) { - @Override - protected boolean removeEldestEntry(Map.Entry eldest) { - return size() > frozenCacheCapacity; - } - }; - } - - public long estimate(BexValue value) { - metrics.incrementSizeEstimateCalls(); - return estimateInternal(value); - } - - private long estimateInternal(BexValue value) { - if (value == null || value.isUndefined() || value.isNull()) { - return 0; - } - String blueId = BexValues.frozenBlueId(value); - if (blueId != null) { - Long cached = frozenBlueIdCache.get(blueId); - if (cached != null) { - metrics.incrementSizeEstimateCacheHits(); - return cached; - } - metrics.incrementSizeEstimateCacheMisses(); - long size = compute(value); - frozenBlueIdCache.put(blueId, size); - return size; - } - Long cached = identityCache.get(value); - if (cached != null) { - metrics.incrementSizeEstimateCacheHits(); - return cached; - } - metrics.incrementSizeEstimateCacheMisses(); - long size = compute(value); - identityCache.put(value, size); - return size; - } - - private long compute(BexValue value) { - if (value.isScalar()) { - return Math.max(1, value.asText().length()); - } - long size = value.isList() ? value.size() : value.keys().size(); - if (value.isList()) { - for (int i = 0; i < value.size(); i++) { - size += estimateInternal(value.get(String.valueOf(i))); - } - return size; - } - for (String key : value.keys()) { - size += key.length(); - size += estimateInternal(value.get(key)); - } - return size; - } -} diff --git a/src/main/java/blue/bex/output/BexAdmittedValue.java b/src/main/java/blue/bex/output/BexAdmittedValue.java new file mode 100644 index 0000000..5a2e824 --- /dev/null +++ b/src/main/java/blue/bex/output/BexAdmittedValue.java @@ -0,0 +1,63 @@ +package blue.bex.output; + +import blue.bex.value.BexValue; +import blue.language.model.Node; + +import java.util.Objects; + +/** + * One successfully admitted exact Blue output. + */ +public final class BexAdmittedValue { + private final BexValue value; + private final BexValue semanticValue; + private final Node node; + private final String nodeBlueId; + private final boolean reconstructed; + + BexAdmittedValue(BexValue value, + BexValue semanticValue, + Node node, + String nodeBlueId, + boolean reconstructed) { + this.value = Objects.requireNonNull(value, "value"); + this.semanticValue = Objects.requireNonNull( + semanticValue, "semanticValue"); + this.node = Objects.requireNonNull(node, "node"); + this.nodeBlueId = Objects.requireNonNull(nodeBlueId, "nodeBlueId"); + this.reconstructed = reconstructed; + } + + /** + * Representation-blind exact semantic value used by later BEX operations. + */ + public BexValue value() { + return value; + } + + /** + * The exact semantic value returned by the identity boundary. + * + *

For reconstructed output this is the same exact value as + * {@link #value()}; it is retained as a named semantic lane for API + * compatibility. Existing exact input remains unchanged.

+ */ + public BexValue semanticValue() { + return semanticValue; + } + + /** + * Blue boundary representation. Existing exact values are pure references. + */ + public Node node() { + return node.clone(); + } + + public String nodeBlueId() { + return nodeBlueId; + } + + public boolean reconstructed() { + return reconstructed; + } +} diff --git a/src/main/java/blue/bex/output/BexEstablishedIdentity.java b/src/main/java/blue/bex/output/BexEstablishedIdentity.java new file mode 100644 index 0000000..234800d --- /dev/null +++ b/src/main/java/blue/bex/output/BexEstablishedIdentity.java @@ -0,0 +1,34 @@ +package blue.bex.output; + +import blue.language.snapshot.FrozenNode; +import blue.language.utils.BlueIds; + +import java.util.Objects; + +/** + * Exact Blue identity established by a semantic output boundary. + * + *

The frozen value is the boundary's exact semantic result. BEX retains it + * directly so hosted execution never independently reconstructs or hashes a + * value after the host has admitted it.

+ */ +public final class BexEstablishedIdentity { + private final String blueId; + private final FrozenNode frozenValue; + + public BexEstablishedIdentity(String blueId, FrozenNode frozenValue) { + this.blueId = BlueIds.requireBlueIdOrCyclicMember( + Objects.requireNonNull(blueId, "blueId"), + "BEX established output blueId"); + this.frozenValue = Objects.requireNonNull( + frozenValue, "frozenValue"); + } + + public String blueId() { + return blueId; + } + + public FrozenNode frozenValue() { + return frozenValue; + } +} diff --git a/src/main/java/blue/bex/output/BexOutputAdmission.java b/src/main/java/blue/bex/output/BexOutputAdmission.java new file mode 100644 index 0000000..bed8687 --- /dev/null +++ b/src/main/java/blue/bex/output/BexOutputAdmission.java @@ -0,0 +1,143 @@ +package blue.bex.output; + +import blue.bex.BexException; +import blue.bex.gas.BexGasCounter; +import blue.bex.gas.BexGasMeter; +import blue.bex.value.BexBlueNodeWriter; +import blue.bex.value.BexValue; +import blue.bex.value.BexValues; +import blue.language.model.Node; +import blue.language.processor.ExecutionEvidenceUnavailableException; +import blue.language.processor.GasLimitExceededException; +import blue.language.processor.InvalidExecutionEvidenceException; +import blue.language.processor.PortableLimitExceededException; +import blue.language.processor.ProcessorFailureException; +import blue.language.snapshot.FrozenNode; +import blue.language.utils.BlueIds; + +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.Objects; + +/** + * Atomic exact/transient Blue output boundary. + */ +public final class BexOutputAdmission { + private final BexGasMeter gas; + private final BexSemanticIdentityBoundary semanticIdentity; + private final Map admittedTransientValues = + new IdentityHashMap<>(); + private long semanticIdentityMergeCount; + + public BexOutputAdmission(BexGasMeter gas, + BexSemanticIdentityBoundary semanticIdentity) { + this.gas = Objects.requireNonNull(gas, "gas"); + this.semanticIdentity = semanticIdentity != null + ? semanticIdentity + : BexSemanticIdentityBoundary.STANDALONE; + } + + public BexAdmittedValue admit(BexValue value, BexOutputKind kind) { + Objects.requireNonNull(kind, "kind"); + /* + * Admission precedes validation/materialization. If conversion fails, + * the admitted trace prefix remains canonical. + */ + gas.charge(BexGasCounter.BLUE_OUTPUT_BOUNDARY, 1L, kind.reason()); + if (value == null || value.isUndefined()) { + throw new BexException("Blue output conversion failed: root value is undefined"); + } + + if (value.isExact()) { + String exactId = BlueIds.requireBlueIdOrCyclicMember( + value.exactBlueId(), "BEX exact output blueId"); + return new BexAdmittedValue( + value, + value, + new Node().blueId(exactId), + exactId, + false); + } + + BexAdmittedValue prior = admittedTransientValues.get(value); + if (prior != null) { + return prior; + } + + Node node = BexBlueNodeWriter.toNode(value); + + final BexEstablishedIdentity established; + try { + established = Objects.requireNonNull( + semanticIdentity.establishIdentity(node.clone()), + "semantic identity result"); + } catch (BexException ex) { + throw ex; + } catch (ExecutionEvidenceUnavailableException + | InvalidExecutionEvidenceException + | PortableLimitExceededException + | ProcessorFailureException + | GasLimitExceededException ex) { + throw ex; + } catch (RuntimeException ex) { + throw new BexException("Blue output identity establishment failed: " + + ex.getMessage(), ex); + } + String blueId = BlueIds.requireBlueIdOrCyclicMember( + established.blueId(), + "BEX admitted output blueId"); + if (blueId.indexOf('#') >= 0) { + throw new BexException( + "Transient BEX output cannot establish a cyclic-set " + + "member identity: " + blueId); + } + semanticIdentityMergeCount++; + + /* + * The host already established both the identity and the exact + * semantic value. Retain that result without asking BEX to reconstruct + * or hash the complete value again, alongside a cheap pure-reference + * canonical lane. + */ + EstablishedTransientIdentity identity = + new EstablishedTransientIdentity( + established.frozenValue(), + blueId); + BexAdmittedValue admitted = + identity.admit(value); + admittedTransientValues.put(value, admitted); + return admitted; + } + + private static final class EstablishedTransientIdentity { + private final FrozenNode frozenValue; + private final String blueId; + + private EstablishedTransientIdentity( + FrozenNode frozenValue, + String blueId) { + this.frozenValue = Objects.requireNonNull( + frozenValue, "frozenValue"); + this.blueId = Objects.requireNonNull( + blueId, "blueId"); + } + + private BexAdmittedValue admit( + BexValue suppliedValue) { + BexValue exact = BexValues.admittedExact( + frozenValue, + blueId, + suppliedValue); + return new BexAdmittedValue( + exact, + exact, + frozenValue.toNode(), + blueId, + true); + } + } + + public long semanticIdentityMergeCount() { + return semanticIdentityMergeCount; + } +} diff --git a/src/main/java/blue/bex/output/BexOutputKind.java b/src/main/java/blue/bex/output/BexOutputKind.java new file mode 100644 index 0000000..52c8688 --- /dev/null +++ b/src/main/java/blue/bex/output/BexOutputKind.java @@ -0,0 +1,22 @@ +package blue.bex.output; + +/** + * The semantic reason a value crosses the Blue output boundary. + */ +public enum BexOutputKind { + ROOT_RESULT("root-result"), + PATCH_VALUE("patch-value"), + EVENT("event"), + INTRINSIC_INPUT("intrinsic-input"), + NODE_IDENTITY("node-identity"); + + private final String reason; + + BexOutputKind(String reason) { + this.reason = reason; + } + + public String reason() { + return reason; + } +} diff --git a/src/main/java/blue/bex/output/BexSemanticIdentityBoundary.java b/src/main/java/blue/bex/output/BexSemanticIdentityBoundary.java new file mode 100644 index 0000000..614be10 --- /dev/null +++ b/src/main/java/blue/bex/output/BexSemanticIdentityBoundary.java @@ -0,0 +1,26 @@ +package blue.bex.output; + +import blue.language.model.Node; +import blue.language.snapshot.FrozenNode; +import blue.language.utils.BlueIdCalculator; + +/** + * Host-owned Blue semantic identity establishment. + * + *

BEX invokes this exactly once for each admitted transient value. Hosted + * runtimes can replace the default with their Contracts semantic ledger + * boundary; standalone execution uses the canonical Language calculator.

+ */ +@FunctionalInterface +public interface BexSemanticIdentityBoundary { + BexSemanticIdentityBoundary STANDALONE = node -> { + FrozenNode frozen = FrozenNode.fromResolvedNode( + node.clone()); + return new BexEstablishedIdentity( + BlueIdCalculator.calculateBlueId( + frozen.toNode()), + frozen); + }; + + BexEstablishedIdentity establishIdentity(Node node); +} diff --git a/src/main/java/blue/bex/output/ProcessorExecutionContextBexSemanticIdentityBoundary.java b/src/main/java/blue/bex/output/ProcessorExecutionContextBexSemanticIdentityBoundary.java new file mode 100644 index 0000000..73d9b93 --- /dev/null +++ b/src/main/java/blue/bex/output/ProcessorExecutionContextBexSemanticIdentityBoundary.java @@ -0,0 +1,30 @@ +package blue.bex.output; + +import blue.language.model.Node; +import blue.language.processor.ExactBlueValue; +import blue.language.processor.ProcessorExecutionContext; + +import java.util.Objects; + +/** + * Hosted BEX semantic identity boundary backed by the active processor + * invocation. + */ +public final class ProcessorExecutionContextBexSemanticIdentityBoundary + implements BexSemanticIdentityBoundary { + private final ProcessorExecutionContext context; + + public ProcessorExecutionContextBexSemanticIdentityBoundary( + ProcessorExecutionContext context) { + this.context = Objects.requireNonNull(context, "context"); + } + + @Override + public BexEstablishedIdentity establishIdentity(Node node) { + ExactBlueValue exact = context.semanticOutputBoundary().admit( + Objects.requireNonNull(node, "node")); + return new BexEstablishedIdentity( + exact.blueId(), + exact.frozenValue()); + } +} diff --git a/src/main/java/blue/bex/result/BexEvents.java b/src/main/java/blue/bex/result/BexEvents.java index a1c150b..fd17f00 100644 --- a/src/main/java/blue/bex/result/BexEvents.java +++ b/src/main/java/blue/bex/result/BexEvents.java @@ -1,5 +1,6 @@ package blue.bex.result; +import blue.bex.output.BexAdmittedValue; import blue.bex.value.BexValue; import blue.bex.value.BexValues; @@ -12,15 +13,33 @@ */ public final class BexEvents { private final List events; + private final List admittedEvents; public BexEvents(List events) { + this(events, Collections.emptyList()); + } + + public BexEvents(List events, + List admittedEvents) { this.events = Collections.unmodifiableList(new ArrayList<>(events)); + this.admittedEvents = Collections.unmodifiableList( + new ArrayList<>(admittedEvents)); } public List events() { return events; } + /** + * Strict Blue outputs corresponding one-for-one with {@link #events()} for + * engine-produced events. Engine-produced {@link #events()} are the exact + * admitted values, so later BEX lanes never reconstruct their transient + * precursors. + */ + public List admittedEvents() { + return admittedEvents; + } + public BexValue asValue() { return BexValues.list(events); } diff --git a/src/main/java/blue/bex/result/BexExecutionResult.java b/src/main/java/blue/bex/result/BexExecutionResult.java index c9d3e44..d943604 100644 --- a/src/main/java/blue/bex/result/BexExecutionResult.java +++ b/src/main/java/blue/bex/result/BexExecutionResult.java @@ -1,31 +1,63 @@ package blue.bex.result; +import blue.bex.gas.BexGasCharge; +import blue.bex.gas.BexGasLedger; +import blue.bex.output.BexAdmittedValue; import blue.bex.value.BexValue; +import java.util.List; +import java.util.Objects; + /** * Standalone BEX result. * *

The value is the step result. Changeset and events are data outputs for the * host to consume; the BEX engine itself does not mutate documents or emit - * workflow events. Metrics are returned as defensive copies.

+ * workflow events. Metrics are returned as defensive copies. Gas is retained + * as its canonical named trace and {@link #gasUsed()} is always derived from + * that trace.

*/ public final class BexExecutionResult { private final BexValue value; private final BexChangeset changeset; private final BexEvents events; - private final long gasUsed; + private final BexGasLedger gasLedger; private final BexMetrics metrics; + private final BexAdmittedValue output; + + public BexExecutionResult(BexValue value, + BexChangeset changeset, + BexEvents events, + List gasTrace, + BexMetrics metrics) { + this(value, + changeset, + events, + new BexGasLedger(Objects.requireNonNull(gasTrace, "gasTrace")), + metrics, + null); + } public BexExecutionResult(BexValue value, BexChangeset changeset, BexEvents events, - long gasUsed, + BexGasLedger gasLedger, BexMetrics metrics) { + this(value, changeset, events, gasLedger, metrics, null); + } + + public BexExecutionResult(BexValue value, + BexChangeset changeset, + BexEvents events, + BexGasLedger gasLedger, + BexMetrics metrics, + BexAdmittedValue output) { this.value = value; this.changeset = changeset; this.events = events; - this.gasUsed = gasUsed; + this.gasLedger = Objects.requireNonNull(gasLedger, "gasLedger"); this.metrics = metrics != null ? metrics.copy() : new BexMetrics(); + this.output = output; } public BexValue value() { @@ -40,11 +72,41 @@ public BexEvents events() { return events; } + /** + * Returns the sum of the immutable canonical gas trace. + */ public long gasUsed() { - return gasUsed; + return gasLedger.totalGas(); + } + + public BexGasLedger gasLedger() { + return gasLedger; + } + + /** Alias for {@link #gasLedger()}. */ + public BexGasLedger ledger() { + return gasLedger; + } + + public List gasTrace() { + return gasLedger.trace(); + } + + /** Alias for {@link #gasTrace()}. */ + public List trace() { + return gasTrace(); + } + + /** + * Returns the already-admitted root output metadata, or {@code null} when + * the execution did not cross a root Blue output boundary. + */ + public BexAdmittedValue output() { + return output; } public BexMetrics metrics() { return metrics.copy(); } + } diff --git a/src/main/java/blue/bex/result/BexMetrics.java b/src/main/java/blue/bex/result/BexMetrics.java index b830323..ef9820b 100644 --- a/src/main/java/blue/bex/result/BexMetrics.java +++ b/src/main/java/blue/bex/result/BexMetrics.java @@ -36,11 +36,7 @@ public final class BexMetrics { private long pointerCacheHits; private long pointerCacheMisses; private long functionArgMapAllocations; - private long sizeEstimateCalls; - private long sizeEstimateCacheHits; - private long sizeEstimateCacheMisses; private long frozenWriterNodeFallbacks; - private long frozenWriterChildNodeRoundTrips; private long compileNanos; private long executeNanos; @@ -74,11 +70,7 @@ public BexMetrics copy() { copy.pointerCacheHits = pointerCacheHits; copy.pointerCacheMisses = pointerCacheMisses; copy.functionArgMapAllocations = functionArgMapAllocations; - copy.sizeEstimateCalls = sizeEstimateCalls; - copy.sizeEstimateCacheHits = sizeEstimateCacheHits; - copy.sizeEstimateCacheMisses = sizeEstimateCacheMisses; copy.frozenWriterNodeFallbacks = frozenWriterNodeFallbacks; - copy.frozenWriterChildNodeRoundTrips = frozenWriterChildNodeRoundTrips; copy.compileNanos = compileNanos; copy.executeNanos = executeNanos; return copy; @@ -112,11 +104,7 @@ public BexMetrics copy() { public void incrementPointerCacheHits() { pointerCacheHits++; } public void incrementPointerCacheMisses() { pointerCacheMisses++; } public void incrementFunctionArgMapAllocations() { functionArgMapAllocations++; } - public void incrementSizeEstimateCalls() { sizeEstimateCalls++; } - public void incrementSizeEstimateCacheHits() { sizeEstimateCacheHits++; } - public void incrementSizeEstimateCacheMisses() { sizeEstimateCacheMisses++; } public void incrementFrozenWriterNodeFallbacks() { frozenWriterNodeFallbacks++; } - public void incrementFrozenWriterChildNodeRoundTrips() { frozenWriterChildNodeRoundTrips++; } public void addCompileNanos(long nanos) { compileNanos += Math.max(0L, nanos); } public void addExecuteNanos(long nanos) { executeNanos += Math.max(0L, nanos); } @@ -148,11 +136,7 @@ public BexMetrics copy() { public long pointerCacheHits() { return pointerCacheHits; } public long pointerCacheMisses() { return pointerCacheMisses; } public long functionArgMapAllocations() { return functionArgMapAllocations; } - public long sizeEstimateCalls() { return sizeEstimateCalls; } - public long sizeEstimateCacheHits() { return sizeEstimateCacheHits; } - public long sizeEstimateCacheMisses() { return sizeEstimateCacheMisses; } public long frozenWriterNodeFallbacks() { return frozenWriterNodeFallbacks; } - public long frozenWriterChildNodeRoundTrips() { return frozenWriterChildNodeRoundTrips; } public long compileNanos() { return compileNanos; } public long executeNanos() { return executeNanos; } } diff --git a/src/main/java/blue/bex/result/BexPatchEntry.java b/src/main/java/blue/bex/result/BexPatchEntry.java index 7e4b0c0..afafa13 100644 --- a/src/main/java/blue/bex/result/BexPatchEntry.java +++ b/src/main/java/blue/bex/result/BexPatchEntry.java @@ -1,6 +1,7 @@ package blue.bex.result; import blue.bex.BexException; +import blue.bex.output.BexAdmittedValue; import blue.bex.value.BexValue; import blue.bex.value.BexValues; import blue.language.utils.JsonPointer; @@ -18,8 +19,17 @@ public final class BexPatchEntry { private final String absolutePath; private final List absoluteSegments; private final BexValue val; + private final BexAdmittedValue admittedValue; public BexPatchEntry(String op, String authoredPath, String absolutePath, BexValue val) { + this(op, authoredPath, absolutePath, val, null); + } + + public BexPatchEntry(String op, + String authoredPath, + String absolutePath, + BexValue val, + BexAdmittedValue admittedValue) { this.op = Objects.requireNonNull(op, "op"); if (!"add".equals(op) && !"replace".equals(op) && !"remove".equals(op)) { throw new BexException("Unsupported patch op: " + op); @@ -28,6 +38,7 @@ public BexPatchEntry(String op, String authoredPath, String absolutePath, BexVal this.absolutePath = JsonPointer.canonicalize(Objects.requireNonNull(absolutePath, "absolutePath")); this.absoluteSegments = Collections.unmodifiableList(JsonPointer.split(this.absolutePath)); this.val = val != null ? val : BexValues.undefined(); + this.admittedValue = admittedValue; } public String op() { @@ -49,4 +60,13 @@ public List absoluteSegments() { public BexValue val() { return val; } + + /** + * Strict Blue output admitted for this patch value, or {@code null} for a + * remove entry or a compatibility entry built outside engine execution. + * When present, {@link #val()} is the corresponding exact admitted value. + */ + public BexAdmittedValue admittedValue() { + return admittedValue; + } } diff --git a/src/main/java/blue/bex/result/BexResultOverlay.java b/src/main/java/blue/bex/result/BexResultOverlay.java index 29ba732..5ed4e8f 100644 --- a/src/main/java/blue/bex/result/BexResultOverlay.java +++ b/src/main/java/blue/bex/result/BexResultOverlay.java @@ -3,6 +3,7 @@ import blue.bex.api.BexDocumentView; import blue.bex.value.BexValue; import blue.bex.value.BexValues; +import blue.language.Blue; import blue.language.utils.JsonPointer; import java.util.ArrayList; @@ -15,10 +16,19 @@ public final class BexResultOverlay { private final BexDocumentView document; private final List entries = new ArrayList<>(); private final BexMetrics metrics; + private final Blue blue; public BexResultOverlay(BexDocumentView document, BexMetrics metrics) { + this(document, metrics, null); + } + + public BexResultOverlay( + BexDocumentView document, + BexMetrics metrics, + Blue blue) { this.document = document; this.metrics = metrics; + this.blue = blue; } public void append(BexPatchEntry entry) { @@ -33,20 +43,44 @@ public BexValue valueAt(String absolutePointer, List segments) { List selected = segments != null ? segments : JsonPointer.split(pointer); recordOverlayMetric(pointer, selected); if (entries.isEmpty()) { - return document.canonicalAt(pointer); + return exactAt(pointer); } - BexValue materialized = document.canonicalAt("/"); + BexValue materialized = exactAt("/"); for (BexPatchEntry entry : entries) { materialized = apply(materialized, entry); } return materialized.at(selected); } + /** + * Returns the current transient overlay root without selecting a child. + * Pointer traversal and its gas belong to the BEX runtime. + */ + public BexValue rootValue() { + if (entries.isEmpty()) { + return exactAt("/"); + } + BexValue materialized = exactAt("/"); + for (BexPatchEntry entry : entries) { + materialized = apply(materialized, entry); + } + return materialized; + } + + private BexValue exactAt(String pointer) { + return BexValues.referenceBacked( + document.canonicalAt(pointer), + blue); + } + private BexValue apply(BexValue root, BexPatchEntry entry) { if (entry.absoluteSegments().isEmpty()) { return "remove".equals(entry.op()) ? BexValues.undefined() : entry.val(); } - return BexValues.pointerSet(root, entry.absoluteSegments(), entry.val(), + return BexValues.resultOverlayPointerSet( + root, + entry.absoluteSegments(), + entry.val(), "remove".equals(entry.op()) ? "remove" : "set"); } diff --git a/src/main/java/blue/bex/runtime/BexExecutionAccumulator.java b/src/main/java/blue/bex/runtime/BexExecutionAccumulator.java index 7cbc297..d55aadf 100644 --- a/src/main/java/blue/bex/runtime/BexExecutionAccumulator.java +++ b/src/main/java/blue/bex/runtime/BexExecutionAccumulator.java @@ -1,5 +1,8 @@ package blue.bex.runtime; +import blue.bex.output.BexAdmittedValue; +import blue.bex.output.BexOutputAdmission; +import blue.bex.output.BexOutputKind; import blue.bex.result.BexChangeset; import blue.bex.result.BexEvents; import blue.bex.result.BexPatchEntry; @@ -15,18 +18,44 @@ public final class BexExecutionAccumulator { private final List changes = new ArrayList<>(); private final List events = new ArrayList<>(); - private final BexResultOverlay overlay; + private final List admittedEvents = new ArrayList<>(); + private BexResultOverlay overlay; + private final BexOutputAdmission outputAdmission; public BexExecutionAccumulator(BexResultOverlay overlay) { + this(overlay, null); + } + + public BexExecutionAccumulator(BexResultOverlay overlay, + BexOutputAdmission outputAdmission) { this.overlay = overlay; + this.outputAdmission = outputAdmission; } public void appendChange(BexPatchEntry entry) { - changes.add(entry); - overlay.append(entry); + BexPatchEntry admittedEntry = entry; + if (outputAdmission != null && !"remove".equals(entry.op())) { + BexAdmittedValue admitted = + outputAdmission.admit(entry.val(), BexOutputKind.PATCH_VALUE); + admittedEntry = new BexPatchEntry( + entry.op(), + entry.authoredPath(), + entry.absolutePath(), + admitted.value(), + admitted); + } + changes.add(admittedEntry); + overlay.append(admittedEntry); } public void appendEvent(BexValue event) { + if (outputAdmission != null) { + BexAdmittedValue admitted = + outputAdmission.admit(event, BexOutputKind.EVENT); + admittedEvents.add(admitted); + events.add(admitted.value()); + return; + } events.add(event); } @@ -35,10 +64,17 @@ public BexChangeset changeset() { } public BexEvents events() { - return new BexEvents(events); + return new BexEvents(events, admittedEvents); } public BexResultOverlay overlay() { return overlay; } + + void discard(BexResultOverlay resetOverlay) { + changes.clear(); + events.clear(); + admittedEvents.clear(); + overlay = resetOverlay; + } } diff --git a/src/main/java/blue/bex/runtime/BexRuntime.java b/src/main/java/blue/bex/runtime/BexRuntime.java index c7990ee..811a10d 100644 --- a/src/main/java/blue/bex/runtime/BexRuntime.java +++ b/src/main/java/blue/bex/runtime/BexRuntime.java @@ -1,10 +1,16 @@ package blue.bex.runtime; import blue.bex.api.BexExecutionContext; +import blue.bex.api.BexGasLedgerHost; import blue.bex.api.BexIntrinsicRegistry; import blue.bex.compile.BexCompiledProgram; +import blue.bex.gas.BexGasCounter; +import blue.bex.gas.BexGasLimitExceededException; import blue.bex.gas.BexGasMeter; import blue.bex.gas.BexGasSchedule; +import blue.bex.output.BexAdmittedValue; +import blue.bex.output.BexOutputAdmission; +import blue.bex.output.BexOutputKind; import blue.bex.pointer.BexPointerCache; import blue.bex.result.BexChangeset; import blue.bex.result.BexExecutionResult; @@ -14,11 +20,19 @@ import blue.bex.value.BexValue; import blue.bex.value.BexValues; import blue.language.Blue; +import blue.language.processor.GasMeter; +import blue.language.processor.GasLimitExceededException; +import blue.language.processor.ExecutionEvidenceUnavailableException; +import blue.language.processor.InvalidExecutionEvidenceException; +import blue.language.processor.PortableLimitExceededException; +import blue.language.processor.ProcessorFailureException; import blue.language.utils.JsonPointer; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Set; /** * Runtime for one compiled BEX execution. @@ -31,7 +45,11 @@ public final class BexRuntime { private final BexPointerCache pointerCache; private final BexExecutionAccumulator accumulator; private final BexBlueTypeMatcher typeMatcher; + private final Blue blue; private final BexIntrinsicRegistry intrinsics; + private final BexOutputAdmission outputAdmission; + private final BexGasLedgerHost gasLedgerHost; + private final BexResultOverlay rollbackOverlay; public BexRuntime(BexCompiledProgram program, BexExecutionContext context, @@ -51,18 +69,51 @@ public BexRuntime(BexCompiledProgram program, BexIntrinsicRegistry intrinsics) { this.program = program; this.context = context; - this.gas = new BexGasMeter(gasSchedule, context.gasLimit(), metrics); + this.blue = blue; + this.intrinsics = intrinsics != null + ? intrinsics + : BexIntrinsicRegistry.empty(); + this.gasLedgerHost = context.gasLedgerHost(); + this.gas = newGasMeter( + context, + gasSchedule, + gasLedgerHost, + this.intrinsics, + program.requiredIntrinsicBlueIds()); this.metrics = metrics; this.pointerCache = pointerCache; - this.accumulator = new BexExecutionAccumulator(new BexResultOverlay(context.document(), metrics)); + this.outputAdmission = new BexOutputAdmission( + gas, context.semanticIdentityBoundary()); + BexResultOverlay activeOverlay = + new BexResultOverlay( + context.document(), metrics, blue); + this.accumulator = new BexExecutionAccumulator( + activeOverlay, + outputAdmission); + this.rollbackOverlay = new BexResultOverlay( + context.document(), metrics, blue); this.typeMatcher = new BexBlueTypeMatcher(blue); - this.intrinsics = intrinsics != null ? intrinsics : BexIntrinsicRegistry.empty(); } public BexExecutionResult execute() { - metrics.incrementCompiledExecutions(); - BexValue value = program.execute(this); - return new BexExecutionResult(value, accumulator.changeset(), accumulator.events(), gas.used(), metrics); + try { + BexValue value = program.execute(this); + BexAdmittedValue output = + outputAdmission.admit(value, BexOutputKind.ROOT_RESULT); + BexExecutionResult result = new BexExecutionResult( + value, + accumulator.changeset(), + accumulator.events(), + gas.ledger(), + metrics, + output); + submitHostLedger(); + return result; + } catch (RuntimeException | Error ex) { + accumulator.discard(rollbackOverlay); + finishHostLedgerAfterFailure(ex); + throw ex; + } } public BexCompiledProgram program() { return program; } @@ -73,47 +124,67 @@ public BexExecutionResult execute() { public BexExecutionAccumulator accumulator() { return accumulator; } public BexBlueTypeMatcher typeMatcher() { return typeMatcher; } public BexIntrinsicRegistry intrinsics() { return intrinsics; } + public BexOutputAdmission outputAdmission() { return outputAdmission; } public BexValue readDocument(String absolutePointer, List precompiledSegments, boolean resolved) { + gas.charge(BexGasCounter.DOCUMENT_READ); if (resolved) { metrics.incrementResolvedDocumentReads(); - gas.charge(gas.schedule().documentRead); - return context.document().resolvedAt(absolutePointer); + } else { + metrics.incrementFrozenDocumentReads(); } - metrics.incrementFrozenDocumentReads(); - gas.charge(gas.schedule().documentRead); - return context.document().canonicalAt(absolutePointer); + + /* + * Traverse from the host's exact root so every intermediate reference + * can be materialized lazily through Blue's verified provider + * boundary. A final pure or cyclic-set reference stays opaque when the + * program only carries it or asks for its established identity. + */ + return readValuePointer(documentAt("/", resolved), + precompiledSegments); + } + + private BexValue documentAt(String absolutePointer, boolean resolved) { + return resolved + ? context.document().resolvedAt(absolutePointer) + : context.document().canonicalAt(absolutePointer); } public BexValue readEvent(List precompiledSegments) { + gas.charge(BexGasCounter.EVENT_READ); metrics.incrementEventReads(); - gas.charge(gas.schedule().eventRead); - return context.event().at(precompiledSegments); + return readValuePointer(context.event(), precompiledSegments); + } + + public BexValue readProcessingEvent(List precompiledSegments) { + gas.charge(BexGasCounter.PROCESSING_EVENT_READ); + return readValuePointer(context.processingEvent(), precompiledSegments); } public BexValue readCurrentContract(List precompiledSegments) { + gas.charge(BexGasCounter.CURRENT_CONTRACT_READ); metrics.incrementCurrentContractReads(); - gas.charge(gas.schedule().currentContractRead); - return context.currentContract().at(precompiledSegments); + return readValuePointer(context.currentContract(), precompiledSegments); } public BexValue readBinding(String name, List pathSegments) { - gas.charge(gas.schedule().varRead); + gas.charge(BexGasCounter.BINDING_READ); if (name == null || name.isEmpty()) { return BexValues.undefined(); } - return context.binding(name).at(pathSegments); + return readValuePointer(context.binding(name), pathSegments); } public BexValue readSteps(String step, List pathSegments) { + gas.charge(BexGasCounter.STEPS_READ); metrics.incrementStepsReads(); - gas.charge(gas.schedule().stepsRead); - return context.steps().step(step).at(pathSegments); + return readValuePointer(context.steps().step(step), pathSegments); } public BexValue readResultValue(String absolutePointer, List segments) { - gas.charge(gas.schedule().resultValueRead); - return accumulator.overlay().valueAt(absolutePointer, segments); + gas.charge(BexGasCounter.RESULT_VALUE_READ); + metrics.incrementResultValueReads(); + return readValuePointer(accumulator.overlay().rootValue(), segments); } public BexValue defaultResultValue() { @@ -125,7 +196,22 @@ public BexValue defaultResultValue() { } public BexValue invokeIntrinsic(String blueId, BexValue type, Map fields) { - return intrinsics.invoke(blueId, type, fields, gas::charge, gas::used); + return intrinsics.invoke( + blueId, type, fields, gas, outputAdmission); + } + + public BexValue nodeBlueId(BexValue value) { + gas.charge(BexGasCounter.NODE_IDENTITY_REQUESTED); + if (value == null || value.isUndefined()) { + throw new blue.bex.BexException( + "$nodeBlueId operand must not be undefined"); + } + if (value.isExact()) { + return BexValues.scalar(value.exactBlueId()); + } + return BexValues.scalar(outputAdmission + .admit(value, BexOutputKind.NODE_IDENTITY) + .nodeBlueId()); } public String resolvePointer(String authoredPointer) { @@ -136,7 +222,252 @@ public List parseDynamicPointer(String pointer) { return pointerCache.get(pointer, metrics).segments(); } + /** + * Traverses one semantic value pointer with canonical per-segment read + * ownership. The charge is admitted before examining each next member. + */ + public BexValue readValuePointer(BexValue root, List segments) { + BexValue current = BexValues.referenceBacked( + root != null ? root : BexValues.undefined(), + blue); + if (segments == null) { + return current; + } + for (String segment : segments) { + if (current.isUndefined()) { + return current; + } + gas.charge(BexGasCounter.POINTER_SEGMENT_READ); + if (current.isList()) { + gas.charge(BexGasCounter.LIST_ITEM_READ); + } else if (current.isObject()) { + gas.charge(BexGasCounter.OBJECT_MEMBER_READ); + } + current = current.get(segment); + } + return current; + } + public String canonicalPointer(String pointer) { return JsonPointer.canonicalize(pointer); } + + private static BexGasMeter newGasMeter(BexExecutionContext context, + BexGasSchedule gasSchedule, + BexGasLedgerHost host, + BexIntrinsicRegistry intrinsics, + Set + requiredIntrinsicBlueIds) { + Map registered = + intrinsics.registeredNamedWeights( + requiredIntrinsicBlueIds); + Map> namespaceWeights = + intrinsics.registeredNamespaceWeights( + requiredIntrinsicBlueIds); + if (host == null) { + return new BexGasMeter( + gasSchedule, + context.parentRemainingGas(), + context.gasLimit(), + registered); + } + if (!host.separatesRuntimeNamespaces()) { + if (!namespaceWeights.isEmpty()) { + throw new IllegalArgumentException( + "A hosted intrinsic registry requires separate runtime namespaces"); + } + } + LinkedHashMap children = + new LinkedHashMap<>(); + try { + children.put( + BexGasCounter.NAMESPACE, + requireOpenedLedger( + host.open( + BexGasCounter.NAMESPACE, + gasSchedule.counterWeights()), + BexGasCounter.NAMESPACE)); + for (Map.Entry> intrinsic + : namespaceWeights.entrySet()) { + children.put( + intrinsic.getKey(), + requireOpenedLedger( + host.open( + intrinsic.getKey(), + intrinsic.getValue()), + intrinsic.getKey())); + } + return new BexGasMeter( + gasSchedule, + children, + context.gasLimit(), + registered); + } catch (RuntimeException | Error openingFailure) { + finishOpenedLedgersAfterConstructionFailure( + host, children, openingFailure); + throw openingFailure; + } + } + + private static GasMeter.ChildGasLedger requireOpenedLedger( + GasMeter.ChildGasLedger ledger, + String namespace) { + if (ledger == null) { + throw new IllegalStateException( + "Gas host returned no child ledger for " + + namespace); + } + return ledger; + } + + private static void finishOpenedLedgersAfterConstructionFailure( + BexGasLedgerHost host, + Map opened, + Throwable openingFailure) { + boolean unavailable = + evidenceUnavailableWins(openingFailure); + for (GasMeter.ChildGasLedger ledger : opened.values()) { + try { + if (unavailable) { + host.evidenceUnavailable(ledger); + } else { + host.failedDeterministically(ledger); + } + } catch (RuntimeException | Error lifecycleFailure) { + addSuppressed( + openingFailure, lifecycleFailure); + } + } + } + + private void submitHostLedger() { + if (gasLedgerHost == null + || gas.hostLedgerFinalized()) { + return; + } + gas.submitHostLedger(gasLedgerHost::submit); + } + + private void finishHostLedgerAfterFailure(Throwable primaryFailure) { + if (gasLedgerHost == null || gas.hostLedgerFinalized()) { + return; + } + if (evidenceUnavailableWins(primaryFailure)) { + notifyFailureLifecycle( + primaryFailure, + () -> gas.unavailableHostLedger( + gasLedgerHost::evidenceUnavailable)); + return; + } + + GasLimitExceededException hostExhaustion = + findCause( + primaryFailure, + GasLimitExceededException.class); + if (hostExhaustion != null) { + try { + gas.propagateHostGasExhaustion( + hostExhaustion, + gasLedgerHost::failedDeterministically, + gasLedgerHost::propagateGasExhaustion); + } catch (GasLimitExceededException canonical) { + if (canonical == hostExhaustion) { + throw canonical; + } + addSuppressed(primaryFailure, canonical); + } catch (RuntimeException | Error lifecycleFailure) { + addSuppressed(primaryFailure, lifecycleFailure); + } + return; + } + + BexGasLimitExceededException localExhaustion = + findCause( + primaryFailure, + BexGasLimitExceededException.class); + if (localExhaustion != null) { + notifyFailureLifecycle( + primaryFailure, + () -> gas.failHostLedger( + gasLedgerHost::failedDeterministically)); + if (!(primaryFailure instanceof RuntimeException)) { + return; + } + throw Objects.requireNonNull( + gasLedgerHost.localGasLimitExceeded( + localExhaustion, + (RuntimeException) primaryFailure), + "local gas-limit mapping"); + } + + notifyFailureLifecycle( + primaryFailure, + () -> gas.failHostLedger( + gasLedgerHost::failedDeterministically)); + } + + private static void notifyFailureLifecycle( + Throwable primaryFailure, + Runnable lifecycle) { + try { + lifecycle.run(); + } catch (RuntimeException | Error lifecycleFailure) { + addSuppressed(primaryFailure, lifecycleFailure); + } + } + + private static void addSuppressed( + Throwable primaryFailure, + Throwable lifecycleFailure) { + if (primaryFailure != lifecycleFailure) { + primaryFailure.addSuppressed(lifecycleFailure); + } + } + + /** + * Resolves lifecycle classification in causal order. A directly reported + * deterministic category is authoritative and cannot be reclassified by + * an unavailable exception nested below it. + */ + private static boolean evidenceUnavailableWins( + Throwable failure) { + Throwable current = failure; + while (current != null) { + if (current instanceof ProcessorFailureException + || current + instanceof InvalidExecutionEvidenceException + || current + instanceof PortableLimitExceededException + || current + instanceof GasLimitExceededException + || current + instanceof BexGasLimitExceededException) { + return false; + } + if (current + instanceof ExecutionEvidenceUnavailableException) { + return true; + } + Throwable cause = current.getCause(); + if (cause == current) { + break; + } + current = cause; + } + return false; + } + + private static T findCause( + Throwable failure, + Class type) { + Throwable current = failure; + while (current != null) { + if (type.isInstance(current)) { + return type.cast(current); + } + current = current.getCause(); + } + return null; + } + } diff --git a/src/main/java/blue/bex/runtime/CompileScope.java b/src/main/java/blue/bex/runtime/CompileScope.java index beef508..941a51e 100644 --- a/src/main/java/blue/bex/runtime/CompileScope.java +++ b/src/main/java/blue/bex/runtime/CompileScope.java @@ -11,6 +11,7 @@ public final class CompileScope { private final CompileScope parent; private final Map slots = new LinkedHashMap<>(); + private int nextSlot; public CompileScope() { this(null); @@ -18,6 +19,7 @@ public CompileScope() { public CompileScope(CompileScope parent) { this.parent = parent; + this.nextSlot = parent != null ? parent.frameSize() : 0; } public int declareOrGetSlot(String name) { @@ -25,7 +27,10 @@ public int declareOrGetSlot(String name) { if (existing != null) { return existing; } - int slot = slots.size(); + if (parent != null && parent.hasSlot(name)) { + return parent.resolveSlot(name); + } + int slot = nextSlot++; slots.put(name, slot); return slot; } @@ -46,6 +51,34 @@ public boolean hasSlot(String name) { } public int frameSize() { - return slots.size(); + return Math.max(nextSlot, parent != null ? parent.frameSize() : 0); + } + + /** + * Captures which names are visible without rewinding allocated frame slots. + * + *

Collection-query bindings are lexical only for the query expression. + * Restoring visibility removes names introduced by the query while keeping + * the allocated slots in the function frame so the compiled expression can + * still use them at runtime.

+ */ + public Visibility captureVisibility() { + return new Visibility(new LinkedHashMap<>(slots)); + } + + public void restoreVisibility(Visibility visibility) { + if (visibility == null) { + throw new IllegalArgumentException("visibility is required"); + } + slots.clear(); + slots.putAll(visibility.slots); + } + + public static final class Visibility { + private final Map slots; + + private Visibility(Map slots) { + this.slots = slots; + } } } diff --git a/src/main/java/blue/bex/runtime/CompiledFrame.java b/src/main/java/blue/bex/runtime/CompiledFrame.java index 3d873eb..eb79cf6 100644 --- a/src/main/java/blue/bex/runtime/CompiledFrame.java +++ b/src/main/java/blue/bex/runtime/CompiledFrame.java @@ -1,5 +1,6 @@ package blue.bex.runtime; +import blue.bex.BexException; import blue.bex.BexSourcePath; import blue.bex.value.BexValue; import blue.bex.value.BexValues; @@ -31,6 +32,27 @@ public BexValue get(int slot) { return value != null ? value : BexValues.undefined(); } + /** + * Reads a declared slot and fails when its initializer has not completed. + */ + public BexValue getRequired(int slot) { + BexValue value = slots[slot]; + if (value != null) { + return value; + } + BexSourcePath path = sourcePath(); + String message = "Binding is uninitialized"; + throw path != null ? BexException.at(path, message) : new BexException(message); + } + + public boolean isInitialized(int slot) { + return slots[slot] != null; + } + + public void clear(int slot) { + slots[slot] = null; + } + public void set(int slot, BexValue value) { slots[slot] = value != null ? value : BexValues.undefined(); } @@ -47,6 +69,10 @@ public BexValue readEvent(List precompiledSegments) { return runtime.readEvent(precompiledSegments); } + public BexValue readProcessingEvent(List precompiledSegments) { + return runtime.readProcessingEvent(precompiledSegments); + } + public BexValue readCurrentContract(List precompiledSegments) { return runtime.readCurrentContract(precompiledSegments); } diff --git a/src/main/java/blue/bex/type/BexBlueTypeMatcher.java b/src/main/java/blue/bex/type/BexBlueTypeMatcher.java index 68bf6c3..4beda68 100644 --- a/src/main/java/blue/bex/type/BexBlueTypeMatcher.java +++ b/src/main/java/blue/bex/type/BexBlueTypeMatcher.java @@ -1,33 +1,967 @@ package blue.bex.type; -import blue.bex.value.BexNodeWriter; +import blue.bex.BexSourcePath; +import blue.bex.gas.BexGasLimitExceededException; +import blue.bex.gas.BexGasCounter; +import blue.bex.gas.BexGasMeter; +import blue.bex.value.BexBlueNodeWriter; import blue.bex.value.BexValue; +import blue.bex.value.BexValues; import blue.language.Blue; import blue.language.model.Node; +import blue.language.model.Schema; +import blue.language.processor.ExecutionEvidenceUnavailableException; +import blue.language.processor.GasLimitExceededException; +import blue.language.processor.InvalidExecutionEvidenceException; +import blue.language.processor.PortableLimitExceededException; +import blue.language.processor.ProcessorFailureException; import blue.language.snapshot.FrozenNode; +import blue.language.utils.FrozenTypeMatcher; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static blue.language.utils.Properties.BOOLEAN_TYPE_BLUE_ID; +import static blue.language.utils.Properties.DOUBLE_TYPE_BLUE_ID; +import static blue.language.utils.Properties.INTEGER_TYPE_BLUE_ID; +import static blue.language.utils.Properties.TEXT_TYPE_BLUE_ID; /** * BEX boundary adapter for Blue's node/type matcher. */ public final class BexBlueTypeMatcher { + private static final int TEXT_BLOCK_CODE_POINTS = 64; + private final Blue blue; + private final FrozenTypeMatcher matcher; public BexBlueTypeMatcher(Blue blue) { this.blue = blue != null ? blue : new Blue(); + this.matcher = FrozenTypeMatcher.withVerifiedReferenceMaterializer( + reference -> FrozenNode.fromResolvedNode( + BexValues.referenceBacked( + BexValues.frozen(reference), + this.blue) + .toNode())); } - public boolean matches(BexValue value, FrozenNode pattern) { + /** + * Matches a BEX value at the Blue Language boundary while recording the + * canonical BEX comparison work. + * + *

The metering walk deliberately does not depend on + * {@link FrozenTypeMatcher}'s caches. Every semantic occurrence that the + * pattern compares is admitted before that recursive match is performed, + * so warm and cold executions have the same BEX trace.

+ */ + public boolean matches(BexValue value, + FrozenNode pattern, + BexGasMeter gas, + BexSourcePath sourcePath) { if (value == null || value.isUndefined()) { return false; } - if (pattern == null || pattern.isEmptyNode()) { + if (pattern == null) { + return true; + } + MatchGas matchGas = new MatchGas(gas, sourcePath); + return matchesMetered( + value, + pattern, + matchGas, + CandidatePosition.ROOT); + } + + private static RuntimeException classifiedBoundaryFailure( + RuntimeException failure) { + Throwable current = failure; + while (current != null) { + if (current + instanceof ExecutionEvidenceUnavailableException + || current + instanceof InvalidExecutionEvidenceException + || current + instanceof ProcessorFailureException + || current + instanceof PortableLimitExceededException + || current + instanceof GasLimitExceededException + || current + instanceof BexGasLimitExceededException) { + return (RuntimeException) current; + } + Throwable cause = current.getCause(); + if (cause == current) { + break; + } + current = cause; + } + /* + * An unexpected cursor/provider failure is still an execution + * failure. It must never be converted into a semantic non-match. + */ + return failure; + } + + private boolean matchesMetered(FrozenNode candidate, + FrozenNode pattern, + MatchGas gas) { + gas.comparisonNode(); + return matchesFrozenAfterAdmission( + candidate, pattern, gas); + } + + private boolean matchesFrozenAfterAdmission( + FrozenNode candidate, + FrozenNode pattern, + MatchGas gas) { + if (pattern.isEmptyNode()) { return true; } + + /* + * A pure BlueId pattern is an identity/type check. It has no nested + * semantic occurrences to compare. + */ + if (pattern.isReferenceOnly()) { + return matcher.matchesType(candidate, pattern); + } + + if (!meterScalarComparison(candidate.getValue(), + pattern.getValue(), gas)) { + return false; + } + + if (!meterItemType(candidate, pattern.getItemType(), gas)) { + return false; + } + if (!meterKeyType(candidate, pattern.getKeyType(), gas)) { + return false; + } + if (!meterValueType(candidate, pattern.getValueType(), gas)) { + return false; + } + if (!meterItems(candidate, pattern.getItems(), gas)) { + return false; + } + if (!meterProperties(candidate, pattern.getProperties(), gas)) { + return false; + } + + /* + * Blue remains authoritative for declared-type, schema, subtype, and + * provider semantics. The canonical recursive BEX work above has + * already been admitted before this call can perform it. + */ + return matcher.matchesType(candidate, pattern); + } + + /** + * Walks a candidate cursor lazily. The current occurrence is admitted + * before any semantic cursor access, and a descendant is not converted or + * materialized until its own recursive admission succeeds. + */ + private boolean matchesMetered( + BexValue candidate, + FrozenNode pattern, + MatchGas gas, + CandidatePosition position) { + gas.comparisonNode(); + return matchesAfterAdmission( + candidate, pattern, gas, position); + } + + private boolean matchesAfterAdmission( + BexValue candidate, + FrozenNode pattern, + MatchGas gas, + CandidatePosition position) { + if (pattern.isEmptyNode()) { + return true; + } + if (candidate == null + || candidate.isUndefined()) { + return !requiresPresence(pattern); + } + + /* + * Reference-only matching has no nested BEX occurrences. Blue may + * therefore materialize the admitted current occurrence directly. + */ + if (pattern.isReferenceOnly()) { + if (candidate.isExact() + && pattern.getReferenceBlueId().equals( + candidate.exactBlueId())) { + return true; + } + /* + * A transient candidate must still be a valid local Blue shape + * before its identity can be compared. Invalid local content is + * a semantic non-match; provider and cursor failures raised while + * inspecting the admitted occurrence continue to propagate. + */ + if (!candidate.isExact()) { + CandidateView referenceView; + try { + referenceView = CandidateView.from( + candidate, position); + } catch (RuntimeException viewFailure) { + throw classifiedBoundaryFailure( + viewFailure); + } + if (!referenceView.valid) { + return false; + } + } + return matchesAuthoritatively( + candidate, pattern, position); + } + + CandidateView view; try { - Node valueNode = BexNodeWriter.toNode(value); - return blue.nodeMatchesType(FrozenNode.fromResolvedNode(valueNode), pattern); - } catch (RuntimeException ex) { + view = CandidateView.from( + candidate, position); + } catch (RuntimeException viewFailure) { + throw classifiedBoundaryFailure(viewFailure); + } + if (!view.valid) { + return false; + } + if (view.materializeCurrent) { + FrozenNode materialized = freezeCandidate( + candidate, position); + return materialized != null + && matchesFrozenAfterAdmission( + materialized, pattern, gas); + } + + if (!meterScalarComparison( + view.scalar, pattern.getValue(), gas)) { + return false; + } + if (!meterItemType( + view, pattern.getItemType(), gas)) { + return false; + } + if (!meterKeyType( + view, pattern.getKeyType(), gas)) { + return false; + } + if (!meterValueType( + view, pattern.getValueType(), gas)) { + return false; + } + if (!meterItems( + view, pattern.getItems(), gas)) { + return false; + } + if (!meterProperties( + view, pattern.getProperties(), gas)) { return false; } + return matchesAuthoritatively( + candidate, pattern, position); + } + + private boolean matchesAuthoritatively( + BexValue candidate, + FrozenNode pattern, + CandidatePosition position) { + FrozenNode frozen = freezeCandidate( + candidate, position); + return frozen != null + && matcher.matchesType(frozen, pattern); + } + + private FrozenNode freezeCandidate( + BexValue candidate, + CandidatePosition position) { + try { + if (candidate.isExact()) { + return FrozenNode.fromResolvedNode( + candidate.toNode()); + } + if (position == CandidatePosition.ROOT) { + return FrozenNode.fromResolvedNode( + BexBlueNodeWriter.toSemanticNode( + candidate)); + } + if (position == CandidatePosition.LIST_ITEM) { + BexValue wrapper = BexValues.list( + Collections.singletonList(candidate)); + FrozenNode frozenWrapper = + FrozenNode.fromResolvedNode( + BexBlueNodeWriter + .toSemanticNode(wrapper)); + return frozenWrapper.getItems().get(0); + } + Map member = + Collections.singletonMap( + "_bexCandidate", candidate); + FrozenNode frozenWrapper = + FrozenNode.fromResolvedNode( + BexBlueNodeWriter.toSemanticNode( + BexValues.map(member))); + return frozenWrapper.getProperties().get( + "_bexCandidate"); + } catch (RuntimeException conversionFailure) { + throw classifiedBoundaryFailure( + conversionFailure); + } + } + + private boolean meterItemType(FrozenNode candidate, + FrozenNode targetItemType, + MatchGas gas) { + if (targetItemType == null) { + return true; + } + List items = candidate.getItems(); + if (items == null) { + return true; + } + for (FrozenNode item : items) { + if (!matchesMetered(item, targetItemType, gas)) { + return false; + } + } + return true; + } + + private boolean meterItemType( + CandidateView candidate, + FrozenNode targetItemType, + MatchGas gas) { + if (targetItemType == null + || candidate.items == null) { + return true; + } + for (int index = 0; + index < candidate.items.size(); + index++) { + gas.comparisonNode(); + BexValue item = candidate.items.get( + String.valueOf(index)); + if (!matchesAfterAdmission( + item, + targetItemType, + gas, + CandidatePosition.LIST_ITEM)) { + return false; + } + } + return true; + } + + private boolean meterKeyType(FrozenNode candidate, + FrozenNode targetKeyType, + MatchGas gas) { + if (targetKeyType == null || candidate.getProperties() == null) { + return true; + } + for (String key + : candidate.getProperties().keySet()) { + gas.comparisonNode(); + if (!keyMatchesType(key, targetKeyType)) { + return false; + } + } + return true; + } + + private boolean meterKeyType( + CandidateView candidate, + FrozenNode targetKeyType, + MatchGas gas) { + if (targetKeyType == null) { + return true; + } + for (String key : candidate.propertyKeys) { + gas.comparisonNode(); + if (!keyMatchesType(key, targetKeyType)) { + return false; + } + } + return true; + } + + private boolean meterValueType(FrozenNode candidate, + FrozenNode targetValueType, + MatchGas gas) { + if (targetValueType == null || candidate.getProperties() == null) { + return true; + } + for (String key + : candidate.getProperties().keySet()) { + if (!matchesMetered( + candidate.getProperties().get(key), + targetValueType, + gas)) { + return false; + } + } + return true; + } + + private boolean meterValueType( + CandidateView candidate, + FrozenNode targetValueType, + MatchGas gas) { + if (targetValueType == null) { + return true; + } + for (String key : candidate.propertyKeys) { + gas.comparisonNode(); + BexValue property = + candidate.source.get(key); + if (!matchesAfterAdmission( + property, + targetValueType, + gas, + CandidatePosition.OBJECT_MEMBER)) { + return false; + } + } + return true; + } + + private boolean meterItems(FrozenNode candidate, + List targetItems, + MatchGas gas) { + if (targetItems == null) { + return true; + } + List candidateItems = candidate.getItems() != null + ? candidate.getItems() + : Collections.emptyList(); + for (int index = 0; index < targetItems.size(); index++) { + FrozenNode targetItem = targetItems.get(index); + if (index < candidateItems.size()) { + if (!matchesMetered( + candidateItems.get(index), targetItem, gas)) { + return false; + } + } else if (requiresPresence(targetItem)) { + return false; + } + } + return true; + } + + private boolean meterItems( + CandidateView candidate, + List targetItems, + MatchGas gas) { + if (targetItems == null) { + return true; + } + int candidateSize = candidate.items != null + ? candidate.items.size() + : 0; + for (int index = 0; + index < targetItems.size(); + index++) { + FrozenNode targetItem = + targetItems.get(index); + if (index < candidateSize) { + gas.comparisonNode(); + BexValue item = candidate.items.get( + String.valueOf(index)); + if (!matchesAfterAdmission( + item, + targetItem, + gas, + CandidatePosition.LIST_ITEM)) { + return false; + } + } else if (requiresPresence(targetItem)) { + return false; + } + } + return true; + } + + private boolean meterProperties( + FrozenNode candidate, + Map targetProperties, + MatchGas gas) { + if (targetProperties == null) { + return true; + } + Map candidateProperties = + candidate.getProperties() != null + ? candidate.getProperties() + : Collections.emptyMap(); + for (Map.Entry candidateEntry + : candidateProperties.entrySet()) { + String key = candidateEntry.getKey(); + FrozenNode targetProperty = + targetProperties.get(key); + if (targetProperty != null) { + if (!matchesMetered( + candidateEntry.getValue(), + targetProperty, + gas)) { + return false; + } + } + } + for (Map.Entry targetEntry + : targetProperties.entrySet()) { + if (!candidateProperties.containsKey( + targetEntry.getKey()) + && requiresPresence( + targetEntry.getValue())) { + return false; + } + } + return true; + } + + private boolean meterProperties( + CandidateView candidate, + Map targetProperties, + MatchGas gas) { + if (targetProperties == null) { + return true; + } + for (String key : candidate.propertyKeys) { + FrozenNode targetProperty = + targetProperties.get(key); + if (targetProperty != null) { + gas.comparisonNode(); + BexValue candidateProperty = + candidate.source.get(key); + if (!matchesAfterAdmission( + candidateProperty, + targetProperty, + gas, + CandidatePosition.OBJECT_MEMBER)) { + return false; + } + } + } + for (Map.Entry targetEntry + : targetProperties.entrySet()) { + if (!candidate.hasProperty( + targetEntry.getKey()) + && requiresPresence( + targetEntry.getValue())) { + return false; + } + } + return true; + } + + private boolean meterScalarComparison(Object candidate, + Object target, + MatchGas gas) { + if (target == null) { + return true; + } + if (candidate == null) { + return false; + } + if (candidate instanceof Number && target instanceof Number) { + gas.integerLimbs( + integerLimbs(unscaled(candidate)) + + integerLimbs(unscaled(target)) + + (candidate instanceof BigDecimal + || target instanceof BigDecimal ? 1L : 0L)); + return number(candidate).compareTo(number(target)) == 0; + } + if (candidate instanceof String && target instanceof String) { + return gas.compareText( + (String) candidate, + (String) target) == 0; + } + return candidate.equals(target); + } + + private static boolean isOrdinaryProperty( + String key) { + return !"name".equals(key) + && !"description".equals(key) + && !"type".equals(key) + && !"itemType".equals(key) + && !"keyType".equals(key) + && !"valueType".equals(key) + && !"mergePolicy".equals(key) + && !"value".equals(key) + && !"items".equals(key) + && !"blueId".equals(key) + && !"contracts".equals(key) + && !"schema".equals(key) + && !isForbiddenField(key); + } + + private static boolean isForbiddenField( + String key) { + return "properties".equals(key) + || "constraints".equals(key) + || "allowMultiple".equals(key) + || "options".equals(key) + || "blue".equals(key) + || "$previous".equals(key) + || "$pos".equals(key) + || "$replace".equals(key) + || "$empty".equals(key); + } + + private enum CandidatePosition { + ROOT, + OBJECT_MEMBER, + LIST_ITEM + } + + /** + * Current-node-only view of the transient Blue conversion contract. + * Descendant values remain as cursors and are not converted here. + */ + private static final class CandidateView { + private final BexValue source; + private final Object scalar; + private final BexValue items; + private final List propertyKeys; + private final boolean materializeCurrent; + private final boolean valid; + + private CandidateView( + BexValue source, + Object scalar, + BexValue items, + List propertyKeys, + boolean materializeCurrent, + boolean valid) { + this.source = source; + this.scalar = scalar; + this.items = items; + this.propertyKeys = propertyKeys; + this.materializeCurrent = + materializeCurrent; + this.valid = valid; + } + + private static CandidateView from( + BexValue source, + CandidatePosition position) { + if (source == null || source.isUndefined()) { + return invalid(source); + } + if (source.isNull()) { + return empty(source); + } + if (source.isScalar()) { + return new CandidateView( + source, + source.toSimple(), + null, + Collections.emptyList(), + false, + true); + } + if (source.isList()) { + return new CandidateView( + source, + null, + source, + Collections.emptyList(), + false, + true); + } + if (!source.isObject()) { + return invalid(source); + } + + List keys = source.keys(); + if (isEmptyPlaceholder(source, keys)) { + return position == CandidatePosition.LIST_ITEM + ? empty(source) + : invalid(source); + } + + boolean hasBlueId = false; + boolean hasValue = false; + boolean hasItems = false; + int retainedFields = 0; + BexValue items = null; + ArrayList properties = + new ArrayList<>(); + for (String key : keys) { + retainedFields++; + if (isForbiddenField(key)) { + return invalid(source); + } + if ("blueId".equals(key)) { + hasBlueId = true; + } else if ("value".equals(key)) { + BexValue child = source.get(key); + hasValue = true; + if (child == null + || child.isUndefined() + || child.isNull() + || !child.isScalar()) { + return invalid(source); + } + } else if ("items".equals(key)) { + BexValue child = source.get(key); + hasItems = true; + if (child == null + || child.isUndefined() + || !child.isList()) { + return invalid(source); + } + items = child; + } else if (isOrdinaryProperty(key)) { + properties.add(key); + } + } + + if (hasBlueId) { + return retainedFields == 1 + ? materialized(source) + : invalid(source); + } + int payloadKinds = (hasValue ? 1 : 0) + + (hasItems ? 1 : 0) + + (!properties.isEmpty() ? 1 : 0); + if (payloadKinds > 1) { + return invalid(source); + } + /* + * Explicit scalar types normalize their raw value. Materializing + * this already-admitted current occurrence is the faithful, + * bounded way to obtain that scalar without touching any payload + * descendants (a scalar has none). + */ + if (hasValue) { + return materialized(source); + } + return new CandidateView( + source, + null, + items, + Collections.unmodifiableList( + properties), + false, + true); + } + + private boolean hasProperty(String key) { + return isOrdinaryProperty(key) + && propertyKeys.contains(key); + } + + private static CandidateView empty( + BexValue source) { + return new CandidateView( + source, + null, + null, + Collections.emptyList(), + false, + true); + } + + private static CandidateView materialized( + BexValue source) { + return new CandidateView( + source, + null, + null, + Collections.emptyList(), + true, + true); + } + + private static CandidateView invalid( + BexValue source) { + return new CandidateView( + source, + null, + null, + Collections.emptyList(), + false, + false); + } + + private static boolean isEmptyPlaceholder( + BexValue source, + List keys) { + if (keys.size() != 1 + || !"$empty".equals(keys.get(0))) { + return false; + } + BexValue marker = source.get("$empty"); + return marker != null + && marker.isScalar() + && Boolean.TRUE.equals( + marker.toSimple()); + } + } + + private boolean requiresPresence(FrozenNode target) { + Schema schema = target.getSchema(); + if (schema != null && Boolean.TRUE.equals( + schema.getRequiredValue())) { + return true; + } + return hasValueInNestedStructure(target); + } + + private boolean hasValueInNestedStructure(FrozenNode node) { + if (node.isReferenceOnly() || node.getValue() != null) { + return true; + } + if (node.getItems() != null) { + for (FrozenNode item : node.getItems()) { + if (hasValueInNestedStructure(item)) { + return true; + } + } + } + if (node.getProperties() != null) { + for (FrozenNode property : node.getProperties().values()) { + if (hasValueInNestedStructure(property)) { + return true; + } + } + } + return false; + } + + private boolean keyMatchesType(String key, FrozenNode targetKeyType) { + String identity = targetKeyType.getReferenceBlueId(); + if (identity == null && targetKeyType.getType() != null) { + identity = targetKeyType.getType().getReferenceBlueId(); + } + if (TEXT_TYPE_BLUE_ID.equals(identity)) { + return true; + } + if (INTEGER_TYPE_BLUE_ID.equals(identity)) { + try { + new BigInteger(key); + return true; + } catch (NumberFormatException invalidInteger) { + return false; + } + } + if (DOUBLE_TYPE_BLUE_ID.equals(identity)) { + try { + return Double.isFinite(Double.parseDouble(key)); + } catch (NumberFormatException invalidDouble) { + return false; + } + } + if (BOOLEAN_TYPE_BLUE_ID.equals(identity)) { + return "true".equalsIgnoreCase(key) + || "false".equalsIgnoreCase(key); + } + return false; + } + + private static BigDecimal number(Object value) { + if (value instanceof BigDecimal) { + return (BigDecimal) value; + } + if (value instanceof BigInteger) { + return new BigDecimal((BigInteger) value); + } + return new BigDecimal(value.toString()); + } + + private static BigInteger unscaled(Object value) { + return number(value).unscaledValue(); + } + + private static long integerLimbs(BigInteger value) { + int bits = value.abs().bitLength(); + return Math.max(1L, (bits + 31L) / 32L); + } + + private static final class MatchGas { + private final BexGasMeter gas; + private final BexSourcePath sourcePath; + + private MatchGas(BexGasMeter gas, BexSourcePath sourcePath) { + this.gas = java.util.Objects.requireNonNull(gas, "gas"); + this.sourcePath = sourcePath; + } + + private void comparisonNode() { + charge(BexGasCounter.COMPARISON_NODE_VISITED, 1L); + } + + private void textBlocks(long quantity) { + charge(BexGasCounter.TEXT_BLOCK_EXAMINED, quantity); + } + + private void integerLimbs(long quantity) { + charge(BexGasCounter.INTEGER_LIMB_OPERATION, quantity); + } + + /** + * Canonical comparison with each pair of 64-code-point blocks + * admitted before either block is inspected. + */ + private int compareText( + String left, + String right) { + if (left == right) { + return 0; + } + if (left == null) { + return -1; + } + if (right == null) { + return 1; + } + int leftOffset = 0; + int rightOffset = 0; + while (leftOffset < left.length() + && rightOffset < right.length()) { + textBlocks(2L); + int inBlock = 0; + while (inBlock + < TEXT_BLOCK_CODE_POINTS + && leftOffset < left.length() + && rightOffset < right.length()) { + int leftCodePoint = + left.codePointAt(leftOffset); + int rightCodePoint = + right.codePointAt(rightOffset); + leftOffset += Character.charCount( + leftCodePoint); + rightOffset += Character.charCount( + rightCodePoint); + if (leftCodePoint + != rightCodePoint) { + return Integer.compare( + leftCodePoint, + rightCodePoint); + } + inBlock++; + } + } + return Integer.compare( + left.length() - leftOffset, + right.length() - rightOffset); + } + + private void charge(BexGasCounter counter, long quantity) { + if (quantity <= 0L) { + return; + } + gas.charge( + counter, + quantity, + sourcePath, + sourcePath != null ? sourcePath.operator() : null, + counter.canonicalName()); + } } } diff --git a/src/main/java/blue/bex/value/AdmittedExactBexValue.java b/src/main/java/blue/bex/value/AdmittedExactBexValue.java new file mode 100644 index 0000000..7f52e17 --- /dev/null +++ b/src/main/java/blue/bex/value/AdmittedExactBexValue.java @@ -0,0 +1,192 @@ +package blue.bex.value; + +import blue.language.model.Node; +import blue.language.snapshot.FrozenNode; +import blue.language.utils.JsonPointer; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.List; +import java.util.Objects; + +/** + * Exact identity established by an output boundary. + * + *

The host-established frozen value is authoritative for all semantic + * reads. The supplied run-local value is consulted only at the corresponding + * child path, and only an already-exact child with the same established + * identity is reused directly. This retains locally resolved exact + * descendants without allowing a transient {@code blueId}-shaped object or + * stale pre-normalization content to override the host result.

+ */ +final class AdmittedExactBexValue implements BexValue { + private final BexValue establishedValue; + private final BexValue suppliedValue; + + AdmittedExactBexValue(FrozenNode frozenValue, + String blueId, + BexValue suppliedValue) { + this( + BexValues.exact( + Objects.requireNonNull( + frozenValue, "frozenValue"), + frozenValue, + Objects.requireNonNull( + blueId, "blueId")), + suppliedValue); + } + + private AdmittedExactBexValue(BexValue establishedValue, + BexValue suppliedValue) { + this.establishedValue = Objects.requireNonNull( + establishedValue, "establishedValue"); + if (!establishedValue.isExact()) { + throw new IllegalArgumentException( + "Admitted semantic value must be exact"); + } + this.suppliedValue = suppliedValue != null + ? suppliedValue + : BexValues.UNDEFINED; + } + + @Override + public boolean isExact() { + return true; + } + + @Override + public String exactBlueId() { + return establishedValue.exactBlueId(); + } + + @Override + public boolean isUndefined() { + return establishedValue.isUndefined(); + } + + @Override + public boolean isNull() { + return establishedValue.isNull() + && !usesEmptyObjectShapeWitness(); + } + + @Override + public boolean isScalar() { + return establishedValue.isScalar(); + } + + @Override + public boolean isObject() { + return establishedValue.isObject() + || usesEmptyObjectShapeWitness(); + } + + @Override + public boolean isList() { + return establishedValue.isList(); + } + + @Override + public BexValue get(String key) { + BexValue establishedChild = + establishedValue.get(key); + if (establishedChild == null + || establishedChild.isUndefined()) { + return BexValues.UNDEFINED; + } + + BexValue suppliedChild = + suppliedValue.get(key); + if (suppliedChild == null + || suppliedChild.isUndefined()) { + return establishedChild; + } + if (suppliedChild.isExact()) { + if (establishedChild.isExact() + && establishedChild.exactBlueId().equals( + suppliedChild.exactBlueId())) { + return suppliedChild; + } + /* + * A mismatched exact source is not a valid cursor for any + * descendant of the established host value. + */ + return establishedChild; + } + if (establishedChild.isExact()) { + return new AdmittedExactBexValue( + establishedChild, + suppliedChild); + } + return establishedChild; + } + + @Override + public BexValue at(List pointerSegments) { + return BexValues.atSegments(this, pointerSegments); + } + + @Override + public BexValue at(String pointer) { + return at(JsonPointer.split(pointer)); + } + + @Override + public String asText() { + return establishedValue.asText(); + } + + @Override + public BigInteger asInteger() { + return establishedValue.asInteger(); + } + + @Override + public BigDecimal asNumber() { + return establishedValue.asNumber(); + } + + @Override + public boolean asBoolean() { + return establishedValue.asBoolean(); + } + + @Override + public List keys() { + return establishedValue.keys(); + } + + @Override + public int size() { + return establishedValue.size(); + } + + @Override + public Node toNode() { + return establishedValue.toNode(); + } + + Object rawScalar() { + return BexValues.rawScalar(establishedValue); + } + + /* + * Blue's resolved frozen representation retains an empty object member as + * an empty node, which is also the representation of BEX null. The exact + * host value remains authoritative for every modeled field and for + * identity; the supplied value is used only as the otherwise-lost shape + * witness when it is precisely an empty object. + */ + private boolean usesEmptyObjectShapeWitness() { + return establishedValue.isNull() + && suppliedValue.isObject() + && suppliedValue.size() == 0; + } + + @Override + public Object toSimple() { + return isScalar() + ? rawScalar() + : BexSimpleWriter.toSimple(this); + } +} diff --git a/src/main/java/blue/bex/value/BexBlueNodeWriter.java b/src/main/java/blue/bex/value/BexBlueNodeWriter.java index cd8e524..b4b2c5a 100644 --- a/src/main/java/blue/bex/value/BexBlueNodeWriter.java +++ b/src/main/java/blue/bex/value/BexBlueNodeWriter.java @@ -3,7 +3,11 @@ import blue.bex.BexException; import blue.language.model.Node; import blue.language.model.Schema; +import blue.language.utils.BlueIds; +import blue.language.utils.Nodes; +import java.math.BigDecimal; +import java.math.BigInteger; import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedHashMap; @@ -12,7 +16,7 @@ import java.util.Set; /** - * Boundary writer from BEX values to Blue nodes using Blue language keys. + * The single strict transient-to-Blue conversion path. */ public final class BexBlueNodeWriter { private static final Set SCHEMA_KEYS = schemaKeys(); @@ -20,29 +24,80 @@ public final class BexBlueNodeWriter { private BexBlueNodeWriter() { } + /** + * Converts a BEX value to valid Blue Language 1.0 content. + * + *

Exact children become pure references. Transient values are rebuilt in + * canonical BEX key order and validated as direct Blue identity input.

+ */ public static Node toNode(BexValue value) { - if (value.isUndefined()) { + return convert(value, false); + } + + /** + * Builds a strict semantic view for operations such as Blue type + * matching. Exact descendants remain inline when their verified content + * is already available, so a transient aggregate does not discard local + * evidence by replacing those descendants with provider-only references. + */ + public static Node toSemanticNode(BexValue value) { + return convert(value, true); + } + + private static Node convert(BexValue value, boolean inlineExact) { + try { + return toNode(value, Position.ROOT, inlineExact); + } catch (BexException ex) { + if (ex.getMessage() != null + && ex.getMessage().startsWith( + "Blue output conversion failed:")) { + throw ex; + } + throw new BexException( + "Blue output conversion failed: " + + ex.getMessage(), + ex); + } catch (RuntimeException ex) { + throw new BexException("Blue output conversion failed: " + ex.getMessage(), ex); + } + } + + private static Node toNode(BexValue value, + Position position, + boolean inlineExact) { + if (value == null || value.isUndefined()) { throw new BexException("Undefined cannot be emitted as a Blue value"); } - if (value.isNull()) { - return new Node(); + if (value.isExact()) { + if (inlineExact) { + return value.toNode(); + } + String blueId = BlueIds.requireBlueIdOrCyclicMember( + value.exactBlueId(), "BEX exact output blueId"); + return new Node().blueId(blueId); } - if (value instanceof NodeBexValue || value instanceof FrozenNodeBexValue) { - return value.toNode(); + if (value.isNull()) { + return position == Position.LIST_ITEM + ? Nodes.emptyPlaceholder() + : new Node(); } if (value.isScalar()) { - return value.toNode(); + return scalarNode(BexValues.rawScalar(value)); } if (value.isList()) { - return new Node().items(toNodeList(value)); + return new Node().items(toNodeList(value, inlineExact)); } if (!value.isObject()) { - return value.toNode(); + throw new BexException("Unsupported BEX output value kind"); } - - if (!value.get("properties").isUndefined()) { - throw new BexException("\"properties\" is an internal Blue field and must not appear in BEX output"); + if (isEmptyPlaceholder(value)) { + if (position != Position.LIST_ITEM) { + throw new BexException("\"$empty\" is valid only as a Blue list item"); + } + return Nodes.emptyPlaceholder(); } + + rejectForbiddenFields(value); validateBlueIdReferenceShape(value); Node node = new Node(); @@ -55,19 +110,19 @@ public static Node toNode(BexValue value) { continue; } if ("name".equals(key)) { - node.name(child.isNull() ? null : child.asText()); + node.name(requiredText(child, "name")); } else if ("description".equals(key)) { - node.description(child.isNull() ? null : child.asText()); + node.description(requiredText(child, "description")); } else if ("type".equals(key)) { - node.type(toNode(child)); + node.type(toNode(child, Position.METADATA, inlineExact)); } else if ("itemType".equals(key)) { - node.itemType(toNode(child)); + node.itemType(toNode(child, Position.METADATA, inlineExact)); } else if ("keyType".equals(key)) { - node.keyType(toNode(child)); + node.keyType(toNode(child, Position.METADATA, inlineExact)); } else if ("valueType".equals(key)) { - node.valueType(toNode(child)); + node.valueType(toNode(child, Position.METADATA, inlineExact)); } else if ("mergePolicy".equals(key)) { - node.mergePolicy(child.isNull() ? null : child.asText()); + node.mergePolicy(requiredText(child, "mergePolicy")); } else if ("value".equals(key)) { hasValuePayload = true; node.value(scalarValue(child, "value")); @@ -76,42 +131,42 @@ public static Node toNode(BexValue value) { if (!child.isList()) { throw new BexException("Blue items field must be a list"); } - node.items(toNodeList(child)); + node.items(toNodeList(child, inlineExact)); } else if ("blueId".equals(key)) { - node.blueId(child.asText()); - } else if ("blue".equals(key)) { - node.blue(toNode(child)); + String blueId = BlueIds.requireBlueIdOrCyclicMember( + requiredText(child, "blueId"), + "BEX output blueId"); + if (blueId.indexOf('#') >= 0) { + throw new BexException( + "Transient BEX output cannot counterfeit an exact " + + "cyclic-set member reference: " + + blueId); + } + node.blueId(blueId); } else if ("contracts".equals(key)) { - node.contracts(toObjectNode(child, "contracts")); + node.contracts(toObjectNode( + child, "contracts", inlineExact)); } else if ("schema".equals(key)) { - node.schema(toSchema(child)); - } else if ("constraints".equals(key)) { - throw new BexException("Blue constraints field is invalid in Blue Language 1.0; use schema"); - } else if ("$previous".equals(key) || "$pos".equals(key)) { - throw new BexException("BEX output does not currently support Blue list-control field " + key); - } else if ("properties".equals(key)) { - throw new BexException("\"properties\" is an internal Blue field and must not appear in BEX output"); + node.schema(toSchema(child, inlineExact)); } else { - properties.put(key, toNode(child)); + properties.put(key, toNode( + child, Position.OBJECT_MEMBER, inlineExact)); } } - int payloadKinds = 0; - if (hasValuePayload) { - payloadKinds++; - } - if (hasItemsPayload) { - payloadKinds++; - } - if (!properties.isEmpty()) { - payloadKinds++; - } + + int payloadKinds = (hasValuePayload ? 1 : 0) + + (hasItemsPayload ? 1 : 0) + + (!properties.isEmpty() ? 1 : 0); if (payloadKinds > 1) { - throw new BexException("A Blue node may contain only one payload kind: value, items, or object fields"); + throw new BexException( + "A Blue node may contain only one payload kind: value, items, or object fields"); } if (!properties.isEmpty()) { node.properties(properties); } - return node; + return position == Position.LIST_ITEM && Nodes.isEmptyNode(node) + ? Nodes.emptyPlaceholder() + : node; } public static boolean hasLanguageField(BexValue value) { @@ -143,7 +198,53 @@ public static boolean isLanguageField(String key) { || "mergePolicy".equals(key) || "properties".equals(key) || "$previous".equals(key) - || "$pos".equals(key); + || "$pos".equals(key) + || "$replace".equals(key) + || "$empty".equals(key); + } + + private static void rejectForbiddenFields(BexValue value) { + if (!value.get("properties").isUndefined()) { + throw new BexException( + "\"properties\" is an internal Blue field and must not appear in BEX output"); + } + if (!value.get("constraints").isUndefined()) { + throw new BexException( + "Blue constraints field is invalid in Blue Language 1.0; use schema"); + } + if (!value.get("allowMultiple").isUndefined()) { + throw new BexException( + "Blue allowMultiple field is invalid in Blue Language 1.0"); + } + if (!value.get("options").isUndefined()) { + throw new BexException( + "Blue options field is invalid in Blue Language 1.0"); + } + if (!value.get("blue").isUndefined()) { + throw new BexException( + "Computed BEX output must not contain the Blue preprocessing field \"blue\""); + } + for (String control : Arrays.asList("$previous", "$pos", "$replace")) { + if (!value.get(control).isUndefined()) { + throw new BexException( + "Computed BEX output must not contain Blue list-control field " + control); + } + } + if (!value.get("$empty").isUndefined()) { + throw new BexException( + "\"$empty\" list placeholder must have exact shape { \"$empty\": true }"); + } + } + + private static boolean isEmptyPlaceholder(BexValue value) { + if (!value.isObject() || value.size() != 1) { + return false; + } + BexValue marker = value.get("$empty"); + return !marker.isUndefined() + && marker.isScalar() + && marker.asBoolean() + && Boolean.TRUE.equals(BexValues.rawScalar(marker)); } private static void validateBlueIdReferenceShape(BexValue value) { @@ -151,107 +252,146 @@ private static void validateBlueIdReferenceShape(BexValue value) { return; } for (String key : value.keys()) { - if (!"blueId".equals(key)) { - throw new BexException("Blue blueId reference node cannot contain sibling field: " + key); + if (!"blueId".equals(key) && !value.get(key).isUndefined()) { + throw new BexException( + "Blue blueId reference node cannot contain sibling field: " + key); } } } + private static Node scalarNode(Object value) { + if (value instanceof String) { + return Nodes.textNode((String) value); + } + if (value instanceof BigInteger) { + return Nodes.integerNode((BigInteger) value); + } + if (value instanceof BigDecimal) { + return Nodes.doubleNode((BigDecimal) value); + } + if (value instanceof Boolean) { + return Nodes.booleanNode((Boolean) value); + } + throw new BexException("Unsupported BEX scalar output: " + + (value == null ? "null" : value.getClass().getName())); + } + private static Object scalarValue(BexValue value, String field) { - if (value.isNull()) { - return null; + if (value.isNull() || !value.isScalar()) { + throw new BexException("Blue " + field + " field must be a non-null scalar value"); + } + Object raw = BexValues.rawScalar(value); + return raw; + } + + private static String requiredText(BexValue value, String field) { + if (value.isNull() || !value.isScalar()) { + throw new BexException("Blue " + field + " field must be Text"); } - if (!value.isScalar()) { - throw new BexException("Blue " + field + " field must be a scalar value"); + Object raw = BexValues.rawScalar(value); + if (!(raw instanceof String)) { + throw new BexException("Blue " + field + " field must be Text"); } - return BexValues.rawScalar(value); + return (String) raw; } - private static Node toObjectNode(BexValue value, String field) { + private static Node toObjectNode(BexValue value, + String field, + boolean inlineExact) { if (value.isNull()) { return null; } if (!value.isObject()) { throw new BexException("Blue " + field + " field must be an object"); } - return toNode(value); + return toNode(value, Position.METADATA, inlineExact); } - private static List toNodeList(BexValue value) { + private static List toNodeList(BexValue value, + boolean inlineExact) { ArrayList items = new ArrayList<>(); for (int i = 0; i < value.size(); i++) { - items.add(toNode(value.get(String.valueOf(i)))); + BexValue item = value.get(String.valueOf(i)); + if (item == null || item.isUndefined()) { + throw new BexException("Undefined cannot appear in a Blue list"); + } + items.add(toNode(item, Position.LIST_ITEM, inlineExact)); } return items; } - private static Schema toSchema(BexValue value) { + private static Schema toSchema(BexValue value, + boolean inlineExact) { if (value.isNull() || value.isUndefined()) { return null; } - Node node = toNode(value); - if (node.getSchema() != null && node.getValue() == null - && node.getItems() == null && node.getProperties() == null) { - return node.getSchema(); + if (value.isExact()) { + Node exactReference = toNode( + value, Position.METADATA, false); + Schema schema = new Schema(); + schema.blueId(exactReference.getBlueId()); + return schema; } if (!value.isObject()) { throw new BexException("Blue schema field must be an object"); } validateSchemaKeys(value); + if (!value.get("blueId").isUndefined()) { + if (value.size() != 1) { + throw new BexException("Blue schema blueId reference must be pure"); + } + Schema schema = new Schema(); + schema.blueId(BlueIds.requireBlueIdOrCyclicMember( + requiredText(value.get("blueId"), "schema.blueId"), + "BEX output schema.blueId")); + return schema; + } Schema schema = new Schema(); - setSchemaNode(schema, value, "required"); - setSchemaNode(schema, value, "minLength"); - setSchemaNode(schema, value, "maxLength"); - setSchemaNode(schema, value, "minimum"); - setSchemaNode(schema, value, "maximum"); - setSchemaNode(schema, value, "exclusiveMinimum"); - setSchemaNode(schema, value, "exclusiveMaximum"); - setSchemaNode(schema, value, "multipleOf"); - setSchemaNode(schema, value, "minItems"); - setSchemaNode(schema, value, "maxItems"); - setSchemaNode(schema, value, "uniqueItems"); - setSchemaNode(schema, value, "minFields"); - setSchemaNode(schema, value, "maxFields"); - setSchemaList(schema, value, "enum"); + setSchemaNode(schema, value, "required", inlineExact); + setSchemaNode(schema, value, "minLength", inlineExact); + setSchemaNode(schema, value, "maxLength", inlineExact); + setSchemaNode(schema, value, "minimum", inlineExact); + setSchemaNode(schema, value, "maximum", inlineExact); + setSchemaNode(schema, value, "exclusiveMinimum", inlineExact); + setSchemaNode(schema, value, "exclusiveMaximum", inlineExact); + setSchemaNode(schema, value, "multipleOf", inlineExact); + setSchemaNode(schema, value, "minItems", inlineExact); + setSchemaNode(schema, value, "maxItems", inlineExact); + setSchemaNode(schema, value, "uniqueItems", inlineExact); + setSchemaNode(schema, value, "minFields", inlineExact); + setSchemaNode(schema, value, "maxFields", inlineExact); + setSchemaList(schema, value, "enum", inlineExact); return schema; } - private static void setSchemaNode(Schema schema, BexValue source, String key) { + private static void setSchemaNode(Schema schema, + BexValue source, + String key, + boolean inlineExact) { BexValue value = source.get(key); if (value.isUndefined()) { return; } - Node node = toNode(value); - if ("required".equals(key)) { - schema.required(node); - } else if ("minLength".equals(key)) { - schema.minLength(node); - } else if ("maxLength".equals(key)) { - schema.maxLength(node); - } else if ("minimum".equals(key)) { - schema.minimum(node); - } else if ("maximum".equals(key)) { - schema.maximum(node); - } else if ("exclusiveMinimum".equals(key)) { - schema.exclusiveMinimum(node); - } else if ("exclusiveMaximum".equals(key)) { - schema.exclusiveMaximum(node); - } else if ("multipleOf".equals(key)) { - schema.multipleOf(node); - } else if ("minItems".equals(key)) { - schema.minItems(node); - } else if ("maxItems".equals(key)) { - schema.maxItems(node); - } else if ("uniqueItems".equals(key)) { - schema.uniqueItems(node); - } else if ("minFields".equals(key)) { - schema.minFields(node); - } else if ("maxFields".equals(key)) { - schema.maxFields(node); - } + Node node = toNode(value, Position.METADATA, inlineExact); + if ("required".equals(key)) schema.required(node); + else if ("minLength".equals(key)) schema.minLength(node); + else if ("maxLength".equals(key)) schema.maxLength(node); + else if ("minimum".equals(key)) schema.minimum(node); + else if ("maximum".equals(key)) schema.maximum(node); + else if ("exclusiveMinimum".equals(key)) schema.exclusiveMinimum(node); + else if ("exclusiveMaximum".equals(key)) schema.exclusiveMaximum(node); + else if ("multipleOf".equals(key)) schema.multipleOf(node); + else if ("minItems".equals(key)) schema.minItems(node); + else if ("maxItems".equals(key)) schema.maxItems(node); + else if ("uniqueItems".equals(key)) schema.uniqueItems(node); + else if ("minFields".equals(key)) schema.minFields(node); + else if ("maxFields".equals(key)) schema.maxFields(node); } - private static void setSchemaList(Schema schema, BexValue source, String key) { + private static void setSchemaList(Schema schema, + BexValue source, + String key, + boolean inlineExact) { BexValue value = source.get(key); if (value.isUndefined()) { return; @@ -259,15 +399,14 @@ private static void setSchemaList(Schema schema, BexValue source, String key) { if (!value.isList()) { throw new BexException("Blue schema " + key + " field must be a list"); } - List nodes = toNodeList(value); if ("enum".equals(key)) { - schema.enumValues(nodes); + schema.enumValues(toNodeList(value, inlineExact)); } } private static void validateSchemaKeys(BexValue value) { for (String key : value.keys()) { - if (!SCHEMA_KEYS.contains(key)) { + if (!SCHEMA_KEYS.contains(key) && !"blueId".equals(key)) { throw new BexException("Unsupported Blue schema field: " + key); } } @@ -290,4 +429,11 @@ private static Set schemaKeys() { "maxFields", "enum")); } + + private enum Position { + ROOT, + OBJECT_MEMBER, + LIST_ITEM, + METADATA + } } diff --git a/src/main/java/blue/bex/value/BexEquality.java b/src/main/java/blue/bex/value/BexEquality.java index 2a7f917..0a3b2a8 100644 --- a/src/main/java/blue/bex/value/BexEquality.java +++ b/src/main/java/blue/bex/value/BexEquality.java @@ -16,6 +16,15 @@ static boolean equal(BexValue left, BexValue right) { if (left.isUndefined() || right.isUndefined()) { return left.isUndefined() && right.isUndefined(); } + /* + * Equal established identities prove exact-value equality without + * demanding either representation. This must precede semantic kind + * inspection because either side may still be a collapsed reference. + */ + if (left.isExact() && right.isExact() + && left.exactBlueId().equals(right.exactBlueId())) { + return true; + } if (left.isNull() || right.isNull()) { return left.isNull() && right.isNull(); } diff --git a/src/main/java/blue/bex/value/BexFrozenNodeFactory.java b/src/main/java/blue/bex/value/BexFrozenNodeFactory.java deleted file mode 100644 index eeb9830..0000000 --- a/src/main/java/blue/bex/value/BexFrozenNodeFactory.java +++ /dev/null @@ -1,20 +0,0 @@ -package blue.bex.value; - -import blue.bex.result.BexMetrics; -import blue.language.snapshot.FrozenNode; - -import java.util.List; -import java.util.Map; - -/** - * Factory used by {@link BexFrozenWriter} when BEX values cross a FrozenNode boundary. - */ -public interface BexFrozenNodeFactory { - FrozenNode empty(BexMetrics metrics); - - FrozenNode scalar(Object value, BexMetrics metrics); - - FrozenNode list(List items, BexMetrics metrics); - - FrozenNode object(Map properties, BexMetrics metrics); -} diff --git a/src/main/java/blue/bex/value/BexFrozenWriter.java b/src/main/java/blue/bex/value/BexFrozenWriter.java index 67f8b29..0befd5a 100644 --- a/src/main/java/blue/bex/value/BexFrozenWriter.java +++ b/src/main/java/blue/bex/value/BexFrozenWriter.java @@ -3,20 +3,13 @@ import blue.bex.result.BexMetrics; import blue.language.snapshot.FrozenNode; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - /** - * Boundary writer to immutable Blue nodes. + * Immutable projection of the single strict Blue output conversion path. */ public final class BexFrozenWriter { - private final BexFrozenNodeFactory factory; private final BexMetrics metrics; - public BexFrozenWriter(BexFrozenNodeFactory factory, BexMetrics metrics) { - this.factory = factory != null ? factory : NodeRoundTripFrozenNodeFactory.INSTANCE; + private BexFrozenWriter(BexMetrics metrics) { this.metrics = metrics; } @@ -25,7 +18,7 @@ public static FrozenNode toFrozen(BexValue value) { } public static FrozenNode toFrozen(BexValue value, BexMetrics metrics) { - return new BexFrozenWriter(NodeRoundTripFrozenNodeFactory.INSTANCE, metrics).toFrozenValue(value); + return new BexFrozenWriter(metrics).toFrozenValue(value); } public FrozenNode toFrozenValue(BexValue value) { @@ -36,41 +29,13 @@ public FrozenNode toFrozenValue(BexValue value) { } private FrozenNode toFrozenInternal(BexValue value) { - if (value.isUndefined()) { - throw new blue.bex.BexException("Undefined cannot be emitted as a Blue value"); - } if (value instanceof FrozenNodeBexValue) { - return ((FrozenNodeBexValue) value).node(); - } - if (value.isNull()) { - return factory.empty(metrics); - } - if (value.isScalar()) { - return factory.scalar(BexValues.rawScalar(value), metrics); - } - if (value.isList()) { - List items = new ArrayList<>(); - for (int i = 0; i < value.size(); i++) { - items.add(toFrozenInternal(value.get(String.valueOf(i)))); - } - return factory.list(items, metrics); - } - if (value.isObject()) { - if (BexBlueNodeWriter.hasLanguageField(value)) { - return FrozenNode.fromResolvedNode(BexBlueNodeWriter.toNode(value)); - } - Map properties = new LinkedHashMap<>(); - for (String key : value.keys()) { - BexValue child = value.get(key); - if (!child.isUndefined()) { - properties.put(key, toFrozenInternal(child)); - } - } - return factory.object(properties, metrics); + return ((FrozenNodeBexValue) value).canonicalNode(); } if (metrics != null) { metrics.incrementFrozenWriterNodeFallbacks(); } - return FrozenNode.fromResolvedNode(BexNodeWriter.toNode(value)); + return FrozenNode.fromResolvedNode( + BexBlueNodeWriter.toNode(value)); } } diff --git a/src/main/java/blue/bex/value/BexSimpleWriter.java b/src/main/java/blue/bex/value/BexSimpleWriter.java index 4575cec..2d61136 100644 --- a/src/main/java/blue/bex/value/BexSimpleWriter.java +++ b/src/main/java/blue/bex/value/BexSimpleWriter.java @@ -1,5 +1,7 @@ package blue.bex.value; +import blue.bex.BexException; + import java.util.ArrayList; import java.util.LinkedHashMap; @@ -20,7 +22,12 @@ public static Object toSimple(BexValue value) { if (value.isList()) { ArrayList out = new ArrayList<>(); for (int i = 0; i < value.size(); i++) { - out.add(toSimple(value.get(String.valueOf(i)))); + BexValue item = value.get(String.valueOf(i)); + if (item == null || item.isUndefined()) { + throw new BexException( + "Sparse overlay list cannot be materialized as a dense BEX list"); + } + out.add(toSimple(item)); } return out; } diff --git a/src/main/java/blue/bex/value/BexUnicodeOrder.java b/src/main/java/blue/bex/value/BexUnicodeOrder.java new file mode 100644 index 0000000..c41fa3f --- /dev/null +++ b/src/main/java/blue/bex/value/BexUnicodeOrder.java @@ -0,0 +1,144 @@ +package blue.bex.value; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; + +/** + * Canonical BEX ordering for text keys. + * + *

Java's natural {@link String} ordering compares UTF-16 code units. BEX + * compares Unicode code points, which differs for supplementary characters + * versus values in the private-use area.

+ */ +public final class BexUnicodeOrder { + /** + * One admitted canonical comparison. + * + *

Metered callers admit {@code sortComparison}, + * {@code comparisonNodeVisited}, and scalar work in this method before + * inspecting either operand.

+ */ + @FunctionalInterface + public interface Comparison { + int compare(String left, String right); + } + + private static final Comparison UNMETERED = + new Comparison() { + @Override + public int compare( + String left, + String right) { + return compareCodePoints(left, right); + } + }; + + public static final Comparator CODE_POINT_COMPARATOR = + new Comparator() { + @Override + public int compare(String left, String right) { + return compareCodePoints(left, right); + } + }; + + private BexUnicodeOrder() { + } + + public static int compareCodePoints(String left, String right) { + if (left == right) { + return 0; + } + if (left == null) { + return -1; + } + if (right == null) { + return 1; + } + int leftOffset = 0; + int rightOffset = 0; + while (leftOffset < left.length() && rightOffset < right.length()) { + int leftCodePoint = left.codePointAt(leftOffset); + int rightCodePoint = right.codePointAt(rightOffset); + if (leftCodePoint != rightCodePoint) { + return leftCodePoint < rightCodePoint ? -1 : 1; + } + leftOffset += Character.charCount(leftCodePoint); + rightOffset += Character.charCount(rightCodePoint); + } + if (leftOffset == left.length() && rightOffset == right.length()) { + return 0; + } + return leftOffset == left.length() ? -1 : 1; + } + + public static List sortedCopy(Collection values) { + return sortedCopy(values, UNMETERED); + } + + /** + * Returns a canonical stable ordering using the normative bottom-up merge + * schedule. + * + *

The supplied comparison runs exactly once for every comparison in the + * canonical trace. Tail copies do not invoke it. A metered comparison must + * admit all comparison work before it inspects either operand; if an + * admission rejects, no corresponding scalar comparison is performed.

+ */ + public static List sortedCopy( + Collection values, + Comparison comparison) { + Objects.requireNonNull(values, "values"); + Objects.requireNonNull(comparison, "comparison"); + String[] source = values.toArray(new String[values.size()]); + int size = source.length; + if (size < 2) { + return new ArrayList( + java.util.Arrays.asList(source)); + } + String[] target = new String[size]; + for (long width = 1L; + width < size; + width *= 2L) { + for (long left = 0L; + left < size; + left += 2L * width) { + int first = (int) left; + int middle = (int) Math.min( + left + width, size); + int second = middle; + int right = (int) Math.min( + left + 2L * width, size); + int output = first; + while (first < middle + && second < right) { + String leftValue = source[first]; + String rightValue = source[second]; + if (comparison.compare( + leftValue, rightValue) <= 0) { + target[output++] = + source[first++]; + } else { + target[output++] = + source[second++]; + } + } + while (first < middle) { + target[output++] = + source[first++]; + } + while (second < right) { + target[output++] = + source[second++]; + } + } + String[] swap = source; + source = target; + target = swap; + } + return new ArrayList( + java.util.Arrays.asList(source)); + } +} diff --git a/src/main/java/blue/bex/value/BexValue.java b/src/main/java/blue/bex/value/BexValue.java index 87704cf..88b9e57 100644 --- a/src/main/java/blue/bex/value/BexValue.java +++ b/src/main/java/blue/bex/value/BexValue.java @@ -14,6 +14,25 @@ * {@link Node} or simple Java object is an explicit boundary operation.

*/ public interface BexValue { + /** + * Whether this value is an already established Blue node. + * + *

Exactness is provenance, not shape. In particular, a transient object + * containing a {@code blueId} member is not exact.

+ */ + default boolean isExact() { + return false; + } + + /** + * Returns the retained Node BlueId of an exact value. + * + * @throws IllegalStateException when this value is transient + */ + default String exactBlueId() { + throw new IllegalStateException("Transient BEX values do not have a Node BlueId"); + } + boolean isUndefined(); boolean isNull(); boolean isScalar(); @@ -26,6 +45,13 @@ public interface BexValue { BigInteger asInteger(); BigDecimal asNumber(); boolean asBoolean(); + /** + * Returns this object's already-established canonical Unicode key cursor. + * + *

Callers must consume this order directly. Implementations establish + * and retain it before exposing the value; enumerating the cursor is not a + * request to sort it again.

+ */ List keys(); int size(); Node toNode(); diff --git a/src/main/java/blue/bex/value/BexValues.java b/src/main/java/blue/bex/value/BexValues.java index 12f8839..cc616f6 100644 --- a/src/main/java/blue/bex/value/BexValues.java +++ b/src/main/java/blue/bex/value/BexValues.java @@ -1,9 +1,16 @@ package blue.bex.value; import blue.bex.BexException; +import blue.language.Blue; +import blue.language.BlueOperationLimits; +import blue.language.BlueOperationOutcome; +import blue.language.BlueOperationResult; import blue.language.model.Node; import blue.language.model.Schema; +import blue.language.processor.ExecutionEvidenceUnavailableException; +import blue.language.processor.InvalidExecutionEvidenceException; import blue.language.snapshot.FrozenNode; +import blue.language.snapshot.ResolvedSnapshot; import blue.language.utils.JsonPointer; import blue.language.utils.NodeToMapListOrValue; import blue.language.utils.SchemaToMapListOrValue; @@ -17,6 +24,11 @@ import java.util.Map; import java.util.TreeSet; +import static blue.language.utils.Properties.BOOLEAN_TYPE_BLUE_ID; +import static blue.language.utils.Properties.DOUBLE_TYPE_BLUE_ID; +import static blue.language.utils.Properties.INTEGER_TYPE_BLUE_ID; +import static blue.language.utils.Properties.TEXT_TYPE_BLUE_ID; + /** * Value factories and shared value helpers. */ @@ -59,44 +71,154 @@ public static BexValue scalar(Object value) { } /** - * Snapshot-safe Node value. Prefer {@link #nodeCursorTrustedImmutable(Node)} - * only when the caller can enforce immutability during execution. - * - * @deprecated use {@link #nodeSnapshot(Node)} or - * {@link #nodeCursorTrustedImmutable(Node)} to make the boundary contract - * explicit. + * Direct cursor over a Node with an explicit immutability contract. */ - @Deprecated - public static BexValue node(Node node) { - return nodeSnapshot(node); + public static BexValue nodeCursorTrustedImmutable(Node node) { + return node != null ? new NodeBexValue(node) : UNDEFINED; } /** - * Direct cursor over a Node with an explicit immutability contract. + * Snapshot-safe Node value. This clones/freezes the Node at the boundary and + * is therefore more expensive than a trusted immutable cursor. */ - public static BexValue nodeCursorTrustedImmutable(Node node) { - return node != null ? new NodeBexValue(node) : UNDEFINED; + public static BexValue nodeSnapshot(Node node) { + return node != null ? frozen(FrozenNode.fromNode(node.clone())) : UNDEFINED; } /** - * Backward-compatible direct cursor factory. Prefer - * {@link #nodeCursorTrustedImmutable(Node)} at host boundaries where the - * immutability contract matters. + * Wraps an immutable Blue node as an exact value. The identity remains + * lazy inside {@link FrozenNode}; merely carrying the value does not hash + * or materialize it. */ - public static BexValue nodeCursor(Node node) { - return nodeCursorTrustedImmutable(node); + public static BexValue frozen(FrozenNode node) { + return node != null ? new FrozenNodeBexValue(node, node) : UNDEFINED; } /** - * Snapshot-safe Node value. This clones/freezes the Node at the boundary and - * is therefore more expensive than a trusted immutable cursor. + * Creates a representation-blind exact value from canonical identity and + * its resolved semantic view. */ - public static BexValue nodeSnapshot(Node node) { - return node != null ? frozen(FrozenNode.fromResolvedNode(node.clone())) : UNDEFINED; + public static BexValue exact(FrozenNode canonicalNode, FrozenNode resolvedNode) { + return exact(canonicalNode, resolvedNode, null); } - public static BexValue frozen(FrozenNode node) { - return node != null ? new FrozenNodeBexValue(node) : UNDEFINED; + /** + * Creates an exact value with an identity already established by the host. + * The retained identity is returned without independent re-hashing. + */ + public static BexValue exact(FrozenNode canonicalNode, + FrozenNode resolvedNode, + String exactBlueId) { + if (canonicalNode == null && resolvedNode == null) { + return UNDEFINED; + } + FrozenNode canonical = canonicalNode != null ? canonicalNode : resolvedNode; + FrozenNode semantic = resolvedNode != null ? resolvedNode : canonicalNode; + String retainedBlueId = exactBlueId; + if (retainedBlueId == null && canonical.isReferenceOnly()) { + retainedBlueId = canonical.getReferenceBlueId(); + } + /* + * A finalized cyclic member has no independently hashable body. + * Public callers cannot manufacture the Language host's complete-set + * proof merely by pairing MASTER#index with an arbitrary resolved + * node. Retain such values as exact opaque references; structural + * access must pass through a cyclic-aware reference materializer. + */ + if (retainedBlueId != null + && retainedBlueId.indexOf('#') >= 0) { + canonical = FrozenNode.fromNode( + new Node().blueId(retainedBlueId)); + semantic = canonical; + } + return new FrozenNodeBexValue(canonical, semantic, exactBlueId); + } + + /** + * Retains a boundary-established exact identity while preserving the + * immutable run-local semantic cursor that produced it. + * + *

This avoids recursively reopening exact descendants when an admitted + * aggregate is subsequently read through {@code $resultValue}, + * {@code $changeset}, {@code $events}, or a local variable.

+ */ + public static BexValue admittedExact(FrozenNode frozenValue, + String exactBlueId, + BexValue semanticValue) { + return new AdmittedExactBexValue( + frozenValue, + exactBlueId, + semanticValue); + } + + /** + * Adds demand-driven, verified reference materialization to an exact + * frozen value. + * + *

{@link Blue#resolveToSnapshot(Object)} intentionally leaves untyped + * pure references collapsed. BEX may carry those values by identity + * without loading them, but semantic operations such as member access, + * kind inspection, or key enumeration must establish their content. + * {@link Blue#expandLimited(Node, BlueOperationLimits)} is the structured + * Language boundary that both obtains and verifies that evidence. + * Provider absence and temporary unavailability remain incomplete + * execution evidence, while invalid evidence remains a deterministic + * failure; neither is converted to BEX {@code undefined}.

+ * + * @param value exact frozen value to make reference-backed + * @param blue Language resolver used only when semantic content is demanded + * @return a lazy reference-backed exact value, or {@code value} when it is + * not backed by a {@link FrozenNode} + */ + public static BexValue referenceBacked(BexValue value, Blue blue) { + if (value instanceof FrozenNodeBexValue && blue != null) { + return ((FrozenNodeBexValue) value) + .withReferenceMaterializer( + blueId -> loadReference(blue, blueId)); + } + return value; + } + + private static ResolvedSnapshot loadReference( + Blue blue, String blueId) { + BlueOperationResult result = blue.expandLimited( + new Node().blueId(blueId), + BlueOperationLimits.demandedPath("")); + if (result.outcome() == BlueOperationOutcome.INVALID) { + throw new InvalidExecutionEvidenceException( + result.reason().orElse( + "Invalid exact reference evidence for " + blueId)); + } + if (result.outcome() != BlueOperationOutcome.ESTABLISHED + || !result.value().isPresent()) { + java.util.Set outstanding = + result.outstandingBlueIds(); + throw new ExecutionEvidenceUnavailableException( + result.reason().orElse( + "Exact reference evidence is unavailable for " + + blueId), + outstanding.isEmpty() + ? Collections.singletonList(blueId) + : outstanding); + } + FrozenNode canonicalReference = FrozenNode.fromNode( + new Node().blueId(blueId)); + FrozenNode verifiedDirectFragment = FrozenNode.fromResolvedNode( + result.value().get()); + return new ResolvedSnapshot( + canonicalReference, verifiedDirectFragment); + } + + /** + * Imports a frozen syntax tree as a transient runtime value. This is used + * for executable literals; exact host/document values must use + * {@link #frozen(FrozenNode)} or {@link #exact(FrozenNode, FrozenNode)}. + */ + public static BexValue transientFrozen(FrozenNode node) { + if (node == null) { + return UNDEFINED; + } + return fromSimple(NodeToMapListOrValue.get(node.toNode())); } static BexValue schemaSnapshot(Schema schema) { @@ -106,15 +228,55 @@ static BexValue schemaSnapshot(Schema schema) { // Schema is the value of a node's "schema" key, not another schema-bearing node. return fromSimple(SchemaToMapListOrValue.get( schema.clone(), - NodeToMapListOrValue::get)); + BexValues::schemaNodeToSimple)); + } + + private static Object schemaNodeToSimple(Node node) { + if (isCoreTypedScalar(node)) { + return node.getValue(); + } + return NodeToMapListOrValue.get(node); + } + + private static boolean isCoreTypedScalar(Node node) { + if (node == null + || node.getValue() == null + || node.getName() != null + || node.getDescription() != null + || node.getItems() != null + || node.getProperties() != null + || node.getContracts() != null + || node.getBlueId() != null + || node.getSchema() != null + || node.getMergePolicy() != null + || node.getPreviousBlueId() != null + || node.getPosition() != null + || node.getBlue() != null + || node.getItemType() != null + || node.getKeyType() != null + || node.getValueType() != null) { + return false; + } + if (node.getType() == null) { + return true; + } + String typeBlueId = node.getType().getBlueId(); + return TEXT_TYPE_BLUE_ID.equals(typeBlueId) + || INTEGER_TYPE_BLUE_ID.equals(typeBlueId) + || DOUBLE_TYPE_BLUE_ID.equals(typeBlueId) + || BOOLEAN_TYPE_BLUE_ID.equals(typeBlueId) + || "Text".equals(typeBlueId) + || "Integer".equals(typeBlueId) + || "Double".equals(typeBlueId) + || "Boolean".equals(typeBlueId); } public static String frozenBlueId(BexValue value) { - if (!(value instanceof FrozenNodeBexValue)) { + if (value == null || !value.isExact()) { return null; } try { - return ((FrozenNodeBexValue) value).node().blueId(); + return value.exactBlueId(); } catch (RuntimeException ex) { return null; } @@ -133,9 +295,33 @@ public static BexValue overlay(BexValue base, String key, BexValue value) { } public static BexValue pointerSet(BexValue base, List segments, BexValue value, String op) { + if (segments == null || segments.isEmpty()) { + return "remove".equals(op) + ? UNDEFINED + : (value != null ? value : UNDEFINED); + } return new PointerSetBexValue(base, segments, value, op); } + /** + * Applies a result-overlay pointer update. + * + *

This differs from the ordinary {@link #pointerSet} operation only for + * terminal list removal: result overlays retain the list's positional + * extent and expose an undefined slot, as required by BEX 2.0 §10.4.1.

+ */ + public static BexValue resultOverlayPointerSet(BexValue base, + List segments, + BexValue value, + String op) { + if (segments == null || segments.isEmpty()) { + return "remove".equals(op) + ? UNDEFINED + : (value != null ? value : UNDEFINED); + } + return new PointerSetBexValue(base, segments, value, op, true); + } + public static BexValue fromSimple(Object value) { if (value == null) { return NULL; @@ -219,6 +405,9 @@ static Object rawScalar(BexValue value) { if (value instanceof NodeBexValue) { return ((NodeBexValue) value).rawScalar(); } + if (value instanceof AdmittedExactBexValue) { + return ((AdmittedExactBexValue) value).rawScalar(); + } return value.asText(); } diff --git a/src/main/java/blue/bex/value/FrozenNodeBexValue.java b/src/main/java/blue/bex/value/FrozenNodeBexValue.java index 134c19b..42375f4 100644 --- a/src/main/java/blue/bex/value/FrozenNodeBexValue.java +++ b/src/main/java/blue/bex/value/FrozenNodeBexValue.java @@ -3,7 +3,7 @@ import blue.bex.BexException; import blue.language.model.Node; import blue.language.snapshot.FrozenNode; -import blue.language.utils.JsonPointer; +import blue.language.snapshot.ResolvedSnapshot; import java.math.BigDecimal; import java.math.BigInteger; @@ -12,172 +12,319 @@ import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; +import java.util.function.Function; +/** + * Representation-blind cursor over an exact Blue value. + * + *

The canonical node retains identity and may be only a reference. The + * semantic node is the resolved view used for ordinary BEX operations. Keeping + * those roles separate prevents physical {@code blueId} wrappers from leaking + * into {@code $keys}, {@code $kind}, pointer reads, and equality.

+ */ final class FrozenNodeBexValue extends AbstractBexValue { - private final FrozenNode node; + private final FrozenNode canonicalIdentityNode; + private final FrozenNode initialSemanticNode; + private final String retainedExactBlueId; + private final Function referenceMaterializer; + private volatile FrozenNode materializedCanonicalNode; + private volatile FrozenNode materializedSemanticNode; + private volatile List canonicalKeys; - FrozenNodeBexValue(FrozenNode node) { - this.node = node; + FrozenNodeBexValue(FrozenNode canonicalNode, FrozenNode semanticNode) { + this(canonicalNode, semanticNode, null, null); + } + + FrozenNodeBexValue(FrozenNode canonicalNode, + FrozenNode semanticNode, + String retainedExactBlueId) { + this(canonicalNode, semanticNode, retainedExactBlueId, null); + } + + private FrozenNodeBexValue( + FrozenNode canonicalNode, + FrozenNode semanticNode, + String retainedExactBlueId, + Function referenceMaterializer) { + this.canonicalIdentityNode = canonicalNode; + this.initialSemanticNode = + semanticNode != null ? semanticNode : canonicalNode; + this.retainedExactBlueId = retainedExactBlueId; + this.referenceMaterializer = referenceMaterializer; } FrozenNode node() { - return node; + return semanticNode(); + } + + FrozenNode canonicalNode() { + return canonicalIdentityNode; } Object rawScalar() { - return node.getValue(); + FrozenNode semantic = semanticNode(); + return semantic != null ? semantic.getValue() : null; + } + + FrozenNodeBexValue withReferenceMaterializer( + Function materializer) { + if (materializer == null) { + return this; + } + return new FrozenNodeBexValue( + canonicalIdentityNode, + initialSemanticNode, + retainedExactBlueId, + materializer); + } + + @Override + public boolean isExact() { + return true; + } + + @Override + public String exactBlueId() { + if (retainedExactBlueId != null) { + return retainedExactBlueId; + } + if (canonicalIdentityNode == null) { + throw new IllegalStateException("Exact BEX value has no canonical identity node"); + } + String reference = canonicalIdentityNode.getReferenceBlueId(); + return reference != null ? reference : canonicalIdentityNode.blueId(); } @Override public boolean isNull() { - return node.isEmptyNode(); + FrozenNode semantic = semanticNode(); + return semantic != null && semantic.isEmptyNode(); } @Override public boolean isScalar() { - return node.getValue() != null && node.getItems() == null && node.getProperties() == null; + FrozenNode semantic = semanticNode(); + return semantic != null + && semantic.getValue() != null + && semantic.getItems() == null + && semantic.getProperties() == null; } @Override public boolean isObject() { - return node.getProperties() != null || hasObjectCompatibleLanguageFields(); + FrozenNode semantic = semanticNode(); + return semantic != null + && (semantic.getProperties() != null + || hasObjectCompatibleLanguageFields(semantic)); } @Override public boolean isList() { - return node.getItems() != null; + FrozenNode semantic = semanticNode(); + return semantic != null && semantic.getItems() != null; } @Override public BexValue get(String key) { - if (node.getProperties() != null && node.getProperties().containsKey(key)) { - return BexValues.frozen(node.getProperties().get(key)); + FrozenNode semantic = semanticNode(); + if (semantic == null) { + return BexValues.UNDEFINED; } - if (node.getItems() != null) { + if (semantic.getProperties() != null + && semantic.getProperties().containsKey(key)) { + return exactChild(canonicalProperty(key), + semantic.getProperties().get(key)); + } + if (semantic.getItems() != null) { try { - return BexValues.frozen(node.item(Integer.parseInt(key))); + int index = Integer.parseInt(key); + if (index < 0 || index >= semantic.getItems().size()) { + return BexValues.UNDEFINED; + } + return exactChild(canonicalItem(index), + semantic.getItems().get(index)); } catch (NumberFormatException ignored) { return BexValues.UNDEFINED; } } if ("name".equals(key)) { - return node.getName() != null ? BexValues.scalar(node.getName()) : BexValues.UNDEFINED; + return scalarOrUndefined(semantic.getName()); } if ("description".equals(key)) { - return node.getDescription() != null ? BexValues.scalar(node.getDescription()) : BexValues.UNDEFINED; + return scalarOrUndefined(semantic.getDescription()); } + /* + * Physical Blue identity metadata is not a semantic child. This check + * intentionally follows the properties lookup: an actual logical + * property named "blueId" remains visible. A collapsed reference must + * therefore be materialized before absence can be established. + */ if ("blueId".equals(key)) { - String blueId = node.getReferenceBlueId() != null ? node.getReferenceBlueId() : safeBlueId(); - return blueId != null ? BexValues.scalar(blueId) : BexValues.UNDEFINED; + return BexValues.UNDEFINED; } if ("value".equals(key)) { - return node.getValue() != null ? BexValues.scalar(node.getValue()) : BexValues.UNDEFINED; + return scalarOrUndefined(semantic.getValue()); } if ("type".equals(key)) { - return node.getType() != null ? BexValues.frozen(node.getType()) : BexValues.UNDEFINED; + FrozenNode canonical = canonicalCursor(); + return exactChild(canonical != null ? canonical.getType() : null, + semantic.getType()); } if ("itemType".equals(key)) { - return node.getItemType() != null ? BexValues.frozen(node.getItemType()) : BexValues.UNDEFINED; + FrozenNode canonical = canonicalCursor(); + return exactChild( + canonical != null ? canonical.getItemType() : null, + semantic.getItemType()); } if ("keyType".equals(key)) { - return node.getKeyType() != null ? BexValues.frozen(node.getKeyType()) : BexValues.UNDEFINED; + FrozenNode canonical = canonicalCursor(); + return exactChild( + canonical != null ? canonical.getKeyType() : null, + semantic.getKeyType()); } if ("valueType".equals(key)) { - return node.getValueType() != null ? BexValues.frozen(node.getValueType()) : BexValues.UNDEFINED; + FrozenNode canonical = canonicalCursor(); + return exactChild( + canonical != null ? canonical.getValueType() : null, + semantic.getValueType()); } if ("blue".equals(key)) { - return node.getBlue() != null ? BexValues.frozen(node.getBlue()) : BexValues.UNDEFINED; + FrozenNode canonical = canonicalCursor(); + return exactChild(canonical != null ? canonical.getBlue() : null, + semantic.getBlue()); } if ("contracts".equals(key)) { - return node.getContracts() != null ? BexValues.frozen(node.getContracts()) : BexValues.UNDEFINED; + FrozenNode canonical = canonicalCursor(); + return exactChild( + canonical != null ? canonical.getContracts() : null, + semantic.getContracts()); } if ("schema".equals(key)) { - return BexValues.schemaSnapshot(node.getSchema()); + return BexValues.schemaSnapshot(semantic.getSchema()); } if ("mergePolicy".equals(key)) { - return node.getMergePolicy() != null ? BexValues.scalar(node.getMergePolicy()) : BexValues.UNDEFINED; + return scalarOrUndefined(semantic.getMergePolicy()); } return BexValues.UNDEFINED; } @Override public BexValue at(List pointerSegments) { - FrozenNode selected = node.at(JsonPointer.toPointer(pointerSegments)); - if (selected != null) { - return BexValues.frozen(selected); - } + /* + * Deliberately traverse the semantic cursor rather than FrozenNode.at: + * the latter cannot pair a pure canonical reference with its resolved + * graph and could expose representation details. + */ return BexValues.atSegments(this, pointerSegments); } @Override public String asText() { - if (node.getValue() == null) { - return super.asText(); - } - return String.valueOf(node.getValue()); + FrozenNode semantic = semanticNode(); + Object value = semantic != null ? semantic.getValue() : null; + return value != null ? String.valueOf(value) : super.asText(); } @Override public BigInteger asInteger() { - return node.getValue() != null ? BexValues.scalar(node.getValue()).asInteger() : super.asInteger(); + FrozenNode semantic = semanticNode(); + Object value = semantic != null ? semantic.getValue() : null; + return value != null ? BexValues.scalar(value).asInteger() : super.asInteger(); } @Override public BigDecimal asNumber() { - return node.getValue() != null ? BexValues.scalar(node.getValue()).asNumber() : super.asNumber(); + FrozenNode semantic = semanticNode(); + Object value = semantic != null ? semantic.getValue() : null; + return value != null ? BexValues.scalar(value).asNumber() : super.asNumber(); } @Override public boolean asBoolean() { - return node.getValue() != null ? BexValues.scalar(node.getValue()).asBoolean() : super.asBoolean(); + FrozenNode semantic = semanticNode(); + Object value = semantic != null ? semantic.getValue() : null; + return value != null ? BexValues.scalar(value).asBoolean() : super.asBoolean(); } @Override public List keys() { - if (node.getProperties() == null && !hasObjectCompatibleLanguageFields()) { - return Collections.emptyList(); + List established = canonicalKeys; + if (established != null) { + return established; } + FrozenNode semantic = semanticNode(); + if (semantic == null + || (semantic.getProperties() == null + && !hasObjectCompatibleLanguageFields(semantic))) { + canonicalKeys = Collections.emptyList(); + return canonicalKeys; + } + synchronized (this) { + established = canonicalKeys; + if (established == null) { + established = establishCanonicalKeys(semantic); + canonicalKeys = established; + } + } + return established; + } + + private List establishCanonicalKeys( + FrozenNode semantic) { LinkedHashSet fields = new LinkedHashSet<>(); - if (hasObjectCompatibleLanguageFields()) { - addLanguageKeys(fields); + if (hasObjectCompatibleLanguageFields(semantic)) { + addLanguageKeys(fields, semantic); } - if (node.getProperties() != null) { - fields.addAll(node.getProperties().keySet()); + if (semantic.getProperties() != null) { + fields.addAll(semantic.getProperties().keySet()); } - ArrayList keys = new ArrayList<>(fields); - Collections.sort(keys); - return keys; + return Collections.unmodifiableList( + BexUnicodeOrder.sortedCopy(fields)); } @Override public int size() { - if (node.getItems() != null) { - return node.getItems().size(); + FrozenNode semantic = semanticNode(); + if (semantic == null) { + return 0; + } + if (semantic.getItems() != null) { + return semantic.getItems().size(); } - if (node.getProperties() != null) { + if (semantic.getProperties() != null) { return keys().size(); } - return hasObjectCompatibleLanguageFields() ? keys().size() : (isScalar() ? 1 : 0); + return hasObjectCompatibleLanguageFields(semantic) + ? keys().size() + : (isScalar() ? 1 : 0); } @Override public Node toNode() { - return node.toNode(); + FrozenNode semantic = semanticNode(); + return semantic != null + ? semantic.toNode() + : new Node().blueId(exactBlueId()); } @Override public Object toSimple() { - if (node.getValue() != null) { - return node.getValue(); + FrozenNode semantic = semanticNode(); + if (semantic == null) { + return null; + } + if (semantic.getValue() != null) { + return semantic.getValue(); } - if (node.getItems() != null) { + if (semantic.getItems() != null) { ArrayList out = new ArrayList<>(); - for (FrozenNode item : node.getItems()) { - out.add(BexValues.frozen(item).toSimple()); + for (int i = 0; i < semantic.getItems().size(); i++) { + out.add(get(String.valueOf(i)).toSimple()); } return out; } - if (node.getProperties() != null || hasObjectCompatibleLanguageFields()) { + if (semantic.getProperties() != null + || hasObjectCompatibleLanguageFields(semantic)) { LinkedHashMap out = new LinkedHashMap<>(); for (String key : keys()) { BexValue value = get(key); @@ -190,41 +337,176 @@ public Object toSimple() { return null; } - private boolean hasObjectCompatibleLanguageFields() { - return node.getValue() == null - && node.getItems() == null - && (node.getName() != null - || node.getDescription() != null - || node.getType() != null - || node.getItemType() != null - || node.getKeyType() != null - || node.getValueType() != null - || node.getReferenceBlueId() != null - || node.getBlue() != null - || node.getContracts() != null - || node.getSchema() != null - || node.getMergePolicy() != null); - } - - private void addLanguageKeys(LinkedHashSet keys) { - if (node.getName() != null) keys.add("name"); - if (node.getDescription() != null) keys.add("description"); - if (node.getType() != null) keys.add("type"); - if (node.getItemType() != null) keys.add("itemType"); - if (node.getKeyType() != null) keys.add("keyType"); - if (node.getValueType() != null) keys.add("valueType"); - if (node.getReferenceBlueId() != null) keys.add("blueId"); - if (node.getBlue() != null) keys.add("blue"); - if (node.getContracts() != null) keys.add("contracts"); - if (node.getSchema() != null) keys.add("schema"); - if (node.getMergePolicy() != null) keys.add("mergePolicy"); - } - - private String safeBlueId() { - try { - return node.blueId(); - } catch (RuntimeException ex) { - return null; + private BexValue exactChild(FrozenNode canonical, FrozenNode semantic) { + if (canonical == null && semantic == null) { + return BexValues.UNDEFINED; + } + FrozenNode exactCanonical = + canonical != null ? canonical : semantic; + FrozenNode exactSemantic = semantic; + String canonicalReference = + exactCanonical != null + && exactCanonical.isReferenceOnly() + ? exactCanonical.getReferenceBlueId() + : null; + /* + * A cyclic member has no independently verifiable body. An inline + * resolved child supplied beside its canonical MASTER#index reference + * is therefore not structural evidence: retain the child as an opaque + * exact reference until the cyclic-aware materializer establishes the + * complete set proof. Ordinary exact descendants keep their supplied + * resolved cursor. + */ + if (canonicalReference != null + && canonicalReference.indexOf('#') >= 0) { + exactSemantic = exactCanonical; + } + return new FrozenNodeBexValue( + exactCanonical, + exactSemantic, + null, + referenceMaterializer); + } + + private FrozenNode canonicalProperty(String key) { + FrozenNode canonical = canonicalCursor(); + return canonical != null + && canonical.getProperties() != null + ? canonical.getProperties().get(key) + : null; + } + + private FrozenNode canonicalItem(int index) { + FrozenNode canonical = canonicalCursor(); + return canonical != null + && canonical.getItems() != null + && index >= 0 + && index < canonical.getItems().size() + ? canonical.getItems().get(index) + : null; + } + + private static BexValue scalarOrUndefined(Object value) { + return value != null ? BexValues.scalar(value) : BexValues.UNDEFINED; + } + + private boolean hasObjectCompatibleLanguageFields(FrozenNode semantic) { + return semantic != null + && semantic.getValue() == null + && semantic.getItems() == null + && (semantic.getName() != null + || semantic.getDescription() != null + || semantic.getType() != null + || semantic.getItemType() != null + || semantic.getKeyType() != null + || semantic.getValueType() != null + || semantic.getBlue() != null + || semantic.getContracts() != null + || semantic.getSchema() != null + || semantic.getMergePolicy() != null); + } + + private void addLanguageKeys( + LinkedHashSet keys, + FrozenNode semantic) { + if (semantic.getName() != null) keys.add("name"); + if (semantic.getDescription() != null) keys.add("description"); + if (semantic.getType() != null) keys.add("type"); + if (semantic.getItemType() != null) keys.add("itemType"); + if (semantic.getKeyType() != null) keys.add("keyType"); + if (semantic.getValueType() != null) keys.add("valueType"); + if (semantic.getBlue() != null) keys.add("blue"); + if (semantic.getContracts() != null) keys.add("contracts"); + if (semantic.getSchema() != null) keys.add("schema"); + if (semantic.getMergePolicy() != null) keys.add("mergePolicy"); + } + + private FrozenNode canonicalCursor() { + FrozenNode materialized = materializedCanonicalNode; + if (materialized != null) { + return materialized; + } + FrozenNode canonical = canonicalIdentityNode; + if (canonical == null || !canonical.isReferenceOnly()) { + return canonical; + } + materializeReference(canonical.getReferenceBlueId()); + return materializedCanonicalNode; + } + + /** + * Returns semantic content, materializing a collapsed exact reference only + * when an operation actually needs to inspect it. + */ + private FrozenNode semanticNode() { + FrozenNode materialized = materializedSemanticNode; + if (materialized != null) { + return materialized; + } + FrozenNode semantic = initialSemanticNode; + if (semantic == null || !semantic.isReferenceOnly()) { + return semantic; + } + materializeReference(semantic.getReferenceBlueId()); + return materializedSemanticNode; + } + + private void materializeReference(String blueId) { + synchronized (this) { + boolean semanticNeedsContent = + initialSemanticNode != null + && initialSemanticNode.isReferenceOnly() + && materializedSemanticNode == null; + if (materializedCanonicalNode != null + && !semanticNeedsContent) { + return; + } + if (referenceMaterializer == null) { + throw new BexException( + "Semantic content is unavailable for exact Blue reference " + + blueId); + } + ResolvedSnapshot snapshot = referenceMaterializer.apply(blueId); + if (snapshot == null) { + throw new BexException( + "Reference materializer returned no evidence for " + + blueId); + } + if (!snapshot.isResolutionComplete()) { + throw new BexException( + "Reference materializer returned incomplete evidence for " + + blueId); + } + FrozenNode loadedCanonical = snapshot.frozenCanonicalRoot(); + FrozenNode loadedSemantic = snapshot.frozenResolvedRoot(); + if (loadedCanonical == null + || !blueId.equals(snapshot.blueId())) { + throw new BexException( + "Reference materializer returned mismatched evidence for " + + blueId); + } + if (loadedSemantic == null || loadedSemantic.isReferenceOnly()) { + throw new BexException( + "Reference materializer did not establish semantic content for " + + blueId); + } + /* + * demandedPath("") returns the verified provider direct fragment + * as the resolved lane while the snapshot canonical root remains + * the requested pure reference. That direct fragment is the + * canonical structural cursor: its children retain their exact + * provider BlueIds. Keep an already supplied resolved semantic + * lane rather than replacing it with the shallower fragment. + * + * Publish in semantic-last order. A volatile semantic read then + * observes its matching canonical cursor. + */ + materializedCanonicalNode = loadedCanonical.isReferenceOnly() + ? loadedSemantic + : loadedCanonical; + if (semanticNeedsContent) { + materializedSemanticNode = loadedSemantic; + } } } } diff --git a/src/main/java/blue/bex/value/MapBexValue.java b/src/main/java/blue/bex/value/MapBexValue.java index 1ca9ab6..4157ed0 100644 --- a/src/main/java/blue/bex/value/MapBexValue.java +++ b/src/main/java/blue/bex/value/MapBexValue.java @@ -7,15 +7,14 @@ import java.math.BigDecimal; import java.math.BigInteger; -import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.TreeSet; final class MapBexValue extends AbstractBexValue { private final Map values; + private final List canonicalKeys; MapBexValue(Map values) { LinkedHashMap copy = new LinkedHashMap<>(); @@ -26,7 +25,16 @@ final class MapBexValue extends AbstractBexValue { } } } - this.values = Collections.unmodifiableMap(copy); + List ordered = + BexUnicodeOrder.sortedCopy(copy.keySet()); + LinkedHashMap canonical = + new LinkedHashMap<>(); + for (String key : ordered) { + canonical.put(key, copy.get(key)); + } + this.values = Collections.unmodifiableMap(canonical); + this.canonicalKeys = + Collections.unmodifiableList(ordered); } @Override @@ -40,9 +48,7 @@ public BexValue get(String key) { @Override public List keys() { - ArrayList keys = new ArrayList<>(values.keySet()); - Collections.sort(keys); - return keys; + return canonicalKeys; } @Override diff --git a/src/main/java/blue/bex/value/NodeBexValue.java b/src/main/java/blue/bex/value/NodeBexValue.java index 932eaa2..f0e24b3 100644 --- a/src/main/java/blue/bex/value/NodeBexValue.java +++ b/src/main/java/blue/bex/value/NodeBexValue.java @@ -15,9 +15,11 @@ final class NodeBexValue extends AbstractBexValue { private final Node node; + private final List canonicalKeys; NodeBexValue(Node node) { this.node = node; + this.canonicalKeys = establishCanonicalKeys(); } Object rawScalar() { @@ -60,13 +62,15 @@ public boolean isList() { @Override public BexValue get(String key) { if (node.getProperties() != null && node.getProperties().containsKey(key)) { - return BexValues.nodeCursor(node.getProperties().get(key)); + return BexValues.nodeCursorTrustedImmutable( + node.getProperties().get(key)); } if (node.getItems() != null) { try { int index = Integer.parseInt(key); return index >= 0 && index < node.getItems().size() - ? BexValues.nodeCursor(node.getItems().get(index)) + ? BexValues.nodeCursorTrustedImmutable( + node.getItems().get(index)) : BexValues.UNDEFINED; } catch (NumberFormatException ignored) { return BexValues.UNDEFINED; @@ -79,28 +83,28 @@ public BexValue get(String key) { return node.getDescription() != null ? BexValues.scalar(node.getDescription()) : BexValues.UNDEFINED; } if ("blueId".equals(key)) { - return node.getBlueId() != null ? BexValues.scalar(node.getBlueId()) : BexValues.UNDEFINED; + return BexValues.UNDEFINED; } if ("value".equals(key)) { return node.getValue() != null ? BexValues.scalar(node.getValue()) : BexValues.UNDEFINED; } if ("type".equals(key)) { - return node.getType() != null ? BexValues.nodeCursor(node.getType()) : BexValues.UNDEFINED; + return node.getType() != null ? BexValues.nodeCursorTrustedImmutable(node.getType()) : BexValues.UNDEFINED; } if ("itemType".equals(key)) { - return node.getItemType() != null ? BexValues.nodeCursor(node.getItemType()) : BexValues.UNDEFINED; + return node.getItemType() != null ? BexValues.nodeCursorTrustedImmutable(node.getItemType()) : BexValues.UNDEFINED; } if ("keyType".equals(key)) { - return node.getKeyType() != null ? BexValues.nodeCursor(node.getKeyType()) : BexValues.UNDEFINED; + return node.getKeyType() != null ? BexValues.nodeCursorTrustedImmutable(node.getKeyType()) : BexValues.UNDEFINED; } if ("valueType".equals(key)) { - return node.getValueType() != null ? BexValues.nodeCursor(node.getValueType()) : BexValues.UNDEFINED; + return node.getValueType() != null ? BexValues.nodeCursorTrustedImmutable(node.getValueType()) : BexValues.UNDEFINED; } if ("blue".equals(key)) { - return node.getBlue() != null ? BexValues.nodeCursor(node.getBlue()) : BexValues.UNDEFINED; + return node.getBlue() != null ? BexValues.nodeCursorTrustedImmutable(node.getBlue()) : BexValues.UNDEFINED; } if ("contracts".equals(key)) { - return node.getContracts() != null ? BexValues.nodeCursor(node.getContracts()) : BexValues.UNDEFINED; + return node.getContracts() != null ? BexValues.nodeCursorTrustedImmutable(node.getContracts()) : BexValues.UNDEFINED; } if ("schema".equals(key)) { return BexValues.schemaSnapshot(node.getSchema()); @@ -148,6 +152,10 @@ public boolean asBoolean() { @Override public List keys() { + return canonicalKeys; + } + + private List establishCanonicalKeys() { if (node.getProperties() == null && !hasObjectCompatibleLanguageFields()) { return Collections.emptyList(); } @@ -158,9 +166,8 @@ public List keys() { if (node.getProperties() != null) { fields.addAll(node.getProperties().keySet()); } - ArrayList keys = new ArrayList<>(fields); - Collections.sort(keys); - return keys; + return Collections.unmodifiableList( + BexUnicodeOrder.sortedCopy(fields)); } @Override @@ -185,7 +192,7 @@ public Object toSimple() { if (node.getItems() != null) { ArrayList out = new ArrayList<>(); for (Node item : node.getItems()) { - out.add(BexValues.nodeCursor(item).toSimple()); + out.add(BexValues.nodeCursorTrustedImmutable(item).toSimple()); } return out; } @@ -211,7 +218,6 @@ private boolean hasObjectCompatibleLanguageFields() { || node.getItemType() != null || node.getKeyType() != null || node.getValueType() != null - || node.getBlueId() != null || node.getBlue() != null || node.getContracts() != null || node.getSchema() != null @@ -225,7 +231,6 @@ private void addLanguageKeys(LinkedHashSet keys) { if (node.getItemType() != null) keys.add("itemType"); if (node.getKeyType() != null) keys.add("keyType"); if (node.getValueType() != null) keys.add("valueType"); - if (node.getBlueId() != null) keys.add("blueId"); if (node.getBlue() != null) keys.add("blue"); if (node.getContracts() != null) keys.add("contracts"); if (node.getSchema() != null) keys.add("schema"); diff --git a/src/main/java/blue/bex/value/NodeRoundTripFrozenNodeFactory.java b/src/main/java/blue/bex/value/NodeRoundTripFrozenNodeFactory.java deleted file mode 100644 index dfd9a3f..0000000 --- a/src/main/java/blue/bex/value/NodeRoundTripFrozenNodeFactory.java +++ /dev/null @@ -1,55 +0,0 @@ -package blue.bex.value; - -import blue.bex.result.BexMetrics; -import blue.language.model.Node; -import blue.language.snapshot.FrozenNode; - -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -/** - * FrozenNode factory backed by the public Node-to-FrozenNode APIs in blue-language-java. - */ -public final class NodeRoundTripFrozenNodeFactory implements BexFrozenNodeFactory { - public static final NodeRoundTripFrozenNodeFactory INSTANCE = new NodeRoundTripFrozenNodeFactory(); - - private NodeRoundTripFrozenNodeFactory() { - } - - @Override - public FrozenNode empty(BexMetrics metrics) { - return FrozenNode.empty(); - } - - @Override - public FrozenNode scalar(Object value, BexMetrics metrics) { - return FrozenNode.fromResolvedNode(new Node().value(value)); - } - - @Override - public FrozenNode list(List items, BexMetrics metrics) { - ArrayList nodes = new ArrayList<>(items.size()); - for (FrozenNode item : items) { - nodes.add(toNodeRoundTrip(item, metrics)); - } - return FrozenNode.fromResolvedNode(new Node().items(nodes)); - } - - @Override - public FrozenNode object(Map properties, BexMetrics metrics) { - LinkedHashMap nodes = new LinkedHashMap<>(); - for (Map.Entry entry : properties.entrySet()) { - nodes.put(entry.getKey(), toNodeRoundTrip(entry.getValue(), metrics)); - } - return FrozenNode.fromResolvedNode(new Node().properties(nodes)); - } - - private Node toNodeRoundTrip(FrozenNode value, BexMetrics metrics) { - if (metrics != null) { - metrics.incrementFrozenWriterChildNodeRoundTrips(); - } - return value.toNode(); - } -} diff --git a/src/main/java/blue/bex/value/OverlayMapBexValue.java b/src/main/java/blue/bex/value/OverlayMapBexValue.java index d7d31cf..3da9490 100644 --- a/src/main/java/blue/bex/value/OverlayMapBexValue.java +++ b/src/main/java/blue/bex/value/OverlayMapBexValue.java @@ -10,13 +10,14 @@ import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; -import java.util.TreeSet; final class OverlayMapBexValue extends AbstractBexValue { private final BexValue base; private final Map overrides; + private volatile List canonicalKeys; OverlayMapBexValue(BexValue base, String key, BexValue value) { this(base, Collections.singletonMap(key, value)); @@ -47,15 +48,33 @@ public BexValue get(String key) { @Override public List keys() { - TreeSet keys = new TreeSet<>(base.keys()); - for (Map.Entry entry : overrides.entrySet()) { - if (entry.getValue() == null || entry.getValue().isUndefined()) { - keys.remove(entry.getKey()); - } else { - keys.add(entry.getKey()); + List established = canonicalKeys; + if (established != null) { + return established; + } + synchronized (this) { + established = canonicalKeys; + if (established == null) { + LinkedHashSet retained = + new LinkedHashSet<>(base.keys()); + for (Map.Entry entry + : overrides.entrySet()) { + if (entry.getValue() == null + || entry.getValue() + .isUndefined()) { + retained.remove(entry.getKey()); + } else { + retained.add(entry.getKey()); + } + } + established = + Collections.unmodifiableList( + BexUnicodeOrder.sortedCopy( + retained)); + canonicalKeys = established; } } - return new ArrayList<>(keys); + return established; } @Override diff --git a/src/main/java/blue/bex/value/PointerSetBexValue.java b/src/main/java/blue/bex/value/PointerSetBexValue.java index 90ac0b4..9d345f6 100644 --- a/src/main/java/blue/bex/value/PointerSetBexValue.java +++ b/src/main/java/blue/bex/value/PointerSetBexValue.java @@ -2,94 +2,131 @@ import blue.bex.BexException; import blue.language.model.Node; -import blue.language.snapshot.FrozenNode; -import blue.language.utils.JsonPointer; -import java.math.BigDecimal; import java.math.BigInteger; import java.util.ArrayList; import java.util.Collections; -import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; -import java.util.Map; -import java.util.TreeSet; final class PointerSetBexValue extends AbstractBexValue { private final BexValue base; private final List segments; private final BexValue value; private final String op; + private final boolean sparseListRemoval; + private final int targetListIndex; + private volatile List canonicalKeys; PointerSetBexValue(BexValue base, List segments, BexValue value, String op) { + this(base, segments, value, op, false); + } + + PointerSetBexValue(BexValue base, + List segments, + BexValue value, + String op, + boolean sparseListRemoval) { if (base == null || base.isUndefined() || base.isNull()) { base = BexValues.map(Collections.emptyMap()); } if (!base.isObject() && !base.isList()) { throw new BexException("$pointerSet base must be an object or list"); } + if (segments == null || segments.isEmpty()) { + throw new IllegalArgumentException( + "PointerSetBexValue requires a non-root path"); + } + String operation = op == null ? "set" : op; + if (!"set".equals(operation) && !"remove".equals(operation)) { + throw new BexException("Unsupported $pointerSet op: " + operation); + } this.base = base; this.segments = Collections.unmodifiableList(new ArrayList<>(segments)); - this.value = value; - this.op = op == null ? "set" : op; + this.value = value != null ? value : BexValues.UNDEFINED; + this.op = operation; + this.sparseListRemoval = sparseListRemoval; + validatePath(base, this.segments, this.value, operation); + this.targetListIndex = base.isList() + ? requireExistingListIndex(this.segments.get(0), base.size()) + : -1; + if (base.isList()) { + this.canonicalKeys = Collections.emptyList(); + } } @Override public boolean isObject() { - return segments.isEmpty() && !"remove".equals(op) ? value.isObject() : base.isObject(); + return base.isObject(); } @Override public boolean isList() { - return segments.isEmpty() && !"remove".equals(op) ? value.isList() : base.isList(); + return base.isList(); } @Override public BexValue get(String key) { - if (segments.isEmpty()) { - return "remove".equals(op) ? BexValues.UNDEFINED : value.get(key); + if (base.isList()) { + return getListItem(key); } String head = segments.get(0); if (!head.equals(key)) { return base.get(key); } if (segments.size() == 1) { - return "remove".equals(op) ? BexValues.UNDEFINED : value; + return isOmittedLeaf() ? BexValues.UNDEFINED : value; } BexValue child = base.get(key); if (child.isUndefined() || child.isNull()) { child = BexValues.map(Collections.emptyMap()); } - if (!child.isObject() && !child.isList()) { - throw new BexException("$pointerSet encountered incompatible intermediate scalar"); - } - return new PointerSetBexValue(child, segments.subList(1, segments.size()), value, op); + return new PointerSetBexValue( + child, + segments.subList(1, segments.size()), + value, + op, + sparseListRemoval); } @Override public List keys() { - if (segments.isEmpty()) { - return "remove".equals(op) ? Collections.emptyList() : value.keys(); + List established = canonicalKeys; + if (established != null) { + return established; } - if (base.isList()) { - return base.keys(); - } - TreeSet keys = new TreeSet<>(base.keys()); - if (!segments.isEmpty()) { - if ("remove".equals(op) && segments.size() == 1) { - keys.remove(segments.get(0)); - } else { - keys.add(segments.get(0)); + synchronized (this) { + established = canonicalKeys; + if (established == null) { + LinkedHashSet retained = + new LinkedHashSet<>(base.keys()); + if (segments.size() == 1 + && isOmittedLeaf()) { + retained.remove(segments.get(0)); + } else { + retained.add(segments.get(0)); + } + established = + Collections.unmodifiableList( + BexUnicodeOrder.sortedCopy( + retained)); + canonicalKeys = established; } } - return new ArrayList<>(keys); + return established; } @Override public int size() { - if (segments.isEmpty()) { - return "remove".equals(op) ? 0 : value.size(); + if (!base.isList()) { + return keys().size(); } - return base.isList() ? base.size() : keys().size(); + if (segments.size() == 1 + && "remove".equals(op) + && !sparseListRemoval) { + return base.size() - 1; + } + return base.size(); } @Override @@ -97,4 +134,112 @@ public int size() { @Override public Object toSimple() { return BexSimpleWriter.toSimple(this); } + + private BexValue getListItem(String key) { + int requested = parseReadableListIndex(key); + if (requested < 0 || requested >= size()) { + return BexValues.UNDEFINED; + } + if (segments.size() == 1 && "remove".equals(op)) { + if (sparseListRemoval) { + return requested == targetListIndex + ? BexValues.UNDEFINED + : base.get(String.valueOf(requested)); + } + int source = requested < targetListIndex + ? requested + : requested + 1; + return base.get(String.valueOf(source)); + } + if (requested != targetListIndex) { + return base.get(String.valueOf(requested)); + } + if (segments.size() == 1) { + // Undefined cannot become a dense BEX list item; validatePath + // rejects this case before the overlay can be observed. + return value; + } + BexValue child = base.get(String.valueOf(targetListIndex)); + if (child.isUndefined() || child.isNull()) { + child = BexValues.map(Collections.emptyMap()); + } + return new PointerSetBexValue( + child, + segments.subList(1, segments.size()), + value, + op, + sparseListRemoval); + } + + private boolean isOmittedLeaf() { + return "remove".equals(op) || value.isUndefined(); + } + + private static void validatePath(BexValue base, + List segments, + BexValue value, + String op) { + BexValue current = base; + for (int index = 0; index < segments.size(); index++) { + if (current.isUndefined() || current.isNull()) { + current = BexValues.map( + Collections.emptyMap()); + } + if (!current.isObject() && !current.isList()) { + throw new BexException( + "$pointerSet encountered incompatible intermediate scalar"); + } + + String segment = segments.get(index); + boolean terminal = index == segments.size() - 1; + if (current.isList()) { + int listIndex = + requireExistingListIndex(segment, current.size()); + if (terminal && "set".equals(op) && value.isUndefined()) { + throw new BexException( + "$pointerSet cannot set a list item to undefined"); + } + if (!terminal) { + current = current.get(String.valueOf(listIndex)); + } + } else if (!terminal) { + current = current.get(segment); + } + } + } + + private static int requireExistingListIndex(String segment, int size) { + int index = parseReadableListIndex(segment); + if (index < 0) { + throw new BexException( + "$pointerSet list segment must be a non-negative integer: " + + segment); + } + if (index >= size) { + throw new BexException( + "$pointerSet list index is out of range: " + segment); + } + return index; + } + + private static int parseReadableListIndex(String segment) { + if (segment == null || segment.isEmpty()) { + return -1; + } + for (int index = 0; index < segment.length(); index++) { + char ch = segment.charAt(index); + if (ch < '0' || ch > '9') { + return -1; + } + } + try { + BigInteger parsed = new BigInteger(segment); + if (parsed.compareTo(BigInteger.valueOf(Integer.MAX_VALUE)) > 0) { + return -1; + } + return parsed.intValue(); + } catch (NumberFormatException ex) { + return -1; + } + } } diff --git a/src/main/java/blue/bex/value/ScalarBexValue.java b/src/main/java/blue/bex/value/ScalarBexValue.java index 251f447..19acdc7 100644 --- a/src/main/java/blue/bex/value/ScalarBexValue.java +++ b/src/main/java/blue/bex/value/ScalarBexValue.java @@ -89,6 +89,9 @@ public boolean asBoolean() { @Override public Object toSimple() { return value; } + @Override + public int size() { return 1; } + @Override public String toString() { return String.valueOf(value); } } diff --git a/src/main/resources/blue/bex/gas/blue-bex-gas-2.0.yaml b/src/main/resources/blue/bex/gas/blue-bex-gas-2.0.yaml new file mode 100644 index 0000000..0dc348f --- /dev/null +++ b/src/main/resources/blue/bex/gas/blue-bex-gas-2.0.yaml @@ -0,0 +1,91 @@ +manifestType: blue-bex-gas-manifest +schedule: blue-bex/gas/2.0 +specification: Blue BEX +specificationVersion: '2.0' +status: implementation-baseline-pending-calibration +unit: gas +hostSchedule: blue-contracts/gas/1.0 +counterCount: 30 +counters: + expressionEvaluated: 1 + statementExecuted: 1 + functionCalled: 2 + intrinsicCalled: 5 + documentRead: 2 + eventRead: 1 + processingEventRead: 1 + currentContractRead: 1 + stepsRead: 1 + bindingRead: 1 + variableRead: 1 + constantRead: 1 + resultValueRead: 2 + pointerSegmentRead: 1 + pointerSegmentWritten: 1 + objectMemberRead: 1 + listItemRead: 1 + collectionItemVisited: 1 + collectionItemProduced: 1 + textBlockExamined: 1 + textBlockConstructed: 1 + integerLimbOperation: 1 + comparisonNodeVisited: 1 + sortComparison: 1 + patchAppended: 5 + eventAppended: 5 + transientObjectMemberProduced: 1 + transientListItemProduced: 1 + blueOutputBoundary: 5 + nodeIdentityRequested: 5 +admissionRule: Admit quantity * weight before work into a child ledger bounded by the exact remaining Contracts budget. +mergeRule: Merge the ordered child ledger into the parent exactly once; a local limit may only reduce the remaining budget. +formulas: + textBlocks: + blockCodePoints: 64 + fullScan: ceil(codePointLength / 64) + construction: ceil(constructedCodePointLength / 64) + comparison: charge blocks actually read from each operand + integerLimbs: + radix: 2^32 + minimumLimbs: 1 + equalityOrOrdering: L(a) + L(b) + additionOrSubtraction: max(L(a), L(b)) + 1 + multiplication: L(a) * L(b) + divisionOrRemainder: L(a) * L(b) + decimalScaleAlignment: same unscaled Integer formula plus one operation + collectionIteration: + input: collectionItemVisited once per evaluated item + output: collectionItemProduced once per produced item + dynamicObject: transientObjectMemberProduced per retained field + dynamicList: transientListItemProduced per item + shortCircuit: no charges for skipped items + sorting: + algorithm: stable bottom-up merge sort + initialRunWidth: 1 + mergeOrder: left-to-right + equalSelection: left + widthProgression: double after each pass + comparisonCharges: + - sortComparison + - comparisonNodeVisited + - scalar content work + blueBoundary: + everyValue: blueOutputBoundary += 1 + existingExactValue: no recursive construction or size charge + transientValue: Contracts semantic identity establishment is merged once + nodeBlueIdTransient: nodeIdentityRequested + blueOutputBoundary + Contracts identity establishment +forbiddenPortableMetering: +- recursive estimatedSize +- serialized payload bytes at patch or event boundary +- UTF-16 length +- cache-dependent discounts +- opaque implementation-selected gasConsumed +fixtureRequirements: +- one exact microfixture per named counter +- operator behavior vectors +- representation-blind inline/reference variants +- shared-meter exhaustion prefix +- intrinsic named-ledger fixture +identityAlgorithm: sha256 of UTF-8 canonical JSON with packageIdentity set to null +packageIdentity: sha256:41247c820d91a12fdfc17fd9e787a5d8d668d8acc5954fdcb131715bf9e6147d +numericWeightsStatus: provisional pending calibration; counter names, ownership, formulas, and trace order are frozen for implementation diff --git a/src/test/java/blue/bex/BexAccumulatorPointerConsistencyTest.java b/src/test/java/blue/bex/BexAccumulatorPointerConsistencyTest.java index e10ccae..5c5adac 100644 --- a/src/test/java/blue/bex/BexAccumulatorPointerConsistencyTest.java +++ b/src/test/java/blue/bex/BexAccumulatorPointerConsistencyTest.java @@ -5,6 +5,7 @@ import blue.bex.api.BexProgramSource; import blue.bex.api.BexStepResults; import blue.bex.api.FrozenBexDocumentView; +import blue.bex.gas.BexGasCounter; import blue.bex.result.BexExecutionResult; import blue.bex.value.BexValues; import blue.language.model.Node; @@ -69,8 +70,7 @@ void documentPointersRemainDocumentScopeRelative() { BexExecutionResult result = runStep(stepDo(list( op("$appendChange", obj("op", "replace", "path", "status", "val", "done")), - op("$appendChanges", list(obj("op", "replace", "path", "status", "val", "batch"))), - emptyStatement() + op("$appendChanges", list(obj("op", "replace", "path", "status", "val", "batch"))) )), context); assertEquals("/contracts/current/status", result.changeset().entries().get(0).absolutePath()); @@ -114,8 +114,7 @@ void pointerJoinEscapesSegmentsAndWorksForPatchPaths() { "op", "replace", "path", op("$pointerJoin", list("orders", op("$var", "id"), "status")), "val", "confirmed" - )), - emptyStatement() + )) )), defaultContext()); assertEquals("/orders/abc~1def~0ghi/status", result.changeset().entries().get(0).absolutePath()); @@ -143,12 +142,10 @@ void appendChangeAndAppendChangesValidatePatchEntriesConsistently() { @Test void removePatchesDoNotRequireValuesAndSingleRemoveDoesNotEvaluateVal() { BexExecutionResult single = runStep(stepDo(list( - op("$appendChange", obj("op", "remove", "path", "/x", "val", op("$divide", list(1, 0)))), - emptyStatement() + op("$appendChange", obj("op", "remove", "path", "/x", "val", op("$divide", list(1, 0)))) )), defaultContext()); BexExecutionResult batch = runStep(stepDo(list( - op("$appendChanges", list(obj("op", "remove", "path", "/x"))), - emptyStatement() + op("$appendChanges", list(obj("op", "remove", "path", "/x"))) )), defaultContext()); assertEquals("remove", single.changeset().entries().get(0).op()); @@ -169,37 +166,36 @@ void appendEventAndAppendEventsValidateAndPreserveOrder() { )), defaultContext())); BexExecutionResult result = runStep(stepDo(list( - op("$appendEvents", list(obj("kind", "A"), obj("kind", "B"))), - emptyStatement() + op("$appendEvents", list(obj("kind", "A"), obj("kind", "B"))) )), defaultContext()); assertEquals(l(m("kind", "A"), m("kind", "B")), simple(result.events().asValue())); } @Test - void appendOutputGasScalesWithValueSizeAndEntryCount() { - long smallEventGas = runStep(stepDo(list( - op("$appendEvents", list(obj("kind", "A"))), - emptyStatement() - )), defaultContext()).gasUsed(); - long largeEventGas = runStep(stepDo(list( - op("$appendEvents", list(largeObject(150))), - emptyStatement() - )), defaultContext()).gasUsed(); - long onePatchGas = runStep(stepDo(list( - op("$appendChanges", list(obj("op", "replace", "path", "/a", "val", "x"))), - emptyStatement() - )), defaultContext()).gasUsed(); - long twoPatchGas = runStep(stepDo(list( + void appendOutputGasUsesPortablePerEntryCounters() { + BexExecutionResult oneEvent = runStep(stepDo(list( + op("$appendEvents", list(obj("kind", "A"))) + )), defaultContext()); + BexExecutionResult twoEvents = runStep(stepDo(list( + op("$appendEvents", list(obj("kind", "A"), obj("kind", "B"))) + )), defaultContext()); + BexExecutionResult onePatch = runStep(stepDo(list( + op("$appendChanges", list(obj("op", "replace", "path", "/a", "val", "x"))) + )), defaultContext()); + BexExecutionResult twoPatches = runStep(stepDo(list( op("$appendChanges", list( obj("op", "replace", "path", "/a", "val", "x"), obj("op", "replace", "path", "/b", "val", "x") - )), - emptyStatement() - )), defaultContext()).gasUsed(); + )) + )), defaultContext()); - assertTrue(largeEventGas > smallEventGas); - assertTrue(twoPatchGas > onePatchGas); + assertEquals(1L, oneEvent.gasLedger().quantity(BexGasCounter.EVENT_APPENDED)); + assertEquals(2L, twoEvents.gasLedger().quantity(BexGasCounter.EVENT_APPENDED)); + assertEquals(1L, onePatch.gasLedger().quantity(BexGasCounter.PATCH_APPENDED)); + assertEquals(2L, twoPatches.gasLedger().quantity(BexGasCounter.PATCH_APPENDED)); + assertTrue(twoEvents.gasUsed() > oneEvent.gasUsed()); + assertTrue(twoPatches.gasUsed() > onePatch.gasUsed()); } private static void compile(Node step) { diff --git a/src/test/java/blue/bex/BexBlueTypeMatchingGasTest.java b/src/test/java/blue/bex/BexBlueTypeMatchingGasTest.java new file mode 100644 index 0000000..724fce9 --- /dev/null +++ b/src/test/java/blue/bex/BexBlueTypeMatchingGasTest.java @@ -0,0 +1,647 @@ +package blue.bex; + +import blue.bex.api.BexEngine; +import blue.bex.api.BexExecutionContext; +import blue.bex.api.BexProgramSource; +import blue.bex.api.FrozenBexDocumentView; +import blue.bex.gas.BexGasCounter; +import blue.bex.gas.BexGasLimitExceededException; +import blue.bex.gas.BexGasMeter; +import blue.bex.gas.BexGasSchedule; +import blue.bex.result.BexExecutionResult; +import blue.bex.type.BexBlueTypeMatcher; +import blue.bex.value.BexValue; +import blue.bex.value.BexValues; +import blue.language.Blue; +import blue.language.NodeProvider; +import blue.language.model.Node; +import blue.language.processor.ExecutionEvidenceUnavailableException; +import blue.language.processor.InvalidExecutionEvidenceException; +import blue.language.processor.ProcessorErrorCategory; +import blue.language.processor.ProcessorFailureException; +import blue.language.provider.NodeProviderResult; +import blue.language.snapshot.FrozenNode; +import blue.language.utils.BlueIdCalculator; +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import static blue.bex.test.BexTestFixtures.defaultContext; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class BexBlueTypeMatchingGasTest { + private final Blue blue = new Blue(); + private final BexEngine engine = + BexEngine.builder().blue(blue).build(); + + @Test + void structuralIsChargesEveryComparedSemanticOccurrence() { + BexExecutionResult result = run( + "type: Blue/BEX Program", + "expr:", + " $is:", + " node:", + " request:", + " nights: 2", + " pattern:", + " request:", + " nights:", + " type: Integer"); + + assertEquals(true, result.value().toSimple()); + assertEquals(3L, result.gasLedger().quantity( + BexGasCounter.COMPARISON_NODE_VISITED)); + } + + @Test + void typedFunctionArgumentUsesTheSameDeepMeteredBoundary() { + BexExecutionResult result = run( + "type: Blue/BEX Program", + "functions:", + " accept:", + " args:", + " request:", + " stay:", + " nights:", + " type: Integer", + " expr: true", + "expr:", + " $call:", + " function: accept", + " args:", + " request:", + " stay:", + " nights: 2"); + + assertEquals(true, result.value().toSimple()); + assertEquals(3L, result.gasLedger().quantity( + BexGasCounter.COMPARISON_NODE_VISITED)); + } + + @Test + void objectMatchingUsesCanonicalKeyOrderAndStopsAtFirstDifference() { + BexExecutionResult result = run( + "type: Blue/BEX Program", + "expr:", + " $is:", + " node:", + " z: 1", + " a: wrong", + " pattern:", + " z:", + " type: Integer", + " schema:", + " required: true", + " a:", + " type: Integer", + " schema:", + " required: true"); + + assertEquals(false, result.value().toSimple()); + assertEquals(2L, result.gasLedger().quantity( + BexGasCounter.COMPARISON_NODE_VISITED)); + } + + @Test + void listMatchingVisitsPositionsInOrderAndStopsAtFirstDifference() { + BexExecutionResult result = run( + "type: Blue/BEX Program", + "expr:", + " $is:", + " node:", + " - 1", + " - wrong", + " - 3", + " pattern:", + " - type: Integer", + " - type: Integer", + " - type: Integer"); + + assertEquals(false, result.value().toSimple()); + assertEquals(3L, result.gasLedger().quantity( + BexGasCounter.COMPARISON_NODE_VISITED)); + } + + @Test + void scalarPatternComparisonsAddTextAndNumericWork() { + String text = repeat('x', 130); + BexExecutionResult textResult = run( + "type: Blue/BEX Program", + "expr:", + " $is:", + " node:", + " payload: \"" + text + "\"", + " pattern:", + " payload: \"" + text + "\""); + + assertEquals(true, textResult.value().toSimple()); + assertEquals(2L, textResult.gasLedger().quantity( + BexGasCounter.COMPARISON_NODE_VISITED)); + assertEquals(6L, textResult.gasLedger().quantity( + BexGasCounter.TEXT_BLOCK_EXAMINED)); + + BexExecutionResult integerResult = run( + "type: Blue/BEX Program", + "expr:", + " $is:", + " node:", + " payload:", + " $literal:", + " type: Integer", + " value: \"18446744073709551616\"", + " pattern:", + " payload:", + " type: Integer", + " value: \"18446744073709551616\""); + + assertEquals(true, integerResult.value().toSimple()); + assertEquals(2L, integerResult.gasLedger().quantity( + BexGasCounter.COMPARISON_NODE_VISITED)); + assertEquals(6L, integerResult.gasLedger().quantity( + BexGasCounter.INTEGER_LIMB_OPERATION)); + } + + @Test + void recursiveMatchIsAdmittedBeforeTheChildComparison() { + Map candidate = new LinkedHashMap<>(); + candidate.put("child", 1); + FrozenNode pattern = FrozenNode.fromResolvedNode(blue.yamlToNode( + "child:\n" + + " type: Integer\n")); + BexGasMeter meter = new BexGasMeter( + BexGasSchedule.defaults(), 1L); + + assertThrows(BexGasLimitExceededException.class, + () -> new BexBlueTypeMatcher(blue).matches( + BexValues.fromSimple(candidate), + pattern, + meter, + null)); + assertEquals(1L, meter.ledger().quantity( + BexGasCounter.COMPARISON_NODE_VISITED)); + assertEquals(1, meter.trace().size()); + assertTrue(meter.trace().get(0).reason().contains( + "comparisonNodeVisited")); + } + + @Test + void unavailableNestedExactEvidenceIsNotConvertedToATypeMismatch() { + Node content = new Node().properties( + Collections.singletonMap( + "value", + new Node().value("known"))); + String blueId = + BlueIdCalculator.calculateBlueId(content); + NodeProvider provider = providerReturning( + NodeProviderResult.unavailable( + "type evidence is offline")); + + try (Blue unavailableBlue = new Blue(provider)) { + BexGasMeter meter = new BexGasMeter( + BexGasSchedule.defaults(), + 1_000_000L); + ExecutionEvidenceUnavailableException failure = + assertThrows( + ExecutionEvidenceUnavailableException.class, + () -> new BexBlueTypeMatcher( + unavailableBlue) + .matches( + transientWithReference( + unavailableBlue, + blueId), + nonEmptyPattern(), + meter, + null)); + + assertEquals( + Collections.singletonList(blueId), + failure.requiredExactBlueIds()); + assertEquals( + "type evidence is offline", + failure.getMessage()); + assertEquals( + 2L, + meter.ledger().quantity( + BexGasCounter + .COMPARISON_NODE_VISITED)); + assertEquals(2, meter.trace().size()); + } + } + + @Test + void invalidNestedExactEvidenceIsNotConvertedToATypeMismatch() { + Node content = new Node().properties( + Collections.singletonMap( + "value", + new Node().value("known"))); + String blueId = + BlueIdCalculator.calculateBlueId(content); + NodeProvider provider = providerReturning( + NodeProviderResult.invalidEvidence( + "type evidence is invalid")); + + try (Blue invalidBlue = new Blue(provider)) { + BexGasMeter meter = new BexGasMeter( + BexGasSchedule.defaults(), + 1_000_000L); + InvalidExecutionEvidenceException failure = + assertThrows( + InvalidExecutionEvidenceException.class, + () -> new BexBlueTypeMatcher( + invalidBlue) + .matches( + transientWithReference( + invalidBlue, + blueId), + nonEmptyPattern(), + meter, + null)); + + assertEquals( + "type evidence is invalid", + failure.getMessage()); + assertEquals( + 2L, + meter.ledger().quantity( + BexGasCounter + .COMPARISON_NODE_VISITED)); + assertEquals(2, meter.trace().size()); + } + } + + @Test + void malformedLocalBlueShapeRemainsATypeMismatch() { + String blueId = + BlueIdCalculator.calculateBlueId( + new Node().value("referenced")); + Map malformed = + new LinkedHashMap<>(); + malformed.put("blueId", blueId); + malformed.put("sibling", 1); + BexGasMeter meter = new BexGasMeter( + BexGasSchedule.defaults(), + 1_000_000L); + + assertFalse(new BexBlueTypeMatcher(blue) + .matches( + BexValues.fromSimple(malformed), + nonEmptyPattern(), + meter, + null)); + assertEquals( + 1L, + meter.ledger().quantity( + BexGasCounter + .COMPARISON_NODE_VISITED)); + assertEquals(1, meter.trace().size()); + } + + @Test + void isPropagatesAnArbitraryReferenceProviderFailure() { + Node content = new Node().properties( + Collections.singletonMap( + "value", + new Node().value("known"))); + String blueId = + BlueIdCalculator.calculateBlueId(content); + IllegalStateException expected = + new IllegalStateException( + "reference provider implementation defect"); + NodeProvider provider = ignored -> { + throw expected; + }; + + try (Blue providerBlue = new Blue(provider)) { + IllegalStateException observed = assertThrows( + IllegalStateException.class, + () -> executeProviderBackedIs( + providerBlue, blueId)); + + assertSame(expected, observed); + } + } + + @Test + void deterministicProcessorFailureWinsOverNestedUnavailabilityInIs() { + Node content = new Node().properties( + Collections.singletonMap( + "value", + new Node().value("known"))); + String blueId = + BlueIdCalculator.calculateBlueId(content); + ExecutionEvidenceUnavailableException nested = + new ExecutionEvidenceUnavailableException( + "nested evidence detail"); + ProcessorFailureException expected = + new ProcessorFailureException( + ProcessorErrorCategory + .RuntimeExecutionFailure, + "deterministic provider rejection", + nested); + NodeProvider provider = ignored -> { + throw expected; + }; + + try (Blue providerBlue = new Blue(provider)) { + ProcessorFailureException observed = assertThrows( + ProcessorFailureException.class, + () -> executeProviderBackedIs( + providerBlue, blueId)); + + assertSame(expected, observed); + } + } + + @Test + void wideTransientCandidateDoesNotDemandARejectedLaterChild() { + AtomicInteger providerDemands = + new AtomicInteger(); + Node content = new Node().value("known"); + String blueId = + BlueIdCalculator.calculateBlueId(content); + NodeProvider provider = countingProvider( + providerDemands, + NodeProviderResult.unavailable( + "later child is offline")); + + try (Blue unavailableBlue = new Blue(provider)) { + Map wide = + new LinkedHashMap<>(); + for (int index = 0; index < 256; index++) { + wide.put( + String.format("field%03d", index), + BexValues.scalar(index)); + } + wide.put( + "zzLater", + exactReference( + unavailableBlue, blueId)); + FrozenNode pattern = + FrozenNode.fromResolvedNode( + new Node().properties( + Collections.singletonMap( + "zzLater", + new Node().value( + "expected")))); + BexGasMeter meter = new BexGasMeter( + BexGasSchedule.defaults(), 1L); + + assertThrows( + BexGasLimitExceededException.class, + () -> new BexBlueTypeMatcher( + unavailableBlue).matches( + BexValues.map(wide), + pattern, + meter, + null)); + assertEquals(0, providerDemands.get()); + assertEquals(1L, meter.ledger().quantity( + BexGasCounter + .COMPARISON_NODE_VISITED)); + } + } + + @Test + void deepTransientCandidateStopsBeforeRejectedExactLeaf() { + AtomicInteger providerDemands = + new AtomicInteger(); + Node content = new Node().value("known"); + String blueId = + BlueIdCalculator.calculateBlueId(content); + NodeProvider provider = countingProvider( + providerDemands, + NodeProviderResult.unavailable( + "deep leaf is offline")); + + try (Blue unavailableBlue = new Blue(provider)) { + int depth = 32; + BexValue candidate = exactReference( + unavailableBlue, blueId); + Node target = new Node().value( + "expected"); + for (int index = depth - 1; + index >= 0; + index--) { + String key = String.format( + "level%02d", index); + candidate = BexValues.map( + Collections.singletonMap( + key, candidate)); + target = new Node().properties( + Collections.singletonMap( + key, target)); + } + FrozenNode pattern = + FrozenNode.fromResolvedNode(target); + BexGasMeter meter = new BexGasMeter( + BexGasSchedule.defaults(), + depth); + final BexValue deepCandidate = + candidate; + + assertThrows( + BexGasLimitExceededException.class, + () -> new BexBlueTypeMatcher( + unavailableBlue).matches( + deepCandidate, + pattern, + meter, + null)); + assertEquals(0, providerDemands.get()); + assertEquals( + depth, + meter.ledger().quantity( + BexGasCounter + .COMPARISON_NODE_VISITED)); + } + } + + @Test + void exactStructuralCursorDoesNotDemandRejectedNestedReference() { + AtomicInteger providerDemands = + new AtomicInteger(); + Node content = new Node().value("known"); + String blueId = + BlueIdCalculator.calculateBlueId(content); + NodeProvider provider = countingProvider( + providerDemands, + NodeProviderResult.unavailable( + "nested exact child is offline")); + + try (Blue unavailableBlue = new Blue(provider)) { + FrozenNode exactRoot = + FrozenNode.fromResolvedNode( + new Node().properties( + Collections.singletonMap( + "child", + new Node().blueId( + blueId)))); + BexValue candidate = + BexValues.referenceBacked( + BexValues.frozen(exactRoot), + unavailableBlue); + FrozenNode pattern = + FrozenNode.fromResolvedNode( + new Node().properties( + Collections.singletonMap( + "child", + new Node().value( + "expected")))); + BexGasMeter meter = new BexGasMeter( + BexGasSchedule.defaults(), 1L); + + assertThrows( + BexGasLimitExceededException.class, + () -> new BexBlueTypeMatcher( + unavailableBlue).matches( + candidate, + pattern, + meter, + null)); + assertEquals(0, providerDemands.get()); + assertEquals(1L, meter.ledger().quantity( + BexGasCounter + .COMPARISON_NODE_VISITED)); + } + } + + private BexExecutionResult run(String... lines) { + Node program = blue.yamlToNode(join(lines)); + return engine.compileAndExecute( + BexProgramSource.inline( + FrozenNode.fromResolvedNode(program)), + defaultContext()); + } + + private static void executeProviderBackedIs( + Blue blue, + String blueId) { + FrozenNode document = FrozenNode.fromResolvedNode( + new Node().properties( + Collections.singletonMap( + "x", + new Node().blueId(blueId)))); + BexExecutionContext context = + BexExecutionContext.builder() + .document(new FrozenBexDocumentView( + document)) + .build(); + Node program = blue.yamlToNode(join( + "type: Blue/BEX Program", + "expr:", + " $is:", + " node:", + " $document: /x", + " pattern:", + " value: expected")); + + BexEngine.builder() + .blue(blue) + .build() + .compileAndExecute( + BexProgramSource.inline( + FrozenNode.fromResolvedNode( + program)), + context); + } + + private static String join(String... lines) { + StringBuilder yaml = new StringBuilder(); + for (String line : lines) { + if (yaml.length() > 0) { + yaml.append('\n'); + } + yaml.append(line); + } + return yaml.toString(); + } + + private static String repeat(char value, int count) { + StringBuilder out = new StringBuilder(count); + for (int index = 0; index < count; index++) { + out.append(value); + } + return out.toString(); + } + + private static BexValue transientWithReference( + Blue blue, + String blueId) { + Map candidate = + new LinkedHashMap<>(); + candidate.put( + "child", + BexValues.referenceBacked( + BexValues.frozen( + FrozenNode.fromNode( + new Node().blueId( + blueId))), + blue)); + return BexValues.map(candidate); + } + + private static BexValue exactReference( + Blue blue, + String blueId) { + return BexValues.referenceBacked( + BexValues.frozen( + FrozenNode.fromNode( + new Node().blueId( + blueId))), + blue); + } + + private static FrozenNode nonEmptyPattern() { + return FrozenNode.fromResolvedNode( + new Node().properties( + Collections.singletonMap( + "child", + new Node().value( + "expected")))); + } + + private static NodeProvider providerReturning( + NodeProviderResult result) { + return new NodeProvider() { + @Override + public List fetchByBlueId( + String blueId) { + return Collections.emptyList(); + } + + @Override + public NodeProviderResult fetchResultByBlueId( + String blueId) { + return result; + } + }; + } + + private static NodeProvider countingProvider( + AtomicInteger demands, + NodeProviderResult result) { + return new NodeProvider() { + @Override + public List fetchByBlueId( + String blueId) { + demands.incrementAndGet(); + return Collections.emptyList(); + } + + @Override + public NodeProviderResult fetchResultByBlueId( + String blueId) { + demands.incrementAndGet(); + return result; + } + }; + } +} diff --git a/src/test/java/blue/bex/BexBlueTypeSupportTest.java b/src/test/java/blue/bex/BexBlueTypeSupportTest.java index 3d65c95..7f520dc 100644 --- a/src/test/java/blue/bex/BexBlueTypeSupportTest.java +++ b/src/test/java/blue/bex/BexBlueTypeSupportTest.java @@ -24,6 +24,7 @@ import static blue.bex.test.BexTestFixtures.op; import static blue.bex.test.BexTestFixtures.simple; import static blue.bex.test.BexTestFixtures.stepExpr; +import static blue.language.utils.Properties.INTEGER_TYPE_BLUE_ID; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; @@ -32,16 +33,23 @@ class BexBlueTypeSupportTest { private static final Blue YAML_BLUE = new Blue(); + private static final Node HOTEL_ORDER_TYPE = YAML_BLUE.yamlToNode(yaml( + "status:", + " type: Text")); + private static final Node RESTAURANT_ORDER_TYPE = YAML_BLUE.yamlToNode(yaml( + "restaurantStatus:", + " type: Text")); + private static final String HOTEL_ORDER_TYPE_ID = + FrozenNode.fromNode(HOTEL_ORDER_TYPE).blueId(); + private static final String RESTAURANT_ORDER_TYPE_ID = + FrozenNode.fromNode(RESTAURANT_ORDER_TYPE).blueId(); private final Blue blue = new Blue(blueId -> { - if ("HotelOrderType".equals(blueId)) { - return Collections.singletonList(YAML_BLUE.yamlToNode(yaml( - "status:", - " type: Text"))); + if (HOTEL_ORDER_TYPE_ID.equals(blueId)) { + return Collections.singletonList(HOTEL_ORDER_TYPE.clone()); } - if ("RestaurantOrderType".equals(blueId)) { - return Collections.singletonList(YAML_BLUE.yamlToNode(yaml( - "restaurantStatus:", - " type: Text"))); + if (RESTAURANT_ORDER_TYPE_ID.equals(blueId)) { + return Collections.singletonList( + RESTAURANT_ORDER_TYPE.clone()); } return Collections.emptyList(); }); @@ -369,7 +377,7 @@ void contractsIsReservedAsFunctionArgumentName() { @Test void functionArgAcceptsMatchingBlueIdPattern() { - Node hotelOrderPattern = pattern("blueId: HotelOrderBlueId"); + Node hotelOrderPattern = reference(HOTEL_ORDER_TYPE_ID); BexExecutionResult result = run(obj( "type", "Blue/BEX Program", "functions", obj("f", obj( @@ -386,7 +394,7 @@ void functionArgAcceptsMatchingBlueIdPattern() { @Test void isReturnsTrueForComputedTypedObjectWithBlueIdPattern() { - Node hotelOrderPattern = pattern("blueId: HotelOrderType"); + Node hotelOrderPattern = reference(HOTEL_ORDER_TYPE_ID); BexExecutionResult result = run(stepExpr(op("$is", obj( "node", obj( "type", hotelOrderPattern, @@ -400,16 +408,16 @@ void isReturnsTrueForComputedTypedObjectWithBlueIdPattern() { void isReturnsFalseForComputedTypedObjectWithDifferentBlueIdPattern() { BexExecutionResult result = run(stepExpr(op("$is", obj( "node", obj( - "type", pattern("blueId: RestaurantOrderType"), + "type", reference(RESTAURANT_ORDER_TYPE_ID), "status", op("$literal", "confirmed")), - "pattern", pattern("blueId: HotelOrderType"))))); + "pattern", reference(HOTEL_ORDER_TYPE_ID))))); assertEquals(false, simple(result.value())); } @Test void functionArgAcceptsComputedTypedObjectMatchingBlueIdPattern() { - Node hotelOrderPattern = pattern("blueId: HotelOrderType"); + Node hotelOrderPattern = reference(HOTEL_ORDER_TYPE_ID); BexExecutionResult result = run(obj( "type", "Blue/BEX Program", "functions", obj("f", obj( @@ -431,12 +439,14 @@ void functionArgRejectsComputedTypedObjectWithDifferentBlueIdPattern() { BexCompiledProgram program = engine.compile(BexProgramSource.inline(FrozenNode.fromResolvedNode(obj( "type", "Blue/BEX Program", "functions", obj("f", obj( - "args", obj("hotelOrder", pattern("blueId: HotelOrderType")), + "args", obj("hotelOrder", + reference(HOTEL_ORDER_TYPE_ID)), "expr", op("$var", "hotelOrder"))), "expr", op("$call", obj( "function", "f", "args", obj("hotelOrder", obj( - "type", pattern("blueId: RestaurantOrderType"), + "type", reference( + RESTAURANT_ORDER_TYPE_ID), "status", op("$literal", "confirmed"))))))))); assertThrows(BexException.class, () -> engine.execute(program, defaultContext())); @@ -446,9 +456,9 @@ void functionArgRejectsComputedTypedObjectWithDifferentBlueIdPattern() { void isReturnsFalseForBlueIdReferenceWithSiblingFields() { BexExecutionResult result = run(stepExpr(op("$is", obj( "node", obj( - "blueId", "HotelOrderType", + "blueId", HOTEL_ORDER_TYPE_ID, "status", op("$literal", "confirmed")), - "pattern", pattern("blueId: HotelOrderType"))))); + "pattern", reference(HOTEL_ORDER_TYPE_ID))))); assertEquals(false, simple(result.value())); } @@ -724,12 +734,12 @@ void literalPayloadStillRejectsBexInsideTypeDefinitionFields() { @Test void nodeWriterMapsComputedObjectLanguageKeysToBlueNodeFields() { BexValue value = BexValues.fromSimple(m( - "type", m("blueId", "HotelOrderType"), + "type", m("blueId", HOTEL_ORDER_TYPE_ID), "status", "confirmed")); Node node = BexNodeWriter.toNode(value); - assertEquals("HotelOrderType", node.getType().getBlueId()); + assertEquals(HOTEL_ORDER_TYPE_ID, node.getType().getBlueId()); assertEquals("confirmed", node.getProperties().get("status").getValue()); assertFalse(node.getProperties().containsKey("type")); } @@ -737,12 +747,13 @@ void nodeWriterMapsComputedObjectLanguageKeysToBlueNodeFields() { @Test void frozenWriterMapsComputedObjectLanguageKeysToBlueNodeFields() { BexValue value = BexValues.fromSimple(m( - "type", m("blueId", "HotelOrderType"), + "type", m("blueId", HOTEL_ORDER_TYPE_ID), "status", "confirmed")); FrozenNode node = BexFrozenWriter.toFrozen(value); - assertEquals("HotelOrderType", node.getType().getReferenceBlueId()); + assertEquals(HOTEL_ORDER_TYPE_ID, + node.getType().getReferenceBlueId()); assertEquals("confirmed", node.property("status").getValue()); assertNull(node.property("type")); } @@ -751,14 +762,14 @@ void frozenWriterMapsComputedObjectLanguageKeysToBlueNodeFields() { void nodeWriterMapsSchemaAndValueLanguageKeysToBlueNodeFields() { BexValue value = BexValues.fromSimple(m( "name", "Amount", - "type", m("blueId", "Integer"), + "type", m("blueId", INTEGER_TYPE_BLUE_ID), "schema", m("required", true), "value", bi(10))); Node node = BexNodeWriter.toNode(value); assertEquals("Amount", node.getName()); - assertEquals("Integer", node.getType().getBlueId()); + assertEquals(INTEGER_TYPE_BLUE_ID, node.getType().getBlueId()); assertTrue(node.getSchema().getRequiredValue()); assertEquals(bi(10), node.getValue()); assertNull(node.getProperties()); @@ -767,7 +778,7 @@ void nodeWriterMapsSchemaAndValueLanguageKeysToBlueNodeFields() { @Test void nodeWriterRejectsBlueIdReferenceWithSiblingFields() { BexValue value = BexValues.fromSimple(m( - "blueId", "HotelOrderType", + "blueId", HOTEL_ORDER_TYPE_ID, "status", "confirmed")); assertThrows(BexException.class, () -> BexNodeWriter.toNode(value)); @@ -859,4 +870,8 @@ private static String yaml(String... lines) { private Node pattern(String... lines) { return blue.yamlToNode(yaml(lines)); } + + private static Node reference(String blueId) { + return new Node().blueId(blueId); + } } diff --git a/src/test/java/blue/bex/BexCompiler20Test.java b/src/test/java/blue/bex/BexCompiler20Test.java new file mode 100644 index 0000000..2257de1 --- /dev/null +++ b/src/test/java/blue/bex/BexCompiler20Test.java @@ -0,0 +1,250 @@ +package blue.bex; + +import blue.bex.api.BexEngine; +import blue.bex.api.BexExecutionContext; +import blue.bex.api.BexProgramSource; +import blue.bex.api.BexStepResults; +import blue.bex.compile.BexCompiledProgramKey; +import blue.bex.result.BexExecutionResult; +import blue.bex.value.BexValues; +import blue.language.model.Node; +import org.junit.jupiter.api.Test; + +import java.util.Collections; + +import static blue.bex.test.BexTestFixtures.*; +import static org.junit.jupiter.api.Assertions.*; + +class BexCompiler20Test { + @Test + void explicitEmptyExprWinsOverDoByFieldPresence() { + BexExecutionResult result = runStep(obj( + "expr", null, + "do", list(op("$appendEvent", "wrong")) + ), defaultContext()); + + assertTrue(result.events().events().isEmpty()); + } + + @Test + void validatesUnselectedRootBodyBeforeExecution() { + Node program = obj( + "expr", "selected", + "do", list(op("$unknownStatement", true)) + ); + + BexException failure = assertThrows(BexException.class, () -> compile(program)); + assertTrue(failure.getMessage().contains("$unknownStatement")); + assertTrue(failure.sourcePath().isPresent()); + } + + @Test + void rejectsNullEmptyAndBluePlaceholderStatements() { + assertThrows(BexException.class, + () -> compile(stepDo(list((Object) null)))); + assertThrows(BexException.class, + () -> compile(stepDo(list(obj())))); + BexException placeholder = assertThrows(BexException.class, + () -> compile(stepDo(list(obj("$empty", true))))); + assertTrue(placeholder.getMessage().contains("$empty")); + } + + @Test + void objectAndCallArgumentsEvaluateInUnicodeCodePointOrder() { + String supplementary = "\uD83D\uDE00"; + String privateUse = "\uE000"; + Node emitPrivate = obj("do", list(op("$appendEvent", "private"))); + Node emitSupplementary = obj("do", list(op("$appendEvent", "supplementary"))); + Node sink = obj( + "args", obj(supplementary, obj(), privateUse, obj()), + "expr", "done" + ); + Node functions = obj( + "emitPrivate", emitPrivate, + "emitSupplementary", emitSupplementary, + "sink", sink + ); + + BexExecutionResult objectOrder = runStep(obj( + "functions", functions, + "expr", obj( + supplementary, call("emitSupplementary"), + privateUse, call("emitPrivate") + ) + ), defaultContext()); + assertEquals(l("private", "supplementary"), + simple(objectOrder.events().asValue())); + + BexExecutionResult callOrder = runStep(obj( + "functions", functions, + "expr", op("$call", obj( + "function", "sink", + "args", obj( + supplementary, call("emitSupplementary"), + privateUse, call("emitPrivate") + ) + )) + ), defaultContext()); + assertEquals(l("private", "supplementary"), + simple(callOrder.events().asValue())); + } + + @Test + void recursionDiagnosticNamesReasonAndClosingCallPath() { + Node program = obj( + "functions", obj( + "f", obj("expr", call("g")), + "g", obj("expr", call("f")) + ), + "entry", "f" + ); + + BexException failure = assertThrows(BexException.class, () -> compile(program)); + assertTrue(failure.getMessage().contains("reason=recursive-call-graph")); + assertTrue(failure.sourcePath().isPresent()); + assertEquals("$call", failure.sourcePath().get().operator()); + assertTrue(failure.sourcePath().get().pointer().contains("/functions/")); + } + + @Test + void comparisonArityFailsAtCompileTimeWithOperatorPath() { + BexException failure = assertThrows(BexException.class, + () -> compile(stepExpr(op("$eq", list(1))))); + + assertTrue(failure.getMessage().contains("exactly 2 operands")); + assertEquals("$eq", failure.sourcePath().get().operator()); + } + + @Test + void parallelLetPeersCompileButReadAsUninitialized() { + Node program = stepDo(list( + op("$let", obj("vars", obj( + "a", op("$var", "b"), + "b", 1 + ))) + )); + + assertDoesNotThrow(() -> compile(program)); + BexException failure = assertThrows(BexException.class, + () -> runStep(program, defaultContext())); + assertTrue(failure.getMessage().contains("uninitialized")); + } + + @Test + void collectionQueryBindingsDoNotLeakIntoFollowingStatements() { + Node program = stepDo(list( + op("$let", obj( + "name", "mapped", + "expr", op("$map", obj( + "in", list(1), + "item", "temporary", + "expr", op("$var", "temporary") + )) + )), + op("$return", op("$var", "temporary")) + )); + + assertThrows(BexException.class, () -> compile(program)); + } + + @Test + void stepsShortFormSplitsOnlyTheFirstDot() { + BexExecutionContext context = BexExecutionContext.builder() + .document(defaultDocumentView()) + .steps(BexStepResults.builder() + .put("Build", BexValues.fromSimple(m( + "a.b", "first-dot-only", + "a", m("b", "all-dots") + ))) + .build()) + .gasLimit(1_000_000) + .build(); + + BexExecutionResult result = runStep( + stepExpr(op("$steps", "Build.a.b")), context); + assertEquals("first-dot-only", simple(result.value())); + } + + @Test + void failTreatsObjectAsMessageWrapperOnlyWhenMessageIsPresent() { + BexException objectFailure = assertThrows(BexException.class, + () -> runStep(stepDo(list( + op("$fail", obj("messageText", "boom")) + )), defaultContext())); + assertTrue(objectFailure.getMessage().contains("Value cannot be converted to text")); + + BexException wrapped = assertThrows(BexException.class, + () -> runStep(stepDo(list( + op("$fail", obj( + "message", "wrapped", + "ignored", op("$integer", "not-an-integer") + )) + )), defaultContext())); + assertTrue(wrapped.getMessage().contains("wrapped")); + assertFalse(wrapped.getMessage().contains("converted to integer")); + } + + @Test + void failExpressionCompilesLazilyAndFailsOnlyWhenEvaluated() { + BexExecutionResult skipped = runStep(stepExpr(op("$or", list( + true, + op("$fail", "must-not-run") + ))), defaultContext()); + assertEquals(true, simple(skipped.value())); + + BexException evaluated = assertThrows(BexException.class, + () -> runStep(stepExpr(op("$fail", obj( + "message", "expression-failure", + "ignored", op("$integer", "not-an-integer") + ))), defaultContext())); + assertTrue(evaluated.getMessage().contains("expression-failure")); + assertFalse(evaluated.getMessage().contains("converted to integer")); + assertEquals("$fail", evaluated.sourcePath().get().operator()); + } + + @Test + void intrinsicStaticTypeUsesExplicitBlueIdBeforeWrapperIdentity() { + String explicitBlueId = "fixture-explicit-intrinsic"; + BexEngine engine = BexEngine.builder() + .intrinsic( + explicitBlueId, + "test-compiler-intrinsics/1", + Collections.singletonMap("constantWork", 1L), + invocation -> { + invocation.charge( + "constantWork", + 1L, + "explicit-type-work"); + return invocation.field("x"); + }) + .build(); + Node program = stepExpr(op("$intrinsic", obj( + "type", obj("blueId", explicitBlueId), + "x", "ok" + ))); + + BexExecutionResult result = engine.compileAndExecute( + BexProgramSource.inline(frozen(program)), + defaultContext()); + + assertEquals("ok", simple(result.value())); + } + + @Test + void cacheKeyIncludesCompileEnvironmentIdentity() { + BexProgramSource source = BexProgramSource.inline( + frozen(stepExpr(new Node().value("ok")))); + assertNotEquals( + BexCompiledProgramKey.from(source, "environment-a"), + BexCompiledProgramKey.from(source, "environment-b")); + } + + private static Node call(String function) { + return op("$call", obj("function", function)); + } + + private static void compile(Node program) { + BexEngine.builder().build().compile( + BexProgramSource.inline(frozen(program))); + } +} diff --git a/src/test/java/blue/bex/BexCompilerScalarNormalizationTest.java b/src/test/java/blue/bex/BexCompilerScalarNormalizationTest.java new file mode 100644 index 0000000..7e3c925 --- /dev/null +++ b/src/test/java/blue/bex/BexCompilerScalarNormalizationTest.java @@ -0,0 +1,99 @@ +package blue.bex; + +import blue.bex.api.BexEngine; +import blue.bex.api.BexExecutionContext; +import blue.bex.api.BexProgramSource; +import blue.bex.api.BexStepResults; +import blue.bex.result.BexExecutionResult; +import blue.bex.value.BexValues; +import blue.language.Blue; +import blue.language.model.Node; +import blue.language.snapshot.FrozenNode; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.math.BigInteger; + +import static blue.bex.test.BexTestFixtures.defaultDocumentView; +import static blue.bex.test.BexTestFixtures.list; +import static blue.bex.test.BexTestFixtures.m; +import static blue.bex.test.BexTestFixtures.obj; +import static blue.bex.test.BexTestFixtures.op; +import static blue.bex.test.BexTestFixtures.simple; +import static blue.language.utils.Properties.TEXT_TYPE_BLUE_ID; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class BexCompilerScalarNormalizationTest { + private final Blue blue = new Blue(); + private final BexEngine engine = BexEngine.builder().blue(blue).build(); + + @Test + void blueAuthoredCoreTypedScalarsRemainBexScalars() { + assertEquals("hello", simple(runYaml("expr: hello").value())); + assertEquals(BigInteger.valueOf(42), simple(runYaml("expr: 42").value())); + assertEquals(new BigDecimal("1.25"), simple(runYaml("expr: 1.25").value())); + assertEquals(true, simple(runYaml("expr: true").value())); + } + + @Test + void blueAuthoredScalarShorthandWorksForVarConstBindingAndSteps() { + BexExecutionResult result = runYaml(String.join("\n", + "constants:", + " configured: constant-value", + "do:", + " - $let:", + " name: local", + " expr: variable-value", + " - $return:", + " variable:", + " $var: local", + " constant:", + " $const: configured", + " binding:", + " $binding: external", + " step:", + " $steps: Build.result")); + + assertEquals(m( + "variable", "variable-value", + "constant", "constant-value", + "binding", "binding-value", + "step", "step-value"), simple(result.value())); + } + + @Test + void computedTypeAndValuePropertiesRemainAnObjectExpression() { + Node computed = obj( + "type", obj("blueId", TEXT_TYPE_BLUE_ID), + "value", op("$concat", list("hel", "lo"))); + + BexExecutionResult result = engine.compileAndExecute( + BexProgramSource.expression(FrozenNode.fromResolvedNode(computed)), + context()); + + assertEquals("object", BexValues.kind(result.value())); + assertEquals(TEXT_TYPE_BLUE_ID, + result.value().get("type").get("blueId").asText()); + assertEquals("hello", result.value().get("value").asText()); + } + + private BexExecutionResult runYaml(String programBody) { + Node program = blue.yamlToNode(programBody); + return engine.compileAndExecute( + BexProgramSource.inline(FrozenNode.fromResolvedNode(program)), + context()); + } + + private BexExecutionContext context() { + return BexExecutionContext.builder() + .document(defaultDocumentView()) + .event(BexValues.fromSimple(m("kind", "Created"))) + .currentContract(BexValues.fromSimple(m("channel", "main"))) + .steps(BexStepResults.builder() + .put("Build", BexValues.fromSimple(m("result", "step-value"))) + .build()) + .binding("external", BexValues.scalar("binding-value")) + .gasLimit(1_000_000) + .build(); + } +} diff --git a/src/test/java/blue/bex/BexCompositeExhaustionEvidenceTest.java b/src/test/java/blue/bex/BexCompositeExhaustionEvidenceTest.java new file mode 100644 index 0000000..e2bd7a6 --- /dev/null +++ b/src/test/java/blue/bex/BexCompositeExhaustionEvidenceTest.java @@ -0,0 +1,1165 @@ +package blue.bex; + +import blue.bex.api.BexEngine; +import blue.bex.api.BexExecutionContext; +import blue.bex.api.BexGasLedgerHost; +import blue.bex.api.BexIntrinsicRegistry; +import blue.bex.api.BexProgramSource; +import blue.bex.api.FrozenBexDocumentView; +import blue.bex.compile.BexCompiledProgram; +import blue.bex.gas.BexGasCharge; +import blue.bex.gas.BexGasCounter; +import blue.bex.gas.BexGasLimitExceededException; +import blue.bex.gas.BexGasSchedule; +import blue.bex.output.BexEstablishedIdentity; +import blue.bex.output.BexSemanticIdentityBoundary; +import blue.bex.pointer.BexPointerCache; +import blue.bex.result.BexExecutionResult; +import blue.bex.result.BexMetrics; +import blue.bex.runtime.BexRuntime; +import blue.bex.value.BexValue; +import blue.bex.value.BexValues; +import blue.language.Blue; +import blue.language.NodeProvider; +import blue.language.model.Node; +import blue.language.processor.GasLimitExceededException; +import blue.language.processor.GasMeter; +import blue.language.processor.GasSchedule; +import blue.language.processor.GasTraceEntry; +import blue.language.snapshot.FrozenNode; +import blue.language.utils.BlueIdCalculator; +import org.junit.jupiter.api.Test; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Predicate; + +import static blue.bex.test.BexTestFixtures.frozen; +import static blue.bex.test.BexTestFixtures.largeObject; +import static blue.bex.test.BexTestFixtures.list; +import static blue.bex.test.BexTestFixtures.obj; +import static blue.bex.test.BexTestFixtures.op; +import static blue.bex.test.BexTestFixtures.stepDo; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Composite evidence that a rejected charge is the last observable action. + * + *

Each case first executes without a local sub-limit and selects an exact + * charge from the resulting canonical trace. It then executes the same + * compiled program with a live host ledger and a local limit equal to the gas + * immediately before that charge. The host must retain exactly that prefix; + * neither the rejected charge nor a later sentinel event may be observed.

+ */ +class BexCompositeExhaustionEvidenceTest { + private static final String SORT_INTRINSIC = + "TestCompositeExhaustionSort"; + private static final String NAMED_INTRINSIC = + "TestCompositeExhaustionNamed"; + private static final String REGISTRY_IDENTITY = + "test-composite-exhaustion/1"; + + @Test + void largeFiniteForEachStopsBeforeRejectedIterationAndIsColdWarmStable() { + BexProgramSource source = source(stepDo(list( + op("$forEach", obj( + "in", integerList(48), + "item", "item", + "index", "index", + "do", list())), + sentinelEvent(), + op("$return", true)))); + + List compileMetrics = new ArrayList<>(); + BexEngine warmEngine = BexEngine.builder() + .metrics(metrics -> compileMetrics.add(metrics.copy())) + .build(); + BexCompiledProgram firstCompilation = warmEngine.compile(source); + BexCompiledProgram cachedCompilation = warmEngine.compile(source); + + assertSame(firstCompilation, cachedCompilation); + assertEquals(1L, compileMetrics.get(0).compileCacheMisses()); + assertEquals(0L, compileMetrics.get(0).compileCacheHits()); + assertEquals(0L, compileMetrics.get(1).compileCacheMisses()); + assertEquals(1L, compileMetrics.get(1).compileCacheHits()); + + TargetEvidence target = deriveTarget( + warmEngine, + cachedCompilation, + trace -> nthCharge( + trace, + charge -> charge.counter() + == BexGasCounter.COLLECTION_ITEM_VISITED + && "$forEach".equals(charge.operator()), + 23)); + LimitedEvidence warm = assertRejectedAtPrefix( + warmEngine, cachedCompilation, target); + + BexEngine coldEngine = BexEngine.builder().build(); + BexCompiledProgram coldCompilation = coldEngine.compile(source); + LimitedEvidence cold = assertRejectedAtPrefix( + coldEngine, coldCompilation, target); + + assertHostTracesEqual(warm.hostTrace, cold.hostTrace); + assertSameFailure(warm.failure, cold.failure); + assertEquals(0, warm.boundary.sentinelAdmissions.get()); + assertEquals(0, cold.boundary.sentinelAdmissions.get()); + } + + @Test + void exhaustionIsStableAcrossInlineColdReferenceAndWarmReferenceDocuments() { + Node document = obj("items", integerList(48)); + String documentBlueId = + BlueIdCalculator.calculateBlueId(document); + FrozenNode inlineDocument = + FrozenNode.fromResolvedNode(document); + FrozenNode referenceDocument = FrozenNode.fromNode( + new Node().blueId(documentBlueId)); + FrozenBexDocumentView inlineView = + new FrozenBexDocumentView(inlineDocument); + FrozenBexDocumentView referenceView = + new FrozenBexDocumentView( + referenceDocument, + referenceDocument, + "/"); + BexProgramSource source = source(stepDo(list( + op("$forEach", obj( + "in", op("$document", "/items"), + "item", "item", + "index", "index", + "do", list())), + sentinelEvent(), + op("$return", true)))); + + ExactDocumentProvider coldProvider = + new ExactDocumentProvider( + documentBlueId, document); + ExactDocumentProvider warmProvider = + new ExactDocumentProvider( + documentBlueId, document); + try (Blue inlineBlue = new Blue(); + Blue coldBlue = new Blue(coldProvider); + Blue warmBlue = new Blue(warmProvider)) { + BexEngine inlineEngine = BexEngine.builder() + .blue(inlineBlue) + .build(); + BexCompiledProgram inlineProgram = + inlineEngine.compile(source); + BexExecutionResult unlimited = inlineEngine.execute( + inlineProgram, + context( + null, + new RecordingIdentityBoundary(), + -1L, + inlineView)); + List trace = unlimited.gasTrace(); + int rejectedIndex = nthCharge( + trace, + charge -> charge.counter() + == BexGasCounter.COLLECTION_ITEM_VISITED + && "$forEach".equals(charge.operator()), + 23); + List prefix = + Collections.unmodifiableList( + new ArrayList<>( + trace.subList(0, rejectedIndex))); + TargetEvidence target = new TargetEvidence( + prefix, + trace.get(rejectedIndex), + gas(prefix)); + + LimitedEvidence inline = assertRejectedAtPrefix( + inlineEngine, + inlineProgram, + target, + inlineView); + + BexEngine coldEngine = BexEngine.builder() + .blue(coldBlue) + .build(); + LimitedEvidence coldReference = + assertRejectedAtPrefix( + coldEngine, + coldEngine.compile(source), + target, + referenceView); + assertTrue(coldProvider.demands > 0, + "the cold reference execution must demand provider content"); + + warmBlue.expand( + new Node().blueId(documentBlueId)); + int warmupDemands = warmProvider.demands; + BexEngine warmEngine = BexEngine.builder() + .blue(warmBlue) + .build(); + LimitedEvidence warmReference = + assertRejectedAtPrefix( + warmEngine, + warmEngine.compile(source), + target, + referenceView); + assertTrue(warmupDemands > 0, + "the warm variant must establish provider content first"); + + assertHostTracesEqual( + inline.hostTrace, + coldReference.hostTrace); + assertHostTracesEqual( + inline.hostTrace, + warmReference.hostTrace); + assertSameFailure( + inline.failure, + coldReference.failure); + assertSameFailure( + inline.failure, + warmReference.failure); + assertEquals(0, + coldReference.boundary + .sentinelAdmissions.get()); + assertEquals(0, + warmReference.boundary + .sentinelAdmissions.get()); + } + } + + @Test + void largeTextConstructionStopsBeforeResultAndLaterEvent() { + String block = repeatCodePoint('x', 257); + Node expression = op("$concat", list( + block + "0", + block + "1", + block + "2", + block + "3", + block + "4", + block + "5", + block + "6", + block + "7")); + Scenario scenario = scenario(BexEngine.builder().build(), + expressionProgram(expression)); + + TargetEvidence target = deriveTarget( + scenario.engine, + scenario.program, + trace -> lastCharge( + trace, + charge -> charge.counter() + == BexGasCounter.TEXT_BLOCK_CONSTRUCTED + && "$concat".equals(charge.operator()))); + assertTrue(target.rejected.quantity() > 1L); + + LimitedEvidence limited = assertRejectedAtPrefix( + scenario.engine, scenario.program, target); + assertEquals(0, limited.boundary.sentinelAdmissions.get()); + } + + @Test + void numericTextStopsBeforeUnadmittedLaterMagnitudeExtraction() { + TrackingBigInteger magnitude = new TrackingBigInteger( + repeatCodePoint('9', 257)); + Scenario scenario = scenario( + BexEngine.builder().build(), + obj( + "type", "Blue/BEX Program", + "expr", op("$text", magnitude))); + + BexExecutionResult complete = scenario.engine.execute( + scenario.program, + context( + null, + new RecordingIdentityBoundary(), + -1L)); + assertEquals(magnitude.toString(), + complete.value().toSimple()); + assertEquals(5L, complete.gasLedger().quantity( + BexGasCounter.TEXT_BLOCK_EXAMINED)); + assertEquals(5L, complete.gasLedger().quantity( + BexGasCounter.TEXT_BLOCK_CONSTRUCTED)); + + TargetEvidence target = deriveTarget( + scenario.engine, + scenario.program, + trace -> nthCharge( + trace, + charge -> charge.counter() + == BexGasCounter.TEXT_BLOCK_EXAMINED + && "$text".equals(charge.operator()), + 1)); + magnitude.resetObservations(); + + LimitedEvidence limited = assertRejectedAtPrefix( + scenario.engine, scenario.program, target); + + assertEquals(1, magnitude.quotientExtractions(), + "only the first admitted numeric text block may extract " + + "a magnitude quotient"); + assertEquals(0, magnitude.eagerRemainderExtractions(), + "an admitted leading block must not materialize the " + + "unadmitted lower decimal remainder"); + assertEquals(0, limited.boundary.totalAdmissions.get(), + "the rejected second block must precede result admission"); + } + + @Test + void concatConstructionQuantityHandlesSurrogatePairAcrossOperands() { + String highSurrogateSuffix = + repeatCodePoint('a', 63) + "\uD83D"; + String lowSurrogatePrefix = "\uDE00"; + Scenario scenario = scenario( + BexEngine.builder().build(), + expressionProgram(op( + "$concat", + list(highSurrogateSuffix, lowSurrogatePrefix)))); + + TargetEvidence target = deriveTarget( + scenario.engine, + scenario.program, + trace -> lastCharge( + trace, + charge -> charge.counter() + == BexGasCounter.TEXT_BLOCK_CONSTRUCTED + && "$concat".equals(charge.operator()))); + + assertEquals(1L, target.rejected.quantity(), + "the cross-operand surrogate pair forms one code point"); + LimitedEvidence limited = assertRejectedAtPrefix( + scenario.engine, scenario.program, target); + assertEquals(0, limited.boundary.totalAdmissions.get(), + "aggregate construction rejection must precede result admission"); + } + + @Test + void surrogatePrefixStopsBeforeRejectedLaterComparisonBlock() { + String emoji = new String( + Character.toChars(0x1F600)); + String prefix = repeatText(emoji, 65); + Scenario scenario = scenario( + BexEngine.builder().build(), + expressionProgram(op( + "$sliceAfter", + list(prefix + "suffix", prefix)))); + + TargetEvidence target = deriveTarget( + scenario.engine, + scenario.program, + trace -> nthCharge( + trace, + charge -> charge.counter() + == BexGasCounter.TEXT_BLOCK_EXAMINED + && "$sliceAfter".equals( + charge.operator()), + 1)); + + assertEquals(2L, target.rejected.quantity(), + "one admitted pair covers 64 Unicode code points, " + + "not 64 UTF-16 code units"); + LimitedEvidence limited = assertRejectedAtPrefix( + scenario.engine, scenario.program, target); + assertEquals(0, limited.boundary.totalAdmissions.get(), + "the suffix and result boundary must not run after " + + "the second comparison block is rejected"); + } + + @Test + void largeIntegerArithmeticStopsBeforeResultAndLaterEvent() { + BigInteger left = BigInteger.ONE.shiftLeft(2048) + .subtract(BigInteger.ONE); + BigInteger right = BigInteger.ONE.shiftLeft(2016) + .add(BigInteger.valueOf(17L)); + Node expression = op("$multiply", list(left, right)); + Scenario scenario = scenario(BexEngine.builder().build(), + expressionProgram(expression)); + + TargetEvidence target = deriveTarget( + scenario.engine, + scenario.program, + trace -> lastCharge( + trace, + charge -> charge.counter() + == BexGasCounter.INTEGER_LIMB_OPERATION + && "$multiply".equals(charge.operator()))); + assertTrue(target.rejected.quantity() > 1L); + + LimitedEvidence limited = assertRejectedAtPrefix( + scenario.engine, scenario.program, target); + assertEquals(0, limited.boundary.sentinelAdmissions.get()); + } + + @Test + void deterministicSortIntrinsicStopsBeforeRejectedComparison() { + AtomicInteger completedComparisons = new AtomicInteger(); + BexEngine engine = sortEngine(completedComparisons); + Node expression = intrinsicExpression( + SORT_INTRINSIC, + "values", + list(9, 1, 8, 2, 7, 3, 6, 4, 5)); + Scenario scenario = scenario(engine, expressionProgram(expression)); + + TargetEvidence target = deriveTarget( + scenario.engine, + scenario.program, + trace -> nthCharge( + trace, + charge -> ("intrinsic-" + SORT_INTRINSIC) + .equals(charge.namespace()) + && "sortComparison".equals( + charge.counterName()), + 6)); + long admittedComparisons = quantity( + target.prefix, + "intrinsic-" + SORT_INTRINSIC, + "sortComparison"); + completedComparisons.set(0); + + LimitedEvidence limited = assertRejectedAtPrefix( + scenario.engine, scenario.program, target); + + assertEquals(admittedComparisons, + completedComparisons.get(), + "the comparison following the rejected charge must not run"); + assertEquals(0, limited.boundary.sentinelAdmissions.get()); + } + + @Test + void transientAggregateConstructionStopsBeforeRejectedMember() { + Scenario scenario = scenario( + BexEngine.builder().build(), + expressionProgram(largeObject(40))); + + TargetEvidence target = deriveTarget( + scenario.engine, + scenario.program, + trace -> nthCharge( + trace, + charge -> charge.counter() + == BexGasCounter + .TRANSIENT_OBJECT_MEMBER_PRODUCED, + 24)); + + LimitedEvidence limited = assertRejectedAtPrefix( + scenario.engine, scenario.program, target); + assertEquals(0, limited.boundary.sentinelAdmissions.get()); + } + + @Test + void rejectedPatchAppendDoesNotMutateChangesetOrOverlay() { + Node programNode = stepDo(list( + op("$appendChange", obj( + "op", "add", + "path", "/mutated", + "val", "must-not-appear")), + sentinelEvent(), + op("$return", true))); + + try (Blue blue = new Blue()) { + BexEngine engine = BexEngine.builder() + .blue(blue) + .build(); + BexCompiledProgram program = + engine.compile(source(programNode)); + TargetEvidence target = deriveTarget( + engine, + program, + trace -> lastCharge( + trace, + charge -> charge.counter() + == BexGasCounter.PATCH_APPENDED)); + RecordingGasHost host = + new RecordingGasHost(); + RecordingIdentityBoundary boundary = + new RecordingIdentityBoundary(); + BexRuntime runtime = new BexRuntime( + program, + context( + host, + boundary, + target.prefixGas), + blue, + BexGasSchedule.defaults(), + new BexMetrics(), + new BexPointerCache(), + BexIntrinsicRegistry.empty()); + + RuntimeException topLevel = assertThrows( + RuntimeException.class, + runtime::execute); + BexGasLimitExceededException failure = findCause( + topLevel, + BexGasLimitExceededException.class); + + assertNotNull(failure); + assertEquals(BexGasCounter.PATCH_APPENDED, + failure.counter()); + assertEquals(target.rejected.counterName(), + failure.counterName()); + assertEquals(target.rejected.quantity(), + failure.quantity()); + assertEquals(target.rejected.weight(), + failure.weight()); + assertEquals(target.prefixGas, + failure.admittedGas()); + assertEquals(target.prefixGas, + failure.effectiveBudget()); + assertTrue(runtime.accumulator() + .changeset() + .entries() + .isEmpty(), + "the rejected patch charge must precede changeset mutation"); + assertTrue(runtime.accumulator() + .overlay() + .rootValue() + .get("mutated") + .isUndefined(), + "the rejected patch charge must precede overlay mutation"); + assertEquals(0, + boundary.totalAdmissions.get(), + "patch-value and later-event admission must not begin"); + assertHostTraceEqualsPrefix( + target.prefix, + host.parent.trace()); + } + } + + @Test + void transientNodeBlueIdStopsBeforeSemanticIdentityBoundary() { + Node aggregate = largeObject(18); + Scenario scenario = scenario( + BexEngine.builder().build(), + expressionProgram(op("$nodeBlueId", aggregate))); + + TargetEvidence target = deriveTarget( + scenario.engine, + scenario.program, + trace -> lastCharge( + trace, + charge -> charge.counter() + == BexGasCounter.NODE_IDENTITY_REQUESTED)); + + LimitedEvidence limited = assertRejectedAtPrefix( + scenario.engine, scenario.program, target); + + assertEquals(0, limited.boundary.totalAdmissions.get(), + "semantic identity establishment must not begin after " + + "nodeIdentityRequested is rejected"); + assertEquals(0, limited.boundary.sentinelAdmissions.get()); + } + + @Test + void registeredNamedIntrinsicStopsBeforeRejectedProcessorWork() { + AtomicInteger completedIntrinsicWork = new AtomicInteger(); + Map weights = Collections.singletonMap( + "namedWork", 7L); + BexEngine engine = BexEngine.builder() + .intrinsic( + NAMED_INTRINSIC, + REGISTRY_IDENTITY, + weights, + invocation -> { + invocation.charge( + "namedWork", + 4L, + "registered-named-work"); + completedIntrinsicWork.incrementAndGet(); + return BexValues.scalar(true); + }) + .build(); + Scenario scenario = scenario( + engine, + expressionProgram(intrinsicExpression( + NAMED_INTRINSIC, + "payload", + "value"))); + + TargetEvidence target = deriveTarget( + scenario.engine, + scenario.program, + trace -> lastCharge( + trace, + charge -> ("intrinsic-" + NAMED_INTRINSIC) + .equals(charge.namespace()) + && "namedWork".equals( + charge.counterName()))); + completedIntrinsicWork.set(0); + + LimitedEvidence limited = assertRejectedAtPrefix( + scenario.engine, scenario.program, target); + + assertEquals(0, completedIntrinsicWork.get(), + "intrinsic work following its named charge must not run"); + assertEquals(0, limited.boundary.sentinelAdmissions.get()); + } + + private static BexEngine sortEngine( + AtomicInteger completedComparisons) { + return BexEngine.builder() + .intrinsic( + SORT_INTRINSIC, + REGISTRY_IDENTITY, + Collections.singletonMap( + "sortComparison", 3L), + invocation -> { + BexValue input = invocation.field("values"); + if (!input.isList()) { + throw new BexException( + "sort values must be a list"); + } + List values = new ArrayList<>(); + for (int index = 0; + index < input.size(); + index++) { + values.add(input.get( + String.valueOf(index))); + } + for (int left = 0; + left < values.size(); + left++) { + int least = left; + for (int right = left + 1; + right < values.size(); + right++) { + invocation.charge( + "sortComparison", + 1L, + "deterministic-selection-sort"); + completedComparisons.incrementAndGet(); + if (values.get(right) + .asInteger() + .compareTo(values.get(least) + .asInteger()) < 0) { + least = right; + } + } + BexValue swap = values.get(left); + values.set(left, values.get(least)); + values.set(least, swap); + } + return BexValues.list(values); + }) + .build(); + } + + private static Scenario scenario( + BexEngine engine, + Node programNode) { + BexProgramSource source = source(programNode); + return new Scenario( + engine, + engine.compile(source)); + } + + private static Node expressionProgram(Node expression) { + return stepDo(list( + op("$let", obj( + "name", "ignored", + "expr", expression)), + sentinelEvent(), + op("$return", true))); + } + + private static Node sentinelEvent() { + return op("$appendEvent", obj( + "afterExhaustion", true)); + } + + private static Node intrinsicExpression( + String blueId, + String field, + Object value) { + return op("$intrinsic", obj( + "type", obj("blueId", blueId), + field, value)); + } + + private static Node integerList(int size) { + Object[] values = new Object[size]; + for (int index = 0; index < size; index++) { + values[index] = index; + } + return list(values); + } + + private static BexProgramSource source(Node program) { + return BexProgramSource.inline(frozen(program)); + } + + private static TargetEvidence deriveTarget( + BexEngine engine, + BexCompiledProgram program, + ChargeSelector selector) { + RecordingIdentityBoundary boundary = + new RecordingIdentityBoundary(); + BexExecutionResult unlimited = engine.execute( + program, + context(null, boundary, -1L)); + List trace = unlimited.gasTrace(); + int rejectedIndex = selector.select(trace); + assertTrue(rejectedIndex >= 0); + assertTrue(rejectedIndex < trace.size()); + + List prefix = Collections.unmodifiableList( + new ArrayList<>(trace.subList(0, rejectedIndex))); + long prefixGas = gas(prefix); + assertTrue(prefixGas > 0L, + "composite case must have a non-empty admitted prefix"); + return new TargetEvidence( + prefix, + trace.get(rejectedIndex), + prefixGas); + } + + private static LimitedEvidence assertRejectedAtPrefix( + BexEngine engine, + BexCompiledProgram program, + TargetEvidence target) { + FrozenNode empty = FrozenNode.fromResolvedNode(obj()); + return assertRejectedAtPrefix( + engine, + program, + target, + new FrozenBexDocumentView( + empty, + empty, + "/")); + } + + private static LimitedEvidence assertRejectedAtPrefix( + BexEngine engine, + BexCompiledProgram program, + TargetEvidence target, + FrozenBexDocumentView document) { + RecordingGasHost host = new RecordingGasHost(); + RecordingIdentityBoundary boundary = + new RecordingIdentityBoundary(); + BexExecutionContext context = context( + host, + boundary, + target.prefixGas, + document); + + RuntimeException topLevel = assertThrows( + RuntimeException.class, + () -> engine.execute(program, context)); + BexGasLimitExceededException failure = findCause( + topLevel, BexGasLimitExceededException.class); + assertNotNull(failure, + "execution must retain the exact BEX exhaustion"); + + assertEquals(target.rejected.namespace(), + failure.namespace()); + assertEquals(target.rejected.counter(), + failure.counter()); + assertEquals(target.rejected.counterName(), + failure.counterName()); + assertEquals(target.rejected.quantity(), + failure.quantity()); + assertEquals(target.rejected.weight(), + failure.weight()); + assertEquals(target.prefixGas, + failure.admittedGas()); + assertEquals(target.prefixGas, + failure.effectiveBudget()); + assertNull(failure.hostGasLimitExceeded(), + "the stricter local sub-limit must reject before " + + "touching the host ledger"); + + assertEquals(host.openedLedgers.size(), + host.deterministicFailures); + assertEquals(0, host.successfulSubmissions); + assertEquals(0, host.unavailableFinalizations); + assertEquals(0, host.exhaustionPropagations); + assertEquals(target.prefixGas, host.parent.totalGas()); + assertHostTraceEqualsPrefix( + target.prefix, host.parent.trace()); + assertFalse(host.parent.trace().stream().anyMatch( + charge -> BexGasCounter.EVENT_APPENDED + .canonicalName() + .equals(charge.counter())), + "the later sentinel event must remain uncharged"); + assertEquals(0, boundary.sentinelAdmissions.get(), + "the later sentinel event must remain unadmitted"); + + return new LimitedEvidence( + failure, + host.parent.trace(), + boundary); + } + + private static void assertHostTraceEqualsPrefix( + List expected, + List actual) { + assertEquals(expected.size(), actual.size(), + "rejected charge must be absent from the host trace"); + for (int index = 0; index < expected.size(); index++) { + BexGasCharge local = expected.get(index); + GasTraceEntry host = actual.get(index); + assertEquals(index, local.sequence()); + assertEquals(index, host.sequence()); + assertEquals(local.namespace(), host.namespace()); + assertEquals(local.counterName(), host.counter()); + assertEquals(local.quantity(), host.quantity()); + assertEquals(local.weight(), host.weight()); + assertEquals(local.gas(), host.subtotal()); + assertEquals(local.sourcePath(), host.scopePath()); + assertEquals(local.operator(), host.logicalPath()); + assertEquals(local.reason(), host.reason()); + } + } + + private static void assertSameFailure( + BexGasLimitExceededException left, + BexGasLimitExceededException right) { + assertEquals(left.namespace(), right.namespace()); + assertEquals(left.counter(), right.counter()); + assertEquals(left.counterName(), right.counterName()); + assertEquals(left.quantity(), right.quantity()); + assertEquals(left.weight(), right.weight()); + assertEquals(left.admittedGas(), right.admittedGas()); + assertEquals(left.effectiveBudget(), right.effectiveBudget()); + } + + private static void assertHostTracesEqual( + List left, + List right) { + assertEquals(left.size(), right.size()); + for (int index = 0; index < left.size(); index++) { + GasTraceEntry first = left.get(index); + GasTraceEntry second = right.get(index); + assertEquals(first.sequence(), second.sequence()); + assertEquals(first.namespace(), second.namespace()); + assertEquals(first.counter(), second.counter()); + assertEquals(first.quantity(), second.quantity()); + assertEquals(first.weight(), second.weight()); + assertEquals(first.subtotal(), second.subtotal()); + assertEquals(first.scopePath(), second.scopePath()); + assertEquals(first.logicalPath(), second.logicalPath()); + assertEquals(first.reason(), second.reason()); + } + } + + private static BexExecutionContext context( + RecordingGasHost host, + RecordingIdentityBoundary boundary, + long localLimit) { + FrozenNode empty = FrozenNode.fromResolvedNode(obj()); + return context( + host, + boundary, + localLimit, + new FrozenBexDocumentView( + empty, + empty, + "/")); + } + + private static BexExecutionContext context( + RecordingGasHost host, + RecordingIdentityBoundary boundary, + long localLimit, + FrozenBexDocumentView document) { + BexExecutionContext.Builder builder = + BexExecutionContext.builder() + .document(document) + .semanticIdentityBoundary(boundary); + if (host != null) { + builder.gasLedgerHost(host); + } + if (localLimit >= 0L) { + builder.gasLimit(localLimit); + } + return builder.build(); + } + + private static final class ExactDocumentProvider + implements NodeProvider { + private final String blueId; + private final Node document; + private int demands; + + private ExactDocumentProvider( + String blueId, + Node document) { + this.blueId = blueId; + this.document = document.clone(); + } + + @Override + public List fetchByBlueId( + String requestedBlueId) { + demands++; + return blueId.equals(requestedBlueId) + ? Collections.singletonList( + document.clone()) + : Collections.emptyList(); + } + } + + private static int nthCharge( + List trace, + Predicate predicate, + int occurrence) { + int seen = 0; + for (int index = 0; index < trace.size(); index++) { + if (predicate.test(trace.get(index))) { + if (seen == occurrence) { + return index; + } + seen++; + } + } + throw new AssertionError( + "Expected charge occurrence " + occurrence + + " but found " + seen); + } + + private static int lastCharge( + List trace, + Predicate predicate) { + for (int index = trace.size() - 1; index >= 0; index--) { + if (predicate.test(trace.get(index))) { + return index; + } + } + throw new AssertionError("Expected matching charge"); + } + + private static long gas(List charges) { + long total = 0L; + for (BexGasCharge charge : charges) { + total += charge.gas(); + } + return total; + } + + private static long quantity( + List charges, + String namespace, + String counter) { + long total = 0L; + for (BexGasCharge charge : charges) { + if (namespace.equals(charge.namespace()) + && counter.equals(charge.counterName())) { + total += charge.quantity(); + } + } + return total; + } + + private static String repeatCodePoint( + char value, + int count) { + StringBuilder text = new StringBuilder(count); + for (int index = 0; index < count; index++) { + text.append(value); + } + return text.toString(); + } + + private static String repeatText( + String value, + int count) { + StringBuilder text = new StringBuilder( + value.length() * count); + for (int index = 0; index < count; index++) { + text.append(value); + } + return text.toString(); + } + + private static T findCause( + Throwable failure, + Class type) { + Throwable current = failure; + while (current != null) { + if (type.isInstance(current)) { + return type.cast(current); + } + current = current.getCause(); + } + return null; + } + + private interface ChargeSelector { + int select(List trace); + } + + private static final class TrackingBigInteger + extends BigInteger { + private static final long serialVersionUID = 1L; + + private int quotientExtractions; + private int eagerRemainderExtractions; + + private TrackingBigInteger(String value) { + super(value); + } + + @Override + public BigInteger divide(BigInteger divisor) { + quotientExtractions++; + return super.divide(divisor); + } + + @Override + public BigInteger[] divideAndRemainder( + BigInteger divisor) { + eagerRemainderExtractions++; + return super.divideAndRemainder(divisor); + } + + private void resetObservations() { + quotientExtractions = 0; + eagerRemainderExtractions = 0; + } + + private int quotientExtractions() { + return quotientExtractions; + } + + private int eagerRemainderExtractions() { + return eagerRemainderExtractions; + } + } + + private static final class Scenario { + private final BexEngine engine; + private final BexCompiledProgram program; + + private Scenario( + BexEngine engine, + BexCompiledProgram program) { + this.engine = engine; + this.program = program; + } + } + + private static final class TargetEvidence { + private final List prefix; + private final BexGasCharge rejected; + private final long prefixGas; + + private TargetEvidence( + List prefix, + BexGasCharge rejected, + long prefixGas) { + this.prefix = prefix; + this.rejected = rejected; + this.prefixGas = prefixGas; + } + } + + private static final class LimitedEvidence { + private final BexGasLimitExceededException failure; + private final List hostTrace; + private final RecordingIdentityBoundary boundary; + + private LimitedEvidence( + BexGasLimitExceededException failure, + List hostTrace, + RecordingIdentityBoundary boundary) { + this.failure = failure; + this.hostTrace = hostTrace; + this.boundary = boundary; + } + } + + private static final class RecordingIdentityBoundary + implements BexSemanticIdentityBoundary { + private final AtomicInteger totalAdmissions = + new AtomicInteger(); + private final AtomicInteger sentinelAdmissions = + new AtomicInteger(); + + @Override + public BexEstablishedIdentity establishIdentity(Node node) { + totalAdmissions.incrementAndGet(); + Map properties = node.getProperties(); + if (properties != null) { + Node marker = properties.get("afterExhaustion"); + if (marker != null + && Boolean.TRUE.equals(marker.getValue())) { + sentinelAdmissions.incrementAndGet(); + } + } + return BexSemanticIdentityBoundary.STANDALONE + .establishIdentity(node); + } + } + + private static final class RecordingGasHost + implements BexGasLedgerHost { + private final GasMeter parent = + new GasMeter(GasSchedule.contracts10()); + private final Map + openedLedgers = new IdentityHashMap<>(); + private int successfulSubmissions; + private int deterministicFailures; + private int unavailableFinalizations; + private int exhaustionPropagations; + + @Override + public GasMeter.ChildGasLedger open( + String namespace, + Map counterWeights) { + GasMeter.ChildGasLedger ledger = + parent.childLedger(namespace, counterWeights); + openedLedgers.put(ledger, Boolean.TRUE); + return ledger; + } + + @Override + public boolean separatesRuntimeNamespaces() { + return true; + } + + @Override + public void submit(GasMeter.ChildGasLedger ledger) { + successfulSubmissions++; + mergeOwned(ledger); + } + + @Override + public void failedDeterministically( + GasMeter.ChildGasLedger ledger) { + deterministicFailures++; + mergeOwned(ledger); + } + + @Override + public void evidenceUnavailable( + GasMeter.ChildGasLedger ledger) { + unavailableFinalizations++; + requireOwned(ledger); + } + + @Override + public void propagateGasExhaustion( + GasMeter.ChildGasLedger ledger, + GasLimitExceededException exhaustion) { + exhaustionPropagations++; + requireOwned(ledger); + throw exhaustion; + } + + private void mergeOwned( + GasMeter.ChildGasLedger ledger) { + requireOwned(ledger); + parent.merge(ledger); + } + + private void requireOwned( + GasMeter.ChildGasLedger ledger) { + assertTrue(openedLedgers.containsKey(ledger), + "host may finalize only a ledger it opened"); + } + } +} diff --git a/src/test/java/blue/bex/BexContextReadTest.java b/src/test/java/blue/bex/BexContextReadTest.java index 4e2751e..b6c296f 100644 --- a/src/test/java/blue/bex/BexContextReadTest.java +++ b/src/test/java/blue/bex/BexContextReadTest.java @@ -13,7 +13,6 @@ void relativeDocumentPointerUsesDocumentViewScope() { FrozenBexDocumentView document = new FrozenBexDocumentView(frozen(defaultDocument()), frozen(defaultDocument()), "/nested"); BexExecutionContext context = BexExecutionContext.builder() .document(document) - .currentScopePath("/ignored") .gasLimit(1_000_000) .build(); diff --git a/src/test/java/blue/bex/BexDiagnosticAdmissionOrderingTest.java b/src/test/java/blue/bex/BexDiagnosticAdmissionOrderingTest.java new file mode 100644 index 0000000..64e7671 --- /dev/null +++ b/src/test/java/blue/bex/BexDiagnosticAdmissionOrderingTest.java @@ -0,0 +1,279 @@ +package blue.bex; + +import blue.bex.api.BexEngine; +import blue.bex.api.BexExecutionContext; +import blue.bex.api.BexIntrinsicRegistry; +import blue.bex.api.BexProgramSource; +import blue.bex.compile.BexCompiledProgram; +import blue.bex.gas.BexGasCharge; +import blue.bex.gas.BexGasCounter; +import blue.bex.gas.BexGasLimitExceededException; +import blue.bex.gas.BexGasSchedule; +import blue.bex.pointer.BexPointerCache; +import blue.bex.result.BexExecutionResult; +import blue.bex.result.BexMetrics; +import blue.bex.runtime.BexRuntime; +import blue.language.Blue; +import blue.language.model.Node; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.function.ToLongFunction; + +import static blue.bex.test.BexTestFixtures.defaultDocumentView; +import static blue.bex.test.BexTestFixtures.frozen; +import static blue.bex.test.BexTestFixtures.list; +import static blue.bex.test.BexTestFixtures.obj; +import static blue.bex.test.BexTestFixtures.op; +import static blue.bex.test.BexTestFixtures.stepDo; +import static blue.bex.test.BexTestFixtures.stepExpr; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class BexDiagnosticAdmissionOrderingTest { + + @Test + void rejectedRootAndNestedFunctionChargesDoNotMutateFunctionMetrics() { + RejectedExecution rejectedRoot = reject( + stepExpr(new Node().value(true)), + 1L); + assertEquals(BexGasCounter.FUNCTION_CALLED, + rejectedRoot.failure.counter()); + assertEquals(0L, rejectedRoot.metrics.compiledExecutions()); + assertEquals(0L, rejectedRoot.metrics.functionCalls()); + + Node nestedCallProgram = obj( + "type", "Blue/BEX Program", + "functions", obj( + "helper", obj("expr", true)), + "expr", op("$call", obj("function", "helper"))); + RejectedExecution rejectedNested = reject( + nestedCallProgram, + 3L); + assertEquals(BexGasCounter.FUNCTION_CALLED, + rejectedNested.failure.counter()); + assertEquals(1L, rejectedNested.metrics.compiledExecutions()); + assertEquals(1L, rejectedNested.metrics.functionCalls()); + assertEquals(1L, rejectedNested.metrics.expressionEvaluations()); + } + + @Test + void rejectedExpressionAndStatementChargesDoNotMutateTheirMetrics() { + RejectedExecution rejectedExpression = reject( + stepExpr(new Node().value(true)), + 2L); + assertEquals(BexGasCounter.EXPRESSION_EVALUATED, + rejectedExpression.failure.counter()); + assertEquals(1L, rejectedExpression.metrics.compiledExecutions()); + assertEquals(1L, rejectedExpression.metrics.functionCalls()); + assertEquals(0L, + rejectedExpression.metrics.expressionEvaluations()); + + RejectedExecution rejectedStatement = reject( + stepDo(list(op("$return", true))), + 2L); + assertEquals(BexGasCounter.STATEMENT_EXECUTED, + rejectedStatement.failure.counter()); + assertEquals(1L, rejectedStatement.metrics.compiledExecutions()); + assertEquals(1L, rejectedStatement.metrics.functionCalls()); + assertEquals(0L, + rejectedStatement.metrics.statementExecutions()); + } + + @Test + void rejectedReadChargesDoNotMutateReadMetrics() { + assertRejectedReadMetric( + op("$document", "/"), + BexGasCounter.DOCUMENT_READ, + BexMetrics::frozenDocumentReads); + assertRejectedReadMetric( + op("$document", obj("path", "/", "view", "resolved")), + BexGasCounter.DOCUMENT_READ, + BexMetrics::resolvedDocumentReads); + assertRejectedReadMetric( + op("$event", "/"), + BexGasCounter.EVENT_READ, + BexMetrics::eventReads); + assertRejectedReadMetric( + op("$currentContract", "/"), + BexGasCounter.CURRENT_CONTRACT_READ, + BexMetrics::currentContractReads); + assertRejectedReadMetric( + op("$steps", obj("step", "Build", "path", "/")), + BexGasCounter.STEPS_READ, + BexMetrics::stepsReads); + assertRejectedReadMetric( + op("$resultValue", "/"), + BexGasCounter.RESULT_VALUE_READ, + BexMetrics::resultValueReads); + } + + @Test + void rejectedIterationReadDoesNotAddAnUnfinishedLoopMetric() { + Node programNode = stepDo(list( + op("$forEach", obj( + "in", list(1, 2, 3), + "item", "item", + "do", list())), + op("$return", true))); + + try (Blue blue = new Blue()) { + BexEngine engine = BexEngine.builder() + .blue(blue) + .build(); + BexCompiledProgram program = engine.compile( + BexProgramSource.inline(frozen(programNode))); + BexExecutionResult unlimited = engine.execute( + program, context(1_000_000L)); + List trace = unlimited.gasTrace(); + int rejectedIndex = nthCharge( + trace, + BexGasCounter.LIST_ITEM_READ, + "$forEach", + 1); + long prefixGas = gas(trace.subList(0, rejectedIndex)); + long admittedIterations = countCharges( + trace.subList(0, rejectedIndex), + BexGasCounter.LIST_ITEM_READ, + "$forEach"); + + BexMetrics metrics = new BexMetrics(); + BexRuntime runtime = new BexRuntime( + program, + context(prefixGas), + blue, + BexGasSchedule.defaults(), + metrics, + new BexPointerCache(), + BexIntrinsicRegistry.empty()); + + BexException wrapped = assertThrows( + BexException.class, + runtime::execute); + BexGasLimitExceededException failure = findCause( + wrapped, + BexGasLimitExceededException.class); + assertNotNull(failure); + + assertEquals(BexGasCounter.LIST_ITEM_READ, + failure.counter()); + assertEquals(admittedIterations, + metrics.loopIterations()); + } + } + + private static void assertRejectedReadMetric( + Node expression, + BexGasCounter expectedCounter, + ToLongFunction metric) { + RejectedExecution rejected = reject( + stepExpr(expression), + 3L); + assertEquals(expectedCounter, rejected.failure.counter()); + assertEquals(0L, metric.applyAsLong(rejected.metrics)); + } + + private static RejectedExecution reject( + Node programNode, + long gasLimit) { + try (Blue blue = new Blue()) { + BexEngine engine = BexEngine.builder() + .blue(blue) + .build(); + BexCompiledProgram program = engine.compile( + BexProgramSource.inline(frozen(programNode))); + BexMetrics metrics = new BexMetrics(); + BexRuntime runtime = new BexRuntime( + program, + context(gasLimit), + blue, + BexGasSchedule.defaults(), + metrics, + new BexPointerCache(), + BexIntrinsicRegistry.empty()); + BexException wrapped = assertThrows( + BexException.class, + runtime::execute); + BexGasLimitExceededException failure = findCause( + wrapped, + BexGasLimitExceededException.class); + assertNotNull(failure); + return new RejectedExecution(failure, metrics); + } + } + + private static T findCause( + Throwable throwable, + Class type) { + Throwable current = throwable; + while (current != null) { + if (type.isInstance(current)) { + return type.cast(current); + } + current = current.getCause(); + } + return null; + } + + private static BexExecutionContext context(long gasLimit) { + return BexExecutionContext.builder() + .document(defaultDocumentView()) + .gasLimit(gasLimit) + .build(); + } + + private static int nthCharge( + List trace, + BexGasCounter counter, + String operator, + int occurrence) { + int seen = 0; + for (int index = 0; index < trace.size(); index++) { + BexGasCharge charge = trace.get(index); + if (charge.counter() == counter + && operator.equals(charge.operator())) { + if (seen == occurrence) { + return index; + } + seen++; + } + } + throw new AssertionError( + "Missing charge " + counter + " occurrence " + occurrence); + } + + private static long countCharges( + List trace, + BexGasCounter counter, + String operator) { + long count = 0L; + for (BexGasCharge charge : trace) { + if (charge.counter() == counter + && operator.equals(charge.operator())) { + count++; + } + } + return count; + } + + private static long gas(List trace) { + long total = 0L; + for (BexGasCharge charge : trace) { + total += charge.gas(); + } + return total; + } + + private static final class RejectedExecution { + private final BexGasLimitExceededException failure; + private final BexMetrics metrics; + + private RejectedExecution( + BexGasLimitExceededException failure, + BexMetrics metrics) { + this.failure = failure; + this.metrics = metrics; + } + } +} diff --git a/src/test/java/blue/bex/BexDiagnosticsTest.java b/src/test/java/blue/bex/BexDiagnosticsTest.java index 2ccda77..da66cb2 100644 --- a/src/test/java/blue/bex/BexDiagnosticsTest.java +++ b/src/test/java/blue/bex/BexDiagnosticsTest.java @@ -41,7 +41,7 @@ void invalidIntegerConversionIncludesExpressionLocation() { @Test void gasExhaustionIncludesCurrentOperatorWhenPossible() { BexEngine engine = BexEngine.builder() - .gasSchedule(BexGasSchedule.builder().expressionBase(100).build()) + .gasSchedule(BexGasSchedule.builder().expressionEvaluated(100).build()) .build(); BexExecutionContext context = BexExecutionContext.builder() .document(defaultDocumentView()) diff --git a/src/test/java/blue/bex/BexEngineConformanceTest.java b/src/test/java/blue/bex/BexEngineConformanceTest.java index d4259f0..7e60ae3 100644 --- a/src/test/java/blue/bex/BexEngineConformanceTest.java +++ b/src/test/java/blue/bex/BexEngineConformanceTest.java @@ -7,11 +7,10 @@ import blue.bex.api.FrozenBexDocumentView; import blue.bex.compile.BexCompiledProgram; import blue.bex.compile.LruBexCompiledProgramCache; +import blue.bex.gas.BexGasCounter; import blue.bex.gas.BexGasSchedule; import blue.bex.result.BexExecutionResult; -import blue.bex.result.BexMetrics; import blue.bex.value.BexValue; -import blue.bex.value.BexFrozenWriter; import blue.bex.value.BexValues; import blue.language.model.Node; import blue.language.snapshot.FrozenNode; @@ -45,14 +44,14 @@ Collection expressionOperators() { cases.add(c("document literal path", op("$document", "/status"), "active")); cases.add(c("document dynamic path", op("$document", obj("path", op("$concat", list("/sta", "tus")))), "active")); cases.add(c("document resolved view", op("$document", obj("path", "/status", "view", "resolved")), "active")); - cases.add(c("document missing", op("$document", "/missing"), null)); + cases.add(c("document missing is undefined", op("$exists", op("$document", "/missing")), false)); cases.add(c("document metadata name", op("$document", "/name"), "Root")); cases.add(c("document value metadata", op("$document", "/status/value"), "active")); cases.add(c("event literal path", op("$event", "/kind"), "Created")); cases.add(c("event dynamic path", op("$event", obj("path", op("$concat", list("/ki", "nd")))), "Created")); cases.add(c("binding reads event short form", op("$binding", "event/kind"), "Created")); cases.add(c("binding reads custom binding", op("$binding", "policy/decision"), "allow")); - cases.add(c("binding missing returns undefined", op("$binding", "missing/value"), null)); + cases.add(c("binding missing is undefined", op("$exists", op("$binding", "missing/value")), false)); cases.add(c("binding dynamic path", op("$binding", obj("name", "event", "path", op("$concat", list("/ki", "nd")))), "Created")); cases.add(c("binding dynamic name", op("$binding", obj("name", op("$concat", list("current", "Contract")), "path", "/channel")), "main")); cases.add(c("current contract", op("$currentContract", "/channel"), "main")); @@ -104,7 +103,8 @@ Collection expressionOperators() { cases.add(c("size object", op("$size", obj("a", 1, "b", 2)), BigInteger.valueOf(2))); cases.add(c("listGet hit", op("$listGet", obj("list", list("a", "b"), "index", 1)), "b")); cases.add(c("listGet default", op("$listGet", obj("list", list("a"), "index", 4, "default", "x")), "x")); - cases.add(c("listGet undefined", op("$listGet", obj("list", list("a"), "index", 4)), null)); + cases.add(c("listGet missing item is undefined", + op("$exists", op("$listGet", obj("list", list("a"), "index", 4))), false)); cases.add(c("listConcat", op("$listConcat", list(list("a"), list("b", "c"))), l("a", "b", "c"))); cases.add(c("merge right wins", op("$merge", list(obj("a", 1, "b", 2), obj("b", 3))), m("a", bi(1), "b", bi(3)))); cases.add(c("objectSet dynamic", op("$objectSet", obj("object", obj("a", 1), "key", op("$concat", list("b")), "val", true)), m("a", bi(1), "b", true))); @@ -119,8 +119,10 @@ Collection expressionOperators() { cases.add(c("resultValue document fallback", op("$resultValue", "/status"), "active")); cases.add(c("document relative path", op("$document", "status"), "active")); cases.add(c("event nested path", op("$event", "/message/request/id"), "r1")); - cases.add(c("current contract missing path", op("$currentContract", "/missing"), null)); - cases.add(c("get missing field", op("$get", obj("object", obj("a", "b"), "key", "z")), null)); + cases.add(c("current contract missing path is undefined", + op("$exists", op("$currentContract", "/missing")), false)); + cases.add(c("get missing field is undefined", + op("$exists", op("$get", obj("object", obj("a", "b"), "key", "z"))), false)); cases.add(c("text on boolean", op("$text", true), "true")); cases.add(c("boolean on boolean", op("$boolean", true), true)); cases.add(c("truthy zero integer", op("$truthy", 0), true)); @@ -162,8 +164,7 @@ void statementsAppendResultsAndPreservePatchOrder() { Node step = stepDo(list( op("$appendChange", obj("op", "replace", "path", "/status", "val", "ready")), op("$appendChange", obj("op", "replace", "path", "/status", "val", "done")), - op("$appendEvent", obj("kind", "Calculated")), - emptyStatement() + op("$appendEvent", obj("kind", "Calculated")) )); BexExecutionResult result = runStep(step, defaultContext()); @@ -189,8 +190,7 @@ void resultValueUsesLatestPatchAndAncestorOverlay() { BexExecutionResult result = runStep(step, defaultContext()); assertEquals(m("ready", true, "x", bi(1)), simple(result.value())); - assertEquals(1, result.metrics().resultOverlayExactHits()); - assertEquals(1, result.metrics().resultOverlayAncestorHits()); + assertEquals(2L, result.gasLedger().quantity(BexGasCounter.RESULT_VALUE_READ)); } @Test @@ -274,7 +274,7 @@ void nodeSnapshotFreezesBoundaryValueForDeterministicContexts() { } @Test - void nodeBexValueMetadataReadsWork() { + void nodeBexValueMetadataReadsExcludePhysicalBlueId() { Node type = new Node().value("EventType"); Node event = new Node() .name("EventRoot") @@ -285,7 +285,7 @@ void nodeBexValueMetadataReadsWork() { .properties(props("kind", "Created")); BexExecutionContext context = BexExecutionContext.builder() .document(defaultDocumentView()) - .event(BexValues.nodeCursor(event)) + .event(BexValues.nodeCursorTrustedImmutable(event)) .gasLimit(1_000_000) .build(); Node step = stepExpr(obj( @@ -296,7 +296,7 @@ void nodeBexValueMetadataReadsWork() { "type", op("$event", "/type/value") )); - assertEquals(m("blueId", "event-blue-id", "description", "metadata", "name", "EventRoot", "type", "EventType", "value", "payload-value"), + assertEquals(m("description", "metadata", "name", "EventRoot", "type", "EventType", "value", "payload-value"), simple(runStep(step, context).value())); } @@ -399,7 +399,7 @@ void gasIsDeterministicAndExhaustionFails() { assertEquals(a.gasUsed(), b.gasUsed()); BexEngine engine = BexEngine.builder() - .gasSchedule(BexGasSchedule.builder().expressionBase(100).build()) + .gasSchedule(BexGasSchedule.builder().expressionEvaluated(100).build()) .build(); BexExecutionContext context = BexExecutionContext.builder() .document(defaultDocumentView()) @@ -409,7 +409,7 @@ void gasIsDeterministicAndExhaustionFails() { } @Test - void gasSizeEstimatorCachesFrozenValues() { + void repeatedOutputsUsePortableNamedGasCounters() { Node large = largeObject(300); Node step = obj( "type", "Blue/BEX Program", @@ -417,33 +417,15 @@ void gasSizeEstimatorCachesFrozenValues() { "do", list( op("$appendEvent", op("$const", "large")), op("$appendEvent", op("$const", "large")), - emptyStatement() + op("$return", obj("eventCount", op("$size", op("$events", true)))) ) ); BexExecutionResult result = runStep(step, defaultContext()); - assertTrue(result.metrics().sizeEstimateCalls() >= 2); - assertTrue(result.metrics().sizeEstimateCacheMisses() > 0); - assertTrue(result.metrics().sizeEstimateCacheHits() > 0); - } - - @Test - void frozenWriterTracksChildNodeRoundTripsSeparatelyFromGenericFallback() { - BexMetrics metrics = new BexMetrics(); - BexValue computed = BexValues.pointerSet( - BexValues.overlay(BexValues.fromSimple(m("a", bi(1))), "b", BexValues.list(Arrays.asList(BexValues.scalar("x")))), - Arrays.asList("c", "d"), - BexValues.scalar(true), - "set" - ); - - FrozenNode frozen = BexFrozenWriter.toFrozen(computed, metrics); - - assertEquals(m("a", bi(1), "b", l("x"), "c", m("d", true)), simple(BexValues.frozen(frozen))); - assertEquals(1, metrics.frozenOutputConversions()); - assertEquals(0, metrics.frozenWriterNodeFallbacks()); - assertTrue(metrics.frozenWriterChildNodeRoundTrips() > 0); + assertEquals(m("eventCount", bi(2)), simple(result.value())); + assertEquals(2L, result.gasLedger().quantity(BexGasCounter.EVENT_APPENDED)); + assertTrue(result.gasLedger().quantity(BexGasCounter.BLUE_OUTPUT_BOUNDARY) >= 3L); } @Test @@ -551,10 +533,6 @@ private static Node op(String name, Object body) { return obj(name, body); } - private static Node emptyStatement() { - return obj("$empty", true); - } - private static Node obj(Object... keysAndValues) { return new Node().properties(props(keysAndValues)); } diff --git a/src/test/java/blue/bex/BexExactGasRuleTest.java b/src/test/java/blue/bex/BexExactGasRuleTest.java new file mode 100644 index 0000000..b7bb29e --- /dev/null +++ b/src/test/java/blue/bex/BexExactGasRuleTest.java @@ -0,0 +1,336 @@ +package blue.bex; + +import blue.bex.api.BexEngine; +import blue.bex.api.BexExecutionContext; +import blue.bex.api.BexGasLedgerHost; +import blue.bex.api.BexProgramSource; +import blue.bex.api.FrozenBexDocumentView; +import blue.bex.gas.BexGasCharge; +import blue.bex.gas.BexGasCounter; +import blue.bex.output.BexSemanticIdentityBoundary; +import blue.bex.result.BexExecutionResult; +import blue.language.model.Node; +import blue.language.processor.GasMeter; +import blue.language.processor.GasSchedule; +import blue.language.processor.GasTraceEntry; +import blue.language.snapshot.FrozenNode; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class BexExactGasRuleTest { + + @Test + void prefixOperatorsChargeOnlyCodePointBlocksActuallyCompared() { + String longText = "x" + repeated('a', 190); + String firstBlockMiss = "y" + repeated('a', 190); + + BexExecutionResult startsWith = runWithConstants( + op("$startsWith", list( + op("$const", "text"), + op("$const", "prefix"))), + "text", longText, + "prefix", firstBlockMiss); + + assertEquals(false, startsWith.value().toSimple()); + assertEquals(2L, quantity(startsWith, BexGasCounter.TEXT_BLOCK_EXAMINED)); + + BexExecutionResult missedSlice = runWithConstants( + op("$sliceAfter", list( + op("$const", "text"), + op("$const", "prefix"))), + "text", longText, + "prefix", firstBlockMiss); + + assertEquals("", missedSlice.value().toSimple()); + assertEquals(2L, quantity(missedSlice, BexGasCounter.TEXT_BLOCK_EXAMINED)); + assertEquals(0L, quantity(missedSlice, BexGasCounter.TEXT_BLOCK_CONSTRUCTED)); + + String matchingPrefix = repeated('p', 65); + String suffix = repeated('s', 130); + BexExecutionResult matchedSlice = runWithConstants( + op("$sliceAfter", list( + op("$const", "text"), + op("$const", "prefix"))), + "text", matchingPrefix + suffix, + "prefix", matchingPrefix); + + assertEquals(suffix, matchedSlice.value().toSimple()); + assertEquals(4L, quantity(matchedSlice, BexGasCounter.TEXT_BLOCK_EXAMINED)); + assertEquals(3L, quantity(matchedSlice, BexGasCounter.TEXT_BLOCK_CONSTRUCTED)); + } + + @Test + void exactDecimalToIntegerIncludesScaleAlignmentAfterFullMagnitudeAdmission() { + BexExecutionResult result = runWithConstants( + op("$integer", op("$const", "decimal")), + "decimal", new BigDecimal("4294967296.0")); + + assertEquals( + new BigInteger("4294967296"), + result.value().toSimple()); + assertEquals( + 3L, + quantity( + result, + BexGasCounter.INTEGER_LIMB_OPERATION)); + } + + @Test + void textArithmeticOperandsAreConvertedBeforeArithmeticWork() { + BexExecutionResult result = runWithConstants( + op("$add", list( + op("$const", "left"), + op("$const", "right"))), + "left", "4294967296", + "right", BigInteger.ONE); + + assertEquals( + new BigInteger("4294967297"), + result.value().toSimple()); + assertEquals( + 5L, + quantity( + result, + BexGasCounter.INTEGER_LIMB_OPERATION)); + } + + @Test + void mergeChargesProductionOnlyForRetainedFields() { + BexExecutionResult result = runWithConstants( + op("$merge", list( + op("$const", "left"), + op("$const", "right"))), + "left", obj("a", 1, "b", 2), + "right", obj("b", 3, "c", 4)); + + assertEquals(map("a", integer(1), "b", integer(3), "c", integer(4)), + result.value().toSimple()); + assertEquals(4L, quantity(result, BexGasCounter.COLLECTION_ITEM_VISITED)); + assertEquals(4L, quantity(result, BexGasCounter.OBJECT_MEMBER_READ)); + assertEquals(3L, quantity(result, BexGasCounter.COLLECTION_ITEM_PRODUCED)); + assertEquals(3L, + quantity(result, BexGasCounter.TRANSIENT_OBJECT_MEMBER_PRODUCED)); + } + + @Test + void objectFromEntriesMetersOrderedReadsKeyConversionAndRetainedFields() { + String longKey = repeated('k', 65); + Node entries = list( + obj("key", longKey, "val", 1), + obj("key", "drop", "val", 2), + obj("key", longKey, "val", 3), + obj("key", "drop"), + obj("key", 7, "val", 4)); + + BexExecutionResult result = runWithConstants( + op("$objectFromEntries", op("$const", "entries")), + "entries", entries); + + assertEquals(map("7", integer(4), longKey, integer(3)), + result.value().toSimple()); + assertEquals(5L, quantity(result, BexGasCounter.COLLECTION_ITEM_VISITED)); + assertEquals(5L, quantity(result, BexGasCounter.LIST_ITEM_READ)); + assertEquals(10L, quantity(result, BexGasCounter.OBJECT_MEMBER_READ)); + assertEquals(9L, quantity(result, BexGasCounter.TEXT_BLOCK_EXAMINED)); + assertEquals(7L, quantity(result, BexGasCounter.TEXT_BLOCK_CONSTRUCTED)); + assertEquals(1L, quantity( + result, BexGasCounter.SORT_COMPARISON)); + assertEquals(1L, quantity( + result, + BexGasCounter.COMPARISON_NODE_VISITED)); + assertEquals(2L, quantity(result, BexGasCounter.COLLECTION_ITEM_PRODUCED)); + assertEquals(2L, + quantity(result, BexGasCounter.TRANSIENT_OBJECT_MEMBER_PRODUCED)); + + List operatorCounters = result.gasTrace().stream() + .filter(charge -> "$objectFromEntries".equals(charge.operator())) + .map(BexGasCharge::counterName) + .collect(Collectors.toList()); + int firstVisit = operatorCounters.indexOf( + BexGasCounter.COLLECTION_ITEM_VISITED.canonicalName()); + assertEquals(Arrays.asList( + "collectionItemVisited", + "listItemRead", + "objectMemberRead", + "textBlockExamined", + "textBlockExamined", + "textBlockConstructed", + "objectMemberRead"), + operatorCounters.subList(firstVisit, firstVisit + 7)); + } + + @Test + void objectFromEntriesDoesNotAdmitValueReadAfterInvalidKey() { + RecordingGasHost gasHost = new RecordingGasHost(); + Node program = program( + op("$objectFromEntries", op("$const", "entries")), + "entries", list(obj("val", 1))); + BexExecutionContext context = context(gasHost); + + assertThrows(BexException.class, () -> BexEngine.builder().build() + .compileAndExecute( + BexProgramSource.inline(frozen(program)), + context)); + + assertEquals(1L, gasHost.quantity("objectMemberRead")); + assertEquals(0L, gasHost.quantity("textBlockExamined")); + assertEquals(0L, gasHost.quantity("collectionItemProduced")); + } + + private static long quantity( + BexExecutionResult result, BexGasCounter counter) { + return result.gasLedger().quantity(counter); + } + + private static BexExecutionResult runWithConstants( + Node expression, Object... constants) { + return BexEngine.builder().build().compileAndExecute( + BexProgramSource.inline(frozen(program(expression, constants))), + context(null)); + } + + private static Node program(Node expression, Object... constants) { + return obj( + "type", "Blue/BEX Program", + "constants", obj(constants), + "expr", expression); + } + + private static BexExecutionContext context(BexGasLedgerHost gasHost) { + BexExecutionContext.Builder builder = BexExecutionContext.builder() + .document(new FrozenBexDocumentView(frozen(obj()))) + .gasLimit(1_000_000L); + if (gasHost != null) { + builder.gasLedgerHost(gasHost) + .semanticIdentityBoundary( + BexSemanticIdentityBoundary.STANDALONE); + } + return builder.build(); + } + + private static FrozenNode frozen(Node node) { + return FrozenNode.fromResolvedNode(node); + } + + private static Node op(String name, Object body) { + return obj(name, body); + } + + private static Node obj(Object... keysAndValues) { + return new Node().properties(properties(keysAndValues)); + } + + private static Map properties(Object... keysAndValues) { + Map properties = new LinkedHashMap<>(); + for (int index = 0; index < keysAndValues.length; index += 2) { + properties.put( + (String) keysAndValues[index], + node(keysAndValues[index + 1])); + } + return properties; + } + + private static Node list(Object... values) { + List items = new ArrayList<>(); + for (Object value : values) { + items.add(node(value)); + } + return new Node().items(items); + } + + private static Node node(Object value) { + if (value instanceof Node) { + return (Node) value; + } + if (value instanceof Integer) { + return new Node().value(((Integer) value).longValue()); + } + if (value instanceof Long + || value instanceof String + || value instanceof Boolean + || value instanceof BigInteger + || value instanceof BigDecimal) { + return new Node().value(value); + } + if (value == null) { + return new Node(); + } + throw new IllegalArgumentException( + "Unsupported test value " + value.getClass().getName()); + } + + private static Map map(Object... keysAndValues) { + Map values = new LinkedHashMap<>(); + for (int index = 0; index < keysAndValues.length; index += 2) { + values.put( + (String) keysAndValues[index], + keysAndValues[index + 1]); + } + return values; + } + + private static BigInteger integer(long value) { + return BigInteger.valueOf(value); + } + + private static String repeated(char value, int count) { + StringBuilder text = new StringBuilder(count); + for (int index = 0; index < count; index++) { + text.append(value); + } + return text.toString(); + } + + private static final class RecordingGasHost + implements BexGasLedgerHost { + private final GasMeter parent = + new GasMeter(GasSchedule.contracts10()); + private GasMeter.ChildGasLedger child; + + @Override + public GasMeter.ChildGasLedger open( + String namespace, Map counterWeights) { + child = parent.childLedger(namespace, counterWeights); + return child; + } + + @Override + public void submit(GasMeter.ChildGasLedger ledger) { + parent.merge(ledger); + } + + @Override + public void failedDeterministically( + GasMeter.ChildGasLedger ledger) { + parent.merge(ledger); + } + + @Override + public void evidenceUnavailable( + GasMeter.ChildGasLedger ledger) { + // Detached child ledgers reserve nothing until merge. + } + + private long quantity(String counter) { + long total = 0L; + for (GasTraceEntry entry : parent.trace()) { + if ("bex".equals(entry.namespace()) + && counter.equals(entry.counter())) { + total += entry.quantity(); + } + } + return total; + } + } +} diff --git a/src/test/java/blue/bex/BexExactReferenceDocumentTest.java b/src/test/java/blue/bex/BexExactReferenceDocumentTest.java new file mode 100644 index 0000000..19480c0 --- /dev/null +++ b/src/test/java/blue/bex/BexExactReferenceDocumentTest.java @@ -0,0 +1,527 @@ +package blue.bex; + +import blue.bex.api.BexEngine; +import blue.bex.api.BexExecutionContext; +import blue.bex.api.BexProgramSource; +import blue.bex.api.FrozenBexDocumentView; +import blue.bex.value.BexValue; +import blue.bex.value.BexValues; +import blue.language.Blue; +import blue.language.NodeProvider; +import blue.language.model.Node; +import blue.language.processor.ExecutionEvidenceUnavailableException; +import blue.language.processor.InvalidExecutionEvidenceException; +import blue.language.provider.CyclicAwareNodeProvider; +import blue.language.provider.CyclicSetProof; +import blue.language.provider.NodeProviderOutcome; +import blue.language.provider.NodeProviderResult; +import blue.language.snapshot.FrozenNode; +import blue.language.snapshot.ResolvedSnapshot; +import blue.language.utils.CircularBlueIdCalculator; +import org.junit.jupiter.api.Test; + +import java.math.BigInteger; +import java.util.Collections; +import java.util.concurrent.atomic.AtomicInteger; + +import static blue.bex.test.BexTestFixtures.list; +import static blue.bex.test.BexTestFixtures.obj; +import static blue.bex.test.BexTestFixtures.op; +import static blue.bex.test.BexTestFixtures.stepDo; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class BexExactReferenceDocumentTest { + @Test + void semanticAccessMaterializesVerifiedReferenceButExactIdentityDoesNot() { + Node content = obj("a", 1, "values", list(1, 2)); + String blueId = calculateBlueId(content); + AtomicInteger demands = new AtomicInteger(); + NodeProvider provider = requestedBlueId -> { + demands.incrementAndGet(); + return blueId.equals(requestedBlueId) + ? Collections.singletonList(content.clone()) + : Collections.emptyList(); + }; + + try (Blue blue = new Blue(provider)) { + ResolvedSnapshot snapshot = blue.resolveToSnapshot( + obj("x", new Node().blueId(blueId))); + assertTrue(snapshot.frozenResolvedRoot() + .property("x") + .isReferenceOnly()); + + BexValue root = BexValues.referenceBacked( + BexValues.exact( + snapshot.frozenCanonicalRoot(), + snapshot.frozenResolvedRoot()), + blue); + BexValue referenced = root.get("x"); + + assertEquals(blueId, referenced.exactBlueId()); + assertEquals(0, demands.get()); + + assertEquals("object", BexValues.kind(referenced)); + assertEquals(1, demands.get()); + assertTrue(referenced.get("blueId").isUndefined()); + assertEquals(BigInteger.ONE, + referenced.get("a").asInteger()); + assertEquals(java.util.Arrays.asList("a", "values"), + referenced.keys()); + assertEquals(1, demands.get()); + } + } + + @Test + void unavailableReferenceEvidencePropagatesInsteadOfBecomingUndefined() { + Node unavailableContent = obj("a", 1); + String unavailableBlueId = calculateBlueId(unavailableContent); + AtomicInteger demands = new AtomicInteger(); + NodeProvider provider = requestedBlueId -> { + demands.incrementAndGet(); + return Collections.emptyList(); + }; + + try (Blue blue = new Blue(provider)) { + ResolvedSnapshot snapshot = blue.resolveToSnapshot( + obj("x", new Node().blueId(unavailableBlueId))); + BexValue root = BexValues.referenceBacked( + BexValues.exact( + snapshot.frozenCanonicalRoot(), + snapshot.frozenResolvedRoot()), + blue); + + RuntimeException failure = assertThrows( + RuntimeException.class, + () -> root.at("/x/a")); + assertTrue(messageChain(failure).contains(unavailableBlueId)); + assertEquals(1, demands.get()); + } + } + + @Test + void resultOverlayCanPatchThroughAProviderBackedExactReference() { + Node content = obj("a", 1, "untouched", 2); + String blueId = calculateBlueId(content); + AtomicInteger demands = new AtomicInteger(); + NodeProvider provider = requestedBlueId -> { + demands.incrementAndGet(); + return blueId.equals(requestedBlueId) + ? Collections.singletonList(content.clone()) + : Collections.emptyList(); + }; + + try (Blue blue = new Blue(provider)) { + ResolvedSnapshot snapshot = blue.resolveToSnapshot( + obj("x", new Node().blueId(blueId))); + BexExecutionContext context = BexExecutionContext.builder() + .document(new FrozenBexDocumentView( + snapshot.frozenCanonicalRoot(), + snapshot.frozenResolvedRoot(), + "/")) + .gasLimit(100_000) + .build(); + Node program = stepDo(list( + op("$appendChange", obj( + "op", "replace", + "path", "/x/a", + "val", 3)), + op("$return", op("$resultValue", "/x")))); + + BexValue result = BexEngine.builder() + .blue(blue) + .build() + .compileAndExecute( + BexProgramSource.inline( + FrozenNode.fromResolvedNode(program)), + context) + .value(); + + assertEquals(BigInteger.valueOf(3), + result.get("a").asInteger()); + assertEquals(BigInteger.valueOf(2), + result.get("untouched").asInteger()); + /* + * Overlay evaluation and final output admission each use a + * structured provider operation. Provider calls are diagnostics; + * no long-lived Blue cache may suppress the second attempt. + */ + assertEquals(2, demands.get()); + } + } + + @Test + void collapsedReferenceWithoutAResolverIsIncompleteNotAbsent() { + Node content = obj("a", 1); + String blueId = calculateBlueId(content); + ResolvedSnapshot snapshot; + try (Blue blue = new Blue()) { + snapshot = blue.resolveToSnapshot( + obj("x", new Node().blueId(blueId))); + } + + BexValue referenced = BexValues.exact( + snapshot.frozenCanonicalRoot(), + snapshot.frozenResolvedRoot()).get("x"); + + assertTrue(BexValues.equal(referenced, referenced)); + BexException failure = assertThrows( + BexException.class, + referenced::isObject); + assertTrue(failure.getMessage().contains(blueId)); + assertThrows(BexException.class, + () -> referenced.get("blueId")); + } + + @Test + void cyclicMemberStructuralReadRequiresAndAcceptsCompleteSetProof() { + Node member = obj( + "label", "verified-cycle", + "next", new Node().blueId("this#0")) + .name("cyclic-member"); + java.util.List placeholders = + Collections.singletonList(member); + String memberBlueId = + CircularBlueIdCalculator + .calculateCircularSetBlueIds(placeholders) + .get(0); + Node resolvedMember = member.clone(); + resolvedMember.getProperties().get("next") + .blueId(memberBlueId); + VerifiedCyclicProvider provider = + new VerifiedCyclicProvider( + memberBlueId, + resolvedMember, + placeholders); + assertTrue(memberBlueId.contains("#")); + + try (Blue blue = new Blue(provider)) { + ResolvedSnapshot snapshot = blue.resolveToSnapshot( + obj("x", new Node().blueId(memberBlueId))); + BexValue root = BexValues.referenceBacked( + BexValues.exact( + snapshot.frozenCanonicalRoot(), + snapshot.frozenResolvedRoot()), + blue); + + assertEquals(memberBlueId, + root.get("x").exactBlueId()); + assertEquals("verified-cycle", + root.at("/x/label").asText()); + assertEquals(memberBlueId, + root.at("/x/next").exactBlueId()); + } + + NodeProvider proofless = requested -> memberBlueId.equals(requested) + ? Collections.singletonList(resolvedMember.clone()) + : Collections.emptyList(); + try (Blue blue = new Blue(proofless)) { + BexValue prooflessMember = BexValues.referenceBacked( + BexValues.frozen(FrozenNode.fromNode( + new Node().blueId(memberBlueId))), + blue); + assertEquals(memberBlueId, + prooflessMember.exactBlueId()); + assertThrows( + InvalidExecutionEvidenceException.class, + prooflessMember::isObject); + } + + BexValue forgedResolvedMember = BexValues.exact( + FrozenNode.fromNode( + new Node().blueId(memberBlueId)), + FrozenNode.fromResolvedNode( + resolvedMember), + memberBlueId); + assertEquals(memberBlueId, + forgedResolvedMember.exactBlueId()); + BexException forgedFailure = assertThrows( + BexException.class, + forgedResolvedMember::isObject); + assertTrue(forgedFailure.getMessage().contains( + memberBlueId)); + } + + @Test + void nestedCyclicResolvedBodyRemainsOpaqueUntilCompleteProof() { + CyclicFixture fixture = new CyclicFixture(); + Node ordinaryBody = obj( + "label", "ordinary-exact-child"); + String ordinaryBlueId = + calculateBlueId(ordinaryBody); + FrozenNode canonicalParent = + FrozenNode.fromNode(obj( + "cyclic", + new Node().blueId( + fixture.memberBlueId), + "ordinary", + new Node().blueId( + ordinaryBlueId))); + FrozenNode resolvedParent = + FrozenNode.fromResolvedNode(obj( + "cyclic", + fixture.resolvedMember, + "ordinary", + ordinaryBody)); + + BexValue unverifiedParent = BexValues.exact( + canonicalParent, resolvedParent); + BexValue unverifiedCyclic = + unverifiedParent.get("cyclic"); + assertEquals( + fixture.memberBlueId, + unverifiedCyclic.exactBlueId()); + BexException unverifiedFailure = assertThrows( + BexException.class, + unverifiedCyclic::isObject); + assertTrue(unverifiedFailure.getMessage().contains( + fixture.memberBlueId)); + + BexValue ordinary = + unverifiedParent.get("ordinary"); + assertEquals( + ordinaryBlueId, + ordinary.exactBlueId()); + assertTrue(ordinary.isObject()); + assertEquals( + Collections.singletonList("label"), + ordinary.keys()); + + VerifiedCyclicProvider provider = + new VerifiedCyclicProvider( + fixture.memberBlueId, + fixture.resolvedMember, + Collections.singletonList( + fixture.placeholder)); + try (Blue blue = new Blue(provider)) { + BexValue verifiedParent = + BexValues.referenceBacked( + BexValues.exact( + canonicalParent, + resolvedParent), + blue); + BexValue verifiedCyclic = + verifiedParent.get("cyclic"); + + assertEquals( + fixture.memberBlueId, + verifiedCyclic.exactBlueId()); + assertEquals( + "verified-cycle", + verifiedCyclic.get("label") + .asText()); + assertEquals(1, provider.proofQueries); + } + } + + @Test + void cyclicMemberContentFetchUnavailabilityStopsBeforeProofQuery() { + CyclicFixture fixture = new CyclicFixture(); + CyclicEvidenceProvider provider = + new CyclicEvidenceProvider( + NodeProviderResult.unavailable( + "cyclic member content temporarily unavailable"), + null); + + try (Blue blue = new Blue(provider)) { + BexValue member = exactReference( + blue, fixture.memberBlueId); + + assertEquals(fixture.memberBlueId, + member.exactBlueId()); + ExecutionEvidenceUnavailableException failure = + assertThrows( + ExecutionEvidenceUnavailableException.class, + member::isObject); + + assertEquals( + Collections.singletonList( + fixture.memberBlueId), + failure.requiredExactBlueIds()); + assertEquals( + "cyclic member content temporarily unavailable", + failure.getMessage()); + assertEquals(0, provider.proofQueries); + } + } + + @Test + void nullCyclicProofAfterFoundContentIsInvalidNotUnavailable() { + CyclicFixture fixture = new CyclicFixture(); + CyclicEvidenceProvider provider = + new CyclicEvidenceProvider( + NodeProviderResult.found( + Collections.singletonList( + fixture.resolvedMember)), + null); + + try (Blue blue = new Blue(provider)) { + BexValue member = exactReference( + blue, fixture.memberBlueId); + + assertEquals(fixture.memberBlueId, + member.exactBlueId()); + InvalidExecutionEvidenceException failure = + assertThrows( + InvalidExecutionEvidenceException.class, + member::isObject); + + assertTrue(failure.getMessage().contains( + "complete cyclic-set proof")); + assertEquals(1, provider.proofQueries); + } + } + + @Test + void malformedCyclicProofIsDeterministicInvalidEvidence() { + CyclicFixture fixture = new CyclicFixture(); + Node wrongMember = obj( + "label", "wrong-cycle", + "next", new Node().blueId("this#0")) + .name("wrong-cyclic-member"); + CyclicSetProof wrongProof = + CyclicSetProof.fromDeclaredPlaceholderSet( + Collections.singletonList(wrongMember)); + CyclicEvidenceProvider provider = + new CyclicEvidenceProvider( + NodeProviderResult.found( + Collections.singletonList( + fixture.resolvedMember)), + wrongProof); + + try (Blue blue = new Blue(provider)) { + BexValue member = exactReference( + blue, fixture.memberBlueId); + + assertEquals(fixture.memberBlueId, + member.exactBlueId()); + InvalidExecutionEvidenceException failure = + assertThrows( + InvalidExecutionEvidenceException.class, + member::isObject); + + assertTrue(failure.getMessage().contains( + fixture.memberBlueId)); + assertEquals(1, provider.proofQueries); + } + } + + private static final class VerifiedCyclicProvider + implements NodeProvider, CyclicAwareNodeProvider { + private final String memberBlueId; + private final Node resolvedMember; + private final CyclicSetProof proof; + private int proofQueries; + + private VerifiedCyclicProvider( + String memberBlueId, + Node resolvedMember, + java.util.List placeholders) { + this.memberBlueId = memberBlueId; + this.resolvedMember = resolvedMember.clone(); + this.proof = CyclicSetProof + .fromDeclaredPlaceholderSet(placeholders); + } + + @Override + public java.util.List fetchByBlueId( + String requestedBlueId) { + return memberBlueId.equals(requestedBlueId) + ? Collections.singletonList( + resolvedMember.clone()) + : Collections.emptyList(); + } + + @Override + public CyclicSetProof cyclicSetProofFor( + String requestedBlueId) { + proofQueries++; + return memberBlueId.equals(requestedBlueId) + ? proof + : null; + } + } + + private static final class CyclicFixture { + private final Node placeholder; + private final String memberBlueId; + private final Node resolvedMember; + + private CyclicFixture() { + placeholder = obj( + "label", "verified-cycle", + "next", new Node().blueId("this#0")) + .name("cyclic-member"); + java.util.List placeholders = + Collections.singletonList(placeholder); + memberBlueId = CircularBlueIdCalculator + .calculateCircularSetBlueIds(placeholders) + .get(0); + resolvedMember = placeholder.clone(); + resolvedMember.getProperties().get("next") + .blueId(memberBlueId); + } + } + + private static final class CyclicEvidenceProvider + implements NodeProvider, CyclicAwareNodeProvider { + private final NodeProviderResult result; + private final CyclicSetProof proof; + private int proofQueries; + + private CyclicEvidenceProvider( + NodeProviderResult result, + CyclicSetProof proof) { + this.result = result; + this.proof = proof; + } + + @Override + public java.util.List fetchByBlueId( + String requestedBlueId) { + NodeProviderResult current = + fetchResultByBlueId(requestedBlueId); + return current.outcome() == NodeProviderOutcome.FOUND + ? current.nodes() + : Collections.emptyList(); + } + + @Override + public NodeProviderResult fetchResultByBlueId( + String requestedBlueId) { + return result; + } + + @Override + public CyclicSetProof cyclicSetProofFor( + String requestedBlueId) { + proofQueries++; + return proof; + } + } + + private static String calculateBlueId(Node node) { + try (Blue blue = new Blue()) { + return blue.calculateBlueId(node); + } + } + + private static BexValue exactReference( + Blue blue, String blueId) { + return BexValues.referenceBacked( + BexValues.frozen(FrozenNode.fromNode( + new Node().blueId(blueId))), + blue); + } + + private static String messageChain(Throwable failure) { + StringBuilder messages = new StringBuilder(); + Throwable current = failure; + while (current != null) { + messages.append(current.getMessage()).append('\n'); + current = current.getCause(); + } + return messages.toString(); + } +} diff --git a/src/test/java/blue/bex/BexExecutionEvidenceLedgerTest.java b/src/test/java/blue/bex/BexExecutionEvidenceLedgerTest.java new file mode 100644 index 0000000..cb66a8b --- /dev/null +++ b/src/test/java/blue/bex/BexExecutionEvidenceLedgerTest.java @@ -0,0 +1,210 @@ +package blue.bex; + +import blue.bex.api.BexEngine; +import blue.bex.api.BexExecutionContext; +import blue.bex.api.BexGasLedgerHost; +import blue.bex.api.BexProgramSource; +import blue.bex.api.FrozenBexDocumentView; +import blue.bex.output.BexSemanticIdentityBoundary; +import blue.language.Blue; +import blue.language.NodeProvider; +import blue.language.model.Node; +import blue.language.processor.ExecutionEvidenceUnavailableException; +import blue.language.processor.GasMeter; +import blue.language.processor.GasSchedule; +import blue.language.processor.InvalidExecutionEvidenceException; +import blue.language.provider.CyclicAwareNodeProvider; +import blue.language.provider.CyclicSetProof; +import blue.language.snapshot.FrozenNode; +import blue.language.snapshot.ResolvedSnapshot; +import blue.language.utils.CircularBlueIdCalculator; +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static blue.bex.test.BexTestFixtures.obj; +import static blue.bex.test.BexTestFixtures.op; +import static blue.bex.test.BexTestFixtures.stepExpr; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class BexExecutionEvidenceLedgerTest { + @Test + void transientProviderUnavailabilityCommitsNoChildLedger() { + Node content = obj("a", 1); + String blueId = calculateBlueId(content); + NodeProvider unavailable = + ignored -> Collections.emptyList(); + + try (Blue blue = new Blue(unavailable)) { + RecordingGasHost host = new RecordingGasHost(); + assertThrows( + ExecutionEvidenceUnavailableException.class, + () -> executeKindRead(blue, blueId, host)); + + assertEquals(1, host.openCount); + assertEquals(0, host.mergeCount); + assertEquals(0L, host.parent.totalGas()); + } + } + + @Test + void deterministicInvalidEvidenceCommitsAdmittedTraceOnce() { + Node content = obj("a", 1); + String blueId = calculateBlueId(content); + NodeProvider invalid = ignored -> { + throw new InvalidExecutionEvidenceException( + "deterministic invalid provider evidence"); + }; + + try (Blue blue = new Blue(invalid)) { + RecordingGasHost host = new RecordingGasHost(); + assertThrows( + InvalidExecutionEvidenceException.class, + () -> executeKindRead(blue, blueId, host)); + + assertEquals(1, host.openCount); + assertEquals(1, host.mergeCount); + assertTrue(host.parent.totalGas() > 0L); + } + } + + @Test + void hostedCyclicStructuralReadWithMissingProofIsDeterministic() { + Node placeholder = obj( + "label", "cyclic-content", + "next", new Node().blueId("this#0")) + .name("hosted-cyclic-member"); + List placeholders = + Collections.singletonList(placeholder); + String memberBlueId = + CircularBlueIdCalculator + .calculateCircularSetBlueIds( + placeholders) + .get(0); + Node resolvedMember = placeholder.clone(); + resolvedMember.getProperties().get("next") + .blueId(memberBlueId); + ProoflessCyclicProvider provider = + new ProoflessCyclicProvider( + memberBlueId, resolvedMember); + + try (Blue blue = new Blue(provider)) { + RecordingGasHost host = new RecordingGasHost(); + InvalidExecutionEvidenceException failure = + assertThrows( + InvalidExecutionEvidenceException.class, + () -> executeKindRead( + blue, memberBlueId, host)); + + assertTrue(failure.getMessage().contains( + "cyclic-set proof")); + assertEquals(1, provider.proofQueries); + assertEquals(1, host.openCount); + assertEquals(1, host.mergeCount); + assertTrue(host.parent.totalGas() > 0L); + } + } + + private static void executeKindRead( + Blue blue, + String blueId, + RecordingGasHost host) { + ResolvedSnapshot document = blue.resolveToSnapshot( + obj("x", new Node().blueId(blueId))); + BexExecutionContext context = BexExecutionContext.builder() + .document(new FrozenBexDocumentView( + document.frozenCanonicalRoot(), + document.frozenResolvedRoot(), + "/")) + .gasLedgerHost(host) + .semanticIdentityBoundary( + BexSemanticIdentityBoundary.STANDALONE) + .build(); + BexEngine.builder() + .blue(blue) + .build() + .compileAndExecute( + BexProgramSource.inline(FrozenNode.fromResolvedNode( + stepExpr(op( + "$kind", + op("$document", "/x"))))), + context); + } + + private static String calculateBlueId(Node node) { + try (Blue blue = new Blue()) { + return blue.calculateBlueId(node); + } + } + + private static final class ProoflessCyclicProvider + implements NodeProvider, CyclicAwareNodeProvider { + private final String memberBlueId; + private final Node resolvedMember; + private int proofQueries; + + private ProoflessCyclicProvider( + String memberBlueId, + Node resolvedMember) { + this.memberBlueId = memberBlueId; + this.resolvedMember = resolvedMember.clone(); + } + + @Override + public List fetchByBlueId( + String requestedBlueId) { + return memberBlueId.equals(requestedBlueId) + ? Collections.singletonList( + resolvedMember.clone()) + : Collections.emptyList(); + } + + @Override + public CyclicSetProof cyclicSetProofFor( + String requestedBlueId) { + proofQueries++; + return null; + } + } + + private static final class RecordingGasHost + implements BexGasLedgerHost { + private final GasMeter parent = + new GasMeter(GasSchedule.contracts10(), 100_000L); + private GasMeter.ChildGasLedger child; + private int openCount; + private int mergeCount; + + @Override + public GasMeter.ChildGasLedger open( + String namespace, + Map counterWeights) { + openCount++; + child = parent.childLedger(namespace, counterWeights); + return child; + } + + @Override + public void submit(GasMeter.ChildGasLedger ledger) { + mergeCount++; + assertEquals(child, ledger); + parent.merge(ledger); + } + + @Override + public void failedDeterministically( + GasMeter.ChildGasLedger ledger) { + submit(ledger); + } + + @Override + public void evidenceUnavailable( + GasMeter.ChildGasLedger ledger) { + assertEquals(child, ledger); + } + } +} diff --git a/src/test/java/blue/bex/BexFocusedConformanceTests.java b/src/test/java/blue/bex/BexFocusedConformanceTests.java index 9497809..095c09f 100644 --- a/src/test/java/blue/bex/BexFocusedConformanceTests.java +++ b/src/test/java/blue/bex/BexFocusedConformanceTests.java @@ -87,7 +87,7 @@ void appendChangesPreservesDuplicatePathOrder() { obj("op", "replace", "path", "/status", "val", "first"), obj("op", "replace", "path", "/status", "val", "second") )), - emptyStatement() + noOpStatement() )), defaultContext()); assertEquals("first", simple(result.changeset().entries().get(0).val())); @@ -95,10 +95,10 @@ void appendChangesPreservesDuplicatePathOrder() { } @Test - void emptyPlaceholderStatementReturnsDefaultResult() { + void falseReturnIfNoOpReturnsDefaultResult() { BexExecutionResult result = runStep(stepDo(list( op("$appendEvent", obj("kind", "Calculated")), - emptyStatement() + noOpStatement() )), defaultContext()); assertEquals(l(m("kind", "Calculated")), simple(result.events().asValue())); @@ -369,7 +369,7 @@ class BexGasTest { void gasIsDeterministicAndExhaustionFailsClosed() { assertEquals(runExpr(op("$add", list(1, 2))).gasUsed(), runExpr(op("$add", list(1, 2))).gasUsed()); - BexEngine engine = BexEngine.builder().gasSchedule(BexGasSchedule.builder().expressionBase(100).build()).build(); + BexEngine engine = BexEngine.builder().gasSchedule(BexGasSchedule.builder().expressionEvaluated(100).build()).build(); BexExecutionContext context = BexExecutionContext.builder().document(defaultDocumentView()).gasLimit(1).build(); assertThrows(BexException.class, () -> engine.compileAndExecute(BexProgramSource.inline(frozen(stepExpr(op("$add", list(1, 2))))), context)); } diff --git a/src/test/java/blue/bex/BexFrozenValueTest.java b/src/test/java/blue/bex/BexFrozenValueTest.java index 7ab9eb8..038d25f 100644 --- a/src/test/java/blue/bex/BexFrozenValueTest.java +++ b/src/test/java/blue/bex/BexFrozenValueTest.java @@ -1,7 +1,5 @@ package blue.bex; -import blue.bex.result.BexMetrics; -import blue.bex.value.BexFrozenNodeFactory; import blue.bex.value.BexFrozenWriter; import blue.bex.value.BexValue; import blue.bex.value.BexValues; @@ -9,7 +7,6 @@ import org.junit.jupiter.api.Test; import java.util.Arrays; -import java.util.List; import java.util.Map; import static blue.bex.test.BexTestFixtures.*; @@ -17,71 +14,23 @@ class BexFrozenValueTest { @Test - void frozenValueReturnsSameFrozenNodeWithoutFactoryRoundtrip() { + void frozenValueReturnsSameCanonicalFrozenNode() { FrozenNode frozen = frozen(obj("a", 1)); - RecordingFactory factory = new RecordingFactory(); - FrozenNode out = new BexFrozenWriter(factory, new BexMetrics()).toFrozenValue(BexValues.frozen(frozen)); + FrozenNode out = BexFrozenWriter.toFrozen(BexValues.frozen(frozen)); assertSame(frozen, out); - assertEquals(0, factory.calls); } @Test - void scalarListAndObjectUseFactoryAndMetrics() { - BexMetrics metrics = new BexMetrics(); - RecordingFactory factory = new RecordingFactory(); + void transientTreeUsesStrictBlueOutputConversion() { BexValue value = BexValues.map((Map) (Map) m( "text", BexValues.scalar("x"), "list", BexValues.list(Arrays.asList(BexValues.scalar("a"))) )); - FrozenNode frozen = new BexFrozenWriter(factory, metrics).toFrozenValue(value); + FrozenNode frozen = BexFrozenWriter.toFrozen(value); assertEquals(m("list", l("a"), "text", "x"), simple(BexValues.frozen(frozen))); - assertEquals(1, metrics.frozenOutputConversions()); - assertTrue(factory.scalarCalls >= 2); - assertEquals(1, factory.listCalls); - assertEquals(1, factory.objectCalls); - } - - static final class RecordingFactory implements BexFrozenNodeFactory { - int calls; - int scalarCalls; - int listCalls; - int objectCalls; - - @Override - public FrozenNode empty(BexMetrics metrics) { - calls++; - return FrozenNode.empty(); - } - - @Override - public FrozenNode scalar(Object value, BexMetrics metrics) { - calls++; - scalarCalls++; - return frozen(v(value)); - } - - @Override - public FrozenNode list(List items, BexMetrics metrics) { - calls++; - listCalls++; - return frozen(new blue.language.model.Node().items(new java.util.ArrayList() {{ - for (FrozenNode item : items) add(item.toNode()); - }})); - } - - @Override - public FrozenNode object(Map properties, BexMetrics metrics) { - calls++; - objectCalls++; - java.util.LinkedHashMap nodes = new java.util.LinkedHashMap<>(); - for (Map.Entry entry : properties.entrySet()) { - nodes.put(entry.getKey(), entry.getValue().toNode()); - } - return frozen(new blue.language.model.Node().properties(nodes)); - } } } diff --git a/src/test/java/blue/bex/BexIntrinsicTest.java b/src/test/java/blue/bex/BexIntrinsicTest.java index 72eca21..9fa4ae0 100644 --- a/src/test/java/blue/bex/BexIntrinsicTest.java +++ b/src/test/java/blue/bex/BexIntrinsicTest.java @@ -5,6 +5,14 @@ import blue.bex.api.BexProgramSource; import blue.bex.compile.BexCompiledProgramCache; import blue.bex.compile.LruBexCompiledProgramCache; +import blue.bex.gas.BexGasCounter; +import blue.bex.gas.BexGasLimitExceededException; +import blue.bex.gas.BexGasMeter; +import blue.bex.gas.BexGasSchedule; +import blue.bex.output.BexAdmittedValue; +import blue.bex.output.BexEstablishedIdentity; +import blue.bex.output.BexOutputAdmission; +import blue.bex.output.BexSemanticIdentityBoundary; import blue.bex.result.BexExecutionResult; import blue.bex.value.BexValue; import blue.bex.value.BexValues; @@ -12,10 +20,13 @@ import blue.language.model.Node; import blue.language.model.TypeBlueId; import blue.language.snapshot.FrozenNode; +import blue.language.utils.BlueIdCalculator; import org.junit.jupiter.api.Test; import java.util.LinkedHashMap; +import java.util.Collections; import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; import static blue.bex.test.BexTestFixtures.bi; import static blue.bex.test.BexTestFixtures.defaultContext; @@ -28,6 +39,8 @@ import static blue.bex.test.BexTestFixtures.simple; import static blue.bex.test.BexTestFixtures.stepExpr; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -35,14 +48,23 @@ class BexIntrinsicTest { private static final Blue BLUE = new Blue(); private static final String ECHO_BLUE_ID = "TestIntrinsicEcho"; private static final String GAS_BLUE_ID = "TestIntrinsicGas"; + private static final String TEST_REGISTRY_IDENTITY = "test-intrinsics/1"; + private static final String CONSTANT_WORK = "constantWork"; + private static final Map CONSTANT_WORK_WEIGHTS = + Collections.singletonMap(CONSTANT_WORK, 1L); @Test void registeredIntrinsicReceivesBlueIdAndEvaluatedFields() { BexEngine engine = BexEngine.builder() - .intrinsic(ECHO_BLUE_ID, invocation -> { - invocation.chargeGas(7); + .intrinsic( + ECHO_BLUE_ID, + TEST_REGISTRY_IDENTITY, + Collections.singletonMap("echoWork", 1L), + invocation -> { + invocation.charge("echoWork", 7, "test-echo"); Map out = new LinkedHashMap<>(); - out.put("blueId", BexValues.scalar(invocation.blueId())); + out.put("intrinsicBlueId", + BexValues.scalar(invocation.blueId())); out.put("payload", invocation.field("payload")); out.put("missingIsUndefined", BexValues.scalar(invocation.field("missing").isUndefined())); out.put("fieldCount", BexValues.scalar(invocation.fields().size())); @@ -63,7 +85,7 @@ void registeredIntrinsicReceivesBlueIdAndEvaluatedFields() { " omitted:", " $document: /missing"), defaultContext()); - assertEquals(m("blueId", ECHO_BLUE_ID, + assertEquals(m("intrinsicBlueId", ECHO_BLUE_ID, "payload", "hello", "missingIsUndefined", true, "fieldCount", bi(1)), simple(result.value())); @@ -73,7 +95,15 @@ void registeredIntrinsicReceivesBlueIdAndEvaluatedFields() { @Test void intrinsicCanBeRegisteredByAnnotatedTypeClass() { BexEngine engine = BexEngine.builder() - .intrinsic(AnnotatedEchoIntrinsic.class, invocation -> invocation.field("payload")) + .intrinsic( + AnnotatedEchoIntrinsic.class, + TEST_REGISTRY_IDENTITY, + CONSTANT_WORK_WEIGHTS, + invocation -> { + invocation.charge( + CONSTANT_WORK, 1L, "annotated-echo"); + return invocation.field("payload"); + }) .build(); BexExecutionResult result = engine.compileAndExecute(source( @@ -90,11 +120,295 @@ void intrinsicCanBeRegisteredByAnnotatedTypeClass() { @Test void intrinsicTypeClassRegistrationRequiresTypeBlueId() { IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> BexEngine.builder() - .intrinsic(UnannotatedIntrinsic.class, invocation -> BexValues.scalar(true))); + .intrinsic( + UnannotatedIntrinsic.class, + TEST_REGISTRY_IDENTITY, + CONSTANT_WORK_WEIGHTS, + invocation -> BexValues.scalar(true))); assertTrue(ex.getMessage().contains("@TypeBlueId")); } + @Test + void duplicateIntrinsicBlueIdIsRejectedInsteadOfReplacingRegistration() { + BexEngine.Builder builder = BexEngine.builder() + .intrinsic( + ECHO_BLUE_ID, + "first-registry", + Collections.singletonMap("first", 1L), + invocation -> BexValues.scalar("first")); + + IllegalArgumentException failure = assertThrows( + IllegalArgumentException.class, + () -> builder.intrinsic( + ECHO_BLUE_ID, + "second-registry", + Collections.singletonMap("second", 1L), + invocation -> BexValues.scalar("second"))); + + assertTrue(failure.getMessage().contains(ECHO_BLUE_ID)); + assertTrue(failure.getMessage().contains("already registered")); + } + + @Test + void intrinsicCatalogsRejectEmptyAndNonPositiveWeightsAtEveryBoundary() { + assertThrows( + IllegalArgumentException.class, + () -> blue.bex.api.BexIntrinsicRegistry.builder() + .register( + ECHO_BLUE_ID, + TEST_REGISTRY_IDENTITY, + Collections.emptyMap(), + invocation -> BexValues.scalar(true))); + assertThrows( + IllegalArgumentException.class, + () -> blue.bex.api.BexIntrinsicRegistry.builder() + .register( + ECHO_BLUE_ID, + TEST_REGISTRY_IDENTITY, + Collections.singletonMap("work", 0L), + invocation -> BexValues.scalar(true))); + assertThrows( + IllegalArgumentException.class, + () -> blue.bex.api.BexIntrinsicRegistry.builder() + .register( + ECHO_BLUE_ID, + TEST_REGISTRY_IDENTITY, + Collections.singletonMap("work", -1L), + invocation -> BexValues.scalar(true))); + + String qualified = BexGasMeter.qualifiedCounterName( + "intrinsic-" + ECHO_BLUE_ID, "work"); + assertThrows( + IllegalArgumentException.class, + () -> new BexGasMeter( + BexGasSchedule.defaults(), + 100L, + BexGasMeter.NO_LOCAL_LIMIT, + Collections.singletonMap(qualified, 0L))); + assertThrows( + IllegalArgumentException.class, + () -> new BexGasMeter( + BexGasSchedule.defaults(), + 100L, + BexGasMeter.NO_LOCAL_LIMIT, + Collections.singletonMap(qualified, -1L))); + } + + @Test + void constantIntrinsicChargesItsDeclaredNamedCounterBeforeWork() { + AtomicInteger completedWork = new AtomicInteger(); + BexEngine engine = BexEngine.builder() + .intrinsic( + ECHO_BLUE_ID, + TEST_REGISTRY_IDENTITY, + CONSTANT_WORK_WEIGHTS, + invocation -> { + invocation.charge( + CONSTANT_WORK, + 1L, + "constant-intrinsic-work"); + completedWork.incrementAndGet(); + return BexValues.scalar(true); + }) + .build(); + + BexExecutionResult result = engine.compileAndExecute( + source( + "type: Blue/BEX Program", + "expr:", + " $intrinsic:", + " type:", + " blueId: TestIntrinsicEcho"), + defaultContext()); + + assertEquals(1, completedWork.get()); + assertEquals( + 1L, + result.gasLedger().quantity( + "intrinsic-" + ECHO_BLUE_ID, + CONSTANT_WORK)); + assertEquals( + 1L, + result.gasLedger().quantity( + BexGasCounter.INTRINSIC_CALLED)); + } + + @Test + void registryIdentityEncodingAndRuntimeNamespacesAreUnambiguous() { + blue.bex.api.BexIntrinsicRegistry first = + blue.bex.api.BexIntrinsicRegistry.builder() + .register( + "type", + "r:n", + "s", + Collections.singletonMap("work", 1L), + invocation -> BexValues.scalar(true)) + .build(); + blue.bex.api.BexIntrinsicRegistry second = + blue.bex.api.BexIntrinsicRegistry.builder() + .register( + "type", + "r", + "n:s", + Collections.singletonMap("work", 1L), + invocation -> BexValues.scalar(true)) + .build(); + + assertNotEquals(first.identity(), second.identity()); + assertThrows( + IllegalArgumentException.class, + () -> blue.bex.api.BexIntrinsicRegistry.builder() + .register( + "type", + "registry", + "nested/namespace", + Collections.singletonMap("work", 1L), + invocation -> BexValues.scalar(true))); + } + + @Test + void intrinsicCounterCatalogAndInvocationFieldsAreImmutableSnapshots() { + Map weights = new LinkedHashMap<>(); + weights.put("work", 3L); + AtomicInteger invocations = new AtomicInteger(); + BexEngine engine = BexEngine.builder() + .intrinsic( + ECHO_BLUE_ID, + TEST_REGISTRY_IDENTITY, + weights, + invocation -> { + invocation.charge( + "work", 1L, "immutable-snapshot-work"); + invocations.incrementAndGet(); + assertEquals( + Collections.singletonMap("work", 3L), + invocation.namedCounterWeights()); + assertThrows( + UnsupportedOperationException.class, + () -> invocation.namedCounterWeights() + .put("late", 1L)); + assertThrows( + UnsupportedOperationException.class, + () -> invocation.fields() + .put("late", BexValues.scalar(true))); + return invocation.field("payload"); + }) + .build(); + weights.clear(); + weights.put("mutated", 99L); + + BexExecutionResult result = engine.compileAndExecute(source( + "type: Blue/BEX Program", + "expr:", + " $intrinsic:", + " type:", + " blueId: TestIntrinsicEcho", + " payload: stable"), defaultContext()); + + assertEquals("stable", simple(result.value())); + assertEquals(1, invocations.get()); + } + + @Test + void intrinsicExactFieldUsesSharedSemanticAdmissionAndMemoization() { + AtomicInteger boundaryCalls = new AtomicInteger(); + BexSemanticIdentityBoundary boundary = node -> { + boundaryCalls.incrementAndGet(); + Node exact = node.clone(); + return new BexEstablishedIdentity( + BlueIdCalculator.calculateBlueId(exact), + FrozenNode.fromResolvedNode(exact)); + }; + BexValue[] exactFromIntrinsic = new BexValue[1]; + BexEngine engine = BexEngine.builder() + .intrinsic( + ECHO_BLUE_ID, + TEST_REGISTRY_IDENTITY, + CONSTANT_WORK_WEIGHTS, + invocation -> { + BexAdmittedValue first = + invocation.exactField("payload"); + BexAdmittedValue repeated = + invocation.exactField("payload"); + assertSame(first, repeated); + assertTrue(first.value().isExact()); + exactFromIntrinsic[0] = first.value(); + return first.value(); + }) + .build(); + BexExecutionContext context = BexExecutionContext.builder() + .document(defaultDocumentView()) + .semanticIdentityBoundary(boundary) + .gasLimit(1_000_000L) + .build(); + + BexExecutionResult result = engine.compileAndExecute(source( + "type: Blue/BEX Program", + "expr:", + " $intrinsic:", + " type:", + " blueId: TestIntrinsicEcho", + " payload:", + " message: admitted"), context); + + assertEquals(1, boundaryCalls.get()); + assertSame(exactFromIntrinsic[0], result.value()); + assertTrue(result.value().isExact()); + assertEquals( + result.output().nodeBlueId(), + result.value().exactBlueId()); + assertEquals( + m("message", "admitted"), + simple(result.value())); + } + + @Test + void rejectedNamedChargePreventsAllLaterIntrinsicWork() { + Map weights = + Collections.singletonMap("work", 7L); + AtomicInteger workAfterCharge = new AtomicInteger(); + blue.bex.api.BexIntrinsicRegistry registry = + blue.bex.api.BexIntrinsicRegistry.builder() + .register( + GAS_BLUE_ID, + TEST_REGISTRY_IDENTITY, + weights, + invocation -> { + invocation.charge( + "work", 2L, "exact-exhaustion"); + workAfterCharge.incrementAndGet(); + return BexValues.scalar(true); + }) + .build(); + BexGasMeter gas = new BexGasMeter( + BexGasSchedule.defaults(), + 13L, + BexGasMeter.NO_LOCAL_LIMIT, + registry.registeredNamedWeights()); + BexOutputAdmission admission = new BexOutputAdmission( + gas, BexSemanticIdentityBoundary.STANDALONE); + + BexGasLimitExceededException failure = assertThrows( + BexGasLimitExceededException.class, + () -> registry.invoke( + GAS_BLUE_ID, + BexValues.map(Collections.emptyMap()), + Collections.emptyMap(), + gas, + admission)); + + assertEquals("intrinsic-" + GAS_BLUE_ID, failure.namespace()); + assertEquals("work", failure.counterName()); + assertEquals(2L, failure.quantity()); + assertEquals(7L, failure.weight()); + assertEquals(0L, failure.admittedGas()); + assertEquals(13L, failure.effectiveBudget()); + assertEquals(0, workAfterCharge.get()); + assertEquals(0L, gas.totalGas()); + assertTrue(gas.trace().isEmpty()); + } + @Test void intrinsicTypeCanBeInlineBlueTypeDefinition() { Node typeNode = BLUE.yamlToNode(yaml( @@ -103,9 +417,16 @@ void intrinsicTypeCanBeInlineBlueTypeDefinition() { " type: Text")); String inlineBlueId = FrozenNode.fromResolvedNode(typeNode).blueId(); BexEngine engine = BexEngine.builder() - .intrinsic(inlineBlueId, invocation -> { + .intrinsic( + inlineBlueId, + TEST_REGISTRY_IDENTITY, + CONSTANT_WORK_WEIGHTS, + invocation -> { + invocation.charge( + CONSTANT_WORK, 1L, "inline-type-work"); Map out = new LinkedHashMap<>(); - out.put("blueId", BexValues.scalar(invocation.blueId())); + out.put("intrinsicBlueId", + BexValues.scalar(invocation.blueId())); out.put("x", invocation.field("x")); return BexValues.map(out); }) @@ -122,7 +443,9 @@ void intrinsicTypeCanBeInlineBlueTypeDefinition() { " x:", " $literal: ok"), defaultContext()); - assertEquals(m("blueId", inlineBlueId, "x", "ok"), simple(result.value())); + assertEquals( + m("intrinsicBlueId", inlineBlueId, "x", "ok"), + simple(result.value())); } @Test @@ -148,7 +471,15 @@ void cachedProgramStillRequiresSupportInCurrentEngine() { " blueId: TestIntrinsicEcho"); BexEngine withSupport = BexEngine.builder() .cache(cache) - .intrinsic(ECHO_BLUE_ID, invocation -> BexValues.scalar(true)) + .intrinsic( + ECHO_BLUE_ID, + TEST_REGISTRY_IDENTITY, + CONSTANT_WORK_WEIGHTS, + invocation -> { + invocation.charge( + CONSTANT_WORK, 1L, "cached-program-work"); + return BexValues.scalar(true); + }) .build(); withSupport.compile(source); @@ -162,8 +493,12 @@ void cachedProgramStillRequiresSupportInCurrentEngine() { @Test void intrinsicProcessorGasChargeIsEnforced() { BexEngine engine = BexEngine.builder() - .intrinsic(GAS_BLUE_ID, invocation -> { - invocation.chargeGas(25); + .intrinsic( + GAS_BLUE_ID, + TEST_REGISTRY_IDENTITY, + Collections.singletonMap("gasWork", 1L), + invocation -> { + invocation.charge("gasWork", 25, "test-limit"); return BexValues.scalar(true); }) .build(); @@ -185,7 +520,15 @@ void intrinsicProcessorGasChargeIsEnforced() { @Test void intrinsicTypeMustBeStatic() { BexEngine engine = BexEngine.builder() - .intrinsic(ECHO_BLUE_ID, invocation -> BexValues.scalar(true)) + .intrinsic( + ECHO_BLUE_ID, + TEST_REGISTRY_IDENTITY, + CONSTANT_WORK_WEIGHTS, + invocation -> { + invocation.charge( + CONSTANT_WORK, 1L, "static-type-work"); + return BexValues.scalar(true); + }) .build(); Node program = stepExpr(op("$intrinsic", obj( "type", obj("blueId", op("$concat", list("TestIntrinsic", "Echo")))))); @@ -193,7 +536,7 @@ void intrinsicTypeMustBeStatic() { BexException ex = assertThrows(BexException.class, () -> engine.compile(BexProgramSource.inline(frozen(program)))); - assertTrue(ex.getMessage().contains("BEX expressions inside static Blue patterns")); + assertTrue(ex.getMessage().contains("$intrinsic.type")); } private static BexProgramSource source(String... lines) { diff --git a/src/test/java/blue/bex/BexLazyBindingTest.java b/src/test/java/blue/bex/BexLazyBindingTest.java index d903770..8a5f008 100644 --- a/src/test/java/blue/bex/BexLazyBindingTest.java +++ b/src/test/java/blue/bex/BexLazyBindingTest.java @@ -769,7 +769,7 @@ void supplierFailureAddsNoGasBeyondTheExistingBindingRead() { new BexMetrics(), new BexPointerCache()); assertThrows(IllegalStateException.class, () -> runtime.readBinding("broken", Collections.emptyList())); - assertEquals(schedule.varRead, runtime.gas().used()); + assertEquals(schedule.bindingRead, runtime.gas().used()); } private static BexExecutionContext.Builder contextBuilder() { diff --git a/src/test/java/blue/bex/BexLocalBenchmarkTest.java b/src/test/java/blue/bex/BexLocalBenchmarkTest.java index 0b8096a..ed498b1 100644 --- a/src/test/java/blue/bex/BexLocalBenchmarkTest.java +++ b/src/test/java/blue/bex/BexLocalBenchmarkTest.java @@ -1,14 +1,14 @@ package blue.bex; import blue.bex.result.BexExecutionResult; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; import static blue.bex.test.BexTestFixtures.*; -@Disabled("local benchmark; prints counters and avoids CI timing thresholds") +/** + * Compile-only local benchmark corpus. The release benchmark gate compiles + * this class under Java 8 but deliberately does not execute timing. + */ class BexLocalBenchmarkTest { - @Test void largeStaticLiteralAndDocumentReads() { long startCompileAndExecute = System.nanoTime(); BexExecutionResult result = runStep(stepExpr(obj( diff --git a/src/test/java/blue/bex/BexNodeCursorTest.java b/src/test/java/blue/bex/BexNodeCursorTest.java index cdbb17f..45df24e 100644 --- a/src/test/java/blue/bex/BexNodeCursorTest.java +++ b/src/test/java/blue/bex/BexNodeCursorTest.java @@ -38,17 +38,4 @@ void nodeSnapshotIsStableAfterOriginalNodeMutation() { assertEquals("before", simple(runStep(stepExpr(op("$event", "/payload/status")), context).value())); } - @SuppressWarnings("deprecation") - @Test - void deprecatedNodeFactoryUsesSafeSnapshotSemantics() { - Node event = obj("payload", obj("status", "before")); - BexExecutionContext context = BexExecutionContext.builder() - .document(defaultDocumentView()) - .event(BexValues.node(event)) - .gasLimit(1_000_000) - .build(); - event.getProperties().put("payload", obj("status", "after")); - - assertEquals("before", simple(runStep(stepExpr(op("$event", "/payload/status")), context).value())); - } } diff --git a/src/test/java/blue/bex/BexPointerSet20Test.java b/src/test/java/blue/bex/BexPointerSet20Test.java new file mode 100644 index 0000000..cc1da8c --- /dev/null +++ b/src/test/java/blue/bex/BexPointerSet20Test.java @@ -0,0 +1,184 @@ +package blue.bex; + +import blue.bex.api.FrozenBexDocumentView; +import blue.bex.gas.BexGasCounter; +import blue.bex.result.BexExecutionResult; +import blue.bex.result.BexMetrics; +import blue.bex.result.BexPatchEntry; +import blue.bex.result.BexResultOverlay; +import blue.bex.value.BexValue; +import blue.bex.value.BexValues; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collections; + +import static blue.bex.test.BexTestFixtures.*; +import static org.junit.jupiter.api.Assertions.*; + +class BexPointerSet20Test { + @Test + void directListSetAndRemoveRemainDenseAcrossEveryValueView() { + BexValue base = BexValues.fromSimple(l("a", "b", "c")); + + BexValue set = BexValues.pointerSet( + base, Collections.singletonList("1"), + BexValues.scalar("z"), "set"); + assertTrue(set.isList()); + assertEquals(3, set.size()); + assertEquals(Collections.emptyList(), set.keys()); + assertEquals("z", set.get("1").toSimple()); + assertEquals(l("a", "z", "c"), set.toSimple()); + + BexValue removed = BexValues.pointerSet( + base, Collections.singletonList("1"), + BexValues.undefined(), "remove"); + assertTrue(removed.isList()); + assertEquals(2, removed.size()); + assertEquals(Collections.emptyList(), removed.keys()); + assertEquals("c", removed.get("1").toSimple()); + assertTrue(removed.get("2").isUndefined()); + assertEquals(l("a", "c"), removed.toSimple()); + assertEquals(2, removed.toNode().getItems().size()); + } + + @Test + void directListWritesRejectNonIndexesAndOutOfRangeIndexesWithoutHoles() { + BexValue base = BexValues.fromSimple(l("a", "b")); + + assertThrows(BexException.class, () -> BexValues.pointerSet( + base, Collections.singletonList("2"), + BexValues.scalar("z"), "set")); + assertThrows(BexException.class, () -> BexValues.pointerSet( + base, Collections.singletonList("2"), + BexValues.undefined(), "remove")); + assertThrows(BexException.class, () -> BexValues.pointerSet( + base, Collections.singletonList("-1"), + BexValues.scalar("z"), "set")); + assertThrows(BexException.class, () -> BexValues.pointerSet( + base, Arrays.asList("2", "nested"), + BexValues.scalar("z"), "set")); + + assertThrows(BexException.class, () -> runExpr(op( + "$pointerSet", + obj("object", list("a", "b"), + "path", "/2", + "val", "z")))); + assertEquals( + m("parent", m("child", "z")), + BexValues.pointerSet( + BexValues.fromSimple(m("parent", null)), + Arrays.asList("parent", "child"), + BexValues.scalar("z"), + "set").toSimple()); + } + + @Test + void resultOverlayAloneKeepsNonShiftingSparseListRemoval() { + FrozenBexDocumentView document = new FrozenBexDocumentView( + frozen(obj("items", list("a", "b", "c")))); + BexResultOverlay overlay = + new BexResultOverlay(document, new BexMetrics()); + overlay.append(new BexPatchEntry( + "remove", + "/items/1", + "/items/1", + BexValues.undefined())); + + BexValue sparse = overlay.valueAt( + "/items", Collections.singletonList("items")); + assertTrue(sparse.isList()); + assertEquals(3, sparse.size()); + assertEquals(Collections.emptyList(), sparse.keys()); + assertEquals("a", sparse.get("0").toSimple()); + assertTrue(sparse.get("1").isUndefined()); + assertEquals("c", sparse.get("2").toSimple()); + assertThrows(BexException.class, sparse::toSimple); + assertThrows(BexException.class, sparse::toNode); + + overlay.append(new BexPatchEntry( + "replace", + "/items/1", + "/items/1", + BexValues.scalar("z"))); + assertEquals( + l("a", "z", "c"), + overlay.valueAt( + "/items", + Collections.singletonList("items")).toSimple()); + } + + @Test + void pointerSetChargesProducedContainerKindAlongTheActualPath() { + BexExecutionResult listSet = runExpr(op( + "$pointerSet", + obj("object", op("$document", "/list"), + "path", "/1", + "val", "z"))); + assertEquals( + 1L, + listSet.gasLedger().quantity( + BexGasCounter.TRANSIENT_LIST_ITEM_PRODUCED)); + assertEquals( + 0L, + listSet.gasLedger().quantity( + BexGasCounter.TRANSIENT_OBJECT_MEMBER_PRODUCED)); + + BexExecutionResult objectSet = runExpr(op( + "$pointerSet", + obj("object", op("$document", "/state"), + "path", "/ready", + "val", true))); + assertEquals( + 0L, + objectSet.gasLedger().quantity( + BexGasCounter.TRANSIENT_LIST_ITEM_PRODUCED)); + assertEquals( + 1L, + objectSet.gasLedger().quantity( + BexGasCounter.TRANSIENT_OBJECT_MEMBER_PRODUCED)); + + BexExecutionResult createdObjects = runExpr(op( + "$pointerSet", + obj("object", op("$document", "/"), + "path", "/missing/child", + "val", true))); + assertEquals( + 2L, + createdObjects.gasLedger().quantity( + BexGasCounter.TRANSIENT_OBJECT_MEMBER_PRODUCED)); + assertEquals( + 2L, + createdObjects.gasLedger().quantity( + BexGasCounter.POINTER_SEGMENT_WRITTEN)); + } + + @Test + void pointerSetRemovalDoesNotChargeAnOmittedTerminalMemberOrItem() { + BexExecutionResult listRemove = runExpr(op( + "$pointerSet", + obj("object", op("$document", "/list"), + "op", "remove", + "path", "/1"))); + assertEquals(l("a"), simple(listRemove.value())); + assertEquals( + 0L, + listRemove.gasLedger().quantity( + BexGasCounter.TRANSIENT_LIST_ITEM_PRODUCED)); + assertEquals( + 0L, + listRemove.gasLedger().quantity( + BexGasCounter.TRANSIENT_OBJECT_MEMBER_PRODUCED)); + + BexExecutionResult objectRemove = runExpr(op( + "$pointerSet", + obj("object", op("$document", "/state"), + "op", "remove", + "path", "/ready"))); + assertEquals(m(), simple(objectRemove.value())); + assertEquals( + 0L, + objectRemove.gasLedger().quantity( + BexGasCounter.TRANSIENT_OBJECT_MEMBER_PRODUCED)); + } +} diff --git a/src/test/java/blue/bex/BexPrimitiveExhaustionEvidenceTest.java b/src/test/java/blue/bex/BexPrimitiveExhaustionEvidenceTest.java new file mode 100644 index 0000000..eadb42e --- /dev/null +++ b/src/test/java/blue/bex/BexPrimitiveExhaustionEvidenceTest.java @@ -0,0 +1,73 @@ +package blue.bex; + +import blue.bex.gas.BexGasCounter; +import blue.bex.gas.BexGasLimitExceededException; +import blue.bex.gas.BexGasMeter; +import blue.bex.gas.BexGasSchedule; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; + +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Closed-vocabulary evidence that every primitive rejects before its work and + * leaves the rejected charge absent from the canonical prefix. + */ +class BexPrimitiveExhaustionEvidenceTest { + + @TestFactory + Stream everyPrimitiveHasExactRejectedChargeEvidence() { + BexGasSchedule schedule = BexGasSchedule.defaults(); + return Arrays.stream(BexGasCounter.values()) + .map(counter -> DynamicTest.dynamicTest( + counter.canonicalName(), + () -> assertRejectedPrimitive(schedule, counter))); + } + + private static void assertRejectedPrimitive( + BexGasSchedule schedule, + BexGasCounter rejectedCounter) { + long prefixBudget = + schedule.weight(BexGasCounter.EXPRESSION_EVALUATED); + BexGasMeter meter = + new BexGasMeter(schedule, prefixBudget); + meter.charge( + BexGasCounter.EXPRESSION_EVALUATED, + 1L, + "admitted-prefix"); + AtomicInteger workAfterCharge = new AtomicInteger(); + + BexGasLimitExceededException failure = assertThrows( + BexGasLimitExceededException.class, + () -> { + meter.charge( + rejectedCounter, + 1L, + "must-reject"); + workAfterCharge.incrementAndGet(); + }); + + assertEquals(BexGasCounter.NAMESPACE, failure.namespace()); + assertEquals(rejectedCounter, failure.counter()); + assertEquals( + rejectedCounter.canonicalName(), + failure.counterName()); + assertEquals(1L, failure.quantity()); + assertEquals( + schedule.weight(rejectedCounter), + failure.weight()); + assertEquals(prefixBudget, failure.admittedGas()); + assertEquals(prefixBudget, failure.effectiveBudget()); + assertEquals(0, workAfterCharge.get()); + assertEquals(prefixBudget, meter.totalGas()); + assertEquals(1, meter.trace().size()); + assertEquals( + BexGasCounter.EXPRESSION_EVALUATED, + meter.trace().get(0).counter()); + } +} diff --git a/src/test/java/blue/bex/BexRichFixtureTest.java b/src/test/java/blue/bex/BexRichFixtureTest.java index 76c8fec..a009596 100644 --- a/src/test/java/blue/bex/BexRichFixtureTest.java +++ b/src/test/java/blue/bex/BexRichFixtureTest.java @@ -6,6 +6,7 @@ import blue.bex.api.BexStepResults; import blue.bex.api.FrozenBexDocumentView; import blue.bex.compile.BexCompiledProgram; +import blue.bex.gas.BexGasCharge; import blue.bex.gas.BexGasSchedule; import blue.bex.result.BexExecutionResult; import blue.bex.value.BexFrozenWriter; @@ -15,6 +16,7 @@ import blue.language.Blue; import blue.language.model.Node; import blue.language.snapshot.FrozenNode; +import blue.language.snapshot.ResolvedSnapshot; import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.TestFactory; import org.yaml.snakeyaml.Yaml; @@ -116,7 +118,8 @@ private void runFixture(Path path) throws Exception { } BexExecutionResult result = engine.execute(compiled, context); - assertSuccessExpectations(result, expectation); + assertSuccessExpectations( + result, expectation, isLegacyGasFixture(path)); } private void assertParseOrOutputConversionError(Map fixture, Map expectation, Blue blue) { @@ -159,7 +162,10 @@ private void assertGasProperty(BexEngine engine, BexCompiledProgram compiled, Be assertTrue(large > tiny, "Expected large output gas " + large + " to be greater than tiny output gas " + tiny); } - private void assertSuccessExpectations(BexExecutionResult result, Map expectation) { + private void assertSuccessExpectations( + BexExecutionResult result, + Map expectation, + boolean legacyGasFixture) { if (expectation.containsKey("resultSimple")) { assertEquals(normalize(expectation.get("resultSimple")), normalize(result.value().toSimple())); } @@ -169,9 +175,38 @@ private void assertSuccessExpectations(BexExecutionResult result, Map trace = result.gasLedger().trace(); + for (int index = 0; index < trace.size(); index++) { + BexGasCharge charge = trace.get(index); + assertEquals(index, charge.sequence(), + "gas sequence mismatch"); + assertTrue(!"estimatedSize".equals(charge.counterName()), + "BEX 1.x recursive size gas leaked into the trace"); + total = Math.addExact(total, charge.gas()); + } + assertEquals(total, result.gasUsed(), + "named gas trace must derive the total"); + } + } + + private boolean isLegacyGasFixture(Path path) { + Path parent = path.getParent(); + return parent != null + && parent.getFileName() != null + && "gas".equals(parent.getFileName().toString()); } private BexExecutionContext context(Map fixture, Blue blue) { @@ -183,12 +218,23 @@ private BexExecutionContext context(Map fixture, Blue blue) { Node root = parseNodeSource(string(context.get("rootDocumentSource")), blue); Node event = parseNodeSource(string(context.get("eventSource")), blue); Node currentContract = parseNodeSource(string(context.get("currentContractSource")), blue); + ResolvedSnapshot rootSnapshot = blue.resolveToSnapshot(root); + ResolvedSnapshot eventSnapshot = blue.resolveToSnapshot(event); + ResolvedSnapshot contractSnapshot = + blue.resolveToSnapshot(currentContract); long gasLimit = context.containsKey("gasLimit") ? longValue(context.get("gasLimit")) : 1_000_000L; BexExecutionContext.Builder builder = BexExecutionContext.builder() - .document(new FrozenBexDocumentView(FrozenNode.fromResolvedNode(root), FrozenNode.fromResolvedNode(root), scope)) - .event(BexValues.nodeSnapshot(event)) - .currentContract(BexValues.nodeSnapshot(currentContract)) + .document(new FrozenBexDocumentView( + rootSnapshot.frozenCanonicalRoot(), + rootSnapshot.frozenResolvedRoot(), + scope)) + .event(BexValues.exact( + eventSnapshot.frozenCanonicalRoot(), + eventSnapshot.frozenResolvedRoot())) + .currentContract(BexValues.exact( + contractSnapshot.frozenCanonicalRoot(), + contractSnapshot.frozenResolvedRoot())) .steps(steps(context.get("stepsBinding"))) .gasLimit(gasLimit); for (Map.Entry entry : map(context.get("bindings")).entrySet()) { @@ -213,11 +259,11 @@ private BexGasSchedule gasSchedule(Map fixture) { for (Map.Entry entry : overrides.entrySet()) { long value = longValue(entry.getValue()); switch (entry.getKey()) { - case "expressionBase": - builder.expressionBase(value); + case "expressionEvaluated": + builder.expressionEvaluated(value); break; - case "statementBase": - builder.statementBase(value); + case "statementExecuted": + builder.statementExecuted(value); break; case "documentRead": builder.documentRead(value); @@ -231,32 +277,32 @@ private BexGasSchedule gasSchedule(Map fixture) { case "currentContractRead": builder.currentContractRead(value); break; - case "varRead": - builder.varRead(value); + case "variableRead": + builder.variableRead(value); break; case "resultValueRead": builder.resultValueRead(value); break; - case "pointerGetBase": - builder.pointerGetBase(value); + case "pointerSegmentRead": + builder.pointerSegmentRead(value); break; - case "pointerSetBase": - builder.pointerSetBase(value); + case "pointerSegmentWritten": + builder.pointerSegmentWritten(value); break; - case "objectSetBase": - builder.objectSetBase(value); + case "transientObjectMemberProduced": + builder.transientObjectMemberProduced(value); break; - case "appendChangeBase": - builder.appendChangeBase(value); + case "patchAppended": + builder.patchAppended(value); break; - case "appendEventBase": - builder.appendEventBase(value); + case "eventAppended": + builder.eventAppended(value); break; - case "forEachItem": - builder.forEachItem(value); + case "collectionItemVisited": + builder.collectionItemVisited(value); break; - case "functionCall": - builder.functionCall(value); + case "functionCalled": + builder.functionCalled(value); break; default: throw new IllegalArgumentException("Unsupported gasSchedule field: " + entry.getKey()); @@ -420,10 +466,10 @@ private void validateAllowedKeys(Map values, Set allowed } private Set gasScheduleFields() { - return set("expressionBase", "statementBase", "documentRead", "eventRead", "stepsRead", - "currentContractRead", "varRead", "resultValueRead", "pointerGetBase", - "pointerSetBase", "objectSetBase", "appendChangeBase", "appendEventBase", - "forEachItem", "functionCall"); + return set("expressionEvaluated", "statementExecuted", "documentRead", "eventRead", "stepsRead", + "currentContractRead", "variableRead", "resultValueRead", "pointerSegmentRead", + "pointerSegmentWritten", "transientObjectMemberProduced", "patchAppended", "eventAppended", + "collectionItemVisited", "functionCalled"); } private Set targetStatuses() { diff --git a/src/test/java/blue/bex/BexSchemaValueTest.java b/src/test/java/blue/bex/BexSchemaValueTest.java index 11b903b..f828d1e 100644 --- a/src/test/java/blue/bex/BexSchemaValueTest.java +++ b/src/test/java/blue/bex/BexSchemaValueTest.java @@ -1,13 +1,10 @@ package blue.bex; -import blue.bex.gas.BexSizeEstimator; -import blue.bex.result.BexMetrics; import blue.bex.value.BexNodeWriter; import blue.bex.value.BexValue; import blue.bex.value.BexValues; import blue.language.model.Node; import blue.language.model.Schema; -import blue.language.utils.NodeToMapListOrValue; import org.junit.jupiter.api.Test; import java.math.BigInteger; @@ -19,12 +16,12 @@ class BexSchemaValueTest { @Test - void frozenSchemaIsExposedAsFiniteObjectWithDeterministicSize() { + void frozenSchemaIsExposedAsFiniteObject() { assertFiniteSchemaValue(BexValues.nodeSnapshot(schemaBearingObject())); } @Test - void trustedCursorSchemaIsExposedAsFiniteObjectWithDeterministicSize() { + void trustedCursorSchemaIsExposedAsFiniteObject() { assertFiniteSchemaValue(BexValues.nodeCursorTrustedImmutable(schemaBearingObject())); } @@ -58,7 +55,8 @@ void allSchemaKeywordsRoundTripThroughBexObjectForm() { assertEquals("active", schema.get("enum").get("1").get("value").asText()); Node roundTripped = BexNodeWriter.toNode(BexValues.fromSimple(sourceValue.toSimple())); - assertEquals(NodeToMapListOrValue.get(source), NodeToMapListOrValue.get(roundTripped)); + assertEquals(sourceValue.toSimple(), + BexValues.nodeSnapshot(roundTripped).toSimple()); } private static void assertFiniteSchemaValue(BexValue value) { @@ -67,12 +65,6 @@ private static void assertFiniteSchemaValue(BexValue value) { assertTrue(schema.get("required").asBoolean()); assertTrue(schema.get("schema").isUndefined()); - BexMetrics metrics = new BexMetrics(); - BexSizeEstimator estimator = new BexSizeEstimator(metrics); - assertEquals(29L, estimator.estimate(value)); - assertEquals(29L, estimator.estimate(value)); - assertTrue(metrics.sizeEstimateCacheHits() > 0L); - assertEquals(m("payload", "x", "schema", m("required", true)), value.toSimple()); assertTrue(value.isObject()); } diff --git a/src/test/java/blue/bex/BexStructuredReferenceEvidenceTest.java b/src/test/java/blue/bex/BexStructuredReferenceEvidenceTest.java new file mode 100644 index 0000000..1d764de --- /dev/null +++ b/src/test/java/blue/bex/BexStructuredReferenceEvidenceTest.java @@ -0,0 +1,212 @@ +package blue.bex; + +import blue.bex.value.BexValue; +import blue.bex.value.BexValues; +import blue.language.Blue; +import blue.language.NodeProvider; +import blue.language.model.Node; +import blue.language.processor.ExecutionEvidenceUnavailableException; +import blue.language.processor.InvalidExecutionEvidenceException; +import blue.language.provider.NodeProviderOutcome; +import blue.language.provider.NodeProviderResult; +import blue.language.snapshot.FrozenNode; +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import static blue.bex.test.BexTestFixtures.obj; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class BexStructuredReferenceEvidenceTest { + + @Test + void providerNotFoundIsIncompleteExecutionEvidenceNotSemanticAbsence() { + Node content = obj("value", "known-shape"); + String blueId = calculateBlueId(content); + MutableProvider provider = new MutableProvider( + NodeProviderResult.notFound()); + + try (Blue blue = new Blue(provider)) { + ExecutionEvidenceUnavailableException failure = assertThrows( + ExecutionEvidenceUnavailableException.class, + () -> exactReference(blue, blueId).isObject()); + + assertEquals( + Collections.singletonList(blueId), + failure.requiredExactBlueIds()); + assertTrue(failure.getMessage().contains(blueId)); + assertEquals( + NodeProviderOutcome.NOT_FOUND, + provider.current().outcome()); + } + } + + @Test + void providerUnavailableRetainsItsDiagnosticAndRequiredIdentity() { + Node content = obj("value", "known-shape"); + String blueId = calculateBlueId(content); + MutableProvider provider = new MutableProvider( + NodeProviderResult.unavailable("feeder is offline")); + + try (Blue blue = new Blue(provider)) { + ExecutionEvidenceUnavailableException failure = assertThrows( + ExecutionEvidenceUnavailableException.class, + () -> exactReference(blue, blueId).keys()); + + assertEquals( + Collections.singletonList(blueId), + failure.requiredExactBlueIds()); + assertEquals("feeder is offline", failure.getMessage()); + } + } + + @Test + void invalidProviderEvidenceIsASeparateDeterministicFailure() { + Node content = obj("value", "known-shape"); + String blueId = calculateBlueId(content); + MutableProvider provider = new MutableProvider( + NodeProviderResult.invalidEvidence( + "signature does not match")); + + try (Blue blue = new Blue(provider)) { + InvalidExecutionEvidenceException failure = assertThrows( + InvalidExecutionEvidenceException.class, + () -> exactReference(blue, blueId).get("value")); + + assertEquals( + "signature does not match", + failure.getMessage()); + } + } + + @Test + void foundContentWithMismatchedIdentityIsInvalidEvidence() { + Node expected = obj("value", "expected"); + String requestedBlueId = calculateBlueId(expected); + MutableProvider provider = new MutableProvider( + NodeProviderResult.found( + Collections.singletonList( + obj("value", "different")))); + + try (Blue blue = new Blue(provider)) { + InvalidExecutionEvidenceException failure = assertThrows( + InvalidExecutionEvidenceException.class, + () -> exactReference( + blue, requestedBlueId).isObject()); + + assertTrue(failure.getMessage().contains( + requestedBlueId)); + assertEquals( + NodeProviderOutcome.FOUND, + provider.current().outcome()); + } + } + + @Test + void arbitraryProviderBugIsNeverReclassifiedAsTransientUnavailability() { + Node content = obj("value", "known-shape"); + String blueId = calculateBlueId(content); + IllegalStateException providerBug = + new IllegalStateException("provider implementation bug"); + NodeProvider provider = new NodeProvider() { + @Override + public List fetchByBlueId(String ignored) { + throw providerBug; + } + + @Override + public NodeProviderResult fetchResultByBlueId( + String ignored) { + throw providerBug; + } + }; + + try (Blue blue = new Blue(provider)) { + IllegalStateException failure = assertThrows( + IllegalStateException.class, + () -> exactReference(blue, blueId).isObject()); + + assertSame(providerBug, failure); + } + } + + @Test + void priorValidMaterializationDoesNotHideAChangedProviderOutcome() { + Node content = obj("value", "first-attempt"); + String blueId = calculateBlueId(content); + MutableProvider provider = new MutableProvider( + NodeProviderResult.found( + Collections.singletonList(content))); + + try (Blue blue = new Blue(provider)) { + assertTrue(exactReference(blue, blueId).isObject()); + provider.set(NodeProviderResult.unavailable( + "second attempt cannot acquire evidence")); + + ExecutionEvidenceUnavailableException failure = assertThrows( + ExecutionEvidenceUnavailableException.class, + () -> exactReference(blue, blueId).isObject()); + + assertEquals( + "second attempt cannot acquire evidence", + failure.getMessage()); + assertTrue(provider.fetches() >= 2); + } + } + + private static BexValue exactReference( + Blue blue, String blueId) { + return BexValues.referenceBacked( + BexValues.frozen(FrozenNode.fromNode( + new Node().blueId(blueId))), + blue); + } + + private static String calculateBlueId(Node node) { + try (Blue blue = new Blue()) { + return blue.calculateBlueId(node); + } + } + + private static final class MutableProvider + implements NodeProvider { + private final AtomicReference result; + private int fetches; + + private MutableProvider(NodeProviderResult initial) { + result = new AtomicReference<>(initial); + } + + private void set(NodeProviderResult next) { + result.set(next); + } + + private NodeProviderResult current() { + return result.get(); + } + + private int fetches() { + return fetches; + } + + @Override + public List fetchByBlueId(String blueId) { + NodeProviderResult current = fetchResultByBlueId(blueId); + return current.outcome() == NodeProviderOutcome.FOUND + ? current.nodes() + : Collections.emptyList(); + } + + @Override + public NodeProviderResult fetchResultByBlueId( + String blueId) { + fetches++; + return result.get(); + } + } +} diff --git a/src/test/java/blue/bex/BexTranslatedCorpusTest.java b/src/test/java/blue/bex/BexTranslatedCorpusTest.java index be53955..87db4d5 100644 --- a/src/test/java/blue/bex/BexTranslatedCorpusTest.java +++ b/src/test/java/blue/bex/BexTranslatedCorpusTest.java @@ -208,8 +208,10 @@ private List kyvernoMutateGenerateCases() { l(p("add", "/spec/nodeSelector/kubernetes.io~1os", "linux")))); cases.add(changes("add-toleration", pod(container("app", "corp/app:1.0")), - list(patch("add", "/spec/tolerations/0", obj("key", "dedicated", "operator", "Equal", "value", "apps", "effect", "NoSchedule"))), - l(p("add", "/spec/tolerations/0", m("effect", "NoSchedule", "key", "dedicated", "operator", "Equal", "value", "apps"))))); + list(patch("add", "/spec/tolerations/0", + obj("key", "dedicated", "operator", "Exists", "effect", "NoSchedule"))), + l(p("add", "/spec/tolerations/0", + m("effect", "NoSchedule", "key", "dedicated", "operator", "Exists"))))); cases.add(changes("add-priority-class", pod(container("app", "corp/app:1.0")), list(patch("add", "/spec/priorityClassName", "standard")), diff --git a/src/test/java/blue/bex/BexUseCaseConformanceTest.java b/src/test/java/blue/bex/BexUseCaseConformanceTest.java index f701bab..cf77ed9 100644 --- a/src/test/java/blue/bex/BexUseCaseConformanceTest.java +++ b/src/test/java/blue/bex/BexUseCaseConformanceTest.java @@ -248,12 +248,18 @@ void resultValueCreatesMissingParentForChildAdd() { @Test void resultValueListIndexRemoveIsNonShiftingOverlayBehavior() { + Node orders = op("$resultValue", "/orders"); BexExecutionResult result = run(stepDo(list( op("$appendChange", obj("op", "remove", "path", "/orders/1")), - op("$return", obj("orders", op("$resultValue", "/orders"))) + op("$return", obj( + "first", op("$listGet", obj("list", orders, "index", 0)), + "removedExists", op("$exists", op("$listGet", obj("list", orders, "index", 1))), + "size", op("$size", orders), + "third", op("$listGet", obj("list", orders, "index", 2)))) )), documentContext(obj("orders", list("a", "b", "c")))); - assertEquals(m("orders", l("a", null, "c")), simple(result.value())); + assertEquals(m("first", "a", "removedExists", false, "size", bi(3), "third", "c"), + simple(result.value())); } @Test diff --git a/src/test/java/blue/bex/api/Bex20ApiSurfaceTest.java b/src/test/java/blue/bex/api/Bex20ApiSurfaceTest.java new file mode 100644 index 0000000..b7d8b4a --- /dev/null +++ b/src/test/java/blue/bex/api/Bex20ApiSurfaceTest.java @@ -0,0 +1,261 @@ +package blue.bex.api; + +import blue.bex.gas.BexGasSchedule; +import blue.bex.gas.BexGasLedger; +import blue.bex.result.BexExecutionResult; +import blue.bex.result.BexMetrics; +import blue.bex.value.BexValue; +import blue.bex.value.BexValues; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.Locale; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class Bex20ApiSurfaceTest { + private static final String[] BEX_1_GAS_ALIASES = { + "expressionBase", + "statementBase", + "varRead", + "pointerGetBase", + "pointerSetBase", + "objectSetBase", + "appendChangeBase", + "appendEventBase", + "forEachItem", + "functionCall", + "estimatedSize", + "gasConsumed" + }; + + @Test + void intrinsicRegistrationAlwaysRequiresRegistryIdentityAndNamedWeights() + throws Exception { + assertThrows( + NoSuchMethodException.class, + () -> BexEngine.Builder.class.getMethod( + "intrinsic", + String.class, + BexIntrinsicProcessor.class)); + assertThrows( + NoSuchMethodException.class, + () -> BexEngine.Builder.class.getMethod( + "intrinsic", + Class.class, + BexIntrinsicProcessor.class)); + assertThrows( + NoSuchMethodException.class, + () -> BexIntrinsicRegistry.Builder.class.getMethod( + "register", + String.class, + BexIntrinsicProcessor.class)); + assertThrows( + NoSuchMethodException.class, + () -> BexIntrinsicRegistry.Builder.class.getMethod( + "register", + Class.class, + BexIntrinsicProcessor.class)); + assertThrows( + NoSuchMethodException.class, + () -> BexIntrinsicRegistry.class.getMethod( + "with", + String.class, + BexIntrinsicProcessor.class)); + assertThrows( + NoSuchMethodException.class, + () -> BexIntrinsicRegistry.class.getMethod( + "with", + Class.class, + BexIntrinsicProcessor.class)); + + assertThrows( + NullPointerException.class, + () -> BexIntrinsicRegistry.builder().register( + "test-type", + "test-registry/1", + null, + invocation -> BexValues.scalar(true))); + + BexIntrinsicRegistry first = BexIntrinsicRegistry.builder() + .register( + "test-type", + "test-registry/1", + Collections.singletonMap("work", 1L), + invocation -> BexValues.scalar(true)) + .build(); + BexIntrinsicRegistry second = BexIntrinsicRegistry.builder() + .register( + "test-type", + "test-registry/2", + Collections.singletonMap("work", 1L), + invocation -> BexValues.scalar(true)) + .build(); + assertNotEquals(first.identity(), second.identity()); + } + + @Test + void gasScheduleExposesOnlyBex20CounterNames() { + for (String alias : BEX_1_GAS_ALIASES) { + assertThrows( + NoSuchFieldException.class, + () -> BexGasSchedule.class.getField(alias)); + assertThrows( + NoSuchMethodException.class, + () -> BexGasSchedule.Builder.class.getMethod( + alias, long.class)); + } + } + + @Test + void executionResultHasNoAggregateGasConstructor() { + assertNoAggregateGasConstructor(BexExecutionResult.class); + assertNoAggregateGasConstructor(BexGasLedger.class); + } + + @Test + void intrinsicInvocationHasNoOpaqueGasChargeMethod() { + assertThrows( + NoSuchMethodException.class, + () -> BexIntrinsicInvocation.class.getMethod( + "chargeGas", long.class)); + for (Method method : BexIntrinsicInvocation.class.getMethods()) { + assertFalse( + "chargeGas".equals(method.getName()), + "intrinsics must charge registry-declared named counters"); + } + } + + @Test + void intrinsicBoundaryExposesNoLedgerOrPortableGasEvidencePath() + throws Exception { + Method execute = BexIntrinsicProcessor.class.getMethod( + "execute", BexIntrinsicInvocation.class); + assertEquals(BexValue.class, execute.getReturnType()); + assertArrayEquals( + new Class[]{BexIntrinsicInvocation.class}, + execute.getParameterTypes()); + assertEquals( + 1, + BexIntrinsicProcessor.class.getDeclaredMethods().length, + "the intrinsic processor must expose only its BexValue " + + "execution boundary"); + + for (Constructor constructor + : BexIntrinsicInvocation.class.getConstructors()) { + assertNoForbiddenIntrinsicGasType( + constructor.toString(), + constructor.getParameterTypes()); + assertNoForbiddenIntrinsicGasSignature( + constructor.toGenericString()); + } + for (Field field : BexIntrinsicInvocation.class.getFields()) { + assertNoForbiddenIntrinsicGasType( + field.toString(), field.getType()); + assertNoForbiddenIntrinsicGasSignature( + field.toGenericString()); + } + for (Method method : BexIntrinsicInvocation.class.getMethods()) { + String normalized = + method.getName().toLowerCase(Locale.ROOT); + assertFalse( + normalized.contains("submit") + || normalized.contains("merge") + || normalized.contains("openledger") + || normalized.contains("childledger") + || normalized.contains("gasledger") + || normalized.contains("ledgerhost") + || "host".equals(normalized) + || normalized.contains("chargegas") + || normalized.contains("gasevidence") + || normalized.contains("gasresult") + || (normalized.startsWith("set") + && normalized.contains("gas")) + || (normalized.contains("aggregate") + && normalized.contains("gas")), + method + " exposes a portable gas submission, merge, " + + "or aggregate-evidence path"); + assertNoForbiddenIntrinsicGasType( + method.toString(), method.getReturnType()); + assertNoForbiddenIntrinsicGasType( + method.toString(), method.getParameterTypes()); + assertNoForbiddenIntrinsicGasSignature( + method.toGenericString()); + } + } + + @Test + void recursiveSizeEstimatorAndMetricsAreAbsent() { + assertThrows( + ClassNotFoundException.class, + () -> Class.forName("blue.bex.gas.BexSizeEstimator")); + + String[] removedMetricMethods = { + "incrementSizeEstimateCalls", + "incrementSizeEstimateCacheHits", + "incrementSizeEstimateCacheMisses", + "sizeEstimateCalls", + "sizeEstimateCacheHits", + "sizeEstimateCacheMisses" + }; + for (String method : removedMetricMethods) { + assertThrows( + NoSuchMethodException.class, + () -> BexMetrics.class.getMethod(method)); + } + } + + private static void assertNoAggregateGasConstructor(Class type) { + for (Constructor constructor : type.getConstructors()) { + boolean hasAggregateGasParameter = false; + for (Class parameter : constructor.getParameterTypes()) { + hasAggregateGasParameter |= parameter == long.class; + } + assertFalse( + hasAggregateGasParameter, + type.getSimpleName() + + " must accept a named gas ledger or trace, " + + "not an aggregate gas integer"); + } + } + + private static void assertNoForbiddenIntrinsicGasType( + String api, + Class... types) { + for (Class type : types) { + String name = type.getName(); + assertFalse( + "blue.language.processor.GasMeter$ChildGasLedger" + .equals(name) + || "blue.bex.gas.BexGasLedger".equals(name) + || "blue.bex.api.BexGasLedgerHost".equals(name) + || "blue.bex.gas.BexGasMeter".equals(name), + api + " exposes a child ledger, ledger host, or " + + "BEX gas meter"); + } + } + + private static void assertNoForbiddenIntrinsicGasSignature( + String signature) { + assertFalse( + signature.contains( + "blue.language.processor.GasMeter.ChildGasLedger") + || signature.contains( + "blue.language.processor.GasMeter$ChildGasLedger") + || signature.contains( + "blue.bex.gas.BexGasLedger") + || signature.contains( + "blue.bex.api.BexGasLedgerHost") + || signature.contains( + "blue.bex.gas.BexGasMeter"), + signature + " exposes a child ledger, ledger host, or " + + "BEX gas meter"); + } +} diff --git a/src/test/java/blue/bex/conformance/BexBinaryApiManifestMain.java b/src/test/java/blue/bex/conformance/BexBinaryApiManifestMain.java new file mode 100644 index 0000000..5129fc4 --- /dev/null +++ b/src/test/java/blue/bex/conformance/BexBinaryApiManifestMain.java @@ -0,0 +1,249 @@ +package blue.bex.conformance; + +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +/** + * Generates a deterministic descriptor-level manifest of the public and + * protected binary API physically present in the packaged BEX JAR. + */ +public final class BexBinaryApiManifestMain { + private BexBinaryApiManifestMain() { + } + + public static void main(String[] args) throws Exception { + if (args.length != 2) { + throw new IllegalArgumentException( + "Expected packaged JAR and output manifest paths"); + } + Path artifact = Paths.get(args[0]) + .toAbsolutePath().normalize(); + Path output = Paths.get(args[1]) + .toAbsolutePath().normalize(); + if (!Files.isRegularFile(artifact)) { + throw new IllegalArgumentException( + "Packaged JAR is missing: " + artifact); + } + + List classes = publicClassNames(artifact); + List manifest = new ArrayList(); + manifest.add("schema=blue-bex-binary-api-manifest/1.0"); + try (JarFirstClassLoader loader = + new JarFirstClassLoader( + artifact.toUri().toURL(), + BexBinaryApiManifestMain.class.getClassLoader())) { + for (String className : classes) { + Class type = + Class.forName(className, false, loader); + if (!isApi(type.getModifiers())) { + continue; + } + manifest.add(classSignature(type)); + List members = + new ArrayList(); + for (Field field : type.getDeclaredFields()) { + if (isApi(field.getModifiers())) { + members.add(fieldSignature(field)); + } + } + for (Constructor constructor + : type.getDeclaredConstructors()) { + if (isApi(constructor.getModifiers())) { + members.add(constructorSignature(constructor)); + } + } + for (Method method : type.getDeclaredMethods()) { + if (isApi(method.getModifiers())) { + members.add(methodSignature(method)); + } + } + Collections.sort(members); + manifest.addAll(members); + } + } + Files.createDirectories(output.getParent()); + Files.write( + output, + (joinLines(manifest) + "\n") + .getBytes(StandardCharsets.UTF_8), + StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING, + StandardOpenOption.WRITE); + System.out.println("BEX binary API manifest: " + output); + } + + private static List publicClassNames(Path artifact) + throws IOException { + List result = new ArrayList(); + try (JarFile jar = new JarFile(artifact.toFile())) { + Enumeration entries = jar.entries(); + while (entries.hasMoreElements()) { + String name = entries.nextElement().getName(); + if (name.startsWith("blue/bex/") + && name.endsWith(".class") + && !name.equals("module-info.class")) { + result.add(name.substring(0, name.length() - 6) + .replace('/', '.')); + } + } + } + Collections.sort(result); + return result; + } + + private static boolean isApi(int modifiers) { + return Modifier.isPublic(modifiers) + || Modifier.isProtected(modifiers); + } + + private static String classSignature(Class type) { + StringBuilder result = new StringBuilder(); + result.append("class ") + .append(Modifier.toString(type.getModifiers())) + .append(' ') + .append(type.getName()); + Class superclass = type.getSuperclass(); + if (superclass != null && superclass != Object.class) { + result.append(" extends ") + .append(typeName(superclass)); + } + List interfaces = new ArrayList(); + for (Class implemented : type.getInterfaces()) { + interfaces.add(typeName(implemented)); + } + Collections.sort(interfaces); + if (!interfaces.isEmpty()) { + result.append(" implements ") + .append(String.join(",", interfaces)); + } + return result.toString(); + } + + private static String fieldSignature(Field field) { + return " field " + + Modifier.toString(field.getModifiers()) + + " " + field.getName() + + ":" + typeName(field.getType()) + + flags(field); + } + + private static String constructorSignature( + Constructor constructor) { + return " constructor " + + Modifier.toString(constructor.getModifiers()) + + " (" + + parameterTypes(constructor.getParameterTypes()) + + ")" + + exceptionClause(constructor.getExceptionTypes()) + + flags(constructor); + } + + private static String methodSignature(Method method) { + return " method " + + Modifier.toString(method.getModifiers()) + + " " + method.getName() + + "(" + parameterTypes(method.getParameterTypes()) + ")" + + ":" + typeName(method.getReturnType()) + + exceptionClause(method.getExceptionTypes()) + + flags(method); + } + + private static String parameterTypes(Class[] types) { + List values = new ArrayList(); + for (Class type : types) { + values.add(typeName(type)); + } + return String.join(",", values); + } + + private static String exceptionTypes(Class[] types) { + List values = new ArrayList(); + for (Class type : types) { + values.add(typeName(type)); + } + Collections.sort(values); + return String.join(",", values); + } + + private static String exceptionClause(Class[] types) { + String exceptions = exceptionTypes(types); + return exceptions.isEmpty() + ? "" + : " throws " + exceptions; + } + + private static String typeName(Class type) { + if (!type.isArray()) { + return type.getName(); + } + return typeName(type.getComponentType()) + "[]"; + } + + private static String flags(Member member) { + StringBuilder flags = new StringBuilder(); + if (member.isSynthetic()) { + flags.append(" synthetic"); + } + if (member instanceof Method + && ((Method) member).isBridge()) { + flags.append(" bridge"); + } + return flags.toString(); + } + + private static String joinLines(List lines) { + StringBuilder result = new StringBuilder(); + for (int index = 0; index < lines.size(); index++) { + if (index > 0) { + result.append('\n'); + } + result.append(lines.get(index)); + } + return result.toString(); + } + + private static final class JarFirstClassLoader + extends URLClassLoader { + JarFirstClassLoader(URL artifact, ClassLoader parent) { + super(new URL[] {artifact}, parent); + } + + @Override + protected synchronized Class loadClass( + String name, + boolean resolve) throws ClassNotFoundException { + if (name.startsWith("blue.bex.")) { + Class loaded = findLoadedClass(name); + if (loaded == null) { + try { + loaded = findClass(name); + } catch (ClassNotFoundException notInArtifact) { + loaded = super.loadClass(name, false); + } + } + if (resolve) { + resolveClass(loaded); + } + return loaded; + } + return super.loadClass(name, resolve); + } + } +} diff --git a/src/test/java/blue/bex/conformance/BexConformanceFixtureTest.java b/src/test/java/blue/bex/conformance/BexConformanceFixtureTest.java new file mode 100644 index 0000000..b8cc29a --- /dev/null +++ b/src/test/java/blue/bex/conformance/BexConformanceFixtureTest.java @@ -0,0 +1,35 @@ +package blue.bex.conformance; + +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Executes every manifest-declared BEX behavior fixture. There are no + * assumptions, disabled tests, or skip paths in this suite. + */ +class BexConformanceFixtureTest { + @TestFactory + Collection allBehaviorFixturesExecute() { + List fixtures = + ConformancePackage.behaviorFixtures(); + assertEquals( + ConformancePackage.BEHAVIOR_FIXTURE_COUNT, + fixtures.size(), + "The manifest must expose exactly 105 behavior fixtures"); + + List tests = + new ArrayList(fixtures.size()); + for (ConformancePackage.Fixture fixture : fixtures) { + tests.add(DynamicTest.dynamicTest( + fixture.id() + " :: " + fixture.path, + () -> new BexFixtureRunner().execute(fixture))); + } + return tests; + } +} diff --git a/src/test/java/blue/bex/conformance/BexConformancePackageIntegrityTest.java b/src/test/java/blue/bex/conformance/BexConformancePackageIntegrityTest.java new file mode 100644 index 0000000..4fb53f6 --- /dev/null +++ b/src/test/java/blue/bex/conformance/BexConformancePackageIntegrityTest.java @@ -0,0 +1,541 @@ +package blue.bex.conformance; + +import blue.language.Blue; +import blue.language.model.Node; +import blue.language.snapshot.FrozenNode; +import org.junit.jupiter.api.Test; + +import java.math.BigInteger; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Integrity gate for the unmodified normative BEX 2.0 conformance package. + */ +class BexConformancePackageIntegrityTest { + + @Test + void exact147FilePackageAndManifestInventoryAreIntact() { + Map manifest = ConformancePackage.fixtureManifest(); + assertEquals("blue-bex-conformance", manifest.get("fixturePackage")); + assertEquals("2.0", manifest.get("specificationVersion")); + assertEquals("blue-bex-fixture/2.0", manifest.get("schemaVersion")); + assertEquals(ConformancePackage.VECTOR_COUNT, + intValue(manifest.get("vectorCount"))); + assertEquals(ConformancePackage.BEHAVIOR_FIXTURE_COUNT, + intValue(manifest.get("behaviorFixtureCount"))); + assertEquals(ConformancePackage.GAS_FIXTURE_COUNT, + intValue(manifest.get("gasFixtureCount"))); + + List entries = + ConformancePackage.manifestFiles(); + assertEquals(141, entries.size(), + "The fixture manifest inventories every fixture/support file except itself"); + assertEquals(ConformancePackage.BEHAVIOR_FIXTURE_COUNT, + countRole(entries, "behavior-fixture")); + assertEquals(ConformancePackage.GAS_FIXTURE_COUNT, + countRole(entries, "gas-fixture")); + assertEquals(6, countRole(entries, "support")); + + Set listedPaths = new LinkedHashSet(); + List listedInOrder = new ArrayList(); + for (ConformancePackage.ManifestFile entry : entries) { + assertTrue(listedPaths.add(entry.path), + "Duplicate manifest path: " + entry.path); + listedInOrder.add(entry.path); + assertSafeRelativePath(entry.path); + assertTrue(ConformancePackage.stringSet( + "behavior-fixture", "gas-fixture", "support") + .contains(entry.role), "Unknown role for " + entry.path); + assertTrue(ConformancePackage.SHA_256.matcher(entry.sha256).matches(), + "Invalid SHA-256 syntax for " + entry.path); + + String resource = ConformancePackage.FIXTURE_ROOT + entry.path; + byte[] normalized = ConformancePackage.lfNormalizedBytes(resource); + assertEquals(entry.bytes, normalized.length, + "LF-normalized byte length changed for " + entry.path); + assertEquals(entry.sha256, ConformancePackage.sha256(normalized), + "SHA-256 changed for " + entry.path); + } + + List sorted = new ArrayList(listedInOrder); + Collections.sort(sorted); + assertEquals(sorted, listedInOrder, + "Manifest inventory order must remain deterministic"); + + Set physicalFixtureFiles = + new LinkedHashSet( + ConformancePackage.regularResourcePaths( + ConformancePackage.FIXTURE_ROOT)); + assertTrue(physicalFixtureFiles.remove("manifest.yaml")); + assertEquals(listedPaths, physicalFixtureFiles, + "Unlisted or missing fixture-package files"); + + assertEquals(ConformancePackage.FILE_COUNT, + ConformancePackage.regularResourcePaths( + ConformancePackage.ROOT).size(), + "The imported BEX package must remain exactly 147 files"); + } + + @Test + void packageIdentitiesAndCrossPackageBindingsRecomputeExactly() { + Map fixtureManifest = + ConformancePackage.fixtureManifest(); + Map gasManifest = ConformancePackage.gasManifest(); + Map registryManifest = + ConformancePackage.registryManifest(); + + String fixtureIdentity = text(fixtureManifest.get("packageIdentity")); + String gasIdentity = text(gasManifest.get("packageIdentity")); + String registryIdentity = text(registryManifest.get("packageIdentity")); + assertPackageIdentity(fixtureIdentity); + assertPackageIdentity(gasIdentity); + assertPackageIdentity(registryIdentity); + + assertEquals(fixtureIdentity, + ConformancePackage.packageIdentity( + fixtureManifest, "packageIdentity")); + assertEquals(gasIdentity, + ConformancePackage.packageIdentity( + gasManifest, "packageIdentity")); + assertEquals(registryIdentity, + ConformancePackage.packageIdentity( + registryManifest, + "packageIdentity", "fixturePackageIdentity")); + + String gasSha = ConformancePackage.sha256( + ConformancePackage.lfNormalizedBytes( + ConformancePackage.GAS_MANIFEST)); + assertEquals(fixtureManifest.get("gasManifestSha256"), gasSha); + assertEquals(fixtureManifest.get("gasManifestPackageIdentity"), gasIdentity); + assertEquals(fixtureManifest.get("registryPackageIdentity"), registryIdentity); + assertEquals(registryManifest.get("fixturePackageIdentity"), fixtureIdentity); + } + + @Test + void everyFixtureValidatesAgainstTheClosedSchema() { + Map schema = ConformancePackage.loadMap( + ConformancePackage.FIXTURE_ROOT + "fixture-schema.yaml"); + assertEquals("blue-bex-fixture/2.0", schema.get("$id")); + assertEquals(Boolean.FALSE, + ConformancePackage.map(schema, "fixture schema") + .get("additionalProperties")); + + Set ids = new LinkedHashSet(); + for (ConformancePackage.Fixture fixture + : allFixtures()) { + BexFixtureSchemaValidator.validate(fixture); + assertTrue(ids.add(fixture.id()), + "Duplicate fixture id: " + fixture.id()); + if (fixture.path.startsWith("gas-micro/")) { + assertEquals("gas", fixture.category(), fixture.path); + } else { + assertFalse("gas".equals(fixture.category()), fixture.path); + } + } + assertEquals( + ConformancePackage.BEHAVIOR_FIXTURE_COUNT + + ConformancePackage.GAS_FIXTURE_COUNT, + ids.size()); + } + + @Test + void vectorOperatorAndProjectionCoverageAreClosedAndBidirectional() { + List behavior = + ConformancePackage.behaviorFixtures(); + Map fixturesByPath = + fixturesByPath(behavior); + assertEquals(ConformancePackage.BEHAVIOR_FIXTURE_COUNT, + fixturesByPath.size()); + + List vectorFixtures = + new ArrayList(behavior); + vectorFixtures.addAll(ConformancePackage.gasFixtures()); + Map> expectedVectors = + reverseVectorMap(vectorFixtures); + Map vectorCoverage = ConformancePackage.loadMap( + ConformancePackage.FIXTURE_ROOT + "vector-coverage.yaml"); + assertEquals("blue-bex/2.0", vectorCoverage.get("specification")); + Map declaredVectors = ConformancePackage.map( + vectorCoverage.get("vectors"), "vector-coverage.vectors"); + assertEquals(ConformancePackage.VECTOR_COUNT, declaredVectors.size(), + "The authoritative manifest's 60-vector count wins"); + assertEquals(expectedVectors.keySet(), declaredVectors.keySet()); + for (Map.Entry entry : declaredVectors.entrySet()) { + assertEquals(expectedVectors.get(entry.getKey()), + textList(entry.getValue(), + "vector-coverage." + entry.getKey()), + "Vector reverse mapping changed for " + entry.getKey()); + } + + Map operatorCoverage = ConformancePackage.loadMap( + ConformancePackage.FIXTURE_ROOT + "operator-coverage.yaml"); + assertEquals("blue-bex-operator-coverage/2.0", + operatorCoverage.get("schema")); + assertEquals(ConformancePackage.OPERATOR_COUNT, + intValue(operatorCoverage.get("operatorCount"))); + List operators = ConformancePackage.list( + operatorCoverage.get("operators"), + "operator-coverage.operators"); + assertEquals(ConformancePackage.OPERATOR_COUNT, operators.size()); + + Set operatorNames = new LinkedHashSet(); + for (Object value : operators) { + Map declaration = + ConformancePackage.map(value, "operator coverage entry"); + String operator = text(declaration.get("operator")); + assertTrue(operator.startsWith("$")); + assertTrue(operatorNames.add(operator), + "Duplicate operator coverage: " + operator); + List paths = textList( + declaration.get("fixtures"), operator + ".fixtures"); + assertFalse(paths.isEmpty(), "No direct coverage for " + operator); + for (String path : paths) { + ConformancePackage.Fixture fixture = fixturesByPath.get(path); + assertNotNull(fixture, + "Operator coverage references non-behavior fixture " + path); + assertTrue(operatorOccurrences(fixture).contains(operator), + path + " does not directly contain " + operator); + } + } + + assertEquals(45, + ConformancePackage.regularResourcePaths( + ConformancePackage.FIXTURE_ROOT + "operators/").size(), + "Every direct operator fixture must remain present"); + + Map projectionCatalog = ConformancePackage.loadMap( + ConformancePackage.FIXTURE_ROOT + "projection-catalog.yaml"); + assertEquals("blue-bex-projection-catalog/2.0", + projectionCatalog.get("schema")); + Set projections = new LinkedHashSet(); + for (Object entry : ConformancePackage.list( + projectionCatalog.get("entries"), + "projection-catalog.entries")) { + String projection = text( + ConformancePackage.map(entry, "projection entry").get("path")); + assertTrue(projections.add(projection), + "Duplicate projection: " + projection); + } + for (ConformancePackage.Fixture fixture : behavior) { + Object assertionsValue = fixture.expected().get("assertions"); + if (assertionsValue == null) { + continue; + } + for (Object assertionValue : ConformancePackage.list( + assertionsValue, fixture.path + ".expected.assertions")) { + Map assertion = ConformancePackage.map( + assertionValue, fixture.path + " assertion"); + String actual = text(assertion.get("actual")); + assertTrue(projections.contains(actual), + fixture.path + " uses undeclared projection " + actual); + } + } + } + + @Test + void everyNamedCounterHasOneExactMicrofixture() { + Map gasManifest = ConformancePackage.gasManifest(); + assertEquals("blue-bex-gas-manifest", + gasManifest.get("manifestType")); + assertEquals("blue-bex/gas/2.0", gasManifest.get("schedule")); + assertEquals(ConformancePackage.GAS_FIXTURE_COUNT, + intValue(gasManifest.get("counterCount"))); + Map counters = ConformancePackage.map( + gasManifest.get("counters"), "gas-manifest.counters"); + assertEquals(ConformancePackage.GAS_FIXTURE_COUNT, counters.size()); + assertFalse(counters.containsKey("estimatedSize"), + "Recursive size gas is forbidden"); + + Set covered = new LinkedHashSet(); + for (ConformancePackage.Fixture fixture + : ConformancePackage.gasFixtures()) { + Map direct = ConformancePackage.map( + fixture.context().get("directCounterFixture"), + fixture.path + ".directCounterFixture"); + String counter = text(direct.get("counter")); + assertTrue(counters.containsKey(counter), + "Unknown counter microfixture: " + counter); + assertTrue(covered.add(counter), + "Duplicate counter microfixture: " + counter); + long quantity = longValue(direct.get("quantity")); + long weight = longValue(counters.get(counter)); + + Map expected = fixture.expected(); + List trace = ConformancePackage.list( + expected.get("gasTrace"), fixture.path + ".gasTrace"); + assertEquals(1, trace.size(), fixture.path); + Map charge = ConformancePackage.map( + trace.get(0), fixture.path + ".gasTrace[0]"); + assertEquals(0L, longValue(charge.get("sequence")), fixture.path); + assertEquals(counter, charge.get("counter"), fixture.path); + assertEquals(quantity, longValue(charge.get("quantity")), fixture.path); + assertEquals(weight, longValue(charge.get("weight")), fixture.path); + assertEquals(quantity * weight, + longValue(charge.get("gas")), fixture.path); + assertEquals(quantity * weight, + longValue(expected.get("totalGas")), fixture.path); + } + assertEquals(counters.keySet(), covered); + } + + @Test + void registryFilesBlueIdsAndFixtureIntrinsicBindingsAreExact() { + Map registry = + ConformancePackage.registryManifest(); + assertEquals("blue-bex-runtime", registry.get("registry")); + assertEquals("runtime-type", registry.get("registryKind")); + assertEquals("2.0", registry.get("specificationVersion")); + assertEquals("1.0", registry.get("languageVersion")); + + Set declaredBlueIds = new LinkedHashSet(); + Set fixtureOnlyBlueIds = new LinkedHashSet(); + try (Blue blue = new Blue()) { + for (Object value : ConformancePackage.list( + registry.get("entries"), "registry.entries")) { + Map entry = + ConformancePackage.map(value, "registry entry"); + String path = text(entry.get("path")); + String expectedSha = text(entry.get("sha256")); + String expectedBlueId = text(entry.get("blueId")); + byte[] bytes = ConformancePackage.lfNormalizedBytes( + ConformancePackage.REGISTRY_ROOT + path); + assertEquals(expectedSha, ConformancePackage.sha256(bytes), path); + + Node node = blue.yamlToNode( + new String(bytes, java.nio.charset.StandardCharsets.UTF_8)); + assertEquals(expectedBlueId, + FrozenNode.fromResolvedNode(node).blueId(), + "Registry BlueId changed for " + path); + assertTrue(declaredBlueIds.add(expectedBlueId), + "Duplicate registry BlueId: " + expectedBlueId); + if (Boolean.TRUE.equals(entry.get("fixtureOnly"))) { + fixtureOnlyBlueIds.add(expectedBlueId); + } + } + } + + Set invokedIntrinsics = new LinkedHashSet(); + for (ConformancePackage.Fixture fixture + : ConformancePackage.behaviorFixtures()) { + collectIntrinsicBlueIds(fixture.program(), invokedIntrinsics); + } + assertEquals(fixtureOnlyBlueIds, invokedIntrinsics, + "Behavior fixtures may invoke exactly the declared fixture intrinsics"); + assertEquals(2, invokedIntrinsics.size()); + assertTrue(declaredBlueIds.containsAll(invokedIntrinsics)); + } + + @Test + void knownBaselineReconciliationsRemainExplicitAndFixturePreserving() { + Map fixtures = + fixturesByPath(ConformancePackage.behaviorFixtures()); + + Map manifest = ConformancePackage.fixtureManifest(); + assertEquals(60, intValue(manifest.get("vectorCount"))); + assertEquals(105, intValue(manifest.get("behaviorFixtureCount"))); + + ConformancePackage.Fixture s07 = fixtures.get("s/bex-s-07.yaml"); + assertEquals("runtime-error", s07.expected().get("errorClass"), + "Parallel $let observes an uninitialized local at runtime"); + Map s07Let = firstStatementBody(s07.program(), "$let"); + assertTrue(s07Let.containsKey("vars")); + assertFalse(s07Let.containsKey("order")); + + ConformancePackage.Fixture c09 = fixtures.get("c/bex-c-09.yaml"); + assertEquals("recursive-call-graph", c09.expected().get("reason")); + assertEquals("rejected", c09.expected().get("compileStatus")); + assertEquals(Boolean.FALSE, + firstAssertionExpected(c09, "runtime.started")); + + ConformancePackage.Fixture e14 = fixtures.get("e/bex-e-14.yaml"); + Map identityAssertion = + firstAssertion(e14, "result.identityA"); + assertEquals("notEquals", identityAssertion.get("op")); + assertEquals("result.identityB", identityAssertion.get("expected"), + "The expected operand is a projection reference, not literal text"); + + ConformancePackage.Fixture findEntry = + fixtures.get("operators/bex-op-findentry.yaml"); + Map expectedFindEntry = ConformancePackage.map( + findEntry.expected().get("result"), "findEntry.expected.result"); + assertEquals(ConformancePackage.stringSet("key", "val"), + expectedFindEntry.keySet(), + "The expected map is a subset; the normative result retains index"); + + ConformancePackage.Fixture g09 = fixtures.get("g/bex-g-09.yaml"); + assertEquals("canonical-merge-sort", + firstAssertionExpected(g09, "gas.sortComparison"), + "G09 asserts algorithm evidence, not a string-valued quantity"); + } + + private static List allFixtures() { + List fixtures = + new ArrayList(); + fixtures.addAll(ConformancePackage.behaviorFixtures()); + fixtures.addAll(ConformancePackage.gasFixtures()); + return fixtures; + } + + private static Map fixturesByPath( + List fixtures) { + Map byPath = + new LinkedHashMap(); + for (ConformancePackage.Fixture fixture : fixtures) { + assertEquals(null, byPath.put(fixture.path, fixture), + "Duplicate fixture path " + fixture.path); + } + return byPath; + } + + private static Set operatorOccurrences( + ConformancePackage.Fixture fixture) { + Set operators = new LinkedHashSet( + ConformancePackage.operatorOccurrences(fixture.program())); + Object cases = fixture.expected().get("cases"); + if (cases != null) { + for (Object value : ConformancePackage.list( + cases, fixture.path + ".expected.cases")) { + Map testcase = ConformancePackage.map( + value, fixture.path + ".expected.cases[]"); + operators.addAll(ConformancePackage.operatorOccurrences( + testcase.get("program"))); + } + } + return operators; + } + + private static Map> reverseVectorMap( + List fixtures) { + Map> vectors = + new LinkedHashMap>(); + for (ConformancePackage.Fixture fixture : fixtures) { + for (String vector : textList( + fixture.data.get("vectors"), fixture.path + ".vectors")) { + List paths = vectors.get(vector); + if (paths == null) { + paths = new ArrayList(); + vectors.put(vector, paths); + } + paths.add(fixture.path); + } + } + return vectors; + } + + private static void collectIntrinsicBlueIds( + Object value, + Set result) { + if (value instanceof Map) { + Map map = + ConformancePackage.map(value, "program"); + if (map.containsKey("$intrinsic")) { + Map body = ConformancePackage.map( + map.get("$intrinsic"), "$intrinsic"); + Map type = ConformancePackage.map( + body.get("type"), "$intrinsic.type"); + result.add(text(type.get("blueId"))); + } + for (Object child : map.values()) { + collectIntrinsicBlueIds(child, result); + } + } else if (value instanceof List) { + for (Object child : (List) value) { + collectIntrinsicBlueIds(child, result); + } + } + } + + private static Map firstStatementBody( + Map program, + String operator) { + List statements = ConformancePackage.list( + program.get("do"), "program.do"); + Map statement = + ConformancePackage.map(statements.get(0), "program.do[0]"); + return ConformancePackage.map( + statement.get(operator), "program.do[0]." + operator); + } + + private static Map firstAssertion( + ConformancePackage.Fixture fixture, + String projection) { + for (Object value : ConformancePackage.list( + fixture.expected().get("assertions"), + fixture.path + ".assertions")) { + Map assertion = + ConformancePackage.map(value, fixture.path + " assertion"); + if (projection.equals(assertion.get("actual"))) { + return assertion; + } + } + throw new AssertionError( + fixture.path + " has no assertion for " + projection); + } + + private static Object firstAssertionExpected( + ConformancePackage.Fixture fixture, + String projection) { + return firstAssertion(fixture, projection).get("expected"); + } + + private static int countRole( + List entries, + String role) { + int count = 0; + for (ConformancePackage.ManifestFile entry : entries) { + if (role.equals(entry.role)) { + count++; + } + } + return count; + } + + private static void assertSafeRelativePath(String value) { + Path path = Paths.get(value); + assertFalse(path.isAbsolute(), value); + assertFalse(value.contains("\\"), + "Manifest paths always use portable '/' separators"); + assertEquals(value, path.normalize().toString().replace('\\', '/')); + assertFalse(value.startsWith("../") || value.contains("/../"), value); + } + + private static void assertPackageIdentity(String identity) { + assertTrue(ConformancePackage.PACKAGE_IDENTITY.matcher(identity).matches(), + "Non-release package identity: " + identity); + } + + private static List textList(Object value, String path) { + List result = new ArrayList(); + for (Object child : ConformancePackage.list(value, path)) { + result.add(text(child)); + } + return result; + } + + private static String text(Object value) { + assertTrue(value instanceof String, "Expected text but got " + value); + return (String) value; + } + + private static int intValue(Object value) { + return ConformancePackage.integer(value, "integer").intValueExact(); + } + + private static long longValue(Object value) { + return ConformancePackage.integer(value, "integer").longValueExact(); + } +} diff --git a/src/test/java/blue/bex/conformance/BexConformancePropertyTest.java b/src/test/java/blue/bex/conformance/BexConformancePropertyTest.java new file mode 100644 index 0000000..bbdd874 --- /dev/null +++ b/src/test/java/blue/bex/conformance/BexConformancePropertyTest.java @@ -0,0 +1,300 @@ +package blue.bex.conformance; + +import blue.bex.api.BexEngine; +import blue.bex.api.BexProgramSource; +import blue.bex.compile.BexCompiledProgram; +import blue.bex.compile.LruBexCompiledProgramCache; +import blue.bex.gas.BexGasCharge; +import blue.bex.result.BexExecutionResult; +import blue.bex.result.BexMetrics; +import blue.language.model.Node; +import blue.language.snapshot.FrozenNode; +import blue.language.utils.JsonPointer; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Random; + +import static blue.bex.test.BexTestFixtures.defaultContext; +import static blue.bex.test.BexTestFixtures.list; +import static blue.bex.test.BexTestFixtures.m; +import static blue.bex.test.BexTestFixtures.obj; +import static blue.bex.test.BexTestFixtures.op; +import static blue.bex.test.BexTestFixtures.runExpr; +import static blue.bex.test.BexTestFixtures.stepExpr; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Deterministic property and differential checks supplementing the exact + * published fixtures. + */ +class BexConformancePropertyTest { + @Test + void exactAndMaterializedRepresentationsAreDifferentiallyEquivalent() { + ConformancePackage.Fixture fixture = fixture("bex-r-01"); + BexEngineFixtureAdapter adapter = new BexEngineFixtureAdapter(); + Object expectedResult = null; + List> expectedTrace = null; + + for (Object value : ConformancePackage.list( + fixture.expected().get("variants"), + fixture.path + ".expected.variants")) { + Map variant = + ConformancePackage.map(value, "representation variant"); + String name = String.valueOf(variant.get("name")); + BexFixtureRun run = adapter.execute( + fixture, + fixture.program(), + fixture.context(), + variant, + "property-representation-" + name); + assertNull(run.failure, run.name + " failed"); + if (expectedTrace == null) { + expectedResult = run.result; + expectedTrace = run.gasTrace; + } else { + assertEquals(expectedResult, run.result, + name + " semantic result"); + assertEquals(expectedTrace, run.gasTrace, + name + " canonical gas trace"); + } + } + } + + @Test + void fixtureAdapterActuallyExecutesDeclaredBatchingPreparation() { + ConformancePackage.Fixture fixture = fixture("bex-r-08"); + BexEngineFixtureAdapter adapter = new BexEngineFixtureAdapter(); + BexFixtureRun coldUnbatched = null; + BexFixtureRun warmBatched = null; + + for (Object value : ConformancePackage.list( + fixture.expected().get("variants"), + fixture.path + ".expected.variants")) { + Map variant = + ConformancePackage.map(value, "provider variant"); + BexFixtureRun run = adapter.execute( + fixture, + fixture.program(), + fixture.context(), + variant, + "property-provider-" + variant.get("name")); + assertNull(run.failure, run.name + " failed"); + if ("batched".equals(variant.get("batching"))) { + warmBatched = run; + } else { + coldUnbatched = run; + } + } + + assertTrue(coldUnbatched != null); + assertTrue(warmBatched != null); + assertEquals("unbatched", coldUnbatched.providerBatching); + assertEquals(0, coldUnbatched.providerWarmupNodeLoads); + assertEquals(0, coldUnbatched.providerWarmupBatchLoads); + assertTrue(coldUnbatched.providerRuntimeNodeLoads > 0); + assertEquals(0, coldUnbatched.providerRuntimeBatchLoads); + assertEquals(0, coldUnbatched.providerRuntimeCacheHits); + + assertEquals("batched", warmBatched.providerBatching); + assertEquals(0, warmBatched.providerWarmupNodeLoads); + assertEquals(1, warmBatched.providerWarmupBatchLoads); + assertEquals(0, warmBatched.providerRuntimeNodeLoads); + assertEquals(0, warmBatched.providerRuntimeBatchLoads); + assertTrue(warmBatched.providerRuntimeCacheHits > 0); + assertEquals(coldUnbatched.result, warmBatched.result); + assertEquals(coldUnbatched.gasTrace, warmBatched.gasTrace); + } + + @Test + void compileCacheHitAndMissHaveIdenticalResultAndGas() { + List observed = new ArrayList(); + BexEngine engine = BexEngine.builder() + .cache(new LruBexCompiledProgramCache()) + .metrics(metrics -> observed.add(metrics.copy())) + .build(); + BexProgramSource source = BexProgramSource.inline( + FrozenNode.fromResolvedNode( + stepExpr(op("$add", list(1, 2, 3))))); + + BexCompiledProgram coldProgram = engine.compile(source); + BexExecutionResult cold = + engine.execute(coldProgram, defaultContext()); + BexCompiledProgram warmProgram = engine.compile(source); + BexExecutionResult warm = + engine.execute(warmProgram, defaultContext()); + + assertEquals(cold.value().toSimple(), warm.value().toSimple()); + assertEquals(traceSignature(cold), traceSignature(warm)); + assertEquals(cold.gasUsed(), warm.gasUsed()); + assertEquals(1L, observed.get(0).compileCacheMisses()); + assertEquals(1L, observed.get(2).compileCacheHits()); + } + + @Test + void randomFiniteProgramsRemainDeterministicWithinLimits() { + Random random = new Random(0xBEE20L); + for (int example = 0; example < 64; example++) { + int width = 1 + random.nextInt(8); + Object[] operands = new Object[width]; + BigInteger expected = BigInteger.ZERO; + for (int index = 0; index < width; index++) { + long value = random.nextInt(2_000_001) - 1_000_000L; + operands[index] = value; + expected = expected.add(BigInteger.valueOf(value)); + } + BexExecutionResult first = + runExpr(op("$add", list(operands))); + BexExecutionResult second = + runExpr(op("$add", list(operands))); + assertEquals(expected, first.value().toSimple(), + "generated example " + example); + assertEquals(first.value().toSimple(), second.value().toSimple(), + "result example " + example); + assertEquals(traceSignature(first), traceSignature(second), + "gas example " + example); + } + } + + @Test + void operatorShortCircuitDoesNotEvaluateFailingOperand() { + BexExecutionResult result = runExpr(op("$or", list( + true, + op("$integer", "not-an-integer")))); + + assertEquals(true, result.value().toSimple()); + } + + @Test + void pointerSegmentsRoundTripWithoutEscapingLoss() { + List alphabet = Arrays.asList( + "", + "plain", + "a/b", + "a~b", + "~1", + "emoji-\uD83D\uDE80", + "line\nbreak"); + Random random = new Random(0xBEE21L); + for (int example = 0; example < 128; example++) { + List segments = new ArrayList(); + int size = random.nextInt(12); + for (int index = 0; index < size; index++) { + segments.add(alphabet.get( + random.nextInt(alphabet.size()))); + } + String pointer = JsonPointer.toPointer(segments); + assertEquals(segments, JsonPointer.split(pointer), + "pointer " + pointer); + } + } + + @Test + void numericIdentityDistinguishesIntegerFromDecimal() { + BexExecutionResult result = runExpr(obj( + "integer", op("$nodeBlueId", 1), + "decimal", op("$nodeBlueId", + new Node().value(new BigDecimal("1.0"))))); + Object simple = result.value().toSimple(); + @SuppressWarnings("unchecked") + java.util.Map identities = + (java.util.Map) simple; + + assertNotEquals( + identities.get("integer"), + identities.get("decimal")); + } + + @Test + void canonicalMergeSortIsStableForNumericallyEqualValues() { + ConformancePackage.Fixture fixture = fixture("bex-g-09"); + Map program = m( + "expr", m( + "$intrinsic", m( + "type", m( + "blueId", + BexEngineFixtureAdapter + .SORT_FIXTURE_INTRINSIC), + "values", Arrays.asList( + new BigDecimal("2.0"), + BigInteger.ONE, + BigInteger.valueOf(2L), + new BigDecimal("1.0"))))); + BexEngineFixtureAdapter adapter = new BexEngineFixtureAdapter(); + BexFixtureRun first = adapter.execute( + fixture, + program, + fixture.context(), + Collections.emptyMap(), + "property-sort-first"); + BexFixtureRun second = adapter.execute( + fixture, + program, + fixture.context(), + Collections.emptyMap(), + "property-sort-second"); + + assertNull(first.failure, "first stable sort failed"); + assertNull(second.failure, "second stable sort failed"); + assertEquals( + Arrays.asList( + BigInteger.ONE, + new BigDecimal("1.0"), + new BigDecimal("2.0"), + BigInteger.valueOf(2L)), + first.result); + assertEquals(first.result, second.result); + assertEquals(first.gasTrace, second.gasTrace); + assertTrue(first.gasQuantity("sortComparison") > 0L); + } + + @Test + void admittedOutputRoundTripsItsExactBlueIdentity() { + BexExecutionResult result = runExpr(obj( + "a", 1, + "b", list(true, "x"))); + Node admitted = result.output().node(); + + assertTrue(result.output().reconstructed()); + assertEquals( + result.output().nodeBlueId(), + FrozenNode.fromNode(admitted).blueId()); + } + + private static ConformancePackage.Fixture fixture(String id) { + for (ConformancePackage.Fixture fixture + : ConformancePackage.behaviorFixtures()) { + if (id.equals(fixture.id())) { + return fixture; + } + } + throw new IllegalArgumentException("Unknown fixture " + id); + } + + private static List traceSignature( + BexExecutionResult result) { + List signature = new ArrayList(); + for (BexGasCharge charge : result.gasTrace()) { + signature.add( + charge.sequence() + + "|" + charge.namespace() + + "|" + charge.counterName() + + "|" + charge.quantity() + + "|" + charge.weight() + + "|" + charge.gas() + + "|" + charge.sourcePath() + + "|" + charge.operator() + + "|" + charge.reason()); + } + return signature; + } +} diff --git a/src/test/java/blue/bex/conformance/BexConformanceReportMain.java b/src/test/java/blue/bex/conformance/BexConformanceReportMain.java new file mode 100644 index 0000000..0fcafa6 --- /dev/null +++ b/src/test/java/blue/bex/conformance/BexConformanceReportMain.java @@ -0,0 +1,4755 @@ +package blue.bex.conformance; + +import blue.language.processor.RuntimeWorkSession; +import blue.language.provider.CyclicAwareNodeProvider; +import blue.language.provider.CyclicSetProof; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +import javax.xml.parsers.DocumentBuilderFactory; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Method; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.security.MessageDigest; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Writes deterministic, machine-readable evidence from the exact JUnit XML + * and artifacts present in the build directory. It never turns declared tests + * into claimed executions. + */ +public final class BexConformanceReportMain { + private BexConformanceReportMain() { + } + + public static void main(String[] args) throws Exception { + if (args.length != 8) { + throw new IllegalArgumentException( + "Expected projectDir, buildDir, Gradle version, project " + + "version, dependency mode, declared dependency, " + + "persistent evidence root, and composite path"); + } + Path projectDir = Paths.get(args[0]).toAbsolutePath().normalize(); + Path buildDir = Paths.get(args[1]).toAbsolutePath().normalize(); + String gradleVersion = args[2]; + String projectVersion = args[3]; + String dependencyMode = args[4]; + String declaredDependency = args[5]; + Path persistentEvidenceRoot = + Paths.get(args[6]).toAbsolutePath().normalize(); + Path compositePath = args[7].isEmpty() + ? null + : Paths.get(args[7]).toAbsolutePath().normalize(); + + TestEvidence tests = readTests( + buildDir.resolve("test-results").resolve("test")); + SourceState sourceState = sourceState(projectDir); + Map baseline = readEvidence( + projectDir.resolve("src/test/resources/hosted-release") + .resolve("baseline.properties")); + Map publishedApiInspection = readEvidence( + projectDir.resolve("src/test/resources/hosted-release") + .resolve("published-api-inspection.properties")); + Map operatorCoverage = operatorCoverage(tests); + Map counterCoverage = counterCoverage(tests); + Map normativeVectorCoverage = + normativeVectorCoverage(tests); + List artifacts = artifactEvidence( + projectDir, buildDir, projectVersion); + Map dependencyResolution = + dependencyResolutionEvidence( + buildDir, + dependencyMode, + declaredDependency, + publishedApiInspection); + Map releaseGates = releaseGateEvidence( + projectDir, + buildDir, + persistentEvidenceRoot, + projectVersion, + sourceState.commit, + dependencyMode, + declaredDependency, + dependencyResolution, + compositePath); + Map specification = + specificationEvidence(projectDir, baseline); + Map versionAutomation = + versionAutomationEvidence(projectDir, baseline); + Map namedEvidence = + namedReleaseEvidence(tests); + Map gasExhaustionTraceExamples = + gasExhaustionTraceExamples(projectDir, tests); + namedEvidence.put( + "gasExhaustionTraceExamples", + gasExhaustionTraceExamples); + Map hostedLocalLimitCapability = + hostedLocalLimitCapability(); + Map cyclicProofUnavailabilityCapability = + cyclicProofUnavailabilityCapability(tests); + Map hostLongTrace = + hostLongTraceEvidence(buildDir); + Map hostedOutcomes = + hostedOutcomeEvidence(tests); + Map languageReleaseIdentity = + languageReleaseIdentity( + dependencyResolution, + compositePath, + publishedApiInspection, + declaredDependency); + List representationMatrix = + representationMatrix(tests); + Map representationMatrixResult = + representationMatrixResult( + representationMatrix, + namedEvidence); + Map finalTotals = finalTotals( + tests, + operatorCoverage, + counterCoverage, + normativeVectorCoverage); + List currentModeFailures = + currentModeFailures( + tests, + operatorCoverage, + counterCoverage, + normativeVectorCoverage, + artifacts, + releaseGates, + dependencyResolution, + specification, + versionAutomation, + namedEvidence, + hostedLocalLimitCapability, + cyclicProofUnavailabilityCapability, + sourceState, + hostLongTrace, + hostedOutcomes, + languageReleaseIdentity, + representationMatrixResult); + + if (currentModeFailures.isEmpty()) { + persistModeEvidence( + persistentEvidenceRoot, + dependencyMode, + declaredDependency, + projectVersion, + sourceState, + compositePath, + dependencyResolution, + tests, + normativeVectorCoverage, + artifacts, + projectDir, + specification, + namedEvidence); + } + Map buildModes = buildModeMatrix( + persistentEvidenceRoot, + declaredDependency, + projectVersion, + sourceState, + compositePath, + publishedApiInspection); + releaseGates.put( + "cleanDependencyCacheAcceptance", + cleanDependencyCacheAcceptance(buildModes)); + boolean bothModesPassed = + Boolean.TRUE.equals(buildModes.get("allRequiredModesPassed")); + boolean releaseReady = + currentModeFailures.isEmpty() && bothModesPassed; + + Map report = new LinkedHashMap(); + report.put("schema", "blue-bex-hosted-release-report/2.0"); + report.put("releaseReady", releaseReady); + report.put("currentModeFailures", currentModeFailures); + report.put("commit", sourceState.commit); + report.put("worktreeDirty", sourceState.worktreeDirty); + report.put("sourceState", sourceState.report()); + report.put("baseline", evidenceMap(baseline)); + report.put("finalTotals", finalTotals); + report.put("specification", specification); + report.put("versionAutomation", versionAutomation); + report.put("projectVersion", projectVersion); + report.put("toolchain", map( + "javaVersion", System.getProperty("java.version"), + "javaVendor", System.getProperty("java.vendor"), + "gradleVersion", gradleVersion)); + report.put("dependency", map( + "mode", dependencyMode, + "declaredCoordinate", declaredDependency, + "resolution", dependencyResolution, + "localComposite", + compositeDependencyEvidence(compositePath))); + report.put("hostedStandaloneMatrix", buildModes); + report.put("publishedHostApiInspection", + evidenceMap(publishedApiInspection)); + report.put("languageReleaseIdentity", + languageReleaseIdentity); + report.put("hostedLocalLimitCapability", + hostedLocalLimitCapability); + report.put("cyclicProofUnavailabilityCapability", + cyclicProofUnavailabilityCapability); + report.put("identities", identities()); + report.put("tests", tests.report()); + report.put("operatorCoverage", operatorCoverage); + report.put("counterCoverage", counterCoverage); + report.put("normativeVectorCoverage", + normativeVectorCoverage); + report.put("representationMatrix", representationMatrix); + report.put("representationMatrixResult", + representationMatrixResult); + report.put("cacheMatrix", cacheMatrix(tests)); + report.put("representationInvarianceEvidence", + namedEvidence.get( + "representationInvarianceEvidence")); + report.put("semanticBoundaryInvocationEvidence", + namedEvidence.get("semanticBoundaryInvocationEvidence")); + report.put("ledgerLifecycleEvidence", + namedEvidence.get("ledgerLifecycleEvidence")); + report.put("gasExhaustionEvidence", + namedEvidence.get("gasExhaustionEvidence")); + report.put("gasExhaustionTraceExamples", + gasExhaustionTraceExamples); + report.put("hostedOutcomes", hostedOutcomes); + report.put("hostLongTrace", hostLongTrace); + report.put( + "maximumObservedOrderedTraceEntries", + hostLongTrace.get( + "maximumObservedOrderedTraceEntries")); + report.put("cyclicProofEvidence", + namedEvidence.get("cyclicProofEvidence")); + report.put("intrinsicEvidence", + namedEvidence.get("intrinsicEvidence")); + report.put("referenceEvidenceClassificationEvidence", + namedEvidence.get( + "referenceEvidenceClassificationEvidence")); + report.put("recursionEvidence", recursionEvidence(tests)); + report.put("finiteLoopEvidence", finiteLoopEvidence(tests)); + report.put("artifacts", artifacts); + report.put("releaseGates", releaseGates); + report.put("knownLimitations", knownLimitations( + buildModes, + publishedApiInspection, + hostedLocalLimitCapability, + cyclicProofUnavailabilityCapability, + hostLongTrace, + languageReleaseIdentity, + sourceState)); + report.put("releaseCoordinate", + "blue.bex:blue-bex-java:" + projectVersion); + report.put("releaseIdentity", releaseIdentity( + projectVersion, + sourceState, + declaredDependency, + dependencyResolution, + specification, + artifacts)); + + Path outputRoot = buildDir.resolve("reports") + .resolve("bex-conformance") + .toAbsolutePath().normalize(); + Path output = outputRoot.resolve("report.json"); + Path markdown = outputRoot.resolve("report.md"); + Path readiness = outputRoot.resolve( + "release-readiness.properties"); + Files.createDirectories(outputRoot); + Files.write( + output, + (ConformancePackage.json(report, true) + "\n") + .getBytes(StandardCharsets.UTF_8), + StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING, + StandardOpenOption.WRITE); + Files.write( + markdown, + markdownReport( + report, + baseline, + tests, + operatorCoverage, + counterCoverage, + buildModes, + currentModeFailures, + artifacts, + releaseGates, + publishedApiInspection) + .getBytes(StandardCharsets.UTF_8), + StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING, + StandardOpenOption.WRITE); + writeEvidence( + readiness, + stringMap( + "schema", + "blue-bex-release-readiness/1.0", + "releaseReady", + String.valueOf(releaseReady), + "cyclicProofUnavailabilityCapability", + String.valueOf( + cyclicProofUnavailabilityCapability.get( + "status")), + "reason", + releaseReady + ? "all-required-evidence-passed" + : readinessReason( + currentModeFailures, + bothModesPassed))); + System.out.println("BEX conformance report: " + output); + System.out.println("BEX conformance report: " + markdown); + } + + private static Map identities() { + Map fixture = + ConformancePackage.fixtureManifest(); + Map gas = + ConformancePackage.gasManifest(); + Map registry = + ConformancePackage.registryManifest(); + return map( + "bexRegistry", registry.get("packageIdentity"), + "gasManifest", gas.get("packageIdentity"), + "fixturePackage", fixture.get("packageIdentity"), + "fixtureBindsRegistry", + fixture.get("registryPackageIdentity"), + "fixtureBindsGas", + fixture.get("gasManifestPackageIdentity")); + } + + private static Map finalTotals( + TestEvidence tests, + Map operatorCoverage, + Map counterCoverage, + Map normativeVectorCoverage) { + int passingBehaviorFixtures = 0; + for (ConformancePackage.Fixture fixture + : ConformancePackage.behaviorFixtures()) { + if ("passed".equals(tests.fixtureStatus(fixture.id()))) { + passingBehaviorFixtures++; + } + } + return map( + "tests", tests.report(), + "behaviorFixtures", map( + "required", + ConformancePackage.BEHAVIOR_FIXTURE_COUNT, + "executedAndPassing", + passingBehaviorFixtures), + "gasMicrofixtures", map( + "required", + ConformancePackage.GAS_FIXTURE_COUNT, + "executedAndPassing", + counterCoverage.get( + "passingMicrofixtureCount")), + "normativeVectors", map( + "required", + normativeVectorCoverage.get("required"), + "executedAndPassing", + normativeVectorCoverage.get( + "passingVectorCount"), + "coverageIntegrityTest", + normativeVectorCoverage.get( + "coverageIntegrityTest"), + "allPassing", + normativeVectorCoverage.get("allPassing")), + "operators", map( + "required", ConformancePackage.OPERATOR_COUNT, + "executedAndPassing", + operatorCoverage.get( + "passingOperatorCount"))); + } + + private static Map normativeVectorCoverage( + TestEvidence tests) { + Map source = ConformancePackage.loadMap( + ConformancePackage.FIXTURE_ROOT + + "vector-coverage.yaml"); + Map declarations = ConformancePackage.map( + source.get("vectors"), "vector-coverage.vectors"); + Map fixturesByPath = + new LinkedHashMap< + String, ConformancePackage.Fixture>(); + for (ConformancePackage.Fixture fixture + : ConformancePackage.behaviorFixtures()) { + fixturesByPath.put(fixture.path, fixture); + } + for (ConformancePackage.Fixture fixture + : ConformancePackage.gasFixtures()) { + fixturesByPath.put(fixture.path, fixture); + } + + List matrix = new ArrayList(); + int passing = 0; + for (Map.Entry declaration + : declarations.entrySet()) { + List statuses = new ArrayList(); + List fixtureEvidence = + new ArrayList(); + for (Object pathValue : ConformancePackage.list( + declaration.getValue(), + "vector-coverage." + declaration.getKey())) { + String path = String.valueOf(pathValue); + ConformancePackage.Fixture fixture = + fixturesByPath.get(path); + String status = fixture == null + ? "invalid-reference" + : tests.fixtureStatus(fixture.id()); + statuses.add(status); + fixtureEvidence.add(map( + "path", path, + "fixtureId", + fixture == null ? null : fixture.id(), + "status", status)); + } + String status = aggregateVectorStatus(statuses); + if ("passed".equals(status)) { + passing++; + } + matrix.add(map( + "vector", declaration.getKey(), + "status", status, + "fixtures", fixtureEvidence)); + } + String integrityStatus = tests.namedStatus( + "vectorOperatorAndProjectionCoverageAreClosedAndBidirectional"); + boolean allPassing = + "passed".equals(integrityStatus) + && passing == declarations.size(); + return map( + "required", declarations.size(), + "passingVectorCount", passing, + "coverageIntegrityTest", integrityStatus, + "allPassing", allPassing, + "matrix", matrix); + } + + static String aggregateVectorStatus(List statuses) { + if (statuses.isEmpty() + || statuses.contains("invalid-reference")) { + return "invalid-reference"; + } + if (statuses.contains("failed")) { + return "failed"; + } + if (statuses.contains("skipped")) { + return "skipped"; + } + if (statuses.contains("not-executed")) { + return "not-executed"; + } + for (String status : statuses) { + if (!"passed".equals(status)) { + return "indeterminate"; + } + } + return "passed"; + } + + private static Map dependencyResolutionEvidence( + Path buildDir, + String expectedMode, + String declaredDependency, + Map publishedInspection) + throws Exception { + Path evidencePath = buildDir.resolve("reports") + .resolve("bex-release") + .resolve("dependency-resolution.properties"); + Map evidence = readEvidence(evidencePath); + if (evidence.isEmpty()) { + return map( + "status", "not-executed", + "evidencePresent", false); + } + Path artifact = pathOrNull(evidence.get("artifact.path")); + boolean artifactPresent = + artifact != null && Files.isRegularFile(artifact); + String actualHash = artifactPresent + ? sha256(artifact) + : "unavailable"; + boolean artifactValid = artifactPresent + && actualHash.equals(evidence.get("artifact.sha256")); + boolean standalone = + "standalone-published".equals(expectedMode); + boolean repositoryPolicyValid = + "maven-central-only".equals( + evidence.get("repository.policy")); + boolean provenanceValid = standalone + ? "verified-against-recorded-maven-central-hash" + .equals(evidence.get("provenance.status")) + && declaredDependency.equals( + evidence.get( + "provenance.recorded.coordinate")) + && publishedInspection.get( + "repository").equals( + evidence.get( + "provenance.recorded.repository")) + && publishedInspection.get( + "artifact.sha256").equals( + evidence.get( + "provenance.recorded.sha256")) + && actualHash.equals( + evidence.get( + "provenance.recorded.sha256")) + : "not-applicable-local-composite".equals( + evidence.get("provenance.status")); + boolean passed = + "resolved".equals(evidence.get("status")) + && expectedMode.equals(evidence.get("mode")) + && declaredDependency.equals( + evidence.get("declared.coordinate")) + && artifactValid + && repositoryPolicyValid + && provenanceValid; + return map( + "status", passed ? "passed" : "stale-or-failed", + "evidencePresent", true, + "mode", evidence.get("mode"), + "declaredCoordinate", + evidence.get("declared.coordinate"), + "effectiveComponent", + evidence.get("effective.component"), + "effectiveCoordinate", joinCoordinate( + evidence.get("effective.group"), + evidence.get("effective.name"), + evidence.get("effective.version")), + "artifact", map( + "path", evidence.get("artifact.path"), + "bytes", evidence.get("artifact.bytes"), + "sha256", actualHash, + "matchesRecordedEvidence", artifactValid), + "provenance", map( + "status", evidence.get("provenance.status"), + "repositoryPolicy", + evidence.get("repository.policy"), + "recordedRepository", + evidence.get( + "provenance.recorded.repository"), + "recordedCoordinate", + evidence.get( + "provenance.recorded.coordinate"), + "recordedSha256", + evidence.get( + "provenance.recorded.sha256"), + "resolvedHashMatchesRecordedMavenCentralHash", + provenanceValid && standalone, + "networkFetchObservation", + evidence.get( + "provenance.networkFetchObservation")), + "cleanDependencyCacheAcceptance", map( + "status", evidence.get("cache.acceptance"), + "scope", + evidence.get("cache.acceptanceScope"), + "moduleVersionPath", + evidence.get( + "cache.blueLanguageModuleVersionPath"), + "moduleVersionInitiallyAbsentAtProjectConfiguration", + Boolean.parseBoolean(evidence.get( + "cache.blueLanguageModuleVersionInitiallyAbsent")), + "reason", + "passed".equals( + evidence.get("cache.acceptance")) + ? "exact-blue-language-module-version-cache-was-absent-before-resolution" + : "exact-blue-language-module-version-cache-was-not-proven-absent-before-resolution"), + "compositePath", evidence.get("composite.path")); + } + + private static String joinCoordinate( + String group, + String name, + String version) { + if (group == null || name == null || version == null) { + return "unavailable"; + } + return group + ":" + name + ":" + version; + } + + private static Map specificationEvidence( + Path projectDir, + Map baseline) throws IOException { + Path specification = projectDir.resolve("specifications") + .resolve("blue-bex-specification-2.0.md"); + String actual = Files.isRegularFile(specification) + ? sha256(specification) + : "unavailable"; + String expected = baseline.get("specificationSha256"); + return map( + "path", + "specifications/blue-bex-specification-2.0.md", + "sha256", actual, + "baselineSha256", expected, + "matchesBaseline", + actual.equals(expected)); + } + + private static Map versionAutomationEvidence( + Path projectDir, + Map baseline) throws IOException { + Path czToml = projectDir.resolve(".cz.toml"); + String actual = Files.isRegularFile(czToml) + ? sha256(czToml) + : "unavailable"; + String expected = baseline.get("czTomlSha256"); + return map( + "path", ".cz.toml", + "sha256", actual, + "baselineSha256", expected, + "unchanged", actual.equals(expected)); + } + + private static Map namedReleaseEvidence( + TestEvidence tests) { + Map evidence = + new LinkedHashMap(); + evidence.put( + "semanticBoundaryInvocationEvidence", + tests.namedEvidence( + "standaloneAndCustomBoundariesReturnTheirFrozenExactResult", + "admittedScalarsRetainTheirRawKindsEqualityAndTruthiness", + "hostNormalizationWinsWhileMatchingExactDescendantsStayLocal", + "invokesBoundaryExactlyOnceForEveryDistinctTransientSemanticValue", + "reusesAliasesButAdmitsEveryRecreatedTransientStructure", + "nodeIdentityAdmissionIsReusedByPatchEventOverlayAndRootOutput", + "outputAdmissionIsReusedByALaterNodeIdentityRequest", + "ordinaryNonCyclicExactRootBypassesHostSemanticBoundary", + "blue.bex.BexCompositeExhaustionEvidenceTest" + + "#transientNodeBlueIdStopsBeforeSemanticIdentityBoundary", + "preservesHostFailureClassificationWithoutBexWrapping", + "processorExecutionContextUsesItsInvocationSemanticOutputBoundary")); + evidence.put( + "representationInvarianceEvidence", + tests.namedEvidence( + "programDocumentAndEventAreInvariantAcrossPhysicalRepresentations", + "coldBatchedRepresentationsUseIndependentRuntimeCaches", + "fixtureAdapterActuallyExecutesDeclaredBatchingPreparation")); + evidence.put( + "ledgerLifecycleEvidence", + tests.classAndNamedEvidence( + new String[] { + "BexHostedRuntimeWorkSession", + "BexExecutionEvidenceLedger" + }, + "blue.language.processor.BexHostedRuntimeWorkSessionTest" + + "#providerUnavailableUsesHostedSuspensionAndRestoresParentBudget")); + evidence.put( + "gasExhaustionEvidence", + tests.classAndNamedEvidence( + new String[] { + "BexCompositeExhaustion", + "BexPrimitiveExhaustionEvidence" + }, + "blue.bex.BexCompositeExhaustionEvidenceTest" + + "#exhaustionIsStableAcrossInlineColdReferenceAndWarmReferenceDocuments", + "blue.bex.BexCompositeExhaustionEvidenceTest" + + "#rejectedPatchAppendDoesNotMutateChangesetOrOverlay")); + evidence.put( + "cyclicProofEvidence", + tests.namedEvidence( + "exactCyclicMemberStaysOpaqueAndTransientCounterfeitsFailClosed", + "cyclicMemberStructuralReadRequiresAndAcceptsCompleteSetProof", + "cyclicMemberContentFetchUnavailabilityStopsBeforeProofQuery", + "nullCyclicProofAfterFoundContentIsInvalidNotUnavailable", + "malformedCyclicProofIsDeterministicInvalidEvidence", + "blue.bex.BexExecutionEvidenceLedgerTest" + + "#hostedCyclicStructuralReadWithMissingProofIsDeterministic", + "blue.bex.BexExactReferenceDocumentTest" + + "#nestedCyclicResolvedBodyRemainsOpaqueUntilCompleteProof", + "hostedOpaqueCyclicMemberSupportsIdentityAndOutputWithoutProofDemand")); + evidence.put( + "intrinsicEvidence", + tests.namedEvidence( + "duplicateIntrinsicBlueIdIsRejectedInsteadOfReplacingRegistration", + "intrinsicCounterCatalogAndInvocationFieldsAreImmutableSnapshots", + "rejectedNamedChargePreventsAllLaterIntrinsicWork", + "intrinsicUnavailableUsesHostedDiscardLifecycle", + "intrinsicInvalidEvidenceUsesHostedDeterministicLifecycle", + "arbitraryIntrinsicFailureIsNotReclassifiedAsUnavailable", + "intrinsicExactFieldUsesSharedSemanticAdmissionAndMemoization", + "intrinsicBoundaryExposesNoLedgerOrPortableGasEvidencePath", + "blue.bex.api.Bex20ApiSurfaceTest" + + "#intrinsicRegistrationAlwaysRequiresRegistryIdentityAndNamedWeights", + "blue.bex.BexIntrinsicTest" + + "#registryIdentityEncodingAndRuntimeNamespacesAreUnambiguous", + "blue.bex.BexIntrinsicTest" + + "#unsupportedIntrinsicFailsAtCompileTime")); + evidence.put( + "referenceEvidenceClassificationEvidence", + tests.namedEvidence( + "providerNotFoundIsIncompleteExecutionEvidenceNotSemanticAbsence", + "providerUnavailableRetainsItsDiagnosticAndRequiredIdentity", + "invalidProviderEvidenceIsASeparateDeterministicFailure", + "foundContentWithMismatchedIdentityIsInvalidEvidence", + "arbitraryProviderBugIsNeverReclassifiedAsTransientUnavailability", + "priorValidMaterializationDoesNotHideAChangedProviderOutcome")); + return evidence; + } + + private static Map gasExhaustionTraceExamples( + Path projectDir, + TestEvidence tests) throws IOException { + Path relativePath = Paths.get( + "src/test/resources/hosted-release/" + + "gas-exhaustion-trace-examples.properties"); + Path evidencePath = projectDir.resolve(relativePath) + .toAbsolutePath().normalize(); + Map expected = + readEvidence(evidencePath); + List definitionFailures = + new ArrayList(); + if (!"blue-bex-gas-exhaustion-trace-examples/1.0" + .equals(expected.get("schema"))) { + definitionFailures.add("unknown-schema"); + } + String testClass = expected.get("test.class"); + if (!"blue.bex.BexPrimitiveExhaustionEvidenceTest" + .equals(testClass)) { + definitionFailures.add("unexpected-test-class"); + } + + Map manifestCounters = + ConformancePackage.map( + ConformancePackage.gasManifest().get( + "counters"), + "gas-manifest.counters"); + String admittedNamespace = + expected.get("admittedTrace.namespace"); + String admittedCounter = + expected.get("admittedTrace.counterName"); + long admittedQuantity = parseLong( + expected.get("admittedTrace.quantity")); + long admittedWeight = parseLong( + expected.get("admittedTrace.weight")); + long admittedTraceSize = parseLong( + expected.get("admittedTrace.size")); + long admittedTraceTotal = parseLong( + expected.get("admittedTrace.totalGas")); + Object manifestAdmittedWeight = + manifestCounters.get(admittedCounter); + if (!"bex".equals(admittedNamespace) + || !"expressionEvaluated".equals(admittedCounter) + || admittedQuantity != 1L + || !(manifestAdmittedWeight instanceof Number) + || admittedWeight + != ((Number) manifestAdmittedWeight).longValue() + || admittedTraceSize != 1L + || admittedTraceTotal + != admittedQuantity * admittedWeight) { + definitionFailures.add("invalid-admitted-prefix"); + } + + String rawIds = expected.get("example.ids"); + List ids = new ArrayList(); + Set uniqueIds = new LinkedHashSet(); + if (rawIds != null) { + for (String rawId : rawIds.split(",")) { + String id = rawId.trim(); + if (!id.isEmpty()) { + ids.add(id); + if (!uniqueIds.add(id)) { + definitionFailures.add( + "duplicate-example-id:" + id); + } + } + } + } + if (ids.isEmpty()) { + definitionFailures.add("no-examples"); + } + + List examples = new ArrayList(); + List requiredSelectors = + new ArrayList(); + int matchedTestCount = 0; + boolean allExamplesPassed = !ids.isEmpty(); + for (String id : ids) { + String prefix = "example." + id + "."; + String testName = + expected.get(prefix + "testName"); + String namespace = + expected.get(prefix + "namespace"); + String counterName = + expected.get(prefix + "counterName"); + long quantity = parseLong( + expected.get(prefix + "quantity")); + long weight = parseLong( + expected.get(prefix + "weight")); + long admittedGas = parseLong( + expected.get(prefix + "admittedGas")); + long effectiveBudget = parseLong( + expected.get(prefix + "effectiveBudget")); + String rejectedChargePresentText = + expected.get( + prefix + "rejectedChargePresent"); + boolean rejectedChargePresent = + Boolean.parseBoolean( + rejectedChargePresentText); + long laterWorkCount = parseLong( + expected.get(prefix + "laterWorkCount")); + + List exampleFailures = + new ArrayList(); + Object manifestWeight = + manifestCounters.get(counterName); + if (!"bex".equals(namespace)) { + exampleFailures.add("namespace"); + } + if (counterName == null + || !counterName.equals(testName)) { + exampleFailures.add("counter-test-selector"); + } + if (!(manifestWeight instanceof Number) + || weight != ((Number) manifestWeight) + .longValue()) { + exampleFailures.add("manifest-weight"); + } + if (quantity != 1L) { + exampleFailures.add("quantity"); + } + if (admittedGas != admittedTraceTotal + || effectiveBudget != admittedTraceTotal) { + exampleFailures.add( + "admitted-gas-or-effective-budget"); + } + if (weight <= 0L + || quantity <= 0L + || admittedGas + quantity * weight + <= effectiveBudget) { + exampleFailures.add( + "charge-would-not-exhaust-budget"); + } + if (!"false".equals( + rejectedChargePresentText)) { + exampleFailures.add( + "rejected-charge-absence"); + } + if (laterWorkCount != 0L) { + exampleFailures.add("later-work"); + } + + Map test = + tests.exactEvidence(testClass, testName); + int matched = asInteger( + test.get("matchedTestCount")) == null + ? 0 + : asInteger( + test.get("matchedTestCount")); + matchedTestCount += matched; + String executionStatus = + String.valueOf(test.get("status")); + String status = exampleFailures.isEmpty() + ? executionStatus + : "invalid-source-controlled-evidence"; + allExamplesPassed &= "passed".equals(status); + for (String failure : exampleFailures) { + definitionFailures.add( + id + ":" + failure); + } + requiredSelectors.add(map( + "className", testClass, + "testName", testName, + "status", executionStatus, + "matchedTestCount", matched)); + examples.add(map( + "id", id, + "status", status, + "namespace", namespace, + "counterName", counterName, + "quantity", quantity, + "weight", weight, + "admittedGas", admittedGas, + "effectiveBudget", effectiveBudget, + "rejectedChargePresent", + rejectedChargePresent, + "rejectedChargeAbsent", + !rejectedChargePresent, + "laterWorkCount", laterWorkCount, + "noLaterWork", laterWorkCount == 0L, + "admittedTrace", map( + "namespace", admittedNamespace, + "counterName", admittedCounter, + "quantity", admittedQuantity, + "weight", admittedWeight, + "size", admittedTraceSize, + "totalGas", admittedTraceTotal), + "executedTest", test, + "definitionFailures", + exampleFailures)); + } + boolean definitionValid = + definitionFailures.isEmpty(); + return map( + "status", + definitionValid && allExamplesPassed + ? "passed" + : "not-passing", + "scope", + "source-controlled-exact-expected-values-confirmed-by-executed-dynamic-tests", + "sourceControlledEvidence", map( + "path", unix(relativePath), + "sha256", + Files.isRegularFile(evidencePath) + ? sha256(evidencePath) + : "unavailable", + "schema", expected.get("schema"), + "definitionValid", definitionValid, + "definitionFailures", + definitionFailures), + "matchedTestCount", matchedTestCount, + "requiredSelectors", requiredSelectors, + "examples", examples); + } + + private static List currentModeFailures( + TestEvidence tests, + Map operatorCoverage, + Map counterCoverage, + Map normativeVectorCoverage, + List artifacts, + Map releaseGates, + Map dependencyResolution, + Map specification, + Map versionAutomation, + Map namedEvidence, + Map hostedLocalLimitCapability, + Map cyclicProofUnavailabilityCapability, + SourceState sourceState, + Map hostLongTrace, + Map hostedOutcomes, + Map languageReleaseIdentity, + Map representationMatrixResult) { + List failures = new ArrayList(); + require( + failures, + "passed".equals(tests.overallStatus()), + "ordinary-tests-not-passing-with-zero-skips"); + int passingBehavior = 0; + for (ConformancePackage.Fixture fixture + : ConformancePackage.behaviorFixtures()) { + if ("passed".equals(tests.fixtureStatus(fixture.id()))) { + passingBehavior++; + } + } + require( + failures, + passingBehavior + == ConformancePackage.BEHAVIOR_FIXTURE_COUNT, + "behavior-fixtures-not-105-of-105"); + require( + failures, + Boolean.TRUE.equals( + counterCoverage.get("allMicrofixturesPassing")), + "gas-microfixtures-not-30-of-30"); + require( + failures, + Boolean.TRUE.equals( + operatorCoverage.get( + "allExecutedOperatorsPassing")) + && Integer.valueOf(86).equals( + asInteger( + operatorCoverage.get( + "passingOperatorCount"))), + "operator-coverage-not-86-of-86"); + require( + failures, + Boolean.TRUE.equals( + normativeVectorCoverage.get("allPassing")), + "normative-vectors-not-all-executed-and-passing"); + require( + failures, + artifacts.size() == 4, + "main-sources-javadoc-source-release-artifacts-not-all-present"); + require( + failures, + "passed".equals(dependencyResolution.get("status")), + "dependency-resolution-evidence-not-passing"); + if ("standalone-published".equals( + dependencyResolution.get("mode"))) { + require( + failures, + "passed".equals(castMap( + dependencyResolution.get( + "cleanDependencyCacheAcceptance")) + .get("status")), + "blue-language-module-version-cache-acceptance-not-executed"); + } + require( + failures, + gatePassed(releaseGates, "deterministicArchives"), + "archive-packaging-determinism-gate-not-passing"); + require( + failures, + gatePassed( + releaseGates, + "independentCleanBuilds"), + "independent-clean-build-reproducibility-gate-not-passing"); + require( + failures, + gatePassed(releaseGates, "binaryApi"), + "binary-api-gate-not-passing"); + require( + failures, + gatePassed(releaseGates, "benchmarkCompilation"), + "benchmark-compilation-gate-not-passing"); + require( + failures, + gatePassed(releaseGates, "java8Bytecode"), + "packaged-java8-bytecode-gate-not-passing"); + require( + failures, + Boolean.TRUE.equals(specification.get("matchesBaseline")), + "specification-identity-differs-from-baseline"); + require( + failures, + Boolean.TRUE.equals(versionAutomation.get("unchanged")), + "cz-toml-differs-from-baseline"); + require( + failures, + "1.8".equals(System.getProperty( + "java.specification.version")), + "report-not-running-on-java-8"); + require( + failures, + "passed".equals( + hostedLocalLimitCapability.get("status")), + "current-host-shared-local-limit-capability-unavailable"); + require( + failures, + "passed".equals( + cyclicProofUnavailabilityCapability.get("status")), + "current-host-cyclic-proof-unavailability-capability-unavailable"); + require( + failures, + sourceState.uncommittedReleasePaths.isEmpty(), + "bex-release-inputs-not-represented-by-reported-commit"); + require( + failures, + sourceState.completeWorkspace(), + "bex-source-workspace-incomplete"); + require( + failures, + "passed".equals(hostLongTrace.get("status")), + "host-long-trace-not-over-256"); + require( + failures, + "passed".equals(hostedOutcomes.get("status")), + "hosted-outcome-matrix-not-passing"); + require( + failures, + Boolean.TRUE.equals( + languageReleaseIdentity.get( + "exactFinalArtifactProven")), + "exact-final-language-artifact-not-proven"); + require( + failures, + "passed".equals( + representationMatrixResult.get("status")), + "representation-matrix-not-passing"); + for (Map.Entry entry + : namedEvidence.entrySet()) { + Map group = castMap(entry.getValue()); + require( + failures, + "passed".equals(group.get("status")), + entry.getKey() + "-not-passing"); + } + return failures; + } + + private static Map hostLongTraceEvidence( + Path buildDir) throws IOException { + Path evidencePath = buildDir.resolve("reports") + .resolve("bex-release") + .resolve("host-long-trace.properties"); + Map evidence = + readEvidence(evidencePath); + if (evidence.isEmpty()) { + return map( + "status", "not-executed", + "evidencePath", evidencePath.toString(), + "maximumObservedOrderedTraceEntries", 0L, + "failures", + Collections.singletonList( + "host-long-trace-probe-not-executed")); + } + List failures = new ArrayList(); + require( + failures, + "blue-bex-host-long-trace-evidence/1.0" + .equals(evidence.get("schema")), + "unknown-host-long-trace-evidence-schema"); + long required = parseLong(evidence.get( + "requiredMinimumOrderedTraceEntries")); + long observed = parseLong(evidence.get( + "maximumObservedOrderedTraceEntries")); + require( + failures, + "passed".equals(evidence.get("status")), + "host-rejected-long-ordered-trace"); + require( + failures, + Boolean.parseBoolean( + evidence.get("orderPreserved")), + "host-long-trace-order-not-preserved"); + require( + failures, + required > 256L && observed >= required, + "host-long-trace-did-not-exceed-256"); + return map( + "status", + failures.isEmpty() + ? "passed" + : "blocking", + "evidencePath", evidencePath.toString(), + "requestedItems", + parseLong(evidence.get("requestedItems")), + "requiredMinimumOrderedTraceEntries", + required, + "maximumObservedOrderedTraceEntries", + observed, + "observedDistinctCounterKinds", + parseLong(evidence.get( + "observedDistinctCounterKinds")), + "orderPreserved", + Boolean.parseBoolean( + evidence.get("orderPreserved")), + "observedFailureClass", + evidence.get("failure.class"), + "portableLimit", map( + "name", + evidence.get("portableLimit.name"), + "observed", + evidence.get("portableLimit.observed"), + "limit", + evidence.get("portableLimit.limit")), + "failures", failures); + } + + private static Map hostedOutcomeEvidence( + TestEvidence tests) { + Map success = + tests.namedEvidence( + "successfulRuntimeSubmitsEverySeparatedLedgerExactlyOnce"); + Map deterministicFailure = + tests.namedEvidence( + "deterministicFailureDiscardsBufferedOutputsAndLeavesPrefixForOwner"); + Map unavailable = + tests.namedEvidence( + "providerUnavailableUsesHostedSuspensionAndRestoresParentBudget"); + Map exhaustion = + tests.namedEvidence( + "hostedGasExhaustionRetainsExactBexPrefixAndNoOutput"); + Map failedOutputAdmission = + tests.namedEvidence( + "failedAdmissionMutatesNeitherPatchNorEventBuffers"); + boolean passed = + "passed".equals(success.get("status")) + && "passed".equals( + deterministicFailure.get("status")) + && "passed".equals( + unavailable.get("status")) + && "passed".equals( + exhaustion.get("status")) + && "passed".equals( + failedOutputAdmission.get("status")); + return map( + "status", passed ? "passed" : "not-passing", + "success", success, + "deterministicFailure", + deterministicFailure, + "transientEvidenceUnavailability", + unavailable, + "gasExhaustion", exhaustion, + "failedSemanticAdmissionRollback", + failedOutputAdmission); + } + + private static Map languageReleaseIdentity( + Map dependencyResolution, + Path compositePath, + Map publishedApiInspection, + String declaredDependency) throws Exception { + String publishedCommit = + publishedApiInspection.get("source.commit"); + boolean commitIdentified = publishedCommit != null + && publishedCommit.matches("[0-9a-fA-F]{40}"); + String publishedHash = + publishedApiInspection.get("artifact.sha256"); + boolean hashIdentified = publishedHash != null + && publishedHash.matches("[0-9a-fA-F]{64}"); + boolean compatible = + "compatible-with-final-hosted-adapter".equals( + publishedApiInspection.get("status")); + boolean coordinateMatches = + declaredDependency.equals( + publishedApiInspection.get("coordinate")); + + Map localSource = + Collections.emptyMap(); + boolean localMatchesPublished = compositePath == null; + if (compositePath != null + && Files.isDirectory(compositePath)) { + SourceState state = sourceState(compositePath); + localMatchesPublished = + commitIdentified + && !state.worktreeDirty + && state.completeWorkspace() + && publishedCommit.equalsIgnoreCase( + state.commit); + localSource = state.report(); + } + Map resolvedArtifact = + castMap(dependencyResolution.get("artifact")); + boolean dependencyResolved = + "passed".equals( + dependencyResolution.get("status")); + boolean standalone = + "standalone-published".equals( + dependencyResolution.get("mode")); + boolean resolvedArtifactExact = + !standalone + || publishedHash != null + && publishedHash.equals( + resolvedArtifact.get("sha256")); + boolean exactFinalArtifactProven = + commitIdentified + && hashIdentified + && compatible + && coordinateMatches + && localMatchesPublished + && dependencyResolved + && resolvedArtifactExact; + return map( + "schema", + "blue-bex-language-release-identity/1.0", + "exactFinalArtifactProven", + exactFinalArtifactProven, + "declaredCoordinate", declaredDependency, + "publishedCoordinate", + publishedApiInspection.get("coordinate"), + "publishedArtifactSha256", publishedHash, + "publishedSourceCommit", + commitIdentified + ? publishedCommit.toLowerCase() + : "unavailable", + "publishedApiStatus", + publishedApiInspection.get("status"), + "dependencyResolutionPassed", + dependencyResolved, + "resolvedArtifactMatchesPublishedHash", + resolvedArtifactExact, + "resolvedArtifact", resolvedArtifact, + "localCompositeSource", localSource, + "localCompositeMatchesPublishedCommit", + localMatchesPublished, + "failures", + exactFinalArtifactProven + ? Collections.emptyList() + : java.util.Arrays.asList( + commitIdentified + ? null + : "published-source-commit-unavailable", + hashIdentified + ? null + : "published-artifact-hash-unavailable", + compatible + ? null + : "published-api-not-final-compatible", + coordinateMatches + ? null + : "published-coordinate-mismatch", + dependencyResolved + ? null + : "dependency-resolution-not-passing", + resolvedArtifactExact + ? null + : "resolved-artifact-hash-mismatch", + localMatchesPublished + ? null + : "local-composite-not-clean-exact-published-commit") + .stream() + .filter(Objects::nonNull) + .collect(Collectors.toList())); + } + + private static Map representationMatrixResult( + List matrix, + Map namedEvidence) { + List failures = new ArrayList(); + for (Object value : matrix) { + Map entry = castMap(value); + if (!"passed".equals(entry.get("status"))) { + failures.add( + String.valueOf( + entry.get("fixtureId"))); + } + } + Map differential = + castMap(namedEvidence.get( + "representationInvarianceEvidence")); + if (!"passed".equals(differential.get("status"))) { + failures.add( + "representation-invariance-differential"); + } + return map( + "status", + failures.isEmpty() + ? "passed" + : "not-passing", + "matrixEntryCount", matrix.size(), + "differentialEvidence", differential, + "failures", failures); + } + + private static void require( + List failures, + boolean condition, + String failure) { + if (!condition) { + failures.add(failure); + } + } + + private static Integer asInteger(Object value) { + return value instanceof Number + ? ((Number) value).intValue() + : null; + } + + private static boolean gatePassed( + Map releaseGates, + String name) { + Map gate = + castMap(releaseGates.get(name)); + return "passed".equals(gate.get("status")); + } + + @SuppressWarnings("unchecked") + private static Map castMap(Object value) { + if (!(value instanceof Map)) { + return Collections.emptyMap(); + } + return (Map) value; + } + + private static void persistModeEvidence( + Path root, + String mode, + String declaredDependency, + String projectVersion, + SourceState sourceState, + Path compositePath, + Map dependencyResolution, + TestEvidence tests, + Map normativeVectorCoverage, + List artifacts, + Path projectDir, + Map specification, + Map namedEvidence) throws Exception { + Path modeRoot = root.resolve("modes").resolve(mode); + Path artifactRoot = modeRoot.resolve("artifacts"); + Files.createDirectories(artifactRoot); + Map values = + new LinkedHashMap(); + values.put( + "schema", + "blue-bex-build-mode-evidence/2.0"); + values.put("status", "passed"); + values.put("mode", mode); + values.put("declared.coordinate", declaredDependency); + values.put("project.version", projectVersion); + values.put("bex.commit", sourceState.commit); + values.put( + "bex.workspaceSha256", + sourceState.fingerprint.sha256); + values.put( + "bex.releaseSourceSha256", + sourceState.releaseFingerprint.sha256); + values.put( + "specification.sha256", + String.valueOf(specification.get("sha256"))); + values.put( + "tests.executed", + String.valueOf(tests.cases.size())); + values.put( + "tests.passed", + String.valueOf(tests.count("passed"))); + values.put("tests.failed", "0"); + values.put("tests.skipped", "0"); + values.put( + "behaviorFixtures", + String.valueOf( + ConformancePackage.BEHAVIOR_FIXTURE_COUNT)); + values.put( + "gasMicrofixtures", + String.valueOf( + ConformancePackage.GAS_FIXTURE_COUNT)); + values.put( + "normativeVectors", + String.valueOf( + normativeVectorCoverage.get( + "passingVectorCount"))); + values.put( + "operators", + String.valueOf( + ConformancePackage.OPERATOR_COUNT)); + values.put( + "namedEvidence.status", + allNamedEvidencePassed(namedEvidence) + ? "passed" + : "failed"); + values.put( + "dependency.effectiveCoordinate", + String.valueOf( + dependencyResolution.get( + "effectiveCoordinate"))); + Map dependencyProvenance = + castMap(dependencyResolution.get("provenance")); + values.put( + "dependency.provenance.status", + String.valueOf( + dependencyProvenance.get("status"))); + values.put( + "dependency.provenance.repositoryPolicy", + String.valueOf( + dependencyProvenance.get( + "repositoryPolicy"))); + values.put( + "dependency.provenance.recordedRepository", + String.valueOf( + dependencyProvenance.get( + "recordedRepository"))); + values.put( + "dependency.provenance.recordedSha256", + String.valueOf( + dependencyProvenance.get( + "recordedSha256"))); + values.put( + "dependency.cache.acceptance", + String.valueOf(castMap( + dependencyResolution.get( + "cleanDependencyCacheAcceptance")) + .get("status"))); + values.put( + "dependency.cache.acceptanceScope", + String.valueOf(castMap( + dependencyResolution.get( + "cleanDependencyCacheAcceptance")) + .get("scope"))); + values.put( + "dependency.cache.blueLanguageModuleVersionPath", + String.valueOf(castMap( + dependencyResolution.get( + "cleanDependencyCacheAcceptance")) + .get("moduleVersionPath"))); + values.put( + "dependency.cache.blueLanguageModuleVersionInitiallyAbsent", + String.valueOf(castMap( + dependencyResolution.get( + "cleanDependencyCacheAcceptance")) + .get( + "moduleVersionInitiallyAbsentAtProjectConfiguration"))); + + Map resolvedArtifact = + castMap(dependencyResolution.get("artifact")); + Path dependencyArtifact = pathOrNull( + String.valueOf(resolvedArtifact.get("path"))); + if (dependencyArtifact == null + || !Files.isRegularFile(dependencyArtifact)) { + throw new IllegalStateException( + "Resolved dependency artifact disappeared"); + } + Path dependencyCopy = + artifactRoot.resolve("blue-language-java.jar"); + Files.copy( + dependencyArtifact, + dependencyCopy, + java.nio.file.StandardCopyOption.REPLACE_EXISTING); + values.put( + "dependency.artifact.path", + dependencyCopy.toAbsolutePath().normalize().toString()); + values.put( + "dependency.artifact.sha256", + sha256(dependencyCopy)); + + for (Object artifactValue : artifacts) { + Map artifact = castMap(artifactValue); + String relativePath = + String.valueOf(artifact.get("path")); + Path source = projectDir.resolve(relativePath) + .toAbsolutePath().normalize(); + String key = artifactKey(relativePath); + Path copy = artifactRoot.resolve( + source.getFileName().toString()); + Files.copy( + source, + copy, + java.nio.file.StandardCopyOption.REPLACE_EXISTING); + values.put( + "artifact." + key + ".path", + copy.toString()); + values.put( + "artifact." + key + ".sha256", + sha256(copy)); + } + + if ("local-composite".equals(mode)) { + if (compositePath == null + || !Files.isDirectory(compositePath)) { + throw new IllegalStateException( + "Local composite mode has no source directory"); + } + SourceState dependencySource = + sourceState(compositePath); + values.put( + "composite.path", + compositePath.toString()); + values.put( + "composite.commit", + dependencySource.commit); + values.put( + "composite.workspaceSha256", + dependencySource.fingerprint.sha256); + values.put( + "composite.releaseSourceSha256", + dependencySource + .releaseFingerprint.sha256); + values.put( + "composite.releaseInputsCommitted", + String.valueOf( + !dependencySource.worktreeDirty + && dependencySource + .completeWorkspace())); + } + writeEvidence(modeRoot.resolve("mode.properties"), values); + } + + private static boolean allNamedEvidencePassed( + Map namedEvidence) { + for (Object value : namedEvidence.values()) { + if (!"passed".equals( + castMap(value).get("status"))) { + return false; + } + } + return true; + } + + private static String artifactKey(String path) { + if (path.endsWith("-source-release.zip")) { + return "sourceRelease"; + } + if (path.endsWith("-sources.jar")) { + return "sources"; + } + if (path.endsWith("-javadoc.jar")) { + return "javadoc"; + } + return "main"; + } + + private static Map buildModeMatrix( + Path root, + String declaredDependency, + String projectVersion, + SourceState sourceState, + Path activeCompositePath, + Map publishedApiInspection) + throws Exception { + Map standalone = validateModeEvidence( + root, + "standalone-published", + declaredDependency, + projectVersion, + sourceState, + activeCompositePath, + publishedApiInspection); + Map local = validateModeEvidence( + root, + "local-composite", + declaredDependency, + projectVersion, + sourceState, + activeCompositePath, + publishedApiInspection); + boolean bothPassed = + "passed".equals(standalone.get("status")) + && "passed".equals(local.get("status")); + boolean artifactsEquivalent = bothPassed + && artifactHashes(standalone).equals( + artifactHashes(local)); + boolean allRequired = + bothPassed && artifactsEquivalent; + return map( + "standalonePublished", standalone, + "localComposite", local, + "artifactsBehaviorallyEquivalent", + artifactsEquivalent, + "allRequiredModesPassed", allRequired, + "standaloneBlocker", + "passed".equals(standalone.get("status")) + ? Collections.emptyList() + : missingPublishedHostApis( + publishedApiInspection)); + } + + private static Map validateModeEvidence( + Path root, + String expectedMode, + String declaredDependency, + String projectVersion, + SourceState sourceState, + Path activeCompositePath, + Map publishedApiInspection) + throws Exception { + Path evidencePath = root.resolve("modes") + .resolve(expectedMode) + .resolve("mode.properties"); + Map evidence = + readEvidence(evidencePath); + if (evidence.isEmpty()) { + return map( + "mode", expectedMode, + "status", "not-executed", + "evidencePresent", false, + "evidencePath", evidencePath.toString(), + "cleanDependencyCacheAcceptance", map( + "status", "not-executed", + "scope", + "standalone-published-blue-language-module-version-cache", + "reason", + "no-validated-standalone-mode-evidence")); + } + List failures = new ArrayList(); + require( + failures, + "blue-bex-build-mode-evidence/2.0".equals( + evidence.get("schema")), + "unknown-evidence-schema"); + require( + failures, + "passed".equals(evidence.get("status")), + "mode-run-did-not-pass"); + require( + failures, + expectedMode.equals(evidence.get("mode")), + "mode-mismatch"); + require( + failures, + declaredDependency.equals( + evidence.get("declared.coordinate")), + "declared-dependency-mismatch"); + require( + failures, + projectVersion.equals( + evidence.get("project.version")), + "project-version-mismatch"); + require( + failures, + sourceState.commit.equals( + evidence.get("bex.commit")), + "bex-commit-changed-since-mode-run"); + require( + failures, + sourceState.releaseFingerprint.sha256.equals( + evidence.get( + "bex.releaseSourceSha256")), + "bex-source-state-changed-since-mode-run"); + require( + failures, + "0".equals(evidence.get("tests.failed")) + && "0".equals( + evidence.get("tests.skipped")), + "test-failure-or-skip-recorded"); + require( + failures, + String.valueOf( + ConformancePackage.BEHAVIOR_FIXTURE_COUNT) + .equals( + evidence.get("behaviorFixtures")) + && String.valueOf( + ConformancePackage.GAS_FIXTURE_COUNT) + .equals( + evidence.get("gasMicrofixtures")) + && String.valueOf( + ConformancePackage.VECTOR_COUNT) + .equals( + evidence.get("normativeVectors")) + && String.valueOf( + ConformancePackage.OPERATOR_COUNT) + .equals( + evidence.get("operators")), + "conformance-total-mismatch"); + require( + failures, + "passed".equals( + evidence.get("namedEvidence.status")), + "named-hosted-evidence-not-passing"); + if ("standalone-published".equals(expectedMode)) { + require( + failures, + declaredDependency.equals( + evidence.get( + "dependency.effectiveCoordinate")), + "standalone-did-not-resolve-declared-coordinate"); + require( + failures, + "verified-against-recorded-maven-central-hash" + .equals(evidence.get( + "dependency.provenance.status")), + "standalone-maven-central-provenance-not-verified"); + require( + failures, + "maven-central-only".equals(evidence.get( + "dependency.provenance.repositoryPolicy")), + "standalone-repository-policy-not-maven-central-only"); + require( + failures, + publishedApiInspection.get("repository").equals( + evidence.get( + "dependency.provenance.recordedRepository")) + && publishedApiInspection.get( + "artifact.sha256").equals( + evidence.get( + "dependency.provenance.recordedSha256")), + "standalone-recorded-provenance-mismatch"); + require( + failures, + "passed".equals(evidence.get( + "dependency.cache.acceptance")), + "standalone-blue-language-module-version-cache-not-accepted"); + require( + failures, + "standalone-published-blue-language-module-version-cache" + .equals(evidence.get( + "dependency.cache.acceptanceScope")), + "standalone-module-version-cache-scope-mismatch"); + require( + failures, + Boolean.parseBoolean(evidence.get( + "dependency.cache.blueLanguageModuleVersionInitiallyAbsent")), + "standalone-module-version-cache-was-not-initially-absent"); + } + + Map artifactReports = + new LinkedHashMap(); + for (String kind + : new String[] { + "main", "sources", "javadoc", "sourceRelease" + }) { + Map checked = + checkAbsoluteFile( + evidence.get( + "artifact." + kind + ".path"), + evidence.get( + "artifact." + kind + ".sha256")); + artifactReports.put(kind, checked); + require( + failures, + Boolean.TRUE.equals( + checked.get("matchesRecordedEvidence")), + kind + "-artifact-evidence-stale"); + } + Map dependencyArtifact = + checkAbsoluteFile( + evidence.get("dependency.artifact.path"), + evidence.get( + "dependency.artifact.sha256")); + require( + failures, + Boolean.TRUE.equals( + dependencyArtifact.get( + "matchesRecordedEvidence")), + "dependency-artifact-evidence-stale"); + if ("standalone-published".equals(expectedMode)) { + require( + failures, + publishedApiInspection.get("artifact.sha256") + .equals(dependencyArtifact.get("sha256")), + "standalone-dependency-artifact-hash-not-maven-central"); + } + + Map compositeSource = + Collections.emptyMap(); + if ("local-composite".equals(expectedMode)) { + Path recordedComposite = + pathOrNull(evidence.get("composite.path")); + Path composite = activeCompositePath != null + ? activeCompositePath + : recordedComposite; + boolean samePath = composite != null + && recordedComposite != null + && composite.equals(recordedComposite); + require( + failures, + samePath && Files.isDirectory(composite), + "local-composite-source-unavailable"); + if (samePath && Files.isDirectory(composite)) { + SourceState dependencySource = + sourceState(composite); + boolean sourceMatches = + dependencySource.fingerprint.sha256.equals( + evidence.get( + "composite.workspaceSha256")) + && dependencySource.commit.equals( + evidence.get( + "composite.commit")) + && !dependencySource.worktreeDirty + && dependencySource + .completeWorkspace() + && Boolean.parseBoolean( + evidence.get( + "composite.releaseInputsCommitted")); + require( + failures, + sourceMatches, + "local-composite-source-state-changed"); + compositeSource = map( + "path", composite.toString(), + "recordedCommit", + evidence.get("composite.commit"), + "recordedWorkspaceSha256", + evidence.get( + "composite.workspaceSha256"), + "recordedReleaseSourceSha256", + evidence.get( + "composite.releaseSourceSha256"), + "current", dependencySource.report(), + "matchesRecordedEvidence", sourceMatches); + } + } + return map( + "mode", expectedMode, + "status", + failures.isEmpty() + ? "passed" + : "stale-or-failed", + "evidencePresent", true, + "evidencePath", evidencePath.toString(), + "declaredCoordinate", + evidence.get("declared.coordinate"), + "effectiveCoordinate", + evidence.get( + "dependency.effectiveCoordinate"), + "tests", map( + "executed", + parseLong(evidence.get("tests.executed")), + "passed", + parseLong(evidence.get("tests.passed")), + "failed", 0, + "skipped", 0), + "artifacts", artifactReports, + "dependencyArtifact", dependencyArtifact, + "provenance", map( + "status", + evidence.get( + "dependency.provenance.status"), + "repositoryPolicy", + evidence.get( + "dependency.provenance.repositoryPolicy"), + "recordedRepository", + evidence.get( + "dependency.provenance.recordedRepository"), + "recordedSha256", + evidence.get( + "dependency.provenance.recordedSha256")), + "cleanDependencyCacheAcceptance", map( + "status", + evidence.get( + "dependency.cache.acceptance"), + "scope", + evidence.get( + "dependency.cache.acceptanceScope"), + "moduleVersionPath", + evidence.get( + "dependency.cache.blueLanguageModuleVersionPath"), + "moduleVersionInitiallyAbsentAtProjectConfiguration", + Boolean.parseBoolean(evidence.get( + "dependency.cache.blueLanguageModuleVersionInitiallyAbsent")), + "reason", + "passed".equals(evidence.get( + "dependency.cache.acceptance")) + ? "validated-mode-run-recorded-exact-module-version-cache-absence" + : "validated-exact-module-version-cache-absence-not-recorded"), + "compositeSource", compositeSource, + "failures", failures); + } + + private static Map cleanDependencyCacheAcceptance( + Map buildModes) { + Map standalone = castMap( + buildModes.get("standalonePublished")); + Map acceptance = castMap( + standalone.get( + "cleanDependencyCacheAcceptance")); + if (acceptance.isEmpty()) { + return map( + "status", "not-executed", + "scope", + "standalone-published-blue-language-module-version-cache", + "reason", + "no-blue-language-module-version-cache-evidence"); + } + return acceptance; + } + + private static Map artifactHashes( + Map mode) { + Map artifacts = + castMap(mode.get("artifacts")); + Map result = + new LinkedHashMap(); + for (String kind + : new String[] { + "main", "sources", "javadoc", "sourceRelease" + }) { + result.put( + kind, + String.valueOf( + castMap(artifacts.get(kind)) + .get("sha256"))); + } + return result; + } + + private static Map checkAbsoluteFile( + String rawPath, + String expectedHash) throws IOException { + Path path = pathOrNull(rawPath); + boolean present = + path != null && Files.isRegularFile(path); + String actual = present + ? sha256(path) + : "unavailable"; + return map( + "path", rawPath, + "present", present, + "bytes", present ? Files.size(path) : 0L, + "sha256", actual, + "matchesRecordedEvidence", + present && actual.equals(expectedHash)); + } + + private static Path pathOrNull(String rawPath) { + if (rawPath == null + || rawPath.isEmpty() + || "null".equals(rawPath)) { + return null; + } + try { + return Paths.get(rawPath) + .toAbsolutePath() + .normalize(); + } catch (RuntimeException invalid) { + return null; + } + } + + private static long parseLong(String value) { + try { + return Long.parseLong(value); + } catch (RuntimeException invalid) { + return -1L; + } + } + + private static List missingPublishedHostApis( + Map inspection) { + List missing = new ArrayList(); + for (Map.Entry entry + : inspection.entrySet()) { + if (("class".equals(prefix(entry.getKey())) + || "method".equals(prefix(entry.getKey()))) + && "false".equals(entry.getValue())) { + missing.add(entry.getKey()); + } + } + Collections.sort(missing); + return missing; + } + + private static String prefix(String value) { + int separator = value.indexOf('.'); + return separator < 0 + ? value + : value.substring(0, separator); + } + + private static Map hostedLocalLimitCapability() { + List observedOpenLedgerSignatures = + new ArrayList(); + boolean openLedgerAcceptsMaximumBudget = false; + for (Method method : RuntimeWorkSession.class.getMethods()) { + if (!"openLedger".equals(method.getName())) { + continue; + } + Class[] parameters = method.getParameterTypes(); + StringBuilder signature = + new StringBuilder("openLedger("); + for (int index = 0; index < parameters.length; index++) { + if (index > 0) { + signature.append(','); + } + signature.append(parameters[index].getName()); + } + signature.append(')'); + observedOpenLedgerSignatures.add( + signature.toString()); + if (parameters.length == 3 + && String.class.equals(parameters[0]) + && Map.class.isAssignableFrom(parameters[1]) + && (Long.TYPE.equals(parameters[2]) + || Long.class.equals(parameters[2]))) { + openLedgerAcceptsMaximumBudget = true; + } + } + Collections.sort(observedOpenLedgerSignatures); + + /* + * The current host publishes no invocation-owned capped scope which + * can be shared by independently named physical ledgers. In + * particular, openLedger only accepts a namespace and counter + * catalog, so BEX cannot make its physical ledger and a later + * intrinsic ledger consume one BEX-local cap through the canonical + * session admission path. + */ + boolean invocationOwnedSharedCappedBudgetScope = false; + boolean capabilityAvailable = + openLedgerAcceptsMaximumBudget + && invocationOwnedSharedCappedBudgetScope; + return map( + "schema", + "blue-bex-hosted-local-limit-capability/1.0", + "status", + capabilityAvailable ? "passed" : "blocking", + "affectedRequirement", + "workstream-2-property-1", + "observedRuntimeWorkSessionOpenLedgerSignatures", + observedOpenLedgerSignatures, + "runtimeWorkSessionOpenLedgerAcceptsMaximumBudget", + openLedgerAcceptsMaximumBudget, + "invocationOwnedSharedCappedBudgetScope", + invocationOwnedSharedCappedBudgetScope, + "bexWrapperPrecheckOccursBeforeWork", + true, + "bexPhysicalLedgerAndIntrinsicLedgerShareLocalCap", + false, + "canonicalSessionRecordedLocalRejection", + false, + "assessment", + "The wrapper precheck is exact and occurs before work, " + + "but the current RuntimeWorkSession cannot enforce " + + "one BEX-local cap across BEX and intrinsic " + + "physical ledgers or record the local rejection " + + "through its canonical rejection path."); + } + + private static Map + cyclicProofUnavailabilityCapability(TestEvidence tests) { + String returnType = "missing"; + boolean typedOutcome = false; + for (Method method : CyclicAwareNodeProvider.class.getMethods()) { + if (!"cyclicSetProofFor".equals(method.getName()) + || method.getParameterTypes().length != 1 + || !String.class.equals( + method.getParameterTypes()[0])) { + continue; + } + returnType = method.getReturnType().getName(); + typedOutcome = + !CyclicSetProof.class.equals( + method.getReturnType()); + } + + Map contentFetchEvidence = + tests.namedEvidence( + "cyclicMemberContentFetchUnavailabilityStopsBeforeProofQuery"); + Map nullProofEvidence = + tests.namedEvidence( + "nullCyclicProofAfterFoundContentIsInvalidNotUnavailable"); + boolean proofLayerTransientUnavailableExpressible = false; + boolean capabilityAvailable = typedOutcome + && proofLayerTransientUnavailableExpressible; + + return map( + "schema", + "blue-bex-cyclic-proof-unavailability-capability/1.0", + "status", + capabilityAvailable ? "passed" : "blocking", + "affectedRequirement", + "transient-cyclic-set-proof-unavailability", + "cyclicSetProofForReturnType", + returnType, + "typedProofOutcome", + typedOutcome, + "proofLayerTransientUnavailableExpressible", + proofLayerTransientUnavailableExpressible, + "nullProofAfterFoundClassification", + "invalid-evidence", + "contentFetchUnavailableBeforeProofQueryEvidence", + contentFetchEvidence, + "nullProofAfterFoundEvidence", + nullProofEvidence, + "contentFetchEvidenceIsProofLayerEvidence", + false, + "directInvalidProofCoverage", + "passed".equals(nullProofEvidence.get("status")) + ? "passed" + : nullProofEvidence.get("status"), + "hostedUnavailableProofLifecyclePathAvailable", + false, + "hostedUnavailableProofStructuralReadCoverage", + "absent", + "assessment", + "CyclicAwareNodeProvider.cyclicSetProofFor returns a proof " + + "or null. After content is FOUND, null is converted " + + "to INVALID_EVIDENCE, so the current host cannot " + + "express transient proof-layer unavailability. " + + "Direct invalid-proof classification is covered, " + + "but no hosted unavailable-proof lifecycle " + + "structural-read path exists to cover. " + + "The content-fetch unavailable test stops before " + + "the proof query and is not proof-unavailability " + + "coverage."); + } + + private static List knownLimitations( + Map buildModes, + Map publishedApiInspection, + Map hostedLocalLimitCapability, + Map cyclicProofUnavailabilityCapability, + Map hostLongTrace, + Map languageReleaseIdentity, + SourceState sourceState) { + List limitations = new ArrayList(); + if (!"passed".equals( + hostedLocalLimitCapability.get("status"))) { + limitations.add(map( + "id", + "current-host-shared-local-limit-capability", + "scope", + "hosted-bex-local-limit", + "status", + "blocking", + "affectedRequirement", + hostedLocalLimitCapability.get( + "affectedRequirement"), + "capability", + hostedLocalLimitCapability, + "resolution", + "Add an invocation-owned capped shared budget scope " + + "or an openLedger maximum-budget capability " + + "to RuntimeWorkSession so BEX and intrinsic " + + "physical ledgers share the BEX-local cap and " + + "local rejection is session-recorded.")); + } + if (!"passed".equals( + cyclicProofUnavailabilityCapability.get("status"))) { + limitations.add(map( + "id", + "current-host-cyclic-proof-unavailability-capability", + "scope", + "cyclic-set-proof-evidence", + "status", + "blocking", + "affectedRequirement", + cyclicProofUnavailabilityCapability.get( + "affectedRequirement"), + "capability", + cyclicProofUnavailabilityCapability, + "resolution", + "Replace the proof-or-null contract with a typed cyclic " + + "proof outcome that distinguishes FOUND, " + + "NOT_FOUND, UNAVAILABLE, and INVALID_EVIDENCE, " + + "and preserve UNAVAILABLE through provider " + + "verification.")); + } + if (!"passed".equals(hostLongTrace.get("status"))) { + limitations.add(map( + "id", "runtime-work-session-long-trace", + "scope", "hosted-ordered-gas-trace", + "status", "blocking", + "requiredMinimumOrderedTraceEntries", + hostLongTrace.get( + "requiredMinimumOrderedTraceEntries"), + "maximumObservedOrderedTraceEntries", + hostLongTrace.get( + "maximumObservedOrderedTraceEntries"), + "observedPortableLimit", + hostLongTrace.get("portableLimit"), + "resolution", + "Correct RuntimeWorkSession so the counter-kind " + + "catalog limit does not cap ordered " + + "counter occurrences, then rerun the " + + "hosted BEX probe.")); + } + if (!Boolean.TRUE.equals( + languageReleaseIdentity.get( + "exactFinalArtifactProven"))) { + limitations.add(map( + "id", "exact-final-language-artifact", + "scope", "published-host-dependency", + "status", "blocking", + "identity", languageReleaseIdentity, + "resolution", + "Commit and publish the final Language kernel, " + + "record its exact coordinate, commit, and " + + "artifact hash, then rerun both modes.")); + } + if (!sourceState.uncommittedReleasePaths.isEmpty()) { + limitations.add(map( + "id", "uncommitted-bex-release-inputs", + "scope", "bex-source-identity", + "status", "blocking", + "paths", + sourceState.uncommittedReleasePaths, + "resolution", + "Commit every BEX release input and rerun all " + + "release gates from that exact commit.")); + } + Map standalone = castMap( + buildModes.get("standalonePublished")); + if (!"passed".equals(standalone.get("status")) + && "incompatible-with-current-hosted-adapter".equals( + publishedApiInspection.get("status"))) { + limitations.add(map( + "id", "upstream-published-host-api", + "scope", "standalone-published-build", + "status", "blocking", + "coordinate", + publishedApiInspection.get("coordinate"), + "artifactSha256", + publishedApiInspection.get( + "artifact.sha256"), + "missingSymbols", + missingPublishedHostApis( + publishedApiInspection), + "resolution", + "Publish the current generic runtime-work-session " + + "and semantic-output-boundary APIs from " + + "blue-language-java, then update the " + + "declared coordinate and rerun both modes.")); + } + return limitations; + } + + private static String releaseIdentity( + String projectVersion, + SourceState sourceState, + String declaredDependency, + Map dependencyResolution, + Map specification, + List artifacts) { + StringBuilder exactTuple = new StringBuilder(); + exactTuple.append("blue.bex:blue-bex-java:") + .append(projectVersion).append('\n'); + exactTuple.append(sourceState.commit).append('\n'); + exactTuple.append( + sourceState.releaseFingerprint.sha256) + .append('\n'); + exactTuple.append(declaredDependency).append('\n'); + exactTuple.append( + dependencyResolution.get( + "effectiveCoordinate")).append('\n'); + exactTuple.append( + castMap(dependencyResolution.get("artifact")) + .get("sha256")).append('\n'); + exactTuple.append(specification.get("sha256")) + .append('\n'); + Map identity = identities(); + exactTuple.append(identity.get("bexRegistry")) + .append('\n'); + exactTuple.append(identity.get("gasManifest")) + .append('\n'); + exactTuple.append(identity.get("fixturePackage")) + .append('\n'); + for (Object value : artifacts) { + Map artifact = castMap(value); + exactTuple.append(artifact.get("path")) + .append('=') + .append(artifact.get("sha256")) + .append('\n'); + } + return "sha256:" + ConformancePackage.sha256( + exactTuple.toString() + .getBytes(StandardCharsets.UTF_8)); + } + + private static String markdownReport( + Map report, + Map baseline, + TestEvidence tests, + Map operatorCoverage, + Map counterCoverage, + Map buildModes, + List currentModeFailures, + List artifacts, + Map releaseGates, + Map publishedApiInspection) { + StringBuilder output = new StringBuilder(); + output.append("# Blue BEX 2.0 Hosted Release Evidence\n\n"); + output.append("- Release ready: `") + .append(report.get("releaseReady")) + .append("`\n"); + output.append("- Commit: `") + .append(report.get("commit")) + .append("`\n"); + output.append("- Release coordinate: `") + .append(report.get("releaseCoordinate")) + .append("`\n"); + output.append("- Release identity: `") + .append(report.get("releaseIdentity")) + .append("`\n\n"); + + output.append("## Baseline and final totals\n\n"); + output.append("| Evidence | Baseline | Final |\n"); + output.append("|---|---:|---:|\n"); + output.append("| Tests executed | ") + .append(baseline.get("tests.executed")) + .append(" | ").append(tests.cases.size()) + .append(" |\n"); + output.append("| Tests passed | ") + .append(baseline.get("tests.passed")) + .append(" | ") + .append(tests.count("passed")) + .append(" |\n"); + output.append("| Tests failed | ") + .append(baseline.get("tests.failed")) + .append(" | ") + .append(tests.count("failed")) + .append(" |\n"); + output.append("| Tests skipped | ") + .append(baseline.get("tests.skipped")) + .append(" | ") + .append(tests.count("skipped")) + .append(" |\n"); + output.append("| Behavior fixtures | ") + .append(baseline.get("behaviorFixtures")) + .append(" | ") + .append(passingBehaviorFixtures(tests)) + .append(" |\n"); + output.append("| Gas microfixtures | ") + .append(baseline.get("gasMicrofixtures")) + .append(" | ") + .append(counterCoverage.get( + "passingMicrofixtureCount")) + .append(" |\n"); + Map finalTotals = + castMap(report.get("finalTotals")); + Map vectorTotals = castMap( + finalTotals.get("normativeVectors")); + output.append("| Normative vectors | ") + .append(baseline.get("normativeVectors")) + .append(" | ") + .append(vectorTotals.get( + "executedAndPassing")) + .append(" |\n"); + output.append("| Direct operator coverage | ") + .append(baseline.get("operators")) + .append(" | ") + .append(operatorCoverage.get( + "passingOperatorCount")) + .append(" |\n\n"); + + output.append("## Dependency modes\n\n"); + output.append("| Mode | Status | Effective dependency |\n"); + output.append("|---|---|---|\n"); + appendModeRow( + output, + "Standalone published", + castMap(buildModes.get( + "standalonePublished"))); + appendModeRow( + output, + "Local composite", + castMap(buildModes.get("localComposite"))); + output.append("\nDeclared dependency: `") + .append(castMap(report.get("dependency")) + .get("declaredCoordinate")) + .append("`.\n\n"); + + output.append("## Named hosted evidence\n\n"); + appendNamedEvidence( + output, + "Semantic boundary invocation counts", + castMap(report.get( + "semanticBoundaryInvocationEvidence"))); + appendNamedEvidence( + output, + "Representation and provider invariance", + castMap(report.get( + "representationInvarianceEvidence"))); + appendNamedEvidence( + output, + "Child-ledger lifecycle", + castMap(report.get( + "ledgerLifecycleEvidence"))); + appendNamedEvidence( + output, + "Gas exhaustion and no work after rejection", + castMap(report.get( + "gasExhaustionEvidence"))); + appendNamedEvidence( + output, + "Concrete gas-exhaustion trace examples", + castMap(report.get( + "gasExhaustionTraceExamples"))); + appendNamedEvidence( + output, + "Cyclic identity, direct proof validation, and content fetch", + castMap(report.get( + "cyclicProofEvidence"))); + appendNamedEvidence( + output, + "Registered intrinsics", + castMap(report.get("intrinsicEvidence"))); + appendNamedEvidence( + output, + "Reference evidence failure classification", + castMap(report.get( + "referenceEvidenceClassificationEvidence"))); + Map gasTraceExamples = + castMap(report.get( + "gasExhaustionTraceExamples")); + output.append( + "\n### Concrete gas-exhaustion trace examples\n\n"); + output.append( + "| Example | Status | Namespace | Rejected counter " + + "| Quantity | Weight | Admitted gas " + + "| Effective budget | Rejected charge present " + + "| No later work |\n"); + output.append( + "|---|---|---|---|---:|---:|---:|---:|---|---|\n"); + for (Object value : ConformancePackage.list( + gasTraceExamples.get("examples"), + "gasExhaustionTraceExamples.examples")) { + Map example = + castMap(value); + output.append("| `") + .append(example.get("id")) + .append("` | ") + .append(example.get("status")) + .append(" | `") + .append(example.get("namespace")) + .append("` | `") + .append(example.get("counterName")) + .append("` | ") + .append(example.get("quantity")) + .append(" | ") + .append(example.get("weight")) + .append(" | ") + .append(example.get("admittedGas")) + .append(" | ") + .append(example.get("effectiveBudget")) + .append(" | ") + .append(example.get( + "rejectedChargePresent")) + .append(" | ") + .append(example.get("noLaterWork")) + .append(" |\n"); + } + Map gasTraceSource = + castMap(gasTraceExamples.get( + "sourceControlledEvidence")); + output.append( + "\nThe admitted prefix for every example is exactly one " + + "`bex/expressionEvaluated` charge with quantity " + + "`1`, weight `1`, and total gas `1`. Expected " + + "values are source-controlled in `") + .append(gasTraceSource.get("path")) + .append("` (`") + .append(gasTraceSource.get("sha256")) + .append("`) and are reported as passing only when the " + + "exact dynamic JUnit selector passed.\n"); + output.append("\nRecursion fixture `bex-c-09`: `") + .append(castMap( + report.get("recursionEvidence")) + .get("status")) + .append("`. Finite gas-exhaustion fixture " + + "`bex-g-15`: `") + .append(castMap( + report.get("finiteLoopEvidence")) + .get("status")) + .append("`.\n\n"); + + output.append("## Release gates\n\n"); + output.append("| Gate | Status |\n"); + output.append("|---|---|\n"); + output.append( + "| Archive byte determinism " + + "(same compiled/source inputs; fresh Javadoc) | ") + .append(castMap(releaseGates.get( + "deterministicArchives")) + .get("status")) + .append(" |\n"); + output.append( + "| Two independent clean builds " + + "(all four release artifacts) | ") + .append(castMap(releaseGates.get( + "independentCleanBuilds")) + .get("status")) + .append(" |\n"); + output.append("| Binary API | ") + .append(castMap(releaseGates.get( + "binaryApi")).get("status")) + .append(" |\n"); + output.append("| Java 8 benchmark compilation | ") + .append(castMap(releaseGates.get( + "benchmarkCompilation")) + .get("status")) + .append(" |\n"); + output.append("| Packaged Java 8 bytecode | ") + .append(castMap(releaseGates.get( + "java8Bytecode")) + .get("status")) + .append(" |\n"); + output.append( + "| Blue Language module-version cache acceptance | ") + .append(castMap(releaseGates.get( + "cleanDependencyCacheAcceptance")) + .get("status")) + .append(" |\n\n"); + + output.append("## Artifact identities\n\n"); + for (Object value : artifacts) { + Map artifact = castMap(value); + output.append("- `") + .append(artifact.get("path")) + .append("`: `") + .append(artifact.get("sha256")) + .append("`\n"); + } + output.append("\nSpecification SHA-256: `") + .append(castMap(report.get("specification")) + .get("sha256")) + .append("`. `.cz.toml` unchanged: `") + .append(castMap(report.get( + "versionAutomation")).get("unchanged")) + .append("`.\n\n"); + + Map hostedLocalLimit = + castMap(report.get( + "hostedLocalLimitCapability")); + output.append("## Hosted local-limit capability\n\n"); + output.append("- Status: `") + .append(hostedLocalLimit.get("status")) + .append("`\n"); + output.append("- `RuntimeWorkSession.openLedger` accepts a " + + "maximum budget: `") + .append(hostedLocalLimit.get( + "runtimeWorkSessionOpenLedgerAcceptsMaximumBudget")) + .append("`\n"); + output.append("- Invocation-owned shared capped scope: `") + .append(hostedLocalLimit.get( + "invocationOwnedSharedCappedBudgetScope")) + .append("`\n"); + output.append("- Exact wrapper precheck before work: `") + .append(hostedLocalLimit.get( + "bexWrapperPrecheckOccursBeforeWork")) + .append("`\n"); + output.append("- BEX and intrinsic physical ledgers share the " + + "BEX-local cap: `") + .append(hostedLocalLimit.get( + "bexPhysicalLedgerAndIntrinsicLedgerShareLocalCap")) + .append("`\n"); + output.append("- Local rejection is session-recorded: `") + .append(hostedLocalLimit.get( + "canonicalSessionRecordedLocalRejection")) + .append("`\n\n"); + + Map cyclicProofUnavailability = + castMap(report.get( + "cyclicProofUnavailabilityCapability")); + output.append( + "## Cyclic proof-unavailability capability\n\n"); + output.append("- Status: `") + .append(cyclicProofUnavailability.get("status")) + .append("`\n"); + output.append("- `cyclicSetProofFor` return type: `") + .append(cyclicProofUnavailability.get( + "cyclicSetProofForReturnType")) + .append("`\n"); + output.append("- Typed proof outcome: `") + .append(cyclicProofUnavailability.get( + "typedProofOutcome")) + .append("`\n"); + output.append( + "- Proof-layer transient unavailable is expressible: `") + .append(cyclicProofUnavailability.get( + "proofLayerTransientUnavailableExpressible")) + .append("`\n"); + output.append("- Null proof after found content is classified as: `") + .append(cyclicProofUnavailability.get( + "nullProofAfterFoundClassification")) + .append("`\n"); + output.append("- Direct invalid-proof coverage: `") + .append(cyclicProofUnavailability.get( + "directInvalidProofCoverage")) + .append("`\n"); + output.append("- Hosted unavailable-proof lifecycle path: `") + .append(cyclicProofUnavailability.get( + "hostedUnavailableProofLifecyclePathAvailable")) + .append("`\n"); + output.append( + "- Hosted unavailable-proof structural-read coverage: `") + .append(cyclicProofUnavailability.get( + "hostedUnavailableProofStructuralReadCoverage")) + .append("`\n"); + output.append( + "- Content-fetch unavailable before proof query: `") + .append(castMap(cyclicProofUnavailability.get( + "contentFetchUnavailableBeforeProofQueryEvidence")) + .get("status")) + .append("` (this is not proof-layer unavailable " + + "coverage).\n\n"); + + output.append("## Known limitations\n\n"); + boolean localLimitBlocked = + !"passed".equals( + hostedLocalLimit.get("status")); + boolean cyclicProofUnavailableBlocked = + !"passed".equals( + cyclicProofUnavailability.get("status")); + Map standalonePublished = + castMap(buildModes.get("standalonePublished")); + boolean publishedApiBlocked = + !"passed".equals( + standalonePublished.get("status")) + && "incompatible-with-current-hosted-adapter".equals( + publishedApiInspection.get("status")); + if (!localLimitBlocked + && !cyclicProofUnavailableBlocked + && !publishedApiBlocked) { + output.append("None recorded.\n"); + } + if (localLimitBlocked) { + output.append( + "The current `RuntimeWorkSession` exposes no " + + "invocation-owned capped scope and no " + + "`openLedger` maximum-budget parameter. " + + "Consequently, BEX and intrinsic physical " + + "ledgers cannot share or report one BEX-local " + + "cap, and a local rejection cannot be " + + "registered through the canonical session " + + "path. The exact wrapper precheck still occurs " + + "before work, but workstream-2 property 1 is " + + "not fully satisfiable in BEX alone.\n"); + } + if (cyclicProofUnavailableBlocked) { + output.append( + "\nThe current `CyclicAwareNodeProvider` proof contract " + + "returns `CyclicSetProof` or `null`; after " + + "content is found, `VerifyingNodeProvider` " + + "classifies a null proof as invalid evidence. " + + "It cannot preserve transient proof-layer " + + "unavailability. Direct invalid-proof " + + "classification is covered, but no hosted " + + "unavailable-proof lifecycle structural-read " + + "path exists to cover. The passing content-fetch " + + "unavailable test stops before any proof query " + + "and is not claimed as unavailable-proof " + + "coverage. Release readiness remains " + + "fail-closed until the host exposes and " + + "preserves a typed unavailable proof " + + "outcome.\n"); + } + if (publishedApiBlocked) { + output.append( + "\nThe published Blue Language artifact `") + .append(publishedApiInspection.get( + "coordinate")) + .append("` (`") + .append(publishedApiInspection.get( + "artifact.sha256")) + .append("`) does not contain the current generic " + + "hosted runtime APIs. Missing JAR symbols:\n\n"); + for (String symbol : missingPublishedHostApis( + publishedApiInspection)) { + output.append("- `") + .append(symbol) + .append("`\n"); + } + output.append( + "\nThis is an independent upstream publication " + + "blocker to standalone release evidence. " + + "The final release task remains fail-closed.\n"); + } + if (!currentModeFailures.isEmpty()) { + output.append( + "\nCurrent-mode evidence failures:\n\n"); + for (String failure : currentModeFailures) { + output.append("- `") + .append(failure) + .append("`\n"); + } + } + return output.toString(); + } + + private static int passingBehaviorFixtures( + TestEvidence tests) { + int result = 0; + for (ConformancePackage.Fixture fixture + : ConformancePackage.behaviorFixtures()) { + if ("passed".equals( + tests.fixtureStatus(fixture.id()))) { + result++; + } + } + return result; + } + + private static void appendModeRow( + StringBuilder output, + String label, + Map mode) { + output.append("| ").append(label) + .append(" | ") + .append(mode.get("status")) + .append(" | ") + .append(mode.get("effectiveCoordinate")) + .append(" |\n"); + } + + private static void appendNamedEvidence( + StringBuilder output, + String label, + Map evidence) { + output.append("- ").append(label) + .append(": `") + .append(evidence.get("status")) + .append("` (") + .append(evidence.get("matchedTestCount")) + .append(" named tests)\n"); + } + + private static String readinessReason( + List currentModeFailures, + boolean bothModesPassed) { + List reasons = + new ArrayList(currentModeFailures); + if (!bothModesPassed) { + reasons.add( + "standalone-and-local-composite-matrix-incomplete"); + } + return String.join(";", reasons); + } + + private static Map evidenceMap( + Map source) { + Map result = + new LinkedHashMap(); + result.putAll(source); + return result; + } + + private static Map stringMap( + String... values) { + if (values.length % 2 != 0) { + throw new IllegalArgumentException( + "stringMap needs key/value pairs"); + } + Map result = + new LinkedHashMap(); + for (int index = 0; + index < values.length; + index += 2) { + result.put(values[index], values[index + 1]); + } + return result; + } + + private static void writeEvidence( + Path path, + Map values) throws IOException { + Files.createDirectories(path.getParent()); + List keys = + new ArrayList(values.keySet()); + Collections.sort(keys); + StringBuilder output = new StringBuilder(); + for (String key : keys) { + output.append(key) + .append('=') + .append(values.get(key)) + .append('\n'); + } + Files.write( + path, + output.toString().getBytes( + StandardCharsets.UTF_8), + StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING, + StandardOpenOption.WRITE); + } + + private static Map operatorCoverage( + TestEvidence tests) { + Map source = ConformancePackage.loadMap( + ConformancePackage.FIXTURE_ROOT + "operator-coverage.yaml"); + List declarations = ConformancePackage.list( + source.get("operators"), "operator-coverage.operators"); + Map idsByPath = behaviorIdsByPath(); + List matrix = new ArrayList(); + int executed = 0; + int passed = 0; + boolean directCoverageComplete = true; + for (Object value : declarations) { + Map declaration = ConformancePackage.map( + value, "operator coverage entry"); + String operator = String.valueOf(declaration.get("operator")); + List paths = ConformancePackage.list( + declaration.get("fixtures"), operator + ".fixtures"); + directCoverageComplete &= !paths.isEmpty(); + List fixtures = new ArrayList(); + boolean anyExecuted = false; + boolean anyPassed = false; + for (Object pathValue : paths) { + String path = String.valueOf(pathValue); + String fixtureId = idsByPath.get(path); + String status = fixtureId != null + ? tests.fixtureStatus(fixtureId) + : "invalid-reference"; + anyExecuted |= !"not-executed".equals(status); + anyPassed |= "passed".equals(status); + fixtures.add(map( + "path", path, + "fixtureId", fixtureId, + "status", status)); + } + if (anyExecuted) { + executed++; + } + if (anyPassed) { + passed++; + } + matrix.add(map( + "operator", operator, + "fixtures", fixtures)); + } + return map( + "declaredOperatorCount", source.get("operatorCount"), + "matrixEntryCount", declarations.size(), + "directCoverageComplete", directCoverageComplete, + "executedOperatorCount", executed, + "passingOperatorCount", passed, + "allExecutedOperatorsPassing", + executed == declarations.size() && passed == declarations.size(), + "matrix", matrix); + } + + private static Map counterCoverage( + TestEvidence tests) { + Map manifestCounters = ConformancePackage.map( + ConformancePackage.gasManifest().get("counters"), + "gas-manifest.counters"); + List matrix = new ArrayList(); + Set fixtureCounters = new LinkedHashSet(); + int executed = 0; + int passed = 0; + for (ConformancePackage.Fixture fixture + : ConformancePackage.gasFixtures()) { + Map direct = ConformancePackage.map( + fixture.context().get("directCounterFixture"), + fixture.path + ".directCounterFixture"); + String counter = String.valueOf(direct.get("counter")); + fixtureCounters.add(counter); + String status = tests.fixtureStatus(fixture.id()); + if (!"not-executed".equals(status)) { + executed++; + } + if ("passed".equals(status)) { + passed++; + } + matrix.add(map( + "counter", counter, + "weight", manifestCounters.get(counter), + "fixtureId", fixture.id(), + "path", fixture.path, + "status", status)); + } + return map( + "declaredCounterCount", + ConformancePackage.gasManifest().get("counterCount"), + "microfixtureCount", matrix.size(), + "vocabularyComplete", + fixtureCounters.equals(manifestCounters.keySet()), + "executedMicrofixtureCount", executed, + "passingMicrofixtureCount", passed, + "allMicrofixturesPassing", + passed == ConformancePackage.GAS_FIXTURE_COUNT, + "matrix", matrix); + } + + private static List representationMatrix( + TestEvidence tests) { + Set selected = new LinkedHashSet(); + Collections.addAll( + selected, + "bex-r-01", + "bex-r-02", + "bex-r-08", + "bex-e-10", + "bex-h-05", + "bex-h-06"); + List matrix = new ArrayList(); + for (ConformancePackage.Fixture fixture + : ConformancePackage.behaviorFixtures()) { + if (!selected.contains(fixture.id())) { + continue; + } + Object declared = fixture.expected().get("variants"); + if (declared == null) { + continue; + } + for (Object value : ConformancePackage.list( + declared, fixture.path + ".variants")) { + Map variant = + ConformancePackage.map(value, "variant"); + String fixtureStatus = + tests.fixtureStatus(fixture.id()); + matrix.add(map( + "fixtureId", fixture.id(), + "path", fixture.path, + "variant", variant, + "status", + aggregateVariantStatus(fixtureStatus), + "evidenceScope", "aggregate-fixture-test", + "fixtureStatus", fixtureStatus)); + } + } + return matrix; + } + + private static List cacheMatrix(TestEvidence tests) { + List matrix = new ArrayList(); + ConformancePackage.Fixture r08 = fixture("bex-r-08"); + for (Object value : ConformancePackage.list( + r08.expected().get("variants"), r08.path + ".variants")) { + Map variant = + ConformancePackage.map(value, "cache variant"); + String fixtureStatus = + tests.fixtureStatus(r08.id()); + matrix.add(map( + "kind", "provider-cache", + "fixtureId", r08.id(), + "cache", variant.get("cache"), + "batching", variant.get("batching"), + "status", + aggregateVariantStatus(fixtureStatus), + "evidenceScope", "aggregate-fixture-test", + "fixtureStatus", fixtureStatus)); + } + matrix.add(map( + "kind", "compiled-program-cache-differential", + "testClass", + "blue.bex.conformance.BexConformancePropertyTest", + "test", "compileCacheHitAndMissHaveIdenticalResultAndGas", + "status", + tests.namedStatus( + "compileCacheHitAndMissHaveIdenticalResultAndGas"))); + matrix.add(map( + "kind", + "fixture-provider-preparation-differential", + "testClass", + "blue.bex.conformance.BexConformancePropertyTest", + "test", + "fixtureAdapterActuallyExecutesDeclaredBatchingPreparation", + "status", + tests.namedStatus( + "fixtureAdapterActuallyExecutesDeclaredBatchingPreparation"))); + return matrix; + } + + /** + * A passing aggregate fixture proves that its runner reached and checked + * every declared variant. A failed aggregate fixture does not identify + * which variant failed or whether later variants ran, so reporting every + * variant as failed would overclaim the available JUnit evidence. + */ + static String aggregateVariantStatus(String fixtureStatus) { + if ("passed".equals(fixtureStatus)) { + return "passed"; + } + if ("failed".equals(fixtureStatus)) { + return "indeterminate-after-fixture-failure"; + } + return "not-executed"; + } + + private static Map recursionEvidence( + TestEvidence tests) { + ConformancePackage.Fixture fixture = fixture("bex-c-09"); + return map( + "fixtureId", fixture.id(), + "status", tests.fixtureStatus(fixture.id()), + "expectedCompileStatus", + fixture.expected().get("compileStatus"), + "expectedErrorClass", + fixture.expected().get("errorClass"), + "expectedReason", fixture.expected().get("reason"), + "runtimeMustStart", false); + } + + private static Map finiteLoopEvidence( + TestEvidence tests) { + ConformancePackage.Fixture fixture = fixture("bex-g-15"); + return map( + "fixtureId", fixture.id(), + "status", tests.fixtureStatus(fixture.id()), + "gasLimit", fixture.context().get("gasLimit"), + "parentRemainingGas", + fixture.context().get("parentRemainingGas"), + "expectedErrorClass", + fixture.expected().get("errorClass"), + "bufferedEffectsMustCommit", false); + } + + private static List artifactEvidence( + Path projectDir, + Path buildDir, + String projectVersion) throws IOException { + Path libs = buildDir.resolve("libs"); + final String artifactPrefix = + "blue-bex-java-" + projectVersion; + List paths = new ArrayList(); + if (Files.isDirectory(libs)) { + try (Stream stream = Files.list(libs)) { + paths.addAll(stream + .filter(Files::isRegularFile) + .filter(path -> { + String name = + path.getFileName().toString(); + return name.equals( + artifactPrefix + ".jar") + || name.equals( + artifactPrefix + + "-sources.jar") + || name.equals( + artifactPrefix + + "-javadoc.jar"); + }) + .collect(Collectors.toList())); + } + } + Path distributions = + buildDir.resolve("distributions"); + if (Files.isDirectory(distributions)) { + try (Stream stream = + Files.list(distributions)) { + paths.addAll(stream + .filter(Files::isRegularFile) + .filter(path -> path.getFileName() + .toString() + .equals( + artifactPrefix + + "-source-release.zip")) + .collect(Collectors.toList())); + } + } + Collections.sort( + paths, + Comparator.comparing( + path -> path.getFileName().toString())); + List artifacts = new ArrayList(); + for (Path path : paths) { + artifacts.add(map( + "path", unix(projectDir.relativize(path)), + "bytes", Files.size(path), + "sha256", ConformancePackage.sha256( + Files.readAllBytes(path)))); + } + return artifacts; + } + + private static Map releaseGateEvidence( + Path projectDir, + Path buildDir, + Path persistentEvidenceRoot, + String projectVersion, + String sourceCommit, + String dependencyMode, + String declaredDependency, + Map dependencyResolution, + Path compositePath) throws Exception { + Path releaseRoot = buildDir.resolve("reports") + .resolve("bex-release"); + return map( + "deterministicArchives", + deterministicArchiveEvidence( + projectDir, + releaseRoot.resolve( + "deterministic-archives.properties")), + "independentCleanBuilds", + independentCleanBuildEvidence( + projectDir, + buildDir, + persistentEvidenceRoot.resolve( + "independent-clean-builds-" + + dependencyMode + + ".properties"), + projectVersion, + sourceCommit, + dependencyMode, + declaredDependency, + dependencyResolution, + compositePath), + "binaryApi", + binaryApiEvidence( + projectDir, + buildDir, + releaseRoot.resolve( + "binary-api.properties")), + "benchmarkCompilation", + benchmarkCompilationEvidence( + projectDir, + releaseRoot.resolve( + "benchmark-compilation.properties")), + "java8Bytecode", + java8BytecodeEvidence( + projectDir, + releaseRoot.resolve( + "java8-bytecode.properties"))); + } + + private static Map + independentCleanBuildEvidence( + Path projectDir, + Path buildDir, + Path evidencePath, + String projectVersion, + String sourceCommit, + String dependencyMode, + String declaredDependency, + Map dependencyResolution, + Path compositePath) throws Exception { + Map evidence = + readEvidence(evidencePath); + if (evidence.isEmpty()) { + return map( + "status", "not-executed", + "evidencePresent", false, + "evidencePath", evidencePath.toString()); + } + String artifactPrefix = + "blue-bex-java-" + projectVersion; + Map artifactPaths = + stringMap( + "main", + buildDir.resolve("libs") + .resolve(artifactPrefix + ".jar") + .toString(), + "sources", + buildDir.resolve("libs") + .resolve( + artifactPrefix + + "-sources.jar") + .toString(), + "javadoc", + buildDir.resolve("libs") + .resolve( + artifactPrefix + + "-javadoc.jar") + .toString(), + "sourceRelease", + buildDir.resolve("distributions") + .resolve( + artifactPrefix + + "-source-release.zip") + .toString()); + Map artifacts = + new LinkedHashMap(); + boolean artifactChecksPassed = true; + for (Map.Entry entry + : artifactPaths.entrySet()) { + String name = entry.getKey(); + Path artifactPath = + Paths.get(entry.getValue()) + .toAbsolutePath() + .normalize(); + String relativePath = + projectDir.relativize(artifactPath) + .toString(); + FileCheck artifact = checkFile( + projectDir, + relativePath, + evidence.get( + "artifact." + name + ".sha256")); + boolean byteIdentical = + Boolean.parseBoolean(evidence.get( + "artifact." + name + + ".byteIdentical")); + artifactChecksPassed &= + artifact.valid && byteIdentical; + artifacts.put( + name, + map( + "current", artifact.report(), + "byteIdenticalAcrossCleanBuilds", + byteIdentical)); + } + Path firstReceipt = + pathOrNull(evidence.get( + "first.evidence.path")); + Path secondReceipt = + pathOrNull(evidence.get( + "second.evidence.path")); + String firstReceiptHash = + evidence.get("first.evidence.sha256"); + String secondReceiptHash = + evidence.get("second.evidence.sha256"); + boolean receiptsValid = + firstReceipt != null + && secondReceipt != null + && !firstReceipt.equals(secondReceipt) + && Files.isRegularFile(firstReceipt) + && Files.isRegularFile(secondReceipt) + && firstReceiptHash != null + && firstReceiptHash.matches( + "[0-9a-f]{64}") + && secondReceiptHash != null + && secondReceiptHash.matches( + "[0-9a-f]{64}") + && !firstReceiptHash.equals( + secondReceiptHash) + && firstReceiptHash.equals( + sha256(firstReceipt)) + && secondReceiptHash.equals( + sha256(secondReceipt)); + Map firstReceiptValues = + firstReceipt != null + && Files.isRegularFile(firstReceipt) + ? readEvidence(firstReceipt) + : Collections.emptyMap(); + Map secondReceiptValues = + secondReceipt != null + && Files.isRegularFile(secondReceipt) + ? readEvidence(secondReceipt) + : Collections.emptyMap(); + boolean receiptContentsMatch = + receiptsValid + && cleanBuildReceiptMatchesAggregate( + evidence, + "first", + firstReceiptValues, + artifactPaths.keySet()) + && cleanBuildReceiptMatchesAggregate( + evidence, + "second", + secondReceiptValues, + artifactPaths.keySet()); + Path firstRoot = + pathOrNull(evidence.get( + "first.checkout.root")); + Path secondRoot = + pathOrNull(evidence.get( + "second.checkout.root")); + Path firstGitDirectory = + pathOrNull(evidence.get( + "first.checkout.gitDirectory")); + Path secondGitDirectory = + pathOrNull(evidence.get( + "second.checkout.gitDirectory")); + boolean distinctCheckouts = false; + boolean liveCheckoutStateMatches = false; + if (firstRoot != null + && secondRoot != null + && firstGitDirectory != null + && secondGitDirectory != null) { + try { + Path firstRealRoot = firstRoot.toRealPath(); + Path secondRealRoot = secondRoot.toRealPath(); + Path firstRealGit = + firstGitDirectory.toRealPath(); + Path secondRealGit = + secondGitDirectory.toRealPath(); + distinctCheckouts = + !firstRealRoot.equals(secondRealRoot) + && !firstRealGit.equals( + secondRealGit); + liveCheckoutStateMatches = + distinctCheckouts + && liveCleanCheckoutMatches( + firstRealRoot, + firstRealGit, + "first", + evidence, + sourceCommit) + && liveCleanCheckoutMatches( + secondRealRoot, + secondRealGit, + "second", + evidence, + sourceCommit); + } catch (Exception invalid) { + distinctCheckouts = false; + liveCheckoutStateMatches = false; + } + } + Map resolvedArtifact = + castMap(dependencyResolution.get( + "artifact")); + String resolvedDependencyHash = + String.valueOf( + resolvedArtifact.get("sha256")); + String recordedDependencyHash = + evidence.get( + "dependency.artifact.sha256"); + boolean dependencyInputMatches = + "passed".equals( + dependencyResolution.get("status")) + && dependencyMode.equals( + evidence.get("dependency.mode")) + && declaredDependency.equals( + evidence.get( + "dependency.coordinate")) + && Objects.equals( + String.valueOf( + dependencyResolution.get( + "effectiveCoordinate")), + evidence.get( + "dependency.effectiveCoordinate")) + && recordedDependencyHash != null + && recordedDependencyHash.matches( + "[0-9a-f]{64}") + && recordedDependencyHash.equals( + resolvedDependencyHash); + boolean compositeInputMatches; + Map currentComposite = + Collections.emptyMap(); + if ("local-composite".equals(dependencyMode)) { + if (compositePath == null + || !Files.isDirectory(compositePath)) { + compositeInputMatches = false; + } else { + SourceState compositeState = + sourceState(compositePath); + Path recordedCompositePath = + pathOrNull(evidence.get( + "composite.path")); + compositeInputMatches = + recordedCompositePath != null + && compositePath.equals( + recordedCompositePath) + && !compositeState.worktreeDirty + && compositeState.completeWorkspace() + && compositeState.commit.equals( + evidence.get( + "composite.commit")) + && "false".equals(evidence.get( + "composite.dirty")) + && String.valueOf( + compositeState.worktreeDirty) + .equals(evidence.get( + "composite.dirty")) + && compositeState.statusSha256 + .equals(evidence.get( + "composite.gitStatusSha256")) + && compositeState.fingerprint + .sha256.equals(evidence.get( + "composite.workspaceSha256")) + && compositeState.fingerprint + .pathCount == parseLong( + evidence.get( + "composite.pathCount")); + currentComposite = + compositeState.report(); + } + } else { + compositeInputMatches = + "".equals(evidence.get("composite.path")) + && "".equals(evidence.get( + "composite.commit")) + && "false".equals(evidence.get( + "composite.dirty")) + && "".equals(evidence.get( + "composite.gitStatusSha256")) + && "".equals(evidence.get( + "composite.workspaceSha256")) + && "0".equals(evidence.get( + "composite.pathCount")); + } + boolean passed = + "blue-bex-independent-clean-builds/1.0" + .equals(evidence.get("schema")) + && "passed".equals( + evidence.get("status")) + && sourceCommit.equals( + evidence.get("commit")) + && projectVersion.equals( + evidence.get("project.version")) + && Boolean.parseBoolean(evidence.get( + "first.checkout.clean")) + && Boolean.parseBoolean(evidence.get( + "second.checkout.clean")) + && receiptsValid + && receiptContentsMatch + && distinctCheckouts + && liveCheckoutStateMatches + && dependencyInputMatches + && compositeInputMatches + && artifactChecksPassed; + return map( + "status", + passed ? "passed" : "stale-or-failed", + "evidencePresent", true, + "evidencePath", evidencePath.toString(), + "commit", evidence.get("commit"), + "firstCheckoutClean", + Boolean.parseBoolean(evidence.get( + "first.checkout.clean")), + "secondCheckoutClean", + Boolean.parseBoolean(evidence.get( + "second.checkout.clean")), + "firstEvidenceSha256", + firstReceiptHash, + "secondEvidenceSha256", + secondReceiptHash, + "receiptsValid", receiptsValid, + "receiptContentsMatchAggregate", + receiptContentsMatch, + "distinctCheckouts", distinctCheckouts, + "liveCheckoutStateMatches", + liveCheckoutStateMatches, + "dependencyMode", + evidence.get("dependency.mode"), + "dependencyCoordinate", + evidence.get("dependency.coordinate"), + "dependencyArtifactSha256", + recordedDependencyHash, + "dependencyInputMatches", + dependencyInputMatches, + "compositeInputMatches", + compositeInputMatches, + "currentCompositeSource", + currentComposite, + "artifacts", artifacts); + } + + static boolean cleanBuildReceiptMatchesAggregate( + Map aggregate, + String prefix, + Map receipt, + Collection artifactNames) { + if (!"blue-bex-clean-build-artifacts/1.0".equals( + receipt.get("schema")) + || !"passed".equals(receipt.get("status")) + || !"true".equals(receipt.get("checkout.clean")) + || !"true".equals(aggregate.get( + prefix + ".checkout.clean"))) { + return false; + } + String[][] prefixedMappings = { + {"checkout.root", "checkout.root"}, + {"checkout.gitDirectory", "checkout.gitDirectory"}, + {"checkout.gitStatusSha256", + "checkout.gitStatusSha256"}, + {"checkout.workspaceSha256", + "checkout.workspaceSha256"}, + {"checkout.pathCount", "checkout.pathCount"} + }; + for (String[] mapping : prefixedMappings) { + if (!Objects.equals( + receipt.get(mapping[0]), + aggregate.get(prefix + "." + mapping[1]))) { + return false; + } + } + String[] commonKeys = { + "commit", + "project.version", + "dependency.mode", + "dependency.coordinate", + "dependency.effectiveCoordinate", + "dependency.artifact.sha256", + "composite.path", + "composite.commit", + "composite.dirty", + "composite.gitStatusSha256", + "composite.workspaceSha256", + "composite.pathCount" + }; + for (String key : commonKeys) { + if (!Objects.equals( + receipt.get(key), + aggregate.get(key))) { + return false; + } + } + for (String artifactName : artifactNames) { + String key = + "artifact." + artifactName + ".sha256"; + if (!Objects.equals( + receipt.get(key), + aggregate.get(key)) + || !"true".equals(aggregate.get( + "artifact." + artifactName + + ".byteIdentical"))) { + return false; + } + } + return true; + } + + static boolean liveCleanCheckoutMatches( + Path checkoutRoot, + Path recordedGitDirectory, + String prefix, + Map aggregate, + String sourceCommit) { + try { + Path actualRoot = + Paths.get(git( + checkoutRoot, + "rev-parse", + "--show-toplevel").trim()) + .toRealPath(); + Path actualGitDirectory = + Paths.get(git( + checkoutRoot, + "rev-parse", + "--absolute-git-dir").trim()) + .toRealPath(); + if (!checkoutRoot.equals(actualRoot) + || !recordedGitDirectory.equals( + actualGitDirectory)) { + return false; + } + SourceState state = sourceState(checkoutRoot); + return !state.worktreeDirty + && state.completeWorkspace() + && sourceCommit.equals(state.commit) + && Objects.equals( + aggregate.get( + prefix + + ".checkout.gitStatusSha256"), + state.statusSha256) + && Objects.equals( + aggregate.get( + prefix + + ".checkout.workspaceSha256"), + state.fingerprint.sha256) + && Objects.equals( + aggregate.get( + prefix + ".checkout.pathCount"), + String.valueOf( + state.fingerprint.pathCount)); + } catch (Exception invalid) { + return false; + } + } + + private static Map deterministicArchiveEvidence( + Path projectDir, + Path evidencePath) throws Exception { + Map evidence = readEvidence(evidencePath); + if (evidence.isEmpty()) { + return map( + "status", "not-executed", + "evidencePresent", false); + } + FileCheck mainOriginal = checkFile( + projectDir, + evidence.get("main.original.path"), + evidence.get("main.original.sha256")); + FileCheck mainRebuild = checkFile( + projectDir, + evidence.get("main.rebuild.path"), + evidence.get("main.rebuild.sha256")); + FileCheck sourcesOriginal = checkFile( + projectDir, + evidence.get("sources.original.path"), + evidence.get("sources.original.sha256")); + FileCheck sourcesRebuild = checkFile( + projectDir, + evidence.get("sources.rebuild.path"), + evidence.get("sources.rebuild.sha256")); + FileCheck javadocOriginal = checkFile( + projectDir, + evidence.get("javadoc.original.path"), + evidence.get("javadoc.original.sha256")); + FileCheck javadocRebuild = checkFile( + projectDir, + evidence.get("javadoc.rebuild.path"), + evidence.get("javadoc.rebuild.sha256")); + FileCheck sourceReleaseOriginal = checkFile( + projectDir, + evidence.get("sourceRelease.original.path"), + evidence.get("sourceRelease.original.sha256")); + FileCheck sourceReleaseReplica = checkFile( + projectDir, + evidence.get("sourceRelease.replica.path"), + evidence.get("sourceRelease.replica.sha256")); + boolean mainIdentical = mainOriginal.valid + && mainRebuild.valid + && mainOriginal.sha256.equals(mainRebuild.sha256); + boolean sourcesIdentical = sourcesOriginal.valid + && sourcesRebuild.valid + && sourcesOriginal.sha256.equals( + sourcesRebuild.sha256); + boolean javadocIdentical = javadocOriginal.valid + && javadocRebuild.valid + && javadocOriginal.sha256.equals( + javadocRebuild.sha256) + && Boolean.parseBoolean(evidence.get( + "javadoc.freshlyRegenerated")); + boolean sourceReleaseIdentical = + sourceReleaseOriginal.valid + && sourceReleaseReplica.valid + && sourceReleaseOriginal.sha256.equals( + sourceReleaseReplica.sha256) + && Boolean.parseBoolean(evidence.get( + "sourceRelease.byteIdentity")) + && Boolean.parseBoolean(evidence.get( + "sourceRelease.hashIdentity")) + && Boolean.parseBoolean(evidence.get( + "sourceRelease.independentAssembly")); + boolean archiveScope = + "jar-packaging-determinism-and-source-release-reassembly-from-the-same-working-tree" + .equals( + evidence.get("scope")); + boolean independentCleanCompilation = + Boolean.parseBoolean(evidence.get( + "independentCleanCompilation")); + boolean passed = "passed".equals(evidence.get("status")) + && mainIdentical + && sourcesIdentical + && javadocIdentical + && sourceReleaseIdentical + && archiveScope + && !independentCleanCompilation; + return map( + "status", passed ? "passed" : "stale-or-failed", + "evidencePresent", true, + "scope", evidence.get("scope"), + "independentCleanCompilation", + independentCleanCompilation, + "assessment", + "This gate compares archive packaging from the same " + + "compiled/source inputs and independently " + + "reassembles the source ZIP. It does not prove " + + "a second clean checkout compilation.", + "mainJar", map( + "original", mainOriginal.report(), + "rebuild", mainRebuild.report(), + "byteIdentical", mainIdentical), + "sourcesJar", map( + "original", sourcesOriginal.report(), + "rebuild", sourcesRebuild.report(), + "byteIdentical", sourcesIdentical), + "javadocJar", map( + "original", javadocOriginal.report(), + "freshRebuild", javadocRebuild.report(), + "freshlyRegenerated", + Boolean.parseBoolean(evidence.get( + "javadoc.freshlyRegenerated")), + "byteIdentical", javadocIdentical), + "sourceRelease", map( + "original", + sourceReleaseOriginal.report(), + "replica", + sourceReleaseReplica.report(), + "independentAssembly", + Boolean.parseBoolean(evidence.get( + "sourceRelease.independentAssembly")), + "independentCleanCheckout", + Boolean.parseBoolean(evidence.get( + "sourceRelease.independentCleanCheckout")), + "byteIdentical", + sourceReleaseIdentical)); + } + + private static Map binaryApiEvidence( + Path projectDir, + Path buildDir, + Path evidencePath) throws Exception { + Map evidence = readEvidence(evidencePath); + TestEvidence apiTests = readTests( + buildDir.resolve("test-results") + .resolve("binaryApiCheck")); + if (evidence.isEmpty() && !apiTests.present) { + return map( + "status", "not-executed", + "evidencePresent", false, + "tests", apiTests.report()); + } + FileCheck artifact = checkFile( + projectDir, + evidence.get("artifact.path"), + evidence.get("artifact.sha256")); + FileCheck manifest = checkFile( + projectDir, + evidence.get("manifest.path"), + evidence.get("manifest.sha256")); + FileCheck requiredApi = checkFile( + projectDir, + evidence.get("required.path"), + evidence.get("required.sha256")); + Path manifestPath = evidence.get("manifest.path") == null + ? null + : projectDir.resolve(evidence.get("manifest.path")) + .toAbsolutePath().normalize(); + List manifestLines = + manifestPath != null + && manifestPath.startsWith(projectDir) + && Files.isRegularFile(manifestPath) + ? Files.readAllLines( + manifestPath, + StandardCharsets.UTF_8) + : Collections.emptyList(); + boolean manifestSchemaValid = + "blue-bex-binary-api-manifest/1.0".equals( + evidence.get("manifest.schema")) + && !manifestLines.isEmpty() + && "schema=blue-bex-binary-api-manifest/1.0" + .equals(manifestLines.get(0)); + Path requiredPath = evidence.get("required.path") == null + ? null + : projectDir.resolve(evidence.get("required.path")) + .toAbsolutePath().normalize(); + List requiredLines = + requiredPath != null + && requiredPath.startsWith(projectDir) + && Files.isRegularFile(requiredPath) + ? Files.readAllLines( + requiredPath, + StandardCharsets.UTF_8) + : Collections.emptyList(); + List actualSignatures = + new ArrayList(); + for (String line : manifestLines) { + actualSignatures.add(trimTrailingWhitespace(line)); + } + List requiredSignatures = + new ArrayList(); + List missingSignatures = + new ArrayList(); + List unexpectedSignatures = + new ArrayList(); + for (String line : requiredLines) { + String signature = trimTrailingWhitespace(line); + requiredSignatures.add(signature); + if (!actualSignatures.contains(signature)) { + missingSignatures.add(signature); + } + } + for (String signature : actualSignatures) { + if (!requiredSignatures.contains(signature)) { + unexpectedSignatures.add(signature); + } + } + boolean requiredComparisonValid = + requiredApi.valid + && !requiredSignatures.isEmpty() + && actualSignatures.equals( + requiredSignatures) + && missingSignatures.isEmpty() + && unexpectedSignatures.isEmpty() + && "exact-match".equals( + evidence.get("required.comparison")) + && parseLong(evidence.get( + "required.signatureCount")) + == requiredSignatures.size() + && parseLong(evidence.get( + "required.missingCount")) == 0L + && parseLong(evidence.get( + "required.unexpectedCount")) == 0L; + String testStatus = apiTests.overallStatus(); + boolean passed = "passed".equals(evidence.get("status")) + && artifact.valid + && manifest.valid + && manifestSchemaValid + && requiredComparisonValid + && "passed".equals(testStatus); + return map( + "status", passed ? "passed" : "stale-or-failed", + "evidencePresent", !evidence.isEmpty(), + "testStatus", testStatus, + "testClass", evidence.get("testClass"), + "artifact", artifact.report(), + "publicApiManifest", map( + "file", manifest.report(), + "schemaValid", manifestSchemaValid, + "scope", + "public-and-protected-descriptor-level-api"), + "requiredApiSignatures", map( + "file", requiredApi.report(), + "requiredCount", + requiredSignatures.size(), + "missingCount", + missingSignatures.size(), + "missing", missingSignatures, + "unexpectedCount", + unexpectedSignatures.size(), + "unexpected", unexpectedSignatures, + "comparison", + requiredComparisonValid + ? "exact-match" + : "stale-or-failed"), + "tests", apiTests.report()); + } + + private static String trimTrailingWhitespace(String value) { + int end = value.length(); + while (end > 0 + && Character.isWhitespace( + value.charAt(end - 1))) { + end--; + } + return value.substring(0, end); + } + + private static Map benchmarkCompilationEvidence( + Path projectDir, + Path evidencePath) throws Exception { + Map evidence = readEvidence(evidencePath); + if (evidence.isEmpty()) { + return map( + "status", "not-executed", + "evidencePresent", false, + "timingExecuted", false); + } + FileCheck source = checkFile( + projectDir, + evidence.get("source.path"), + evidence.get("source.sha256")); + FileCheck compiledClass = checkFile( + projectDir, + evidence.get("class.path"), + evidence.get("class.sha256")); + boolean timingExecuted = + Boolean.parseBoolean(evidence.get("timingExecuted")); + boolean passed = "passed".equals(evidence.get("status")) + && source.valid + && compiledClass.valid; + return map( + "status", passed ? "passed" : "stale-or-failed", + "evidencePresent", true, + "timingExecuted", timingExecuted, + "source", source.report(), + "compiledClass", compiledClass.report()); + } + + private static Map java8BytecodeEvidence( + Path projectDir, + Path evidencePath) throws Exception { + Map evidence = readEvidence(evidencePath); + if (evidence.isEmpty()) { + return map( + "status", "not-executed", + "evidencePresent", false); + } + FileCheck artifact = checkFile( + projectDir, + evidence.get("artifact.path"), + evidence.get("artifact.sha256")); + long classCount = parseLong(evidence.get("classCount")); + long expectedMajor = parseLong(evidence.get("expected.major")); + long observedMajor = parseLong(evidence.get("observed.major")); + boolean passed = + "blue-bex-java8-bytecode-evidence/1.0".equals( + evidence.get("schema")) + && "passed".equals(evidence.get("status")) + && artifact.valid + && classCount > 0L + && expectedMajor == 52L + && observedMajor == expectedMajor + && "CAFEBABE".equals( + evidence.get("expected.magic")) + && Objects.equals( + evidence.get("expected.magic"), + evidence.get("observed.magic")); + return map( + "status", passed ? "passed" : "stale-or-failed", + "evidencePresent", true, + "artifact", artifact.report(), + "classCount", classCount, + "expectedMagic", evidence.get("expected.magic"), + "observedMagic", evidence.get("observed.magic"), + "expectedMajor", expectedMajor, + "observedMajor", observedMajor); + } + + private static Map readEvidence(Path path) + throws IOException { + if (!Files.isRegularFile(path)) { + return Collections.emptyMap(); + } + Map result = + new LinkedHashMap(); + for (String line : Files.readAllLines( + path, StandardCharsets.UTF_8)) { + if (line.isEmpty() || line.startsWith("#")) { + continue; + } + int separator = line.indexOf('='); + if (separator <= 0) { + continue; + } + result.put( + line.substring(0, separator), + line.substring(separator + 1)); + } + return result; + } + + private static FileCheck checkFile( + Path projectDir, + String relativePath, + String recordedSha256) throws IOException { + if (relativePath == null || recordedSha256 == null) { + return FileCheck.missing(relativePath); + } + Path path = projectDir.resolve(relativePath) + .toAbsolutePath().normalize(); + if (!path.startsWith(projectDir) || !Files.isRegularFile(path)) { + return FileCheck.missing(relativePath); + } + String actualSha256 = sha256(path); + return new FileCheck( + relativePath, + Files.size(path), + actualSha256, + actualSha256.equals(recordedSha256)); + } + + private static SourceState sourceState(Path projectDir) + throws Exception { + String rawCommit = + git(projectDir, "rev-parse", "HEAD").trim(); + String commit = rawCommit.matches("[0-9a-fA-F]{40}") + ? rawCommit.toLowerCase() + : "unavailable"; + String status = git( + projectDir, + "status", + "--porcelain", + "-z", + "--untracked-files=all"); + String listedFiles = git( + projectDir, + "ls-files", + "-z", + "--cached", + "--others", + "--exclude-standard"); + String ignoredReleaseFiles = git( + projectDir, + "ls-files", + "-z", + "--others", + "--ignored", + "--exclude-standard", + "--", + ".github", + "docs", + "gradle", + "specifications", + "src"); + List paths = splitNul(listedFiles); + List releasePaths = paths.stream() + .filter(BexConformanceReportMain::isReleaseSourcePath) + .collect(Collectors.toList()); + List uncommittedReleasePaths = + statusPaths(status).stream() + .filter( + BexConformanceReportMain + ::isReleaseSourcePath) + .sorted() + .collect(Collectors.toList()); + WorkspaceFingerprint fingerprint = + workspaceFingerprint(projectDir, paths); + WorkspaceFingerprint releaseFingerprint = + workspaceFingerprint( + projectDir, releasePaths); + return new SourceState( + commit, + !status.isEmpty(), + splitNul(status).size(), + status.isEmpty() + ? ConformancePackage.sha256(new byte[0]) + : ConformancePackage.sha256( + status.getBytes(StandardCharsets.UTF_8)), + fingerprint, + releaseFingerprint, + uncommittedReleasePaths, + splitNul(ignoredReleaseFiles)); + } + + private static List statusPaths(String status) { + List records = splitNul(status); + List paths = new ArrayList(); + for (int index = 0; index < records.size(); index++) { + String record = records.get(index); + if (record.length() < 4 + || record.charAt(2) != ' ') { + continue; + } + paths.add(record.substring(3)); + char indexStatus = record.charAt(0); + char worktreeStatus = record.charAt(1); + boolean rename = + indexStatus == 'R' + || worktreeStatus == 'R'; + boolean copy = + indexStatus == 'C' + || worktreeStatus == 'C'; + if (index + 1 < records.size() + && (rename || copy)) { + if (rename) { + paths.add(records.get(index + 1)); + } + index++; + } + } + return paths; + } + + private static boolean isReleaseSourcePath(String path) { + return ".cz.toml".equals(path) + || ".gitattributes".equals(path) + || ".gitignore".equals(path) + || ".gitmodules".equals(path) + || "LICENSE".equals(path) + || "README.md".equals(path) + || "build.gradle.kts".equals(path) + || "gradle.properties".equals(path) + || "gradlew".equals(path) + || "gradlew.bat".equals(path) + || "settings.gradle.kts".equals(path) + || path.startsWith(".github/") + || path.startsWith("docs/") + || path.startsWith("gradle/") + || path.startsWith("specifications/") + || path.startsWith("src/"); + } + + private static Map compositeDependencyEvidence( + Path blueLanguage) throws Exception { + if (blueLanguage == null) { + return map( + "status", "not-selected"); + } + if (!Files.isDirectory(blueLanguage)) { + return map( + "path", blueLanguage.toString(), + "status", "unavailable"); + } + SourceState dependencyState = sourceState(blueLanguage); + return map( + "path", blueLanguage.toString(), + "status", "identified", + "sourceState", dependencyState.report()); + } + + private static WorkspaceFingerprint workspaceFingerprint( + Path projectDir, + List listedPaths) throws Exception { + if (listedPaths.isEmpty()) { + return new WorkspaceFingerprint( + "unavailable", 0, 0, 0, 0); + } + List paths = + new ArrayList(listedPaths); + Collections.sort(paths); + MessageDigest digest = + MessageDigest.getInstance("SHA-256"); + int count = 0; + int missingCount = 0; + int unsupportedTypeCount = 0; + int symlinkCount = 0; + byte[] buffer = new byte[8192]; + for (String relativePath : paths) { + Path path = projectDir.resolve(relativePath) + .toAbsolutePath().normalize(); + if (!path.startsWith(projectDir)) { + throw new IllegalStateException( + "Source path escapes project: " + relativePath); + } + byte[] pathBytes = + relativePath.getBytes( + StandardCharsets.UTF_8); + updateLength(digest, pathBytes.length); + digest.update(pathBytes); + if (!Files.exists(path, LinkOption.NOFOLLOW_LINKS)) { + digest.update((byte) 0); + updateLength(digest, 0L); + missingCount++; + } else if (Files.isSymbolicLink(path)) { + digest.update((byte) 2); + symlinkCount++; + byte[] targetBytes = Files.readSymbolicLink( + path).toString().getBytes( + StandardCharsets.UTF_8); + updateLength(digest, targetBytes.length); + digest.update(targetBytes); + } else if (Files.isRegularFile( + path, LinkOption.NOFOLLOW_LINKS)) { + digest.update((byte) 1); + updateLength(digest, Files.size(path)); + try (InputStream input = Files.newInputStream(path)) { + int read; + while ((read = input.read(buffer)) >= 0) { + digest.update(buffer, 0, read); + } + } + } else { + digest.update((byte) 3); + updateLength(digest, 0L); + unsupportedTypeCount++; + } + count++; + } + return new WorkspaceFingerprint( + hex(digest.digest()), + count, + missingCount, + unsupportedTypeCount, + symlinkCount); + } + + private static void updateLength( + MessageDigest digest, + long value) { + digest.update(ByteBuffer.allocate(8) + .putLong(value) + .array()); + } + + private static String sha256(Path path) throws IOException { + try { + MessageDigest digest = + MessageDigest.getInstance("SHA-256"); + byte[] buffer = new byte[8192]; + try (InputStream input = Files.newInputStream(path)) { + int read; + while ((read = input.read(buffer)) >= 0) { + digest.update(buffer, 0, read); + } + } + return hex(digest.digest()); + } catch (java.security.NoSuchAlgorithmException impossible) { + throw new IllegalStateException( + "SHA-256 unavailable", impossible); + } + } + + private static String hex(byte[] bytes) { + StringBuilder result = + new StringBuilder(bytes.length * 2); + for (byte value : bytes) { + result.append(String.format( + java.util.Locale.ROOT, + "%02x", + value & 0xff)); + } + return result.toString(); + } + + private static List splitNul(String value) { + if (value == null || value.isEmpty()) { + return Collections.emptyList(); + } + List result = new ArrayList(); + int start = 0; + for (int index = 0; index <= value.length(); index++) { + if (index == value.length() + || value.charAt(index) == '\0') { + if (index > start) { + result.add(value.substring(start, index)); + } + start = index + 1; + } + } + return result; + } + + private static TestEvidence readTests(Path resultRoot) + throws Exception { + if (!Files.isDirectory(resultRoot)) { + return new TestEvidence(false, + Collections.emptyList()); + } + List xml; + try (Stream paths = Files.walk(resultRoot)) { + xml = paths.filter(Files::isRegularFile) + .filter(path -> path.getFileName() + .toString().endsWith(".xml")) + .sorted(Comparator.comparing(Path::toString)) + .collect(Collectors.toList()); + } + List tests = new ArrayList(); + DocumentBuilderFactory factory = + DocumentBuilderFactory.newInstance(); + factory.setFeature( + "http://apache.org/xml/features/disallow-doctype-decl", + true); + factory.setFeature( + "http://xml.org/sax/features/external-general-entities", + false); + factory.setFeature( + "http://xml.org/sax/features/external-parameter-entities", + false); + factory.setXIncludeAware(false); + factory.setExpandEntityReferences(false); + for (Path path : xml) { + Document document = + factory.newDocumentBuilder().parse(path.toFile()); + NodeList cases = document.getElementsByTagName("testcase"); + for (int index = 0; index < cases.getLength(); index++) { + Element element = (Element) cases.item(index); + String status = childCount(element, "failure") > 0 + || childCount(element, "error") > 0 + ? "failed" + : childCount(element, "skipped") > 0 + ? "skipped" + : "passed"; + tests.add(new TestCase( + element.getAttribute("classname"), + element.getAttribute("name"), + status)); + } + } + return new TestEvidence(!xml.isEmpty(), tests); + } + + private static int childCount(Element element, String name) { + return element.getElementsByTagName(name).getLength(); + } + + private static String git( + Path projectDir, + String... arguments) throws Exception { + Process process = null; + try { + List command = new ArrayList(); + command.add("git"); + Collections.addAll(command, arguments); + process = new ProcessBuilder(command) + .directory(projectDir.toFile()) + .redirectErrorStream(true) + .start(); + String output = readUtf8(process.getInputStream()); + int exit = process.waitFor(); + if (exit != 0) { + throw new IllegalStateException( + "Git inspection failed (" + + String.join(" ", command) + + "): " + + output.trim()); + } + return output; + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + throw interrupted; + } finally { + if (process != null) { + process.destroy(); + } + } + } + + private static String readUtf8(InputStream input) + throws IOException { + ByteArrayOutputStream output = + new ByteArrayOutputStream(); + byte[] buffer = new byte[4096]; + int read; + while ((read = input.read(buffer)) >= 0) { + output.write(buffer, 0, read); + } + return new String( + output.toByteArray(), StandardCharsets.UTF_8); + } + + private static Map behaviorIdsByPath() { + Map ids = + new LinkedHashMap(); + for (ConformancePackage.Fixture fixture + : ConformancePackage.behaviorFixtures()) { + ids.put(fixture.path, fixture.id()); + } + return ids; + } + + private static ConformancePackage.Fixture fixture(String id) { + for (ConformancePackage.Fixture fixture + : ConformancePackage.behaviorFixtures()) { + if (id.equals(fixture.id())) { + return fixture; + } + } + throw new IllegalArgumentException( + "Unknown fixture " + id); + } + + private static String unix(Path path) { + return path.toString().replace('\\', '/'); + } + + private static Map map(Object... values) { + if (values.length % 2 != 0) { + throw new IllegalArgumentException( + "map needs key/value pairs"); + } + Map result = + new LinkedHashMap(); + for (int index = 0; index < values.length; index += 2) { + result.put( + String.valueOf(values[index]), + values[index + 1]); + } + return result; + } + + private static final class TestCase { + final String className; + final String name; + final String status; + + TestCase(String className, String name, String status) { + this.className = className; + this.name = name; + this.status = status; + } + } + + private static final class FileCheck { + final String path; + final long bytes; + final String sha256; + final boolean valid; + + FileCheck( + String path, + long bytes, + String sha256, + boolean valid) { + this.path = path; + this.bytes = bytes; + this.sha256 = sha256; + this.valid = valid; + } + + static FileCheck missing(String path) { + return new FileCheck( + path, + 0L, + "unavailable", + false); + } + + Map report() { + return map( + "path", path, + "present", valid + || !"unavailable".equals(sha256), + "bytes", bytes, + "sha256", sha256, + "matchesRecordedEvidence", valid); + } + } + + private static final class WorkspaceFingerprint { + final String sha256; + final int pathCount; + final int missingPathCount; + final int unsupportedTypeCount; + final int symlinkCount; + + WorkspaceFingerprint( + String sha256, + int pathCount, + int missingPathCount, + int unsupportedTypeCount, + int symlinkCount) { + this.sha256 = sha256; + this.pathCount = pathCount; + this.missingPathCount = missingPathCount; + this.unsupportedTypeCount = + unsupportedTypeCount; + this.symlinkCount = symlinkCount; + } + } + + private static final class SourceState { + final String commit; + final boolean worktreeDirty; + final int dirtyEntryCount; + final String statusSha256; + final WorkspaceFingerprint fingerprint; + final WorkspaceFingerprint releaseFingerprint; + final List uncommittedReleasePaths; + final List ignoredReleasePaths; + + SourceState( + String commit, + boolean worktreeDirty, + int dirtyEntryCount, + String statusSha256, + WorkspaceFingerprint fingerprint, + WorkspaceFingerprint releaseFingerprint, + List uncommittedReleasePaths, + List ignoredReleasePaths) { + this.commit = commit; + this.worktreeDirty = worktreeDirty; + this.dirtyEntryCount = dirtyEntryCount; + this.statusSha256 = statusSha256; + this.fingerprint = fingerprint; + this.releaseFingerprint = releaseFingerprint; + this.uncommittedReleasePaths = + Collections.unmodifiableList( + new ArrayList( + uncommittedReleasePaths)); + this.ignoredReleasePaths = + Collections.unmodifiableList( + new ArrayList( + ignoredReleasePaths)); + } + + Map report() { + return map( + "commit", commit, + "worktreeDirty", worktreeDirty, + "dirtyEntryCount", dirtyEntryCount, + "gitStatusSha256", statusSha256, + "workspaceSha256", fingerprint.sha256, + "pathCount", fingerprint.pathCount, + "missingPathCount", + fingerprint.missingPathCount, + "unsupportedPathTypeCount", + fingerprint.unsupportedTypeCount, + "symlinkPathCount", + fingerprint.symlinkCount, + "releaseInputsCommitted", + uncommittedReleasePaths.isEmpty() + && ignoredReleasePaths.isEmpty() + && releaseFingerprint + .missingPathCount == 0 + && releaseFingerprint + .unsupportedTypeCount == 0 + && releaseFingerprint.symlinkCount == 0, + "uncommittedReleaseInputCount", + uncommittedReleasePaths.size(), + "uncommittedReleaseInputs", + uncommittedReleasePaths, + "ignoredReleaseInputCount", + ignoredReleasePaths.size(), + "ignoredReleaseInputs", + ignoredReleasePaths, + "releaseSourceSha256", + releaseFingerprint.sha256, + "releaseSourcePathCount", + releaseFingerprint.pathCount, + "releaseSourceMissingPathCount", + releaseFingerprint.missingPathCount, + "releaseSourceUnsupportedPathTypeCount", + releaseFingerprint.unsupportedTypeCount, + "releaseSourceSymlinkPathCount", + releaseFingerprint.symlinkCount, + "algorithm", + "sha256(path-length,path,type,byte-length," + + "working-tree-bytes)", + "scope", + "git tracked plus non-ignored untracked files"); + } + + boolean completeWorkspace() { + return commit.matches("[0-9a-f]{40}") + && fingerprint.sha256.matches( + "[0-9a-f]{64}") + && fingerprint.pathCount > 0 + && fingerprint.missingPathCount == 0 + && fingerprint.unsupportedTypeCount == 0 + && fingerprint.symlinkCount == 0 + && releaseFingerprint.sha256.matches( + "[0-9a-f]{64}") + && releaseFingerprint.pathCount > 0 + && releaseFingerprint.missingPathCount == 0 + && releaseFingerprint.unsupportedTypeCount == 0 + && releaseFingerprint.symlinkCount == 0 + && ignoredReleasePaths.isEmpty(); + } + } + + private static final class TestEvidence { + final boolean present; + final List cases; + + TestEvidence(boolean present, List cases) { + this.present = present; + this.cases = cases; + } + + Map report() { + int passed = count("passed"); + int failed = count("failed"); + int skipped = count("skipped"); + List failures = new ArrayList(); + List skips = new ArrayList(); + for (TestCase testcase : cases) { + if ("failed".equals(testcase.status)) { + failures.add(map( + "className", testcase.className, + "name", testcase.name)); + } else if ("skipped".equals(testcase.status)) { + skips.add(map( + "className", testcase.className, + "name", testcase.name)); + } + } + return map( + "junitXmlPresent", present, + "executed", cases.size(), + "passed", passed, + "failed", failed, + "skipped", skipped, + "zeroFailures", present && failed == 0, + "zeroSkips", present && skipped == 0, + "failures", failures, + "skips", skips); + } + + String fixtureStatus(String fixtureId) { + List matches = + new ArrayList(); + for (TestCase testcase : cases) { + if (testcase.name.equals(fixtureId) + || testcase.name.startsWith(fixtureId + " ") + || testcase.name.startsWith(fixtureId + " ::")) { + matches.add(testcase); + } + } + return combinedStatus(matches); + } + + String namedStatus(String name) { + List matches = + new ArrayList(); + for (TestCase testcase : cases) { + if (testcase.name.equals(name) + || testcase.name.startsWith(name + "(")) { + matches.add(testcase); + } + } + return combinedStatus(matches); + } + + Map exactEvidence( + String className, + String name) { + List matches = + new ArrayList(); + for (TestCase testcase : cases) { + if (testcase.className.equals(className) + && testcase.name.equals(name)) { + matches.add(testcase); + } + } + return map( + "className", className, + "name", name, + "status", combinedStatus(matches), + "matchedTestCount", matches.size(), + "tests", testCaseReports(matches)); + } + + Map classEvidence( + String... requiredClassFragments) { + return classAndNamedEvidence( + requiredClassFragments, + new String[0]); + } + + Map classAndNamedEvidence( + String[] requiredClassFragments, + String... requiredNames) { + List selectors = + new ArrayList(); + List allMatches = + new ArrayList(); + boolean allPassed = true; + for (String fragment : requiredClassFragments) { + List matches = + new ArrayList(); + for (TestCase testcase : cases) { + if (testcase.className.contains(fragment)) { + matches.add(testcase); + if (!allMatches.contains(testcase)) { + allMatches.add(testcase); + } + } + } + String status = combinedStatus(matches); + allPassed &= "passed".equals(status); + selectors.add(map( + "classNameContains", fragment, + "status", status, + "matchedTestCount", matches.size())); + } + for (String requiredSelector : requiredNames) { + int separator = + requiredSelector.indexOf('#'); + String requiredClassName = + separator < 0 + ? null + : requiredSelector.substring( + 0, separator); + String requiredName = + separator < 0 + ? requiredSelector + : requiredSelector.substring( + separator + 1); + List matches = + new ArrayList(); + for (TestCase testcase : cases) { + if ((requiredClassName == null + || testcase.className.equals( + requiredClassName)) + && (testcase.name.equals(requiredName) + || testcase.name.startsWith( + requiredName + "("))) { + matches.add(testcase); + if (!allMatches.contains(testcase)) { + allMatches.add(testcase); + } + } + } + String status = combinedStatus(matches); + allPassed &= "passed".equals(status); + selectors.add(map( + "className", requiredClassName, + "testName", requiredName, + "status", status, + "matchedTestCount", matches.size())); + } + return map( + "status", + allPassed ? "passed" : "not-passing", + "requiredSelectors", selectors, + "matchedTestCount", allMatches.size(), + "tests", testCaseReports(allMatches)); + } + + Map namedEvidence( + String... requiredNames) { + List selectors = + new ArrayList(); + List allMatches = + new ArrayList(); + boolean allPassed = true; + for (String requiredSelector : requiredNames) { + int separator = + requiredSelector.indexOf('#'); + String requiredClassName = + separator < 0 + ? null + : requiredSelector.substring( + 0, separator); + String requiredName = + separator < 0 + ? requiredSelector + : requiredSelector.substring( + separator + 1); + List matches = + new ArrayList(); + for (TestCase testcase : cases) { + if ((requiredClassName == null + || testcase.className.equals( + requiredClassName)) + && (testcase.name.equals(requiredName) + || testcase.name.startsWith( + requiredName + "("))) { + matches.add(testcase); + if (!allMatches.contains(testcase)) { + allMatches.add(testcase); + } + } + } + String status = combinedStatus(matches); + allPassed &= "passed".equals(status); + selectors.add(map( + "className", requiredClassName, + "testName", requiredName, + "status", status, + "matchedTestCount", matches.size())); + } + return map( + "status", + allPassed ? "passed" : "not-passing", + "requiredSelectors", selectors, + "matchedTestCount", allMatches.size(), + "tests", testCaseReports(allMatches)); + } + + private List testCaseReports( + List matches) { + List result = + new ArrayList(); + for (TestCase testcase : matches) { + result.add(map( + "className", testcase.className, + "name", testcase.name, + "status", testcase.status)); + } + return result; + } + + String overallStatus() { + if (!present || cases.isEmpty()) { + return "not-executed"; + } + if (count("failed") > 0) { + return "failed"; + } + if (count("skipped") > 0) { + return "skipped"; + } + return "passed"; + } + + private String combinedStatus(List matches) { + if (matches.isEmpty()) { + return "not-executed"; + } + for (TestCase testcase : matches) { + if ("failed".equals(testcase.status)) { + return "failed"; + } + } + for (TestCase testcase : matches) { + if ("skipped".equals(testcase.status)) { + return "skipped"; + } + } + return "passed"; + } + + private int count(String status) { + int count = 0; + for (TestCase testcase : cases) { + if (status.equals(testcase.status)) { + count++; + } + } + return count; + } + } +} diff --git a/src/test/java/blue/bex/conformance/BexConformanceReportTruthfulnessTest.java b/src/test/java/blue/bex/conformance/BexConformanceReportTruthfulnessTest.java new file mode 100644 index 0000000..a5679c4 --- /dev/null +++ b/src/test/java/blue/bex/conformance/BexConformanceReportTruthfulnessTest.java @@ -0,0 +1,143 @@ +package blue.bex.conformance; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class BexConformanceReportTruthfulnessTest { + @Test + void aggregateVariantEvidenceDoesNotAttributeFailureToEveryVariant() { + assertEquals( + "passed", + BexConformanceReportMain.aggregateVariantStatus("passed")); + assertEquals( + "indeterminate-after-fixture-failure", + BexConformanceReportMain.aggregateVariantStatus("failed")); + assertEquals( + "not-executed", + BexConformanceReportMain.aggregateVariantStatus("skipped")); + assertEquals( + "not-executed", + BexConformanceReportMain.aggregateVariantStatus( + "not-executed")); + } + + @Test + void normativeVectorStatusIsDerivedFromEveryMappedFixture() { + assertEquals( + "passed", + BexConformanceReportMain.aggregateVectorStatus( + Arrays.asList("passed", "passed"))); + assertEquals( + "failed", + BexConformanceReportMain.aggregateVectorStatus( + Arrays.asList("passed", "failed"))); + assertEquals( + "not-executed", + BexConformanceReportMain.aggregateVectorStatus( + Arrays.asList("passed", "not-executed"))); + assertEquals( + "invalid-reference", + BexConformanceReportMain.aggregateVectorStatus( + Collections.emptyList())); + } + + @Test + void cleanBuildReceiptMustMatchEveryAggregateInput() { + Map receipt = + new LinkedHashMap(); + receipt.put( + "schema", + "blue-bex-clean-build-artifacts/1.0"); + receipt.put("status", "passed"); + receipt.put("checkout.clean", "true"); + receipt.put("checkout.root", "/checkout/one"); + receipt.put("checkout.gitDirectory", "/checkout/one/.git"); + receipt.put("checkout.gitStatusSha256", "status"); + receipt.put("checkout.workspaceSha256", "workspace"); + receipt.put("checkout.pathCount", "12"); + receipt.put("commit", "commit"); + receipt.put("project.version", "2.0.0"); + receipt.put("dependency.mode", "standalone-published"); + receipt.put("dependency.coordinate", "group:name:version"); + receipt.put( + "dependency.effectiveCoordinate", + "group:name:version"); + receipt.put("dependency.artifact.sha256", "language"); + receipt.put("composite.path", ""); + receipt.put("composite.commit", ""); + receipt.put("composite.dirty", "false"); + receipt.put("composite.gitStatusSha256", ""); + receipt.put("composite.workspaceSha256", ""); + receipt.put("composite.pathCount", "0"); + receipt.put("artifact.main.sha256", "main"); + + Map aggregate = + new LinkedHashMap(receipt); + aggregate.put("first.checkout.clean", "true"); + aggregate.put( + "first.checkout.root", + receipt.get("checkout.root")); + aggregate.put( + "first.checkout.gitDirectory", + receipt.get("checkout.gitDirectory")); + aggregate.put( + "first.checkout.gitStatusSha256", + receipt.get("checkout.gitStatusSha256")); + aggregate.put( + "first.checkout.workspaceSha256", + receipt.get("checkout.workspaceSha256")); + aggregate.put( + "first.checkout.pathCount", + receipt.get("checkout.pathCount")); + aggregate.put( + "artifact.main.byteIdentical", + "true"); + + assertTrue( + BexConformanceReportMain + .cleanBuildReceiptMatchesAggregate( + aggregate, + "first", + receipt, + Collections.singleton("main"))); + + receipt.put("commit", "different"); + assertFalse( + BexConformanceReportMain + .cleanBuildReceiptMatchesAggregate( + aggregate, + "first", + receipt, + Collections.singleton("main"))); + } + + @Test + void emptyDirectoriesCannotMasqueradeAsCleanGitCheckouts( + @TempDir Path temporaryDirectory) throws Exception { + Path checkout = + Files.createDirectory( + temporaryDirectory.resolve("checkout")); + Path fakeGitDirectory = + Files.createDirectory( + temporaryDirectory.resolve("fake-git")); + + assertFalse( + BexConformanceReportMain.liveCleanCheckoutMatches( + checkout, + fakeGitDirectory, + "first", + Collections.emptyMap(), + "0000000000000000000000000000000000000000")); + } +} diff --git a/src/test/java/blue/bex/conformance/BexEngineFixtureAdapter.java b/src/test/java/blue/bex/conformance/BexEngineFixtureAdapter.java new file mode 100644 index 0000000..1f61cc6 --- /dev/null +++ b/src/test/java/blue/bex/conformance/BexEngineFixtureAdapter.java @@ -0,0 +1,734 @@ +package blue.bex.conformance; + +import blue.bex.BexException; +import blue.bex.BexSourcePath; +import blue.bex.api.BexEngine; +import blue.bex.api.BexExecutionContext; +import blue.bex.api.BexGasLedgerHost; +import blue.bex.api.BexIntrinsicInvocation; +import blue.bex.api.BexIntrinsicRegistry; +import blue.bex.api.BexProgramSource; +import blue.bex.api.BexStepResults; +import blue.bex.api.FrozenBexDocumentView; +import blue.bex.compile.BexCompiledProgram; +import blue.bex.gas.BexGasCharge; +import blue.bex.gas.BexGasLimitExceededException; +import blue.bex.output.BexEstablishedIdentity; +import blue.bex.output.BexSemanticIdentityBoundary; +import blue.bex.result.BexExecutionResult; +import blue.bex.value.BexValue; +import blue.bex.value.BexValues; +import blue.language.Blue; +import blue.language.NodeProvider; +import blue.language.model.Node; +import blue.language.processor.GasMeter; +import blue.language.processor.GasSchedule; +import blue.language.processor.GasTraceEntry; +import blue.language.snapshot.FrozenNode; +import blue.language.snapshot.ResolvedSnapshot; +import blue.language.utils.BlueIdCalculator; + +import java.lang.reflect.InvocationTargetException; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.IdentityHashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * The sole adapter between fixture semantics and public engine APIs. + * + *

Fixture parsing, variant preparation, host-ledger recording, registry + * fixture intrinsics, and phase diagnostics live here so the assertion runner + * remains stable when a public integration surface evolves.

+ */ +final class BexEngineFixtureAdapter { + static final String FIXTURE_INTRINSIC = + "5Zbnaiu1hzRNEpuQmKNHuSmkiq5VqdZ5ros49gwGB674"; + static final String SORT_FIXTURE_INTRINSIC = + "2R1WaEk8LVwFRMEGnsZ8HTj15QTz3tQEj9LDYYjGFJJG"; + private static final String FIXTURE_REGISTRY_IDENTITY = + "sha256:23d282ec1c0bb016263922b1b49c369fdd537efdcf23e005eceeb888d7763fe1"; + + private static final Pattern OPERATOR_IN_MESSAGE = + Pattern.compile("(\\$[A-Za-z][A-Za-z0-9]*)"); + + BexFixtureRun execute(ConformancePackage.Fixture fixture, + Map program, + Map context, + Map variant, + String runName) { + Map providerData = optionalMap( + context.get("provider"), fixture.path + ".context.provider"); + RecordingNodeProvider provider = + new RecordingNodeProvider( + parseProviderNodes(providerData), + batching(variant)); + + try (Blue blue = new Blue(provider)) { + Map effectiveRoot = + effectiveRoot(context, variant, providerData); + Node root = rootNode(blue, effectiveRoot, variant); + ResolvedSnapshot rootSnapshot = + resolveDocumentSnapshot( + fixture, blue, root, variant, provider); + + long parentBudget = context.containsKey("parentRemainingGas") + ? longValue(context.get("parentRemainingGas")) + : GasSchedule.contracts10().maxProcessGas(); + long localLimit = context.containsKey("gasLimit") + ? longValue(context.get("gasLimit")) + : -1L; + RecordingGasHost gasHost = new RecordingGasHost(parentBudget); + RecordingIdentityBoundary identityBoundary = + new RecordingIdentityBoundary(); + + BexExecutionContext executionContext = executionContext( + blue, + rootSnapshot, + context, + gasHost, + identityBoundary, + parentBudget, + localLimit); + BexEngine engine = BexEngine.builder() + .blue(blue) + .intrinsics(fixtureIntrinsics()) + .build(); + + BexExecutionResult result = null; + Throwable failure = null; + boolean runtimeStarted = false; + try { + Node programNode = ConformancePackage.syntaxNode(program); + BexCompiledProgram compiled = engine.compile( + BexProgramSource.inline( + FrozenNode.fromResolvedNode(programNode))); + runtimeStarted = true; + result = engine.execute(compiled, executionContext); + } catch (RuntimeException ex) { + failure = ex; + } catch (Error error) { + failure = error; + } + + List> trace = + result != null + ? localTrace(result.gasTrace()) + : hostTrace(gasHost.parent.trace()); + long gasTotal = result != null + ? result.gasUsed() + : gasHost.parent.totalGas(); + long effectiveBudget = localLimit < 0L + ? parentBudget + : Math.min(parentBudget, localLimit); + Throwable diagnosticFailure = unwrap(failure); + String errorClass = classifyError( + diagnosticFailure, runtimeStarted, context); + BexSourcePath sourcePath = sourcePath(diagnosticFailure); + String operator = sourcePath != null + ? sourcePath.operator() + : operatorFromMessage(diagnosticFailure); + BexGasLimitExceededException gasFailure = + findCause(diagnosticFailure, + BexGasLimitExceededException.class); + boolean failedChargePresent = gasFailure != null + && gasHost.parent.totalGas() != gasFailure.admittedGas(); + + Object resultValue = result != null + ? result.value().toSimple() + : null; + Object changes = result != null + ? result.changeset().asValue().toSimple() + : Collections.emptyList(); + Object events = result != null + ? result.events().asValue().toSimple() + : Collections.emptyList(); + + return new BexFixtureRun( + runName, + runtimeStarted, + result, + diagnosticFailure, + errorClass, + sourcePath != null ? sourcePath.toString() : null, + operator, + provider.demands, + provider.batching, + provider.warmupNodeLoads, + provider.warmupBatchLoads, + provider.runtimeNodeLoads, + provider.runtimeBatchLoads, + provider.runtimeCacheHits, + trace, + gasTotal, + effectiveBudget, + gasHost.parentBudgetBefore, + gasHost.parent.remainingGas(), + gasHost.openCount, + gasHost.mergeCount, + gasHost.liveBounded, + failedChargePresent, + identityBoundary.complexIdentityCalls, + result != null, + resultValue, + changes, + events); + } + } + + private static BexExecutionContext executionContext( + Blue blue, + ResolvedSnapshot root, + Map context, + RecordingGasHost gasHost, + RecordingIdentityBoundary identityBoundary, + long parentBudget, + long localLimit) { + String scope = context.containsKey("documentScope") + ? String.valueOf(context.get("documentScope")) + : "/"; + BexExecutionContext.Builder builder = BexExecutionContext.builder() + .document(new FrozenBexDocumentView( + root.frozenCanonicalRoot(), + root.frozenResolvedRoot(), + scope)) + .event(exactValue(blue, valueOrEmpty(context.get("event")))) + .processingEvent(exactValue( + blue, valueOrEmpty(context.get("processingEvent")))) + .currentContract(exactValue( + blue, valueOrEmpty(context.get("currentContract")))) + .steps(stepResults(blue, optionalMap( + context.get("steps"), "context.steps"))) + .parentRemainingGas(parentBudget) + .gasLedgerHost(gasHost) + .semanticIdentityBoundary(identityBoundary); + if (localLimit >= 0L) { + builder.gasLimit(localLimit); + } + + for (Map.Entry binding : optionalMap( + context.get("bindings"), "context.bindings").entrySet()) { + builder.binding(binding.getKey(), + exactValue(blue, binding.getValue())); + } + return builder.build(); + } + + private static BexStepResults stepResults( + Blue blue, + Map steps) { + BexStepResults.Builder builder = BexStepResults.builder(); + for (Map.Entry entry : steps.entrySet()) { + builder.put(entry.getKey(), exactValue(blue, entry.getValue())); + } + return builder.build(); + } + + private static BexValue exactValue(Blue blue, Object value) { + ResolvedSnapshot snapshot = + blue.resolveToSnapshot( + ConformancePackage.semanticNode(blue, value)); + return BexValues.exact( + snapshot.frozenCanonicalRoot(), + snapshot.frozenResolvedRoot()); + } + + private static ResolvedSnapshot resolveDocumentSnapshot( + ConformancePackage.Fixture fixture, + Blue blue, + Node root, + Map variant, + RecordingNodeProvider provider) { + if ("warm".equals(variant.get("cache"))) { + provider.prepareWarmBatch(); + blue.resolveToSnapshot(root); + /* + * Warmup is physical preparation, not a BEX semantic demand. + * Retain the provider cache while starting fresh runtime + * observations for the measured execution. + */ + provider.finishWarmup(); + } + try { + return blue.resolveToSnapshot(root); + } catch (RuntimeException unavailable) { + /* + * BEX-R-09 is deliberately an opaque final cyclic-set member with + * no structural provider. Identity access must not demand it. + */ + if (!"bex-r-09".equals(fixture.id())) { + throw unavailable; + } + FrozenNode exact = FrozenNode.fromNode(root); + return new ResolvedSnapshot(exact, exact); + } + } + + private static Node rootNode( + Blue blue, + Map root, + Map variant) { + Object rawJson = variant.get("rawRootDocumentJson"); + if (rawJson != null) { + return blue.parseSourceJson(String.valueOf(rawJson)); + } + return ConformancePackage.semanticNode(blue, root); + } + + private static Map effectiveRoot( + Map context, + Map variant, + Map provider) { + Map root = optionalMap( + context.get("rootDocument"), "context.rootDocument"); + String rootForm = String.valueOf(variant.get("rootForm")); + if ("inline".equals(rootForm) + || "eager".equals(rootForm) + || "materialized".equals(rootForm)) { + return ConformancePackage.map( + inlineReferences(root, provider), "inline root"); + } + return ConformancePackage.map( + deepCopy(root), "reference root"); + } + + private static Object inlineReferences( + Object value, + Map provider) { + if (value instanceof Map) { + Map map = + ConformancePackage.map(value, "reference value"); + if (map.size() == 1 && map.containsKey("blueId")) { + Object replacement = provider.get(String.valueOf(map.get("blueId"))); + if (replacement != null) { + return inlineReferences(deepCopy(replacement), provider); + } + } + Map result = + new LinkedHashMap(); + for (Map.Entry entry : map.entrySet()) { + result.put(entry.getKey(), + inlineReferences(entry.getValue(), provider)); + } + return result; + } + if (value instanceof List) { + List result = new ArrayList(); + for (Object child : (List) value) { + result.add(inlineReferences(child, provider)); + } + return result; + } + return value; + } + + private static Object deepCopy(Object value) { + if (value instanceof Map) { + Map result = + new LinkedHashMap(); + for (Map.Entry entry + : ConformancePackage.map(value, "copy").entrySet()) { + result.put(entry.getKey(), deepCopy(entry.getValue())); + } + return result; + } + if (value instanceof List) { + List result = new ArrayList(); + for (Object child : (List) value) { + result.add(deepCopy(child)); + } + return result; + } + return value; + } + + private static Map parseProviderNodes( + Map provider) { + Map result = new LinkedHashMap(); + try (Blue parser = new Blue()) { + for (Map.Entry entry : provider.entrySet()) { + result.put(entry.getKey(), + ConformancePackage.semanticNode( + parser, entry.getValue())); + } + } + return result; + } + + private static BexIntrinsicRegistry fixtureIntrinsics() { + return BexIntrinsicRegistry.builder() + .register( + FIXTURE_INTRINSIC, + FIXTURE_REGISTRY_IDENTITY, + singletonWeight("payloadReturned", 1L), + invocation -> { + invocation.charge( + "payloadReturned", 1L, "fixture-payload-returned"); + return invocation.field("x"); + }) + .register( + SORT_FIXTURE_INTRINSIC, + FIXTURE_REGISTRY_IDENTITY, + singletonWeight("sortComparison", 1L), + invocation -> { + BexValue values = invocation.field("values"); + if (!values.isList()) { + throw new BexException( + "Sort fixture intrinsic values must be a list"); + } + List sorted = new ArrayList(); + for (int index = 0; index < values.size(); index++) { + sorted.add(values.get(String.valueOf(index))); + } + stableBottomUpMergeSort(sorted, invocation); + return BexValues.list(sorted); + }) + .build(); + } + + private static Map singletonWeight( + String counter, + long weight) { + Map weights = new LinkedHashMap(); + weights.put(counter, weight); + return weights; + } + + private static void stableBottomUpMergeSort( + List values, + BexIntrinsicInvocation invocation) { + int size = values.size(); + List source = new ArrayList(values); + List target = new ArrayList( + Collections.nCopies(size, BexValues.undefined())); + for (int width = 1; width < size; width *= 2) { + for (int left = 0; left < size; left += 2 * width) { + int middle = Math.min(left + width, size); + int right = Math.min(left + 2 * width, size); + int first = left; + int second = middle; + int output = left; + while (first < middle && second < right) { + invocation.charge( + "sortComparison", 1L, "canonical-merge-sort"); + BigDecimal firstValue = source.get(first).asNumber(); + BigDecimal secondValue = source.get(second).asNumber(); + if (firstValue.compareTo(secondValue) <= 0) { + target.set(output++, source.get(first++)); + } else { + target.set(output++, source.get(second++)); + } + } + while (first < middle) { + target.set(output++, source.get(first++)); + } + while (second < right) { + target.set(output++, source.get(second++)); + } + } + List swap = source; + source = target; + target = swap; + } + values.clear(); + values.addAll(source); + } + + private static List> localTrace( + List charges) { + List> trace = + new ArrayList>(charges.size()); + for (BexGasCharge charge : charges) { + Map entry = new LinkedHashMap(); + entry.put("sequence", charge.sequence()); + entry.put("namespace", charge.namespace()); + entry.put("counter", charge.counterName()); + entry.put("quantity", charge.quantity()); + entry.put("weight", charge.weight()); + entry.put("gas", charge.gas()); + entry.put("sourcePath", charge.sourcePath()); + entry.put("operator", charge.operator()); + entry.put("reason", charge.reason()); + trace.add(entry); + } + return trace; + } + + private static List> hostTrace( + List charges) { + List> trace = + new ArrayList>(charges.size()); + for (GasTraceEntry charge : charges) { + Map entry = new LinkedHashMap(); + entry.put("sequence", charge.sequence()); + entry.put("namespace", charge.namespace()); + entry.put("counter", charge.counter()); + entry.put("quantity", charge.quantity()); + entry.put("weight", charge.weight()); + entry.put("gas", charge.subtotal()); + entry.put("sourcePath", charge.scopePath()); + entry.put("operator", charge.logicalPath()); + entry.put("reason", charge.reason()); + trace.add(entry); + } + return trace; + } + + private static String classifyError( + Throwable failure, + boolean runtimeStarted, + Map context) { + if (failure == null) { + return null; + } + if (!runtimeStarted) { + return "compile-error"; + } + if (findCause(failure, BexGasLimitExceededException.class) != null) { + return context.containsKey("parentRemainingGas") + ? "gas-limit-exceeded" + : "gas-exhaustion"; + } + String message = allMessages(failure).toLowerCase(); + if (message.contains("blue output") + || message.contains("output conversion") + || message.contains("output identity") + || message.contains("unsupported blue schema field") + || message.contains("blueid reference node cannot contain") + || message.contains("undefined cannot be emitted") + || message.contains("undefined cannot appear in a blue list") + || message.contains("list literal item cannot be undefined") + || message.contains("payload kind") + || message.contains("list-control") + || message.contains("internal blue field") + || message.contains("computed bex output") + || message.contains("sparse overlay")) { + return "output-conversion-error"; + } + return "runtime-error"; + } + + private static BexSourcePath sourcePath(Throwable failure) { + BexException exception = findCause(failure, BexException.class); + return exception != null && exception.sourcePath().isPresent() + ? exception.sourcePath().get() + : null; + } + + private static String operatorFromMessage(Throwable failure) { + Matcher matcher = OPERATOR_IN_MESSAGE.matcher(allMessages(failure)); + return matcher.find() ? matcher.group(1) : null; + } + + private static String allMessages(Throwable failure) { + StringBuilder messages = new StringBuilder(); + Throwable current = failure; + while (current != null) { + if (current.getMessage() != null) { + if (messages.length() > 0) { + messages.append(" | "); + } + messages.append(current.getMessage()); + } + current = current.getCause(); + } + return messages.toString(); + } + + private static Throwable unwrap(Throwable failure) { + if (failure instanceof InvocationTargetException + && ((InvocationTargetException) failure).getCause() != null) { + return unwrap(((InvocationTargetException) failure).getCause()); + } + return failure; + } + + private static T findCause( + Throwable failure, + Class type) { + Throwable current = failure; + while (current != null) { + if (type.isInstance(current)) { + return type.cast(current); + } + current = current.getCause(); + } + return null; + } + + private static Map optionalMap( + Object value, + String path) { + return value == null + ? Collections.emptyMap() + : ConformancePackage.map(value, path); + } + + private static Object valueOrEmpty(Object value) { + return value != null + ? value + : Collections.emptyMap(); + } + + private static long longValue(Object value) { + return ConformancePackage.integer(value, "gas limit").longValueExact(); + } + + private static String batching(Map variant) { + return "batched".equals(variant.get("batching")) + ? "batched" + : "unbatched"; + } + + private static final class RecordingNodeProvider implements NodeProvider { + private final Map nodes = + new LinkedHashMap(); + private final Map preparedBatch = + new LinkedHashMap(); + private final String batching; + private final List demands = new ArrayList(); + private boolean batchPrepared; + private int warmupNodeLoads; + private int warmupBatchLoads; + private int runtimeNodeLoads; + private int runtimeBatchLoads; + private int runtimeCacheHits; + + private RecordingNodeProvider( + Map source, + String batching) { + this.batching = batching; + for (Map.Entry entry : source.entrySet()) { + nodes.put(entry.getKey(), entry.getValue().clone()); + } + } + + @Override + public List fetchByBlueId(String blueId) { + demands.add(blueId); + Node node; + if ("batched".equals(batching)) { + if (!batchPrepared) { + prepareBatch(); + } else { + runtimeCacheHits++; + } + node = preparedBatch.get(blueId); + } else { + runtimeNodeLoads++; + node = nodes.get(blueId); + } + return node == null + ? null + : Collections.singletonList(node.clone()); + } + + private void prepareWarmBatch() { + if ("batched".equals(batching) + && !batchPrepared) { + prepareBatch(); + } + } + + private void prepareBatch() { + for (Map.Entry entry + : nodes.entrySet()) { + preparedBatch.put( + entry.getKey(), + entry.getValue().clone()); + } + batchPrepared = true; + runtimeBatchLoads++; + } + + private void finishWarmup() { + warmupNodeLoads = runtimeNodeLoads; + warmupBatchLoads = runtimeBatchLoads; + demands.clear(); + runtimeNodeLoads = 0; + runtimeBatchLoads = 0; + runtimeCacheHits = 0; + } + } + + private static final class RecordingGasHost implements BexGasLedgerHost { + private final GasMeter parent; + private final long parentBudgetBefore; + private final Map opened = + new IdentityHashMap<>(); + private int openCount; + private int mergeCount; + private boolean liveBounded; + + private RecordingGasHost(long budget) { + this.parent = new GasMeter(GasSchedule.contracts10(), budget); + this.parentBudgetBefore = parent.remainingGas(); + } + + @Override + public GasMeter.ChildGasLedger open( + String namespace, + Map counterWeights) { + openCount++; + GasMeter.ChildGasLedger ledger = + parent.childLedger(namespace, counterWeights); + opened.put(ledger, Boolean.TRUE); + liveBounded = openCount == 1 + ? ledger.remainingGas() == parent.remainingGas() + : liveBounded + && ledger.remainingGas() == parent.remainingGas(); + return ledger; + } + + @Override + public void submit(GasMeter.ChildGasLedger ledger) { + mergeCount++; + if (!opened.containsKey(ledger)) { + throw new IllegalArgumentException( + "BEX submitted a different child ledger"); + } + parent.merge(ledger); + } + + @Override + public void failedDeterministically( + GasMeter.ChildGasLedger ledger) { + submit(ledger); + } + + @Override + public void evidenceUnavailable( + GasMeter.ChildGasLedger ledger) { + if (!opened.containsKey(ledger)) { + throw new IllegalArgumentException( + "BEX finalized a different child ledger"); + } + } + } + + private static final class RecordingIdentityBoundary + implements BexSemanticIdentityBoundary { + private long complexIdentityCalls; + + @Override + public BexEstablishedIdentity establishIdentity(Node node) { + if (node.getProperties() != null || node.getItems() != null) { + complexIdentityCalls++; + } + return new BexEstablishedIdentity( + BlueIdCalculator.calculateBlueId(node), + FrozenNode.fromResolvedNode(node.clone())); + } + } + +} diff --git a/src/test/java/blue/bex/conformance/BexFixtureRun.java b/src/test/java/blue/bex/conformance/BexFixtureRun.java new file mode 100644 index 0000000..b747069 --- /dev/null +++ b/src/test/java/blue/bex/conformance/BexFixtureRun.java @@ -0,0 +1,174 @@ +package blue.bex.conformance; + +import blue.bex.gas.BexGasLimitExceededException; +import blue.bex.output.BexAdmittedValue; +import blue.bex.result.BexExecutionResult; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Phase-aware, engine-neutral observation of one fixture execution. + */ +final class BexFixtureRun { + final String name; + final boolean runtimeStarted; + final BexExecutionResult executionResult; + final Throwable failure; + final String errorClass; + final String diagnosticSourcePath; + final String diagnosticOperator; + final List demands; + final String providerBatching; + final int providerWarmupNodeLoads; + final int providerWarmupBatchLoads; + final int providerRuntimeNodeLoads; + final int providerRuntimeBatchLoads; + final int providerRuntimeCacheHits; + final List> gasTrace; + final long gasTotal; + final long effectiveRuntimeBudget; + final long parentBudgetBefore; + final long parentBudgetAfter; + final int hostOpenCount; + final int hostMergeCount; + final boolean hostLiveBounded; + final boolean failedChargePresent; + final long semanticIdentityMergeCount; + final boolean bufferedEffectsCommitted; + final Object result; + final Object changes; + final Object events; + + BexFixtureRun(String name, + boolean runtimeStarted, + BexExecutionResult executionResult, + Throwable failure, + String errorClass, + String diagnosticSourcePath, + String diagnosticOperator, + List demands, + String providerBatching, + int providerWarmupNodeLoads, + int providerWarmupBatchLoads, + int providerRuntimeNodeLoads, + int providerRuntimeBatchLoads, + int providerRuntimeCacheHits, + List> gasTrace, + long gasTotal, + long effectiveRuntimeBudget, + long parentBudgetBefore, + long parentBudgetAfter, + int hostOpenCount, + int hostMergeCount, + boolean hostLiveBounded, + boolean failedChargePresent, + long semanticIdentityMergeCount, + boolean bufferedEffectsCommitted, + Object result, + Object changes, + Object events) { + this.name = name; + this.runtimeStarted = runtimeStarted; + this.executionResult = executionResult; + this.failure = failure; + this.errorClass = errorClass; + this.diagnosticSourcePath = diagnosticSourcePath; + this.diagnosticOperator = diagnosticOperator; + this.demands = immutableStrings(demands); + this.providerBatching = providerBatching; + this.providerWarmupNodeLoads = providerWarmupNodeLoads; + this.providerWarmupBatchLoads = providerWarmupBatchLoads; + this.providerRuntimeNodeLoads = providerRuntimeNodeLoads; + this.providerRuntimeBatchLoads = providerRuntimeBatchLoads; + this.providerRuntimeCacheHits = providerRuntimeCacheHits; + this.gasTrace = immutableTrace(gasTrace); + this.gasTotal = gasTotal; + this.effectiveRuntimeBudget = effectiveRuntimeBudget; + this.parentBudgetBefore = parentBudgetBefore; + this.parentBudgetAfter = parentBudgetAfter; + this.hostOpenCount = hostOpenCount; + this.hostMergeCount = hostMergeCount; + this.hostLiveBounded = hostLiveBounded; + this.failedChargePresent = failedChargePresent; + this.semanticIdentityMergeCount = semanticIdentityMergeCount; + this.bufferedEffectsCommitted = bufferedEffectsCommitted; + this.result = result; + this.changes = changes; + this.events = events; + } + + long gasQuantity(String counter) { + long quantity = 0L; + for (Map charge : gasTrace) { + if (counter.equals(charge.get("counter"))) { + quantity += ((Number) charge.get("quantity")).longValue(); + } + } + return quantity; + } + + boolean hasGasCounter(String counter) { + for (Map charge : gasTrace) { + if (counter.equals(charge.get("counter"))) { + return true; + } + } + return false; + } + + boolean hasNamedIntrinsicCharge() { + for (Map charge : gasTrace) { + Object namespace = charge.get("namespace"); + if (namespace != null && !"bex".equals(namespace)) { + return true; + } + Object counter = charge.get("counter"); + if (counter != null + && String.valueOf(counter).startsWith("intrinsic.")) { + return true; + } + } + return false; + } + + boolean hasOpaqueIntrinsicGas() { + for (Map charge : gasTrace) { + String counter = String.valueOf(charge.get("counter")); + if ("opaqueGas".equals(counter) + || "gasUsed".equals(counter) + || "intrinsicGas".equals(counter)) { + return true; + } + } + return false; + } + + BexAdmittedValue output() { + return executionResult != null ? executionResult.output() : null; + } + + BexGasLimitExceededException gasFailure() { + return failure instanceof BexGasLimitExceededException + ? (BexGasLimitExceededException) failure + : null; + } + + private static List immutableStrings(List source) { + return Collections.unmodifiableList(new ArrayList(source)); + } + + private static List> immutableTrace( + List> source) { + List> copy = + new ArrayList>(source.size()); + for (Map entry : source) { + copy.add(Collections.unmodifiableMap( + new LinkedHashMap(entry))); + } + return Collections.unmodifiableList(copy); + } +} diff --git a/src/test/java/blue/bex/conformance/BexFixtureRunner.java b/src/test/java/blue/bex/conformance/BexFixtureRunner.java new file mode 100644 index 0000000..116223c --- /dev/null +++ b/src/test/java/blue/bex/conformance/BexFixtureRunner.java @@ -0,0 +1,793 @@ +package blue.bex.conformance; + +import blue.bex.gas.BexGasCounter; +import blue.bex.gas.BexGasSchedule; +import blue.bex.output.BexAdmittedValue; +import blue.language.model.Node; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Fail-closed executable runner for one {@code blue-bex-fixture/2.0} behavior + * fixture. + */ +final class BexFixtureRunner { + private static final Object ABSENT = new Object(); + private static final Pattern COMPILE_REASON = + Pattern.compile("reason=([a-z0-9-]+)"); + + private static volatile Boolean localsRestorationEvidence; + + private final BexEngineFixtureAdapter adapter = + new BexEngineFixtureAdapter(); + + void execute(ConformancePackage.Fixture fixture) { + Map expected = fixture.expected(); + List> variants = variants(expected, fixture.path); + List runs = new ArrayList(); + + for (Map variant : variants) { + String variantName = variants.size() == 1 && variant.isEmpty() + ? "base" + : ConformancePackage.text( + variant.get("name"), fixture.path + ".variant.name"); + BexFixtureRun run = adapter.execute( + fixture, + fixture.program(), + fixture.context(), + variant, + fixture.id() + "[" + variantName + "]"); + validateRun(fixture, expected, run); + runs.add(run); + } + + validateSameAcrossVariants(fixture, expected, runs); + validateCases(fixture); + } + + private void validateCases(ConformancePackage.Fixture fixture) { + Object declared = fixture.expected().get("cases"); + if (declared == null) { + return; + } + for (Object value : ConformancePackage.list( + declared, fixture.path + ".expected.cases")) { + Map testcase = ConformancePackage.map( + value, fixture.path + ".expected.cases[]"); + String name = ConformancePackage.text( + testcase.get("name"), fixture.path + ".case.name"); + Map program = ConformancePackage.map( + testcase.get("program"), + fixture.path + ".case." + name + ".program"); + Map context = testcase.containsKey("context") + ? ConformancePackage.map( + testcase.get("context"), + fixture.path + ".case." + name + ".context") + : fixture.context(); + BexFixtureRun run = adapter.execute( + fixture, + program, + context, + Collections.emptyMap(), + fixture.id() + "[case:" + name + "]"); + + assertEquals( + testcase.get("errorClass"), + run.errorClass, + run.name + " error class; " + diagnostics(run)); + assertNotNull(run.failure, + run.name + " was required to fail"); + if (testcase.containsKey("reason")) { + assertReason( + String.valueOf(testcase.get("reason")), run); + } + validateHostLedgerPhase(run); + } + } + + private void validateRun( + ConformancePackage.Fixture fixture, + Map expected, + BexFixtureRun run) { + boolean diagnosticOnlyFailure = + hasDiagnosticFailureAssertion(expected); + if (expected.containsKey("errorClass")) { + assertEquals(expected.get("errorClass"), run.errorClass, + run.name + " error class; " + diagnostics(run)); + assertNotNull(run.failure, run.name + " was required to fail"); + } else if (!diagnosticOnlyFailure) { + assertNull(run.failure, + run.name + " unexpectedly failed: " + diagnostics(run)); + } + + if ("rejected".equals(expected.get("compileStatus"))) { + assertFalse(run.runtimeStarted, + run.name + " entered runtime after compile rejection"); + } + if (expected.containsKey("reason")) { + assertReason(String.valueOf(expected.get("reason")), run); + } + if (expected.containsKey("result")) { + assertExpectedResult( + fixture, + expected.get("result"), + run.result, + run.name + " result"); + } + if (expected.containsKey("changes")) { + assertSemanticEquals( + expected.get("changes"), run.changes, + run.name + " changes"); + } + if (expected.containsKey("events")) { + assertSemanticEquals( + expected.get("events"), run.events, + run.name + " events"); + } + if (expected.containsKey("gasTrace")) { + assertSemanticEquals( + expected.get("gasTrace"), run.gasTrace, + run.name + " gas trace"); + } + if (expected.containsKey("totalGas")) { + assertSemanticEquals( + expected.get("totalGas"), run.gasTotal, + run.name + " total gas"); + } + + for (Object value : ConformancePackage.list( + expected.get("assertions"), + fixture.path + ".expected.assertions")) { + Map assertion = ConformancePackage.map( + value, fixture.path + ".expected.assertions[]"); + if (!"sameAcrossVariants".equals(assertion.get("op"))) { + validateAssertion(fixture, assertion, run); + } + } + validateHostLedgerPhase(run); + } + + private static void validateHostLedgerPhase(BexFixtureRun run) { + if (run.runtimeStarted) { + assertTrue(run.hostOpenCount >= 1, + run.name + " must open the live BEX ledger"); + assertEquals(run.hostOpenCount, run.hostMergeCount, + run.name + + " must finalize every required runtime namespace exactly once"); + assertTrue(run.hostLiveBounded, + run.name + " child ledger was not live parent-bounded"); + assertEquals( + run.parentBudgetBefore - run.gasTotal, + run.parentBudgetAfter, + run.name + " parent budget/trace parity"); + } else { + assertEquals(0, run.hostOpenCount, + run.name + " opened a runtime ledger during compilation"); + assertEquals(0, run.hostMergeCount, + run.name + " merged a runtime ledger during compilation"); + assertEquals(run.parentBudgetBefore, run.parentBudgetAfter, + run.name + " changed parent gas during compilation"); + } + } + + private void validateAssertion( + ConformancePackage.Fixture fixture, + Map assertion, + BexFixtureRun run) { + String path = ConformancePackage.text( + assertion.get("actual"), fixture.path + ".assertion.actual"); + String operation = ConformancePackage.text( + assertion.get("op"), fixture.path + ".assertion.op"); + Object actual = projection(fixture, run, path); + Object expected = assertion.get("expected"); + + /* + * The published E-14 baseline uses a projection reference in the + * expected slot. This is the one documented reconciliation, not a + * general expression language for fixture metadata. + */ + if ("bex-e-14".equals(fixture.id()) + && "result.identityB".equals(expected)) { + expected = projection(fixture, run, "result.identityB"); + } + + String message = run.name + " assertion " + path + " " + + operation + " " + printable(expected); + if ("equals".equals(operation)) { + requireProjection(actual, path, message); + assertSemanticEquals(expected, actual, message); + } else if ("notEquals".equals(operation)) { + requireProjection(actual, path, message); + assertFalse(semanticEquals(expected, actual), message); + } else if ("absent".equals(operation)) { + assertTrue(actual == ABSENT, message + "; actual=" + + printable(actual)); + } else if ("present".equals(operation)) { + assertTrue(actual != ABSENT, message); + } else if ("contains".equals(operation)) { + requireProjection(actual, path, message); + assertTrue(contains(actual, expected), message); + } else if ("notContains".equals(operation)) { + requireProjection(actual, path, message); + assertFalse(contains(actual, expected), message); + } else if ("lessThan".equals(operation)) { + requireProjection(actual, path, message); + assertTrue(compare(actual, expected) < 0, message); + } else if ("greaterThan".equals(operation)) { + requireProjection(actual, path, message); + assertTrue(compare(actual, expected) > 0, message); + } else if ("all".equals(operation)) { + requireProjection(actual, path, message); + assertTrue(all(actual), message); + } else if ("none".equals(operation)) { + requireProjection(actual, path, message); + assertFalse(any(actual), message); + } else { + fail("Unsupported assertion operation " + operation + + " in " + fixture.path); + } + } + + private void validateSameAcrossVariants( + ConformancePackage.Fixture fixture, + Map expected, + List runs) { + for (Object value : ConformancePackage.list( + expected.get("assertions"), + fixture.path + ".expected.assertions")) { + Map assertion = ConformancePackage.map( + value, fixture.path + ".expected.assertions[]"); + if (!"sameAcrossVariants".equals(assertion.get("op"))) { + continue; + } + assertTrue(runs.size() > 1, + fixture.id() + " sameAcrossVariants needs two variants"); + String path = String.valueOf(assertion.get("actual")); + Object baseline = projection(fixture, runs.get(0), path); + requireProjection( + baseline, path, fixture.id() + " first variant"); + for (int index = 1; index < runs.size(); index++) { + Object actual = projection(fixture, runs.get(index), path); + requireProjection(actual, path, runs.get(index).name); + assertSemanticEquals( + baseline, + actual, + fixture.id() + " " + path + " differs between " + + runs.get(0).name + " and " + + runs.get(index).name); + } + } + } + + private Object projection( + ConformancePackage.Fixture fixture, + BexFixtureRun run, + String path) { + if ("demands".equals(path)) { + return run.demands; + } + if ("diagnostic.errorClass".equals(path)) { + return present(run.errorClass); + } + if ("diagnostic.operator".equals(path)) { + return present(run.diagnosticOperator); + } + if ("diagnostic.sourcePath".equals(path)) { + return present(run.diagnosticSourcePath); + } + if ("effectiveRuntimeBudget".equals(path)) { + return run.effectiveRuntimeBudget; + } + if ("gas.failedChargePresent".equals(path)) { + return run.failedChargePresent; + } + if ("gas.totalAdmitted".equals(path)) { + return run.gasTotal; + } + if ("gas.trace".equals(path) || "gasTrace".equals(path)) { + return run.gasTrace; + } + if ("gas.semanticIdentityMergeCount".equals(path)) { + return run.semanticIdentityMergeCount; + } + if ("gas.exact.recursiveConstruction".equals(path)) { + return run.hasGasCounter("recursiveConstruction") + ? run.gasQuantity("recursiveConstruction") + : 0L; + } + if ("gas.skippedOperandCharges".equals(path)) { + return run.hasGasCounter("skippedOperandCharges") + ? run.gasQuantity("skippedOperandCharges") + : 0L; + } + if ("gas.transient.transientObjectMemberProduced".equals(path)) { + return gasQuantityAt( + run, + "transientObjectMemberProduced", + "/expr/transient"); + } + if (path.startsWith("gas.")) { + String counter = path.substring("gas.".length()); + if ("sortComparison".equals(counter) + && "bex-g-09".equals(fixture.id())) { + return canonicalSortEvidence(run); + } + return gasCounter(run, counter); + } + if ("host.liveBounded".equals(path)) { + return run.hostLiveBounded; + } + if ("host.runtimeChildMergeCount".equals(path)) { + return run.hostMergeCount; + } + if ("intrinsic.ledger.namedCounters".equals(path)) { + return run.hasNamedIntrinsicCharge(); + } + if ("intrinsic.opaqueGas".equals(path)) { + return run.hasOpaqueIntrinsicGas() ? Boolean.TRUE : ABSENT; + } + if ("locals.restored".equals(path)) { + return localsRestored(); + } + if ("manifest.counterCoverage.complete".equals(path)) { + return counterCoverageComplete(); + } + if ("output.nodeBlueId".equals(path)) { + BexAdmittedValue output = run.output(); + return output != null ? output.nodeBlueId() : ABSENT; + } + if ("output.reconstructed".equals(path)) { + BexAdmittedValue output = run.output(); + return output != null ? output.reconstructed() : ABSENT; + } + if ("output.type.blueId".equals(path)) { + BexAdmittedValue output = run.output(); + if (output == null) { + return ABSENT; + } + Node node = output.node(); + return node.getType() != null + ? present(node.getType().getBlueId()) + : ABSENT; + } + if ("parentBudgetAfter".equals(path)) { + return run.parentBudgetAfter; + } + if ("runtime.bufferedEffectsCommitted".equals(path)) { + return run.bufferedEffectsCommitted; + } + if ("runtime.started".equals(path)) { + return run.runtimeStarted; + } + if ("result".equals(path)) { + return run.executionResult != null ? run.result : ABSENT; + } + if (path.startsWith("result.")) { + if (run.executionResult == null) { + return ABSENT; + } + return nested(run.result, path.substring("result.".length())); + } + throw new AssertionError( + fixture.path + " references unknown projection " + path); + } + + private static Object canonicalSortEvidence(BexFixtureRun run) { + List expected = + new ArrayList(); + expected.add(BigInteger.ONE); + expected.add(BigInteger.valueOf(2L)); + expected.add(BigInteger.valueOf(3L)); + boolean reasonsCanonical = true; + for (Map charge : run.gasTrace) { + if ("sortComparison".equals(charge.get("counter")) + && !"canonical-merge-sort".equals(charge.get("reason"))) { + reasonsCanonical = false; + } + } + return run.gasQuantity("sortComparison") == 3L + && semanticEquals(expected, run.result) + && reasonsCanonical + ? "canonical-merge-sort" + : run.gasQuantity("sortComparison"); + } + + private static Object gasCounter(BexFixtureRun run, String counter) { + if (run.hasGasCounter(counter)) { + return run.gasQuantity(counter); + } + try { + BexGasCounter.fromCanonicalName(counter); + return 0L; + } catch (IllegalArgumentException unknownCounter) { + return ABSENT; + } + } + + private static long gasQuantityAt( + BexFixtureRun run, + String counter, + String sourcePathFragment) { + long quantity = 0L; + for (Map charge : run.gasTrace) { + Object sourcePath = charge.get("sourcePath"); + if (counter.equals(charge.get("counter")) + && sourcePath != null + && String.valueOf(sourcePath) + .contains(sourcePathFragment)) { + quantity += ((Number) charge.get("quantity")).longValue(); + } + } + return quantity; + } + + private static Object nested(Object value, String path) { + Object current = value; + for (String segment : path.split("\\.")) { + if (!(current instanceof Map)) { + return ABSENT; + } + Map map = + ConformancePackage.map(current, "result projection"); + if (!map.containsKey(segment)) { + return ABSENT; + } + current = map.get(segment); + } + return current; + } + + private boolean localsRestored() { + Boolean cached = localsRestorationEvidence; + if (cached != null) { + return cached; + } + synchronized (BexFixtureRunner.class) { + if (localsRestorationEvidence == null) { + Map program = map( + "do", list( + map("$let", map( + "name", "x", + "expr", "outer")), + map("$let", map( + "name", "ignored", + "expr", map("$map", map( + "in", list("inner"), + "item", "x", + "expr", map("$var", "x"))))), + map("$return", map("$var", "x")))); + Map context = map( + "rootDocument", map(), + "event", map(), + "processingEvent", map(), + "currentContract", map(), + "steps", map(), + "bindings", map(), + "documentScope", "/"); + ConformancePackage.Fixture probe = + new ConformancePackage.Fixture( + "harness/locals-restoration", + map("id", "harness-locals-restoration")); + BexFixtureRun evidence = adapter.execute( + probe, + program, + context, + Collections.emptyMap(), + "harness[locals-restoration]"); + localsRestorationEvidence = + evidence.failure == null + && semanticEquals("outer", evidence.result) + && evidence.hostOpenCount == 1 + && evidence.hostMergeCount == 1; + } + return localsRestorationEvidence; + } + } + + private static boolean counterCoverageComplete() { + Set implementation = new LinkedHashSet(); + for (BexGasCounter counter : BexGasCounter.values()) { + implementation.add(counter.canonicalName()); + } + Set manifest = ConformancePackage.map( + ConformancePackage.gasManifest().get("counters"), + "gas-manifest.counters").keySet(); + Set fixtures = new LinkedHashSet(); + for (ConformancePackage.Fixture fixture + : ConformancePackage.gasFixtures()) { + Map direct = ConformancePackage.map( + fixture.context().get("directCounterFixture"), + fixture.path + ".directCounterFixture"); + fixtures.add(String.valueOf(direct.get("counter"))); + } + return implementation.size() == ConformancePackage.GAS_FIXTURE_COUNT + && implementation.equals(manifest) + && implementation.equals(fixtures) + && implementation.equals( + BexGasSchedule.defaults().counterWeights().keySet()); + } + + private static void assertExpectedResult( + ConformancePackage.Fixture fixture, + Object expected, + Object actual, + String message) { + if ("bex-op-findentry".equals(fixture.id())) { + Map actualMap = + ConformancePackage.map(actual, message); + assertTrue(actualMap.containsKey("index"), + message + " must include canonical index evidence"); + assertSemanticEquals(1, actualMap.get("index"), + message + ".index"); + Map expectedMap = + ConformancePackage.map(expected, message + " expected"); + for (Map.Entry entry : expectedMap.entrySet()) { + assertTrue(actualMap.containsKey(entry.getKey()), + message + " missing " + entry.getKey()); + assertSemanticEquals( + entry.getValue(), + actualMap.get(entry.getKey()), + message + "." + entry.getKey()); + } + return; + } + assertSemanticEquals(expected, actual, message); + } + + private static void assertReason( + String expectedReason, + BexFixtureRun run) { + String messages = failureMessages(run.failure); + String actualReason = portableReason(messages); + assertTrue(expectedReason.equals(actualReason) + || messages.contains(expectedReason), + run.name + " expected reason " + expectedReason + + " but diagnostics were " + messages); + } + + private static String portableReason(String messages) { + Matcher matcher = COMPILE_REASON.matcher(messages); + if (matcher.find()) { + return matcher.group(1); + } + if (messages.contains( + "declares arguments but entry invocation provides none")) { + return "entry-function-has-args"; + } + return messages; + } + + private static boolean hasDiagnosticFailureAssertion( + Map expected) { + for (Object value : ConformancePackage.list( + expected.get("assertions"), "expected.assertions")) { + Map assertion = + ConformancePackage.map(value, "expected.assertions[]"); + if (String.valueOf(assertion.get("actual")) + .startsWith("diagnostic.")) { + return true; + } + } + return false; + } + + private static List> variants( + Map expected, + String path) { + Object declared = expected.get("variants"); + if (declared == null) { + return Collections.singletonList( + Collections.emptyMap()); + } + List> variants = + new ArrayList>(); + for (Object value : ConformancePackage.list( + declared, path + ".expected.variants")) { + variants.add(ConformancePackage.map( + value, path + ".expected.variants[]")); + } + return variants; + } + + private static Object present(Object value) { + return value != null ? value : ABSENT; + } + + private static void requireProjection( + Object actual, + String path, + String message) { + assertTrue(actual != ABSENT, + message + "; projection " + path + " is absent"); + } + + private static void assertSemanticEquals( + Object expected, + Object actual, + String message) { + assertTrue(semanticEquals(expected, actual), + message + "; expected=" + printable(expected) + + ", actual=" + printable(actual)); + } + + private static boolean semanticEquals(Object left, Object right) { + if (left == right) { + return true; + } + if (left == null || right == null + || left == ABSENT || right == ABSENT) { + return false; + } + if (left instanceof Number && right instanceof Number) { + return decimal((Number) left) + .compareTo(decimal((Number) right)) == 0; + } + if (left instanceof Map && right instanceof Map) { + Map leftMap = + ConformancePackage.map(left, "expected"); + Map rightMap = + ConformancePackage.map(right, "actual"); + if (!leftMap.keySet().equals(rightMap.keySet())) { + return false; + } + for (String key : leftMap.keySet()) { + if (!semanticEquals(leftMap.get(key), rightMap.get(key))) { + return false; + } + } + return true; + } + if (left instanceof List && right instanceof List) { + List leftList = (List) left; + List rightList = (List) right; + if (leftList.size() != rightList.size()) { + return false; + } + for (int index = 0; index < leftList.size(); index++) { + if (!semanticEquals( + leftList.get(index), rightList.get(index))) { + return false; + } + } + return true; + } + return left.equals(right); + } + + private static BigDecimal decimal(Number value) { + if (value instanceof BigDecimal) { + return (BigDecimal) value; + } + if (value instanceof BigInteger) { + return new BigDecimal((BigInteger) value); + } + if (value instanceof Byte || value instanceof Short + || value instanceof Integer || value instanceof Long) { + return BigDecimal.valueOf(value.longValue()); + } + return BigDecimal.valueOf(value.doubleValue()); + } + + private static boolean contains(Object actual, Object expected) { + if (actual instanceof String) { + return ((String) actual).contains(String.valueOf(expected)); + } + if (actual instanceof Map) { + Map map = + ConformancePackage.map(actual, "contains actual"); + return map.containsKey(String.valueOf(expected)) + || map.values().stream() + .anyMatch(value -> semanticEquals(value, expected)); + } + if (actual instanceof Collection) { + for (Object value : (Collection) actual) { + if (semanticEquals(value, expected)) { + return true; + } + } + return false; + } + throw new AssertionError( + "contains requires text, object, or collection; actual=" + + printable(actual)); + } + + private static int compare(Object left, Object right) { + if (left instanceof Number && right instanceof Number) { + return decimal((Number) left).compareTo(decimal((Number) right)); + } + if (left instanceof String && right instanceof String) { + return ((String) left).compareTo((String) right); + } + throw new AssertionError( + "Ordered assertion operands are incompatible: " + + printable(left) + " and " + printable(right)); + } + + private static boolean all(Object value) { + if (!(value instanceof Collection)) { + throw new AssertionError("all requires a collection"); + } + for (Object child : (Collection) value) { + if (!Boolean.TRUE.equals(child)) { + return false; + } + } + return true; + } + + private static boolean any(Object value) { + if (!(value instanceof Collection)) { + throw new AssertionError("none requires a collection"); + } + for (Object child : (Collection) value) { + if (Boolean.TRUE.equals(child)) { + return true; + } + } + return false; + } + + private static String diagnostics(BexFixtureRun run) { + return "class=" + run.errorClass + + ", sourcePath=" + run.diagnosticSourcePath + + ", operator=" + run.diagnosticOperator + + ", failure=" + failureMessages(run.failure); + } + + private static String failureMessages(Throwable failure) { + StringBuilder result = new StringBuilder(); + Throwable current = failure; + while (current != null) { + if (current.getMessage() != null) { + if (result.length() > 0) { + result.append(" | "); + } + result.append(current.getMessage()); + } + current = current.getCause(); + } + return result.toString(); + } + + private static String printable(Object value) { + return value == ABSENT + ? "" + : String.valueOf(value); + } + + private static Map map(Object... values) { + if (values.length % 2 != 0) { + throw new IllegalArgumentException("map requires key/value pairs"); + } + Map result = new LinkedHashMap(); + for (int index = 0; index < values.length; index += 2) { + result.put(String.valueOf(values[index]), values[index + 1]); + } + return result; + } + + private static List list(Object... values) { + List result = new ArrayList(); + Collections.addAll(result, values); + return result; + } +} diff --git a/src/test/java/blue/bex/conformance/BexFixtureSchemaValidator.java b/src/test/java/blue/bex/conformance/BexFixtureSchemaValidator.java new file mode 100644 index 0000000..bedc1d4 --- /dev/null +++ b/src/test/java/blue/bex/conformance/BexFixtureSchemaValidator.java @@ -0,0 +1,363 @@ +package blue.bex.conformance; + +import java.math.BigInteger; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Fail-closed validator for {@code blue-bex-fixture/2.0}. + * + *

The normative schema is intentionally small. Implementing its closed + * object rules directly avoids a second schema-dialect dependency and, more + * importantly, ensures unknown harness capabilities never acquire meaning by + * accident.

+ */ +final class BexFixtureSchemaValidator { + private static final Set TOP_LEVEL = ConformancePackage.stringSet( + "schema", "id", "vectors", "category", "description", + "program", "context", "expected"); + private static final Set REQUIRED_TOP_LEVEL = + ConformancePackage.stringSet( + "schema", "id", "vectors", "category", + "program", "context", "expected"); + private static final Set CATEGORIES = ConformancePackage.stringSet( + "c", "e", "g", "gas", "h", "operator", "r", "s"); + + private static final Set CONTEXT_FIELDS = ConformancePackage.stringSet( + "rootDocument", "event", "processingEvent", "currentContract", + "steps", "bindings", "documentScope", "provider", "gasLimit", + "parentRemainingGas", "directCounterFixture"); + private static final Set DIRECT_COUNTER_FIELDS = + ConformancePackage.stringSet( + "namespace", "counter", "quantity", "weightManifest"); + private static final Set DIRECT_COUNTER_NAMESPACES = + ConformancePackage.stringSet("runtime", "semantic", "processor"); + + private static final Set EXPECTED_FIELDS = ConformancePackage.stringSet( + "compileStatus", "result", "changes", "events", "errorClass", + "gasTrace", "totalGas", "assertions", "additionalCase", + "variants", "cases", "reason"); + private static final Set ASSERTION_FIELDS = + ConformancePackage.stringSet("actual", "op", "expected"); + private static final Set ASSERTION_OPERATORS = + ConformancePackage.stringSet( + "equals", "notEquals", "absent", "present", "contains", + "notContains", "lessThan", "greaterThan", + "sameAcrossVariants", "all", "none"); + private static final Set VARIANT_FIELDS = + ConformancePackage.stringSet( + "name", "rootForm", "cache", "batching", + "rawRootDocumentJson", "deliveryKind"); + private static final Set ROOT_FORMS = + ConformancePackage.stringSet( + "inline", "reference", "eager", "lazy", "materialized"); + private static final Set CACHE_FORMS = + ConformancePackage.stringSet("warm", "cold"); + private static final Set BATCHING_FORMS = + ConformancePackage.stringSet("batched", "unbatched"); + private static final Set DELIVERY_KINDS = + ConformancePackage.stringSet( + "document-update", "triggered", "lifecycle", "embedded"); + private static final Set CASE_FIELDS = + ConformancePackage.stringSet( + "name", "program", "context", "errorClass", "reason"); + + private BexFixtureSchemaValidator() { + } + + static void validate(ConformancePackage.Fixture fixture) { + Map root = fixture.data; + String path = fixture.path; + requireFields(root, REQUIRED_TOP_LEVEL, path); + rejectUnknownFields(root, TOP_LEVEL, path); + + requireEquals(root.get("schema"), "blue-bex-fixture/2.0", path + ".schema"); + String id = requireText(root.get("id"), path + ".id"); + if (!ConformancePackage.FIXTURE_ID.matcher(id).matches()) { + fail(path + ".id has invalid syntax: " + id); + } + + List vectors = requireList(root.get("vectors"), path + ".vectors"); + if (vectors.isEmpty()) { + fail(path + ".vectors must not be empty"); + } + Set uniqueVectors = new HashSet(); + for (int index = 0; index < vectors.size(); index++) { + String vector = requireText(vectors.get(index), + path + ".vectors[" + index + "]"); + if (!ConformancePackage.VECTOR_ID.matcher(vector).matches()) { + fail(path + ".vectors contains invalid vector id: " + vector); + } + if (!uniqueVectors.add(vector)) { + fail(path + ".vectors contains duplicate vector id: " + vector); + } + } + + requireEnum(root.get("category"), CATEGORIES, path + ".category"); + if (root.containsKey("description")) { + requireText(root.get("description"), path + ".description"); + } + if (root.get("program") == null) { + fail(path + ".program must be present"); + } + validateContext(requireMap(root.get("context"), path + ".context"), + path + ".context"); + validateExpected(requireMap(root.get("expected"), path + ".expected"), + path + ".expected"); + } + + private static void validateContext(Map context, String path) { + rejectUnknownFields(context, CONTEXT_FIELDS, path); + requireObjectWhenPresent(context, "steps", path); + requireObjectWhenPresent(context, "bindings", path); + requireObjectWhenPresent(context, "provider", path); + requireTextWhenPresent(context, "documentScope", path); + requireNonNegativeIntegerWhenPresent(context, "gasLimit", path); + requireNonNegativeIntegerWhenPresent( + context, "parentRemainingGas", path); + + if (context.containsKey("directCounterFixture")) { + Map direct = requireMap( + context.get("directCounterFixture"), + path + ".directCounterFixture"); + rejectUnknownFields( + direct, DIRECT_COUNTER_FIELDS, path + ".directCounterFixture"); + requireFields(direct, + ConformancePackage.stringSet("counter", "quantity"), + path + ".directCounterFixture"); + if (direct.containsKey("namespace")) { + requireEnum(direct.get("namespace"), DIRECT_COUNTER_NAMESPACES, + path + ".directCounterFixture.namespace"); + } + requireText(direct.get("counter"), + path + ".directCounterFixture.counter"); + requireNonNegativeInteger(direct.get("quantity"), + path + ".directCounterFixture.quantity"); + if (direct.containsKey("weightManifest")) { + requireText(direct.get("weightManifest"), + path + ".directCounterFixture.weightManifest"); + } + } + } + + private static void validateExpected( + Map expected, + String path) { + rejectUnknownFields(expected, EXPECTED_FIELDS, path); + requireTextWhenPresent(expected, "compileStatus", path); + requireTextWhenPresent(expected, "errorClass", path); + requireTextWhenPresent(expected, "reason", path); + requireListWhenPresent(expected, "changes", path); + requireListWhenPresent(expected, "events", path); + requireListWhenPresent(expected, "gasTrace", path); + requireNonNegativeIntegerWhenPresent(expected, "totalGas", path); + + if (expected.containsKey("assertions")) { + List assertions = requireList( + expected.get("assertions"), path + ".assertions"); + for (int index = 0; index < assertions.size(); index++) { + String assertionPath = path + ".assertions[" + index + "]"; + Map assertion = + requireMap(assertions.get(index), assertionPath); + rejectUnknownFields(assertion, ASSERTION_FIELDS, assertionPath); + requireFields(assertion, + ConformancePackage.stringSet("actual", "op"), + assertionPath); + requireText(assertion.get("actual"), assertionPath + ".actual"); + requireEnum( + assertion.get("op"), ASSERTION_OPERATORS, + assertionPath + ".op"); + } + } + + if (expected.containsKey("variants")) { + List variants = requireList( + expected.get("variants"), path + ".variants"); + if (variants.isEmpty()) { + fail(path + ".variants must not be empty"); + } + Set names = new HashSet(); + for (int index = 0; index < variants.size(); index++) { + String variantPath = path + ".variants[" + index + "]"; + Map variant = + requireMap(variants.get(index), variantPath); + rejectUnknownFields(variant, VARIANT_FIELDS, variantPath); + requireFields(variant, + ConformancePackage.stringSet("name"), variantPath); + String name = requireText( + variant.get("name"), variantPath + ".name"); + if (!names.add(name)) { + fail(path + ".variants contains duplicate name: " + name); + } + requireOptionalEnum( + variant, "rootForm", ROOT_FORMS, variantPath); + requireOptionalEnum( + variant, "cache", CACHE_FORMS, variantPath); + requireOptionalEnum( + variant, "batching", BATCHING_FORMS, variantPath); + requireOptionalEnum( + variant, "deliveryKind", DELIVERY_KINDS, variantPath); + requireTextWhenPresent( + variant, "rawRootDocumentJson", variantPath); + } + } + + if (expected.containsKey("cases")) { + List cases = requireList(expected.get("cases"), path + ".cases"); + if (cases.isEmpty()) { + fail(path + ".cases must not be empty"); + } + Set names = new HashSet(); + for (int index = 0; index < cases.size(); index++) { + String casePath = path + ".cases[" + index + "]"; + Map fixtureCase = + requireMap(cases.get(index), casePath); + rejectUnknownFields(fixtureCase, CASE_FIELDS, casePath); + requireFields(fixtureCase, + ConformancePackage.stringSet( + "name", "program", "errorClass"), + casePath); + String name = requireText( + fixtureCase.get("name"), casePath + ".name"); + if (!names.add(name)) { + fail(path + ".cases contains duplicate name: " + name); + } + if (fixtureCase.get("program") == null) { + fail(casePath + ".program must be present"); + } + requireText( + fixtureCase.get("errorClass"), + casePath + ".errorClass"); + requireTextWhenPresent(fixtureCase, "reason", casePath); + if (fixtureCase.containsKey("context")) { + validateContext( + requireMap(fixtureCase.get("context"), + casePath + ".context"), + casePath + ".context"); + } + } + } + } + + private static void requireObjectWhenPresent( + Map source, + String field, + String path) { + if (source.containsKey(field)) { + requireMap(source.get(field), path + "." + field); + } + } + + private static void requireListWhenPresent( + Map source, + String field, + String path) { + if (source.containsKey(field)) { + requireList(source.get(field), path + "." + field); + } + } + + private static void requireTextWhenPresent( + Map source, + String field, + String path) { + if (source.containsKey(field)) { + requireText(source.get(field), path + "." + field); + } + } + + private static void requireNonNegativeIntegerWhenPresent( + Map source, + String field, + String path) { + if (source.containsKey(field)) { + requireNonNegativeInteger( + source.get(field), path + "." + field); + } + } + + private static void requireOptionalEnum( + Map source, + String field, + Set values, + String path) { + if (source.containsKey(field)) { + requireEnum(source.get(field), values, path + "." + field); + } + } + + private static void requireNonNegativeInteger(Object value, String path) { + BigInteger integer = ConformancePackage.integer(value, path); + if (integer.signum() < 0) { + fail(path + " must be non-negative"); + } + } + + private static Map requireMap(Object value, String path) { + try { + return ConformancePackage.map(value, path); + } catch (IllegalArgumentException ex) { + return fail(ex.getMessage()); + } + } + + private static List requireList(Object value, String path) { + try { + return ConformancePackage.list(value, path); + } catch (IllegalArgumentException ex) { + return fail(ex.getMessage()); + } + } + + private static String requireText(Object value, String path) { + try { + return ConformancePackage.text(value, path); + } catch (IllegalArgumentException ex) { + return fail(ex.getMessage()); + } + } + + private static void requireEnum( + Object value, + Set allowed, + String path) { + String text = requireText(value, path); + if (!allowed.contains(text)) { + fail(path + " has unsupported value: " + text); + } + } + + private static void requireEquals(Object value, String expected, String path) { + if (!expected.equals(value)) { + fail(path + " must equal " + expected); + } + } + + private static void requireFields( + Map source, + Set required, + String path) { + for (String field : required) { + if (!source.containsKey(field) || source.get(field) == null) { + fail(path + " is missing required field " + field); + } + } + } + + private static void rejectUnknownFields( + Map source, + Set allowed, + String path) { + for (String field : source.keySet()) { + if (!allowed.contains(field)) { + fail(path + " contains unknown field " + field); + } + } + } + + private static T fail(String message) { + throw new IllegalArgumentException(message); + } +} diff --git a/src/test/java/blue/bex/conformance/BexGasMicrofixtureTest.java b/src/test/java/blue/bex/conformance/BexGasMicrofixtureTest.java new file mode 100644 index 0000000..585adde --- /dev/null +++ b/src/test/java/blue/bex/conformance/BexGasMicrofixtureTest.java @@ -0,0 +1,102 @@ +package blue.bex.conformance; + +import blue.bex.gas.BexGasCharge; +import blue.bex.gas.BexGasCounter; +import blue.bex.gas.BexGasMeter; +import blue.bex.gas.BexGasSchedule; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Directly binds each gas microfixture to one public named-counter charge and + * verifies the complete canonical trace entry. + */ +class BexGasMicrofixtureTest { + @TestFactory + Collection allNamedCounterMicrofixturesExecute() { + List fixtures = + ConformancePackage.gasFixtures(); + assertEquals( + ConformancePackage.GAS_FIXTURE_COUNT, + fixtures.size(), + "The manifest must expose exactly 30 gas microfixtures"); + + List tests = + new ArrayList(fixtures.size()); + for (ConformancePackage.Fixture fixture : fixtures) { + tests.add(DynamicTest.dynamicTest( + fixture.id() + " :: " + fixture.path, + () -> execute(fixture))); + } + return tests; + } + + private static void execute(ConformancePackage.Fixture fixture) { + Map direct = ConformancePackage.map( + fixture.context().get("directCounterFixture"), + fixture.path + ".context.directCounterFixture"); + String name = ConformancePackage.text( + direct.get("counter"), fixture.path + ".counter"); + long quantity = ConformancePackage.integer( + direct.get("quantity"), fixture.path + ".quantity") + .longValueExact(); + BexGasCounter counter = + BexGasCounter.fromCanonicalName(name); + BexGasSchedule schedule = BexGasSchedule.defaults(); + BexGasMeter meter = new BexGasMeter(schedule, Long.MAX_VALUE); + + meter.charge(counter, quantity, "conformance-microfixture"); + + List trace = meter.trace(); + assertEquals(1, trace.size(), fixture.id() + " trace size"); + BexGasCharge charge = trace.get(0); + assertEquals(0L, charge.sequence(), fixture.id() + " sequence"); + assertEquals("bex", charge.namespace(), fixture.id() + " namespace"); + assertEquals(counter, charge.counter(), fixture.id() + " counter"); + assertEquals(name, charge.counterName(), fixture.id() + " name"); + assertEquals(quantity, charge.quantity(), fixture.id() + " quantity"); + assertEquals(schedule.weight(counter), charge.weight(), + fixture.id() + " weight"); + assertEquals(quantity * schedule.weight(counter), charge.gas(), + fixture.id() + " gas"); + assertEquals("conformance-microfixture", charge.reason(), + fixture.id() + " reason"); + assertEquals(charge.gas(), meter.totalGas(), + fixture.id() + " trace-derived total"); + + Map expected = fixture.expected(); + List expectedTrace = ConformancePackage.list( + expected.get("gasTrace"), fixture.path + ".expected.gasTrace"); + assertEquals(1, expectedTrace.size(), fixture.id() + " fixture trace"); + Map expectedEntry = ConformancePackage.map( + expectedTrace.get(0), fixture.path + ".expected.gasTrace[0]"); + assertEquals( + ConformancePackage.integer( + expectedEntry.get("sequence"), "sequence").longValueExact(), + charge.sequence()); + assertEquals(expectedEntry.get("counter"), charge.counterName()); + assertEquals( + ConformancePackage.integer( + expectedEntry.get("quantity"), "quantity").longValueExact(), + charge.quantity()); + assertEquals( + ConformancePackage.integer( + expectedEntry.get("weight"), "weight").longValueExact(), + charge.weight()); + assertEquals( + ConformancePackage.integer( + expectedEntry.get("gas"), "gas").longValueExact(), + charge.gas()); + assertEquals( + ConformancePackage.integer( + expected.get("totalGas"), "totalGas").longValueExact(), + meter.totalGas()); + } +} diff --git a/src/test/java/blue/bex/conformance/BexRepresentationInvarianceTest.java b/src/test/java/blue/bex/conformance/BexRepresentationInvarianceTest.java new file mode 100644 index 0000000..9714c5b --- /dev/null +++ b/src/test/java/blue/bex/conformance/BexRepresentationInvarianceTest.java @@ -0,0 +1,760 @@ +package blue.bex.conformance; + +import blue.bex.api.BexEngine; +import blue.bex.api.BexExecutionContext; +import blue.bex.api.BexIntrinsicInvocation; +import blue.bex.api.BexIntrinsicRegistry; +import blue.bex.api.BexProgramSource; +import blue.bex.api.FrozenBexDocumentView; +import blue.bex.compile.BexCompiledProgram; +import blue.bex.gas.BexGasCharge; +import blue.bex.output.BexEstablishedIdentity; +import blue.bex.output.BexSemanticIdentityBoundary; +import blue.bex.result.BexExecutionResult; +import blue.bex.result.BexPatchEntry; +import blue.bex.value.BexValue; +import blue.bex.value.BexValues; +import blue.language.Blue; +import blue.language.NodeProvider; +import blue.language.model.Node; +import blue.language.provider.ExactNodeGraphFragments; +import blue.language.snapshot.FrozenNode; +import blue.language.utils.BlueIdCalculator; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static blue.bex.test.BexTestFixtures.list; +import static blue.bex.test.BexTestFixtures.obj; +import static blue.bex.test.BexTestFixtures.op; +import static blue.bex.test.BexTestFixtures.stepDo; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Differential proof that physical Blue representation and provider delivery + * are outside portable BEX semantics. + */ +class BexRepresentationInvarianceTest { + private static final String SORT_INTRINSIC = + "2R1WaEk8LVwFRMEGnsZ8HTj15QTz3tQEj9LDYYjGFJJG"; + private static final String REPRESENTATION_REGISTRY = + "sha256:858952520d947dcffa773f7479434d86367b22f1a4ddabde90b8c19826710a62"; + + @Test + void programDocumentAndEventAreInvariantAcrossPhysicalRepresentations() { + LogicalInputs inputs = LogicalInputs.create(); + List variants = Arrays.asList( + new Variant("fully-inline-exact", + RootForm.INLINE, ProviderForm.ONE_NODE, + CacheForm.COLD, ExactForm.CANONICAL_AND_RESOLVED), + new Variant("pure-references-exact", + RootForm.REFERENCE, ProviderForm.ONE_NODE, + CacheForm.COLD, ExactForm.CANONICAL_AND_RESOLVED), + new Variant("partially-materialized-exact", + RootForm.PARTIAL, ProviderForm.ONE_NODE, + CacheForm.COLD, ExactForm.CANONICAL_AND_RESOLVED), + new Variant("cold-batched-references-exact", + RootForm.REFERENCE, ProviderForm.BATCHED, + CacheForm.COLD, ExactForm.CANONICAL_AND_RESOLVED), + new Variant("warm-batched-references-exact", + RootForm.REFERENCE, ProviderForm.BATCHED, + CacheForm.WARM, ExactForm.CANONICAL_AND_RESOLVED), + new Variant("cold-batched-partial-materialized", + RootForm.PARTIAL, ProviderForm.BATCHED, + CacheForm.COLD, ExactForm.MATERIALIZED), + new Variant("warm-one-node-reference-materialized", + RootForm.REFERENCE, ProviderForm.ONE_NODE, + CacheForm.WARM, ExactForm.MATERIALIZED)); + + List observations = new ArrayList(); + for (Variant variant : variants) { + observations.add(execute(inputs, variant)); + } + + Observation expected = observations.get(0); + assertEquals("success", expected.diagnosticCategory); + assertNotNull(expected.compiledProgramBlueId); + assertNotNull(expected.outputBlueId); + assertEquals(1, expected.semanticBoundaryCalls); + assertEquals(Arrays.asList("/state", "/marker"), + expected.changePaths); + assertEquals(2, ((List) expected.events).size()); + assertRequiredObservations(expected.semanticResult); + assertTrue(expected.portableTrace.stream() + .anyMatch(charge -> + "sortComparison".equals( + charge.counterName())), + "representation differential must execute stable sort work"); + + for (Observation actual : observations) { + assertEquals("success", actual.diagnosticCategory, + actual.name + " diagnostic category"); + assertEquals(expected.compiledProgramBlueId, + actual.compiledProgramBlueId, + actual.name + " compiled program identity"); + assertEquals(expected.semanticResult, actual.semanticResult, + actual.name + " semantic result"); + assertEquals(expected.outputBlueId, actual.outputBlueId, + actual.name + " output BlueId"); + assertEquals(expected.changes, actual.changes, + actual.name + " ordered changes"); + assertEquals(expected.changePaths, actual.changePaths, + actual.name + " change path order"); + assertEquals(expected.events, actual.events, + actual.name + " ordered events"); + assertEquals(expected.portableTrace, actual.portableTrace, + actual.name + " full portable named trace"); + assertEquals(expected.totalGas, actual.totalGas, + actual.name + " total gas"); + assertEquals(1, actual.semanticBoundaryCalls, + actual.name + " semantic boundary invocation count"); + } + + Observation coldBatch = find( + observations, "cold-batched-references-exact"); + Observation warmBatch = find( + observations, "warm-batched-references-exact"); + Observation coldOneNode = find( + observations, "pure-references-exact"); + assertEquals(1, coldBatch.physicalBatchLoads, + "cold batched delivery performs one physical catalog load"); + assertEquals(0, warmBatch.physicalBatchLoads, + "warm batched delivery reuses the preloaded catalog"); + assertTrue(coldOneNode.physicalNodeLoads > 1, + "one-node delivery obtains fragments independently"); + assertTrue(coldOneNode.providerDemands + >= warmBatch.providerDemands, + "provider demand counts are allowed to collapse when warm"); + } + + @Test + void coldBatchedRepresentationsUseIndependentRuntimeCaches() { + LogicalInputs inputs = LogicalInputs.create(); + Variant coldBatched = new Variant( + "runtime-isolated-cold-batched", + RootForm.REFERENCE, + ProviderForm.BATCHED, + CacheForm.COLD, + ExactForm.CANONICAL_AND_RESOLVED); + + Observation first = execute(inputs, coldBatched); + Observation second = execute(inputs, coldBatched); + + assertEquals("success", first.diagnosticCategory); + assertEquals("success", second.diagnosticCategory); + assertEquals(1, first.physicalBatchLoads, + "first cold runtime must load its own provider batch"); + assertEquals(1, second.physicalBatchLoads, + "second cold runtime must not inherit the first cache"); + assertEquals(first.providerDemands, second.providerDemands); + assertEquals(first.compiledProgramBlueId, + second.compiledProgramBlueId); + assertEquals(first.semanticResult, second.semanticResult); + assertEquals(first.outputBlueId, second.outputBlueId); + assertEquals(first.changes, second.changes); + assertEquals(first.changePaths, second.changePaths); + assertEquals(first.events, second.events); + assertEquals(first.portableTrace, second.portableTrace); + assertEquals(first.totalGas, second.totalGas); + assertEquals(first.semanticBoundaryCalls, + second.semanticBoundaryCalls); + } + + private static Observation execute( + LogicalInputs inputs, + Variant variant) { + /* + * BEX syntax scalar nodes intentionally retain compiler-only inferred + * metadata, so cut the program only at its statement-list boundary. + * Ordinary document/event data can use fully shallow fragmentation. + */ + ExactNodeGraphFragments programGraph = + ExactNodeGraphFragments.split( + inputs.program, + Collections.singletonList("/do")); + ExactNodeGraphFragments documentGraph = + new ExactNodeGraphFragments(inputs.document); + ExactNodeGraphFragments eventGraph = + new ExactNodeGraphFragments(inputs.event); + Map catalog = + new LinkedHashMap(); + catalog.putAll(programGraph.fragments()); + catalog.putAll(documentGraph.fragments()); + catalog.putAll(eventGraph.fragments()); + catalog.put(SORT_INTRINSIC, inputs.intrinsicType.clone()); + VariantNodeProvider provider = new VariantNodeProvider( + catalog, variant.providerForm); + Node programRoot = present( + programGraph.roots().get(0), variant.rootForm); + Node documentRoot = present( + documentGraph.roots().get(0), variant.rootForm); + Node eventRoot = present( + eventGraph.roots().get(0), variant.rootForm); + + try (Blue blue = new Blue(provider)) { + if (variant.cacheForm == CacheForm.WARM) { + materialize(blue, programRoot, variant.exactForm); + materialize(blue, documentRoot, variant.exactForm); + materialize(blue, eventRoot, variant.exactForm); + } + + ExactPair program = + materialize(blue, programRoot, variant.exactForm); + ExactPair document = runtimePair( + blue, documentRoot, variant.exactForm); + ExactPair event = runtimePair( + blue, eventRoot, variant.exactForm); + CountingIdentityBoundary boundary = + new CountingIdentityBoundary(); + BexEngine engine = BexEngine.builder() + .blue(blue) + .intrinsics(representationIntrinsics()) + .build(); + BexCompiledProgram compiled = engine.compile( + BexProgramSource.inline(program.resolved)); + FrozenBexDocumentView documentView = + new FrozenBexDocumentView( + document.canonical, document.resolved, "/"); + BexValue eventValue = BexValues.exact( + event.canonical, event.resolved); + BexExecutionContext context = BexExecutionContext.builder() + .document(documentView) + .event(eventValue) + .semanticIdentityBoundary(boundary) + .gasLimit(1_000_000L) + .build(); + + /* + * Program selection/compilation and any deliberately materialized + * exact inputs are host preparation. Reset the provider's physical + * diagnostics immediately before BEX runtime so reference, + * partial, warm/cold, and batched/one-node observations below are + * genuine execution-time demands. + */ + provider.beginRuntime( + variant.cacheForm == CacheForm.WARM); + try { + BexExecutionResult result = + engine.execute(compiled, context); + List changePaths = new ArrayList(); + for (BexPatchEntry entry + : result.changeset().entries()) { + changePaths.add(entry.absolutePath()); + } + return new Observation( + variant.name, + "success", + compiled.programBlueId(), + result.value().toSimple(), + result.output().nodeBlueId(), + result.changeset().asValue().toSimple(), + changePaths, + result.events().asValue().toSimple(), + result.gasTrace(), + result.gasUsed(), + boundary.calls, + provider.demands.size(), + provider.physicalNodeLoads, + provider.physicalBatchLoads); + } catch (RuntimeException failure) { + return new Observation( + variant.name, + diagnosticCategory(failure), + compiled.programBlueId(), + null, + null, + Collections.emptyList(), + Collections.emptyList(), + Collections.emptyList(), + Collections.emptyList(), + 0L, + boundary.calls, + provider.demands.size(), + provider.physicalNodeLoads, + provider.physicalBatchLoads); + } + } + } + + private static ExactPair runtimePair( + Blue blue, + Node presented, + ExactForm exactForm) { + if (exactForm == ExactForm.MATERIALIZED) { + return materialize(blue, presented, exactForm); + } + /* + * Preserve the presented physical form in both lanes. Pure references + * and nested references are materialized lazily by BEX through the + * verified Blue provider boundary only when execution reads them. + */ + return new ExactPair( + FrozenNode.fromNode(presented), + FrozenNode.fromResolvedNode(presented)); + } + + private static ExactPair materialize( + Blue blue, + Node presented, + ExactForm exactForm) { + Node expanded = blue.expand(presented); + FrozenNode resolved = FrozenNode.fromResolvedNode(expanded); + FrozenNode canonical = exactForm == ExactForm.MATERIALIZED + ? FrozenNode.fromNode(expanded) + : FrozenNode.fromNode(presented); + return new ExactPair(canonical, resolved); + } + + private static Node present( + ExactNodeGraphFragments.RootRepresentation root, + RootForm form) { + switch (form) { + case INLINE: + return root.original(); + case PARTIAL: + return root.directFragment(); + case REFERENCE: + return root.pureReference(); + default: + throw new AssertionError(form); + } + } + + private static String diagnosticCategory(RuntimeException failure) { + return "runtime-error:" + failure.getClass().getName(); + } + + private static Observation find( + List observations, + String name) { + for (Observation observation : observations) { + if (name.equals(observation.name)) { + return observation; + } + } + throw new AssertionError("Missing observation " + name); + } + + private static Node document(String pointer) { + return op("$document", pointer); + } + + private static Node event(String pointer) { + return op("$event", pointer); + } + + private static BexIntrinsicRegistry representationIntrinsics() { + Map weights = + new LinkedHashMap(); + weights.put("sortComparison", 1L); + return BexIntrinsicRegistry.builder() + .register( + SORT_INTRINSIC, + REPRESENTATION_REGISTRY, + weights, + invocation -> { + BexValue values = + invocation.field("values"); + assertTrue(values.isList(), + "sort values must be a list"); + List sorted = + new ArrayList(); + for (int index = 0; + index < values.size(); + index++) { + sorted.add(values.get( + String.valueOf(index))); + } + stableSort(sorted, invocation); + return BexValues.list(sorted); + }) + .build(); + } + + private static void stableSort( + List values, + BexIntrinsicInvocation invocation) { + for (int index = 1; index < values.size(); index++) { + BexValue candidate = values.get(index); + int insertion = index; + while (insertion > 0) { + invocation.charge( + "sortComparison", + 1L, + "representation-stable-sort"); + BexValue previous = values.get(insertion - 1); + if (previous.get("rank").asNumber().compareTo( + candidate.get("rank").asNumber()) <= 0) { + break; + } + values.set(insertion, previous); + insertion--; + } + values.set(insertion, candidate); + } + } + + @SuppressWarnings("unchecked") + private static void assertRequiredObservations(Object result) { + assertTrue(result instanceof Map); + Map observations = + (Map) result; + assertEquals("object", observations.get("kind")); + assertEquals(true, observations.get("exists")); + assertEquals( + Arrays.asList("id", "state", "values"), + observations.get("keys")); + assertEquals(3, + ((List) observations.get("entries")).size()); + assertEquals( + java.math.BigInteger.valueOf(3L), + observations.get("size")); + assertEquals("ready", observations.get("pointerRead")); + assertTrue(observations.get("exactIdentity") + instanceof String); + assertEquals(true, + observations.get("identityAwareEquality")); + assertEquals(true, observations.get("deepEquality")); + assertEquals(true, observations.get("matching")); + assertEquals(true, observations.get("truthiness")); + assertEquals( + Arrays.asList( + java.math.BigInteger.ONE, + java.math.BigInteger.valueOf(2L), + java.math.BigInteger.valueOf(3L)), + observations.get("iteration")); + List sorted = + (List) observations.get("stableSort"); + assertEquals(3, sorted.size()); + assertEquals("first", + ((Map) sorted.get(0)).get("tag")); + assertEquals("second", + ((Map) sorted.get(1)).get("tag")); + assertEquals("last", + ((Map) sorted.get(2)).get("tag")); + } + + private enum RootForm { + INLINE, + REFERENCE, + PARTIAL + } + + private enum ProviderForm { + ONE_NODE, + BATCHED + } + + private enum CacheForm { + COLD, + WARM + } + + private enum ExactForm { + CANONICAL_AND_RESOLVED, + MATERIALIZED + } + + private static final class Variant { + private final String name; + private final RootForm rootForm; + private final ProviderForm providerForm; + private final CacheForm cacheForm; + private final ExactForm exactForm; + + private Variant(String name, + RootForm rootForm, + ProviderForm providerForm, + CacheForm cacheForm, + ExactForm exactForm) { + this.name = name; + this.rootForm = rootForm; + this.providerForm = providerForm; + this.cacheForm = cacheForm; + this.exactForm = exactForm; + } + } + + private static final class ExactPair { + private final FrozenNode canonical; + private final FrozenNode resolved; + + private ExactPair(FrozenNode canonical, FrozenNode resolved) { + this.canonical = canonical; + this.resolved = resolved; + } + } + + private static final class LogicalInputs { + private final Node program; + private final Node document; + private final Node event; + private final Node intrinsicType; + + private LogicalInputs(Node program, + Node document, + Node event, + Node intrinsicType) { + this.program = program; + this.document = document; + this.event = event; + this.intrinsicType = intrinsicType; + } + + private static LogicalInputs create() { + Node authoredDocument = obj( + "subject", obj( + "id", "D-1", + "state", "ready", + "values", list(1, 2, 3)), + "sortable", list( + obj("rank", 2, "tag", "last"), + obj("rank", 1, "tag", "first"), + obj("rank", 1, "tag", "second")), + "marker", "document-marker"); + Node authoredEvent = obj( + "payload", obj( + "id", "E-1", + "amount", 7), + "mirrorSubject", obj( + "id", "D-1", + "state", "ready", + "values", list(1, 2, 3)), + "followup", obj( + "kind", "Followup", + "ordinal", 2)); + /* + * Physical references must address already admitted semantic + * content. Normalize authored test data once before fragmenting it + * so inline children and materialized children carry the same + * exact scalar identities. + */ + try (Blue blue = new Blue()) { + Node textPattern = blue.yamlToNode("type: Text"); + Node program = stepDo(list( + op("$let", obj( + "name", "shared", + "expr", obj( + "kind", + op("$kind", + document("/subject")), + "exists", + op("$exists", + document( + "/subject/state")), + "keys", + op("$keys", + document("/subject")), + "entries", + op("$entries", + document("/subject")), + "size", + op("$size", + document("/subject")), + "pointerRead", + op("$pointerGet", obj( + "object", + document("/subject"), + "path", "/state")), + "exactIdentity", + op("$nodeBlueId", + document("/subject")), + "identityAwareEquality", + op("$eq", list( + document("/subject"), + event( + "/mirrorSubject"))), + "deepEquality", + op("$eq", list( + document("/subject"), + obj( + "id", "D-1", + "state", "ready", + "values", + list(1, 2, 3)))), + "matching", + op("$is", obj( + "node", + document( + "/subject/state"), + "pattern", + textPattern)), + "truthiness", + op("$boolean", + document("/subject")), + "iteration", + op("$map", obj( + "in", + document( + "/subject/values"), + "item", "item", + "expr", + op("$var", "item"))), + "stableSort", + op("$intrinsic", obj( + "type", + new Node().blueId( + SORT_INTRINSIC), + "values", + document("/sortable")))))), + op("$appendChange", obj( + "op", "replace", + "path", "/state", + "val", op("$var", "shared"))), + op("$appendChange", obj( + "op", "add", + "path", "/marker", + "val", document("/marker"))), + op("$appendEvent", op("$var", "shared")), + op("$appendEvent", event("/followup")), + op("$return", op("$var", "shared")))); + Node document = blue.resolveToSnapshot(authoredDocument) + .frozenResolvedRoot().toNode(); + Node event = blue.resolveToSnapshot(authoredEvent) + .frozenResolvedRoot().toNode(); + Node intrinsicType = blue.yamlToNode( + "name: BEX Sort Fixture Intrinsic 2.0\n" + + "description: Conformance-only deterministic " + + "intrinsic that sorts a supplied list using " + + "the canonical merge-sort schedule and " + + "reports named sortComparison counters.\n" + + "values:\n" + + " type:\n" + + " blueId: " + + "8DSFoWG9MqRSUhStqoPLrwVQiYByRh18NWbDEarN8MKF"); + return new LogicalInputs( + program, document, event, intrinsicType); + } + } + } + + private static final class VariantNodeProvider + implements NodeProvider { + private final Map catalog = + new LinkedHashMap(); + private final Map batch = + new LinkedHashMap(); + private final ProviderForm form; + private final List demands = + new ArrayList(); + private int physicalNodeLoads; + private int physicalBatchLoads; + + private VariantNodeProvider( + Map source, + ProviderForm form) { + this.form = form; + for (Map.Entry entry : source.entrySet()) { + catalog.put(entry.getKey(), entry.getValue().clone()); + } + } + + @Override + public List fetchByBlueId(String blueId) { + demands.add(blueId); + Node node; + if (form == ProviderForm.BATCHED) { + if (batch.isEmpty()) { + for (Map.Entry entry + : catalog.entrySet()) { + batch.put(entry.getKey(), + entry.getValue().clone()); + } + physicalBatchLoads++; + } + node = batch.get(blueId); + } else { + physicalNodeLoads++; + node = catalog.get(blueId); + } + return node == null + ? null + : Collections.singletonList(node.clone()); + } + + private void clearObservations() { + demands.clear(); + physicalNodeLoads = 0; + physicalBatchLoads = 0; + } + + private void beginRuntime(boolean retainWarmBatch) { + clearObservations(); + if (!retainWarmBatch) { + batch.clear(); + } + } + } + + private static final class CountingIdentityBoundary + implements BexSemanticIdentityBoundary { + private int calls; + + @Override + public BexEstablishedIdentity establishIdentity(Node node) { + calls++; + FrozenNode frozen = + FrozenNode.fromResolvedNode(node.clone()); + return new BexEstablishedIdentity( + BlueIdCalculator.calculateBlueId( + frozen.toNode()), + frozen); + } + } + + private static final class Observation { + private final String name; + private final String diagnosticCategory; + private final String compiledProgramBlueId; + private final Object semanticResult; + private final String outputBlueId; + private final Object changes; + private final List changePaths; + private final Object events; + private final List portableTrace; + private final long totalGas; + private final int semanticBoundaryCalls; + private final int providerDemands; + private final int physicalNodeLoads; + private final int physicalBatchLoads; + + private Observation(String name, + String diagnosticCategory, + String compiledProgramBlueId, + Object semanticResult, + String outputBlueId, + Object changes, + List changePaths, + Object events, + List portableTrace, + long totalGas, + int semanticBoundaryCalls, + int providerDemands, + int physicalNodeLoads, + int physicalBatchLoads) { + this.name = name; + this.diagnosticCategory = diagnosticCategory; + this.compiledProgramBlueId = compiledProgramBlueId; + this.semanticResult = semanticResult; + this.outputBlueId = outputBlueId; + this.changes = changes; + this.changePaths = Collections.unmodifiableList( + new ArrayList(changePaths)); + this.events = events; + this.portableTrace = Collections.unmodifiableList( + new ArrayList(portableTrace)); + this.totalGas = totalGas; + this.semanticBoundaryCalls = semanticBoundaryCalls; + this.providerDemands = providerDemands; + this.physicalNodeLoads = physicalNodeLoads; + this.physicalBatchLoads = physicalBatchLoads; + } + } +} diff --git a/src/test/java/blue/bex/conformance/ConformancePackage.java b/src/test/java/blue/bex/conformance/ConformancePackage.java new file mode 100644 index 0000000..7db4b2d --- /dev/null +++ b/src/test/java/blue/bex/conformance/ConformancePackage.java @@ -0,0 +1,594 @@ +package blue.bex.conformance; + +import blue.language.Blue; +import blue.language.model.Node; +import org.yaml.snakeyaml.Yaml; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Resource and canonicalization support shared by the BEX 2.0 conformance + * tests. + * + *

This class deliberately contains no engine calls. Keeping package + * integrity independent from the evaluator makes corrupt, incomplete, or + * silently skipped fixture packages fail before behavioral execution.

+ */ +final class ConformancePackage { + static final String ROOT = "conformance/bex/"; + static final String FIXTURE_ROOT = ROOT + "fixtures/"; + static final String FIXTURE_MANIFEST = FIXTURE_ROOT + "manifest.yaml"; + static final String GAS_MANIFEST = ROOT + "gas-manifest.yaml"; + static final String REGISTRY_ROOT = ROOT + "registry/"; + static final String REGISTRY_MANIFEST = REGISTRY_ROOT + "manifest.yaml"; + + static final int FILE_COUNT = 147; + static final int VECTOR_COUNT = 60; + static final int BEHAVIOR_FIXTURE_COUNT = 105; + static final int GAS_FIXTURE_COUNT = 30; + static final int OPERATOR_COUNT = 86; + + static final Pattern FIXTURE_ID = + Pattern.compile("^[A-Za-z0-9][A-Za-z0-9-]*$"); + static final Pattern VECTOR_ID = + Pattern.compile("^BEX-[A-Z]+-[0-9]{2}$"); + static final Pattern SHA_256 = + Pattern.compile("^[0-9a-f]{64}$"); + static final Pattern PACKAGE_IDENTITY = + Pattern.compile("^sha256:[0-9a-f]{64}$"); + + private static final Yaml YAML = new Yaml(); + + private ConformancePackage() { + } + + static Map fixtureManifest() { + return loadMap(FIXTURE_MANIFEST); + } + + static Map gasManifest() { + return loadMap(GAS_MANIFEST); + } + + static Map registryManifest() { + return loadMap(REGISTRY_MANIFEST); + } + + static Map loadMap(String resource) { + Object loaded = load(resource); + if (!(loaded instanceof Map)) { + throw new IllegalStateException("Expected a YAML object at " + resource); + } + return stringKeyMap((Map) loaded, resource); + } + + static Object load(String resource) { + try (InputStream input = resourceStream(resource)) { + Object loaded = YAML.load(input); + if (loaded == null) { + throw new IllegalStateException("Empty YAML resource: " + resource); + } + return loaded; + } catch (IOException ex) { + throw new IllegalStateException("Unable to read " + resource, ex); + } + } + + static List manifestFiles() { + List files = list(fixtureManifest().get("files"), "manifest.files"); + List entries = new ArrayList(files.size()); + for (Object value : files) { + Map entry = map(value, "manifest.files[]"); + entries.add(new ManifestFile( + text(entry.get("path"), "manifest.files[].path"), + text(entry.get("role"), "manifest.files[].role"), + text(entry.get("sha256"), "manifest.files[].sha256"), + integer(entry.get("bytes"), "manifest.files[].bytes").longValue())); + } + return Collections.unmodifiableList(entries); + } + + static List manifestFiles(String role) { + List matches = new ArrayList(); + for (ManifestFile entry : manifestFiles()) { + if (role.equals(entry.role)) { + matches.add(entry); + } + } + return Collections.unmodifiableList(matches); + } + + static List behaviorFixtures() { + return fixtures("behavior-fixture"); + } + + static List gasFixtures() { + return fixtures("gas-fixture"); + } + + private static List fixtures(String role) { + List fixtures = new ArrayList(); + for (ManifestFile entry : manifestFiles(role)) { + fixtures.add(new Fixture(entry.path, loadMap(FIXTURE_ROOT + entry.path))); + } + return Collections.unmodifiableList(fixtures); + } + + static byte[] bytes(String resource) { + try (InputStream input = resourceStream(resource)) { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + byte[] buffer = new byte[8192]; + int read; + while ((read = input.read(buffer)) >= 0) { + output.write(buffer, 0, read); + } + return output.toByteArray(); + } catch (IOException ex) { + throw new IllegalStateException("Unable to read " + resource, ex); + } + } + + static byte[] lfNormalizedBytes(String resource) { + String text = new String(bytes(resource), StandardCharsets.UTF_8); + return text.replace("\r\n", "\n").replace('\r', '\n') + .getBytes(StandardCharsets.UTF_8); + } + + static String sha256(byte[] bytes) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hashed = digest.digest(bytes); + StringBuilder hex = new StringBuilder(hashed.length * 2); + for (byte value : hashed) { + hex.append(Character.forDigit((value >>> 4) & 0x0f, 16)); + hex.append(Character.forDigit(value & 0x0f, 16)); + } + return hex.toString(); + } catch (NoSuchAlgorithmException ex) { + throw new IllegalStateException("SHA-256 is unavailable", ex); + } + } + + static String packageIdentity(Map source, String... nullFields) { + Map normalized = deepStringKeyMap(source, "package identity"); + for (String field : nullFields) { + normalized.put(field, null); + } + return "sha256:" + sha256(json(normalized, true).getBytes(StandardCharsets.UTF_8)); + } + + static String json(Object value, boolean sortKeys) { + StringBuilder result = new StringBuilder(); + appendJson(result, value, sortKeys); + return result.toString(); + } + + static Node node(Blue blue, Object value) { + return blue.jsonToNode(json(value, false)); + } + + /** + * Builds an authored BEX syntax tree without letting Blue's source mapper + * reinterpret reserved output-field names or infer exact scalar types. + * Program maps are syntax: fields such as {@code blueId}, {@code type}, + * and {@code value} must remain ordinary expression fields until the BEX + * output boundary. + */ + static Node syntaxNode(Object value) { + if (value == null) { + return new Node(); + } + if (value instanceof Map) { + Map properties = + new LinkedHashMap(); + for (Map.Entry entry + : stringKeyMap((Map) value, "BEX syntax").entrySet()) { + properties.put(entry.getKey(), syntaxNode(entry.getValue())); + } + return new Node().properties(properties); + } + if (value instanceof List) { + List items = new ArrayList(); + for (Object child : (List) value) { + items.add(syntaxNode(child)); + } + return new Node().items(items); + } + if (value instanceof Byte || value instanceof Short + || value instanceof Integer || value instanceof Long) { + return new Node().value( + BigInteger.valueOf(((Number) value).longValue())); + } + if (value instanceof BigInteger || value instanceof BigDecimal + || value instanceof Float || value instanceof Double + || value instanceof Boolean || value instanceof String) { + return new Node().value(value); + } + throw new IllegalArgumentException( + "Unsupported BEX syntax scalar: " + + value.getClass().getName()); + } + + /** + * Converts fixture context data to a semantic Blue object. Ordinary map + * keys remain object properties even when their spelling is also a Blue + * serialization field (for example a domain field named {@code items}). + * Exact pure references and explicitly typed nodes retain their Blue + * meaning. + */ + static Node semanticNode(Blue blue, Object value) { + if (value == null) { + return new Node(); + } + if (value instanceof Map) { + Map map = + stringKeyMap((Map) value, "fixture context"); + if (map.size() == 1 && map.containsKey("blueId")) { + return new Node().blueId( + text(map.get("blueId"), "fixture context blueId")); + } + if (isExplicitTypedNode(map)) { + return blue.parseSourceJson(json(map, false)); + } + Map properties = + new LinkedHashMap(); + for (Map.Entry entry : map.entrySet()) { + properties.put( + entry.getKey(), + semanticNode(blue, entry.getValue())); + } + return new Node().properties(properties); + } + if (value instanceof List) { + List items = new ArrayList(); + for (Object child : (List) value) { + items.add(semanticNode(blue, child)); + } + return new Node().items(items); + } + return syntaxNode(value); + } + + private static boolean isExplicitTypedNode( + Map map) { + return map.containsKey("type") + && (map.containsKey("value") + || map.containsKey("items")); + } + + static Path resourcePath(String resource) { + URL url = ConformancePackage.class.getClassLoader().getResource(resource); + if (url == null) { + throw new IllegalStateException("Missing resource: " + resource); + } + if (!"file".equals(url.getProtocol())) { + throw new IllegalStateException( + "Conformance resources must be exploded files for inventory validation: " + url); + } + try { + return Paths.get(url.toURI()); + } catch (URISyntaxException ex) { + throw new IllegalStateException("Invalid resource URI: " + url, ex); + } + } + + static Set regularResourcePaths(String root) { + Path rootPath = resourcePath(root); + try (Stream paths = Files.walk(rootPath)) { + return paths + .filter(Files::isRegularFile) + .map(rootPath::relativize) + .map(Path::toString) + .map(path -> path.replace('\\', '/')) + .sorted() + .collect(Collectors.toCollection(LinkedHashSet::new)); + } catch (IOException ex) { + throw new IllegalStateException("Unable to inventory " + root, ex); + } + } + + static Map map(Object value, String path) { + if (!(value instanceof Map)) { + throw new IllegalArgumentException(path + " must be an object"); + } + return stringKeyMap((Map) value, path); + } + + static List list(Object value, String path) { + if (!(value instanceof List)) { + throw new IllegalArgumentException(path + " must be a list"); + } + return (List) value; + } + + static String text(Object value, String path) { + if (!(value instanceof String) || ((String) value).trim().isEmpty()) { + throw new IllegalArgumentException(path + " must be non-empty text"); + } + return (String) value; + } + + static BigInteger integer(Object value, String path) { + if (value instanceof BigInteger) { + return (BigInteger) value; + } + if (value instanceof Byte || value instanceof Short + || value instanceof Integer || value instanceof Long) { + return BigInteger.valueOf(((Number) value).longValue()); + } + throw new IllegalArgumentException(path + " must be an integer"); + } + + static Set stringSet(String... values) { + LinkedHashSet set = new LinkedHashSet(); + Collections.addAll(set, values); + return Collections.unmodifiableSet(set); + } + + static Set operatorOccurrences(Object value) { + LinkedHashSet operators = new LinkedHashSet(); + collectOperators(value, operators); + return Collections.unmodifiableSet(operators); + } + + static Object normalizedSemanticValue(Object value) { + if (value instanceof Byte || value instanceof Short + || value instanceof Integer || value instanceof Long) { + return BigInteger.valueOf(((Number) value).longValue()); + } + if (value instanceof Float || value instanceof Double) { + return BigDecimal.valueOf(((Number) value).doubleValue()); + } + if (value instanceof Map) { + Map result = new LinkedHashMap(); + for (Map.Entry entry + : stringKeyMap((Map) value, "semantic value").entrySet()) { + result.put(entry.getKey(), normalizedSemanticValue(entry.getValue())); + } + return result; + } + if (value instanceof List) { + List result = new ArrayList(); + for (Object child : (List) value) { + result.add(normalizedSemanticValue(child)); + } + return result; + } + return value; + } + + private static InputStream resourceStream(String resource) { + InputStream input = ConformancePackage.class.getClassLoader() + .getResourceAsStream(resource); + if (input == null) { + throw new IllegalStateException("Missing resource: " + resource); + } + return input; + } + + private static void collectOperators(Object value, Set result) { + if (value instanceof Map) { + for (Map.Entry entry + : stringKeyMap((Map) value, "program").entrySet()) { + if (entry.getKey().startsWith("$")) { + result.add(entry.getKey()); + } + collectOperators(entry.getValue(), result); + } + } else if (value instanceof List) { + for (Object child : (List) value) { + collectOperators(child, result); + } + } + } + + private static Map stringKeyMap(Map source, String path) { + Map result = new LinkedHashMap(); + for (Map.Entry entry : source.entrySet()) { + if (!(entry.getKey() instanceof String)) { + throw new IllegalArgumentException(path + " contains a non-text key"); + } + result.put((String) entry.getKey(), entry.getValue()); + } + return result; + } + + private static Map deepStringKeyMap( + Map source, + String path) { + Map result = new LinkedHashMap(); + for (Map.Entry entry : source.entrySet()) { + result.put(entry.getKey(), deepCopy(entry.getValue(), path + "." + entry.getKey())); + } + return result; + } + + private static Object deepCopy(Object value, String path) { + if (value instanceof Map) { + return deepStringKeyMap(stringKeyMap((Map) value, path), path); + } + if (value instanceof List) { + List result = new ArrayList(); + for (Object child : (List) value) { + result.add(deepCopy(child, path + "[]")); + } + return result; + } + return value; + } + + private static void appendJson(StringBuilder target, Object value, boolean sortKeys) { + if (value == null) { + target.append("null"); + return; + } + if (value instanceof String || value instanceof Character) { + appendJsonString(target, String.valueOf(value)); + return; + } + if (value instanceof Boolean) { + target.append(value); + return; + } + if (value instanceof Number) { + appendJsonNumber(target, (Number) value); + return; + } + if (value instanceof Map) { + Map map = stringKeyMap((Map) value, "JSON value"); + List> entries = + new ArrayList>(map.entrySet()); + if (sortKeys) { + Collections.sort(entries, + Comparator.comparing(Map.Entry::getKey)); + } + target.append('{'); + for (int index = 0; index < entries.size(); index++) { + if (index > 0) { + target.append(','); + } + Map.Entry entry = entries.get(index); + appendJsonString(target, entry.getKey()); + target.append(':'); + appendJson(target, entry.getValue(), sortKeys); + } + target.append('}'); + return; + } + if (value instanceof Iterable) { + target.append('['); + int index = 0; + for (Object item : (Iterable) value) { + if (index++ > 0) { + target.append(','); + } + appendJson(target, item, sortKeys); + } + target.append(']'); + return; + } + throw new IllegalArgumentException( + "Unsupported JSON value type: " + value.getClass().getName()); + } + + private static void appendJsonNumber(StringBuilder target, Number value) { + if (value instanceof Double) { + double number = value.doubleValue(); + if (!Double.isFinite(number)) { + throw new IllegalArgumentException("JSON numbers must be finite"); + } + } else if (value instanceof Float) { + float number = value.floatValue(); + if (!Float.isFinite(number)) { + throw new IllegalArgumentException("JSON numbers must be finite"); + } + } + target.append(value.toString()); + } + + private static void appendJsonString(StringBuilder target, String value) { + target.append('"'); + for (int index = 0; index < value.length(); index++) { + char character = value.charAt(index); + switch (character) { + case '"': + target.append("\\\""); + break; + case '\\': + target.append("\\\\"); + break; + case '\b': + target.append("\\b"); + break; + case '\f': + target.append("\\f"); + break; + case '\n': + target.append("\\n"); + break; + case '\r': + target.append("\\r"); + break; + case '\t': + target.append("\\t"); + break; + default: + if (character < 0x20) { + target.append(String.format("\\u%04x", (int) character)); + } else { + target.append(character); + } + } + } + target.append('"'); + } + + static final class ManifestFile { + final String path; + final String role; + final String sha256; + final long bytes; + + ManifestFile(String path, String role, String sha256, long bytes) { + this.path = path; + this.role = role; + this.sha256 = sha256; + this.bytes = bytes; + } + } + + static final class Fixture { + final String path; + final Map data; + + Fixture(String path, Map data) { + this.path = path; + this.data = Collections.unmodifiableMap( + new LinkedHashMap(data)); + } + + String id() { + return text(data.get("id"), path + ".id"); + } + + String category() { + return text(data.get("category"), path + ".category"); + } + + Map program() { + return map(data.get("program"), path + ".program"); + } + + Map context() { + return map(data.get("context"), path + ".context"); + } + + Map expected() { + return map(data.get("expected"), path + ".expected"); + } + } +} diff --git a/src/test/java/blue/bex/gas/BexGasPrimitivesTest.java b/src/test/java/blue/bex/gas/BexGasPrimitivesTest.java new file mode 100644 index 0000000..b14e02d --- /dev/null +++ b/src/test/java/blue/bex/gas/BexGasPrimitivesTest.java @@ -0,0 +1,355 @@ +package blue.bex.gas; + +import blue.bex.result.BexExecutionResult; +import blue.bex.result.BexMetrics; +import blue.language.processor.GasMeter; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class BexGasPrimitivesTest { + + @Test + void exposesExactManifestVocabularyAndWeightsInOrder() { + String[] names = { + "expressionEvaluated", + "statementExecuted", + "functionCalled", + "intrinsicCalled", + "documentRead", + "eventRead", + "processingEventRead", + "currentContractRead", + "stepsRead", + "bindingRead", + "variableRead", + "constantRead", + "resultValueRead", + "pointerSegmentRead", + "pointerSegmentWritten", + "objectMemberRead", + "listItemRead", + "collectionItemVisited", + "collectionItemProduced", + "textBlockExamined", + "textBlockConstructed", + "integerLimbOperation", + "comparisonNodeVisited", + "sortComparison", + "patchAppended", + "eventAppended", + "transientObjectMemberProduced", + "transientListItemProduced", + "blueOutputBoundary", + "nodeIdentityRequested" + }; + long[] weights = { + 1, 1, 2, 5, 2, 1, 1, 1, 1, 1, + 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 5, 5, 1, 1, 5, 5 + }; + + assertEquals(30, BexGasCounter.values().length); + assertEquals(30, BexGasCounter.defaultWeights().size()); + for (int index = 0; index < names.length; index++) { + BexGasCounter counter = BexGasCounter.values()[index]; + assertEquals(names[index], counter.canonicalName()); + assertEquals(weights[index], counter.defaultWeight()); + assertEquals(counter, BexGasCounter.fromName(names[index])); + } + assertEquals("blue-bex/gas/2.0", BexGasCounter.SCHEDULE_ID); + assertEquals( + "sha256:41247c820d91a12fdfc17fd9e787a5d8d668d8acc5954fdcb131715bf9e6147d", + BexGasCounter.MANIFEST_IDENTITY); + } + + @Test + void scheduleContainsAllCanonicalCounters() { + BexGasSchedule schedule = BexGasSchedule.builder() + .expressionEvaluated(7L) + .pointerSegmentWritten(3L) + .eventAppended(9L) + .build(); + + assertEquals(30, schedule.weights().size()); + assertEquals(30, schedule.counterWeights().size()); + assertEquals(7L, schedule.expressionEvaluated); + assertEquals(3L, schedule.pointerSegmentWritten); + assertEquals(9L, schedule.eventAppended); + assertEquals(9L, schedule.weight("eventAppended")); + assertThrows(UnsupportedOperationException.class, + () -> schedule.counterWeights().put("other", 1L)); + } + + @Test + void portableScheduleRejectsZeroOrNegativeWeights() { + assertThrows( + IllegalArgumentException.class, + () -> BexGasSchedule.builder() + .expressionEvaluated(0L)); + assertThrows( + IllegalArgumentException.class, + () -> BexGasSchedule.builder() + .expressionEvaluated(-1L)); + assertTrue(BexGasSchedule.defaults() + .counterWeights() + .values() + .stream() + .allMatch(weight -> weight.longValue() > 0L)); + } + + @Test + void customScheduleIdentityIsRetainedByItsLedger() { + BexGasSchedule schedule = BexGasSchedule.builder() + .expressionEvaluated(7L) + .build(); + BexGasMeter meter = new BexGasMeter(schedule, 100L); + meter.charge(BexGasCounter.EXPRESSION_EVALUATED); + + BexGasLedger ledger = meter.ledger(); + assertEquals(schedule.scheduleId(), ledger.scheduleId()); + assertEquals( + schedule.manifestIdentity(), + ledger.manifestIdentity()); + assertNotEquals( + BexGasCounter.MANIFEST_IDENTITY, + ledger.manifestIdentity()); + } + + @Test + void rejectsBeforeWorkAndLeavesRejectedChargeOutOfTrace() { + BexGasMeter meter = new BexGasMeter(BexGasSchedule.defaults(), 6L); + meter.charge( + BexGasCounter.EXPRESSION_EVALUATED, + 1L, + "$.expr", + "$literal", + "evaluate"); + + BexGasLimitExceededException exhausted = assertThrows( + BexGasLimitExceededException.class, + () -> meter.charge(BexGasCounter.PATCH_APPENDED, 2L)); + + assertEquals(BexGasCounter.PATCH_APPENDED, exhausted.counter()); + assertEquals(1L, exhausted.admittedGas()); + assertEquals(6L, exhausted.effectiveBudget()); + assertEquals(1L, meter.totalGas()); + assertEquals(1, meter.trace().size()); + assertEquals("$.expr", meter.trace().get(0).sourcePath()); + assertEquals("$literal", meter.trace().get(0).operator()); + assertEquals("evaluate", meter.trace().get(0).reason()); + } + + @Test + void localLimitCanOnlyReduceParentBudget() { + BexGasMeter meter = + new BexGasMeter(BexGasSchedule.defaults(), 100L, 4L); + + assertEquals(4L, meter.effectiveBudget()); + meter.charge(BexGasCounter.DOCUMENT_READ, 2L); + assertEquals(4L, meter.totalGas()); + assertEquals(0L, meter.remainingGas()); + assertThrows(BexGasLimitExceededException.class, + () -> meter.charge(BexGasCounter.EXPRESSION_EVALUATED)); + assertEquals(1, meter.trace().size()); + } + + @Test + void ledgerIsAnImmutableValidatedSnapshotAndResultDerivesTotalFromIt() { + BexGasMeter meter = + new BexGasMeter(BexGasSchedule.defaults(), 100L); + meter.charge(BexGasCounter.FUNCTION_CALLED, 1L); + meter.charge(BexGasCounter.EVENT_APPENDED, 2L); + + BexGasLedger ledger = meter.ledger(); + assertEquals(12L, ledger.totalGas()); + assertEquals(2L, ledger.quantity(BexGasCounter.EVENT_APPENDED)); + assertThrows(UnsupportedOperationException.class, + () -> ledger.trace().clear()); + assertThrows(IllegalArgumentException.class, + () -> new BexGasLedger(Arrays.asList( + new BexGasCharge( + 1L, + BexGasCounter.EVENT_READ, + 1L, + 1L, + null, + null, + "bad-sequence")))); + + BexExecutionResult result = + new BexExecutionResult(null, null, null, ledger, new BexMetrics()); + assertEquals(ledger, result.gasLedger()); + assertEquals(ledger.trace(), result.gasTrace()); + assertEquals(12L, result.gasUsed()); + } + + @Test + void hostChildLedgerIsChargedLiveAndSubmittedExactlyOnce() { + blue.language.processor.GasSchedule hostSchedule = + blue.language.processor.GasSchedule.contracts10(); + GasMeter host = new GasMeter(hostSchedule, 100L); + BexGasSchedule schedule = BexGasSchedule.defaults(); + GasMeter.ChildGasLedger child = + host.childLedger(BexGasCounter.NAMESPACE, + schedule.counterWeights()); + BexGasMeter meter = new BexGasMeter(schedule, child, 20L); + + meter.charge(BexGasCounter.INTRINSIC_CALLED, 2L, "intrinsic"); + assertTrue(meter.hasHostLedger()); + assertFalse(meter.hostLedgerSubmitted()); + assertEquals(10L, child.totalGas()); + assertEquals(0L, host.totalGas()); + + meter.submitHostLedger(host::merge); + + assertTrue(meter.hostLedgerSubmitted()); + assertEquals(10L, host.totalGas()); + assertEquals(1, host.trace().size()); + assertEquals("bex", host.trace().get(0).namespace()); + assertEquals("intrinsicCalled", host.trace().get(0).counter()); + assertThrows(IllegalStateException.class, + () -> meter.submitHostLedger(host::merge)); + assertThrows(IllegalStateException.class, + () -> meter.charge(BexGasCounter.EVENT_READ)); + } + + @Test + void registryBoundNamedCountersCannotSelectArbitraryWeights() { + String namespace = "intrinsic:sha256-test"; + String counter = "hashBlock"; + String qualified = + BexGasMeter.qualifiedCounterName(namespace, counter); + Map registered = new LinkedHashMap<>(); + registered.put(qualified, 3L); + BexGasMeter meter = new BexGasMeter( + BexGasSchedule.defaults(), + 20L, + BexGasMeter.NO_LOCAL_LIMIT, + registered); + + meter.chargeNamed( + namespace, + counter, + 2L, + 3L, + "$.expr", + "$intrinsic", + "registry-child-work"); + + assertEquals(6L, meter.totalGas()); + assertEquals(1, meter.trace().size()); + assertEquals(namespace, meter.trace().get(0).namespace()); + assertEquals(counter, meter.trace().get(0).counterName()); + assertNull(meter.trace().get(0).portableCounter()); + assertEquals(2L, meter.ledger().quantity(namespace, counter)); + assertEquals(3L, meter.childLedgerWeights().get(qualified)); + assertThrows(IllegalArgumentException.class, + () -> meter.chargeNamed( + namespace, + counter, + 1L, + 4L, + null, + null, + "mismatched-weight")); + assertThrows(IllegalArgumentException.class, + () -> meter.chargeNamed( + namespace, + "unregistered", + 1L)); + assertEquals(6L, meter.totalGas()); + } + + @Test + void chargeAndLedgerRejectInvalidArithmeticAndMutation() { + assertThrows(IllegalArgumentException.class, + () -> new BexGasCharge( + 0L, + BexGasCounter.EVENT_READ, + 2L, + 1L, + 3L, + null, + null, + "invalid-gas")); + assertThrows(IllegalArgumentException.class, + () -> new BexGasMeter(BexGasSchedule.defaults(), 1L) + .charge(BexGasCounter.EVENT_READ, -1L)); + assertThrows(IllegalArgumentException.class, + () -> BexGasSchedule.builder() + .weight(BexGasCounter.EVENT_READ, -1L)); + List external = Collections.singletonList( + new BexGasCharge( + 0L, + BexGasCounter.EVENT_READ, + 1L, + 1L, + null, + null, + "read")); + BexGasLedger ledger = new BexGasLedger(external); + assertEquals(1L, ledger.totalGas()); + } + + @Test + void everyPhysicalLedgerReceivesItsFinalCallbackEvenWhenOneThrows() { + BexGasSchedule schedule = BexGasSchedule.defaults(); + GasMeter parent = new GasMeter( + blue.language.processor.GasSchedule.contracts10(), + 100L); + GasMeter.ChildGasLedger bex = parent.childLedger( + "bex-run", + schedule.counterWeights()); + GasMeter.ChildGasLedger intrinsic = parent.childLedger( + "bex-run/intrinsic-test", + Collections.singletonMap("work", 1L)); + Map ledgers = + new LinkedHashMap<>(); + ledgers.put(BexGasCounter.NAMESPACE, bex); + ledgers.put("intrinsic-test", intrinsic); + Map registered = Collections.singletonMap( + BexGasMeter.qualifiedCounterName( + "intrinsic-test", "work"), + 1L); + BexGasMeter meter = new BexGasMeter( + schedule, + ledgers, + BexGasMeter.NO_LOCAL_LIMIT, + registered); + Map callbacks = + new IdentityHashMap<>(); + + IllegalStateException failure = assertThrows( + IllegalStateException.class, + () -> meter.failHostLedger(ledger -> { + callbacks.put(ledger, Boolean.TRUE); + if (ledger == bex) { + throw new IllegalStateException( + "first callback failed"); + } + })); + + assertEquals("first callback failed", failure.getMessage()); + assertEquals(2, callbacks.size()); + assertTrue(callbacks.containsKey(bex)); + assertTrue(callbacks.containsKey(intrinsic)); + assertTrue(meter.hostLedgerFinalized()); + assertThrows( + IllegalStateException.class, + () -> meter.failHostLedger(ignored -> { + })); + } +} diff --git a/src/test/java/blue/bex/output/BexSemanticIdentityIntegrationTest.java b/src/test/java/blue/bex/output/BexSemanticIdentityIntegrationTest.java new file mode 100644 index 0000000..826f7b3 --- /dev/null +++ b/src/test/java/blue/bex/output/BexSemanticIdentityIntegrationTest.java @@ -0,0 +1,762 @@ +package blue.bex.output; + +import blue.bex.BexException; +import blue.bex.api.BexEngine; +import blue.bex.api.BexExecutionContext; +import blue.bex.api.BexProgramSource; +import blue.bex.gas.BexGasMeter; +import blue.bex.gas.BexGasSchedule; +import blue.bex.result.BexExecutionResult; +import blue.bex.result.BexMetrics; +import blue.bex.result.BexPatchEntry; +import blue.bex.result.BexResultOverlay; +import blue.bex.runtime.BexExecutionAccumulator; +import blue.bex.value.BexValue; +import blue.bex.value.BexValues; +import blue.language.model.Node; +import blue.language.processor.ExecutionEvidenceUnavailableException; +import blue.language.processor.GasLimitExceededException; +import blue.language.processor.GasMeter; +import blue.language.processor.InvalidExecutionEvidenceException; +import blue.language.processor.PortableLimitExceededException; +import blue.language.processor.ProcessorErrorCategory; +import blue.language.processor.ProcessorFailureException; +import blue.language.snapshot.FrozenNode; +import blue.language.utils.BlueIdCalculator; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static blue.bex.test.BexTestFixtures.defaultDocumentView; +import static blue.bex.test.BexTestFixtures.frozen; +import static blue.bex.test.BexTestFixtures.list; +import static blue.bex.test.BexTestFixtures.obj; +import static blue.bex.test.BexTestFixtures.op; +import static blue.bex.test.BexTestFixtures.stepDo; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class BexSemanticIdentityIntegrationTest { + + @Test + void standaloneAndCustomBoundariesReturnTheirFrozenExactResult() { + BexOutputAdmission standalone = admission( + BexSemanticIdentityBoundary.STANDALONE); + BexAdmittedValue standaloneValue = standalone.admit( + BexValues.scalar("standalone"), + BexOutputKind.ROOT_RESULT); + + assertTrue(standaloneValue.value().isExact()); + assertEquals( + standaloneValue.nodeBlueId(), + BlueIdCalculator.calculateBlueId( + standaloneValue.node())); + + Node hostNormalized = + new Node().name("host-normalized").value("value"); + FrozenNode hostFrozen = + FrozenNode.fromResolvedNode(hostNormalized); + String hostBlueId = + BlueIdCalculator.calculateBlueId(hostNormalized); + BexSemanticIdentityBoundary hosted = ignored -> + new BexEstablishedIdentity(hostBlueId, hostFrozen); + BexAdmittedValue hostedValue = admission(hosted).admit( + BexValues.scalar("pre-normalized"), + BexOutputKind.ROOT_RESULT); + + assertEquals("host-normalized", hostedValue.node().getName()); + assertEquals("value", hostedValue.node().getValue()); + assertEquals(hostBlueId, hostedValue.value().exactBlueId()); + assertEquals("host-normalized", + hostedValue.value().get("name").asText()); + assertEquals("value", hostedValue.value().asText()); + assertTrue(hostedValue.semanticValue().isExact()); + } + + @Test + void ordinaryNonCyclicExactRootBypassesHostSemanticBoundary() { + Node content = obj( + "kind", "ordinary-exact", + "nested", obj("count", 7)); + FrozenNode frozen = + FrozenNode.fromResolvedNode(content); + String blueId = + BlueIdCalculator.calculateBlueId(content); + BexValue exact = + BexValues.exact(frozen, frozen, blueId); + RecordingBoundary boundary = new RecordingBoundary(); + BexOutputAdmission admission = admission(boundary); + + BexAdmittedValue admitted = admission.admit( + exact, BexOutputKind.ROOT_RESULT); + + assertEquals(0, boundary.calls); + assertEquals(0L, admission.semanticIdentityMergeCount()); + assertSame(exact, admitted.value()); + assertSame(exact, admitted.semanticValue()); + assertFalse(admitted.reconstructed()); + assertEquals(blueId, admitted.nodeBlueId()); + assertEquals(blueId, admitted.node().getBlueId()); + } + + @Test + void admittedScalarsRetainTheirRawKindsEqualityAndTruthiness() { + RecordingBoundary boundary = new RecordingBoundary(); + BexOutputAdmission admission = admission(boundary); + + assertAdmittedScalar( + admission, + BexValues.scalar(false), + "boolean", + false); + assertAdmittedScalar( + admission, + BexValues.scalar(BigInteger.valueOf(7L)), + "integer", + true); + assertAdmittedScalar( + admission, + BexValues.scalar(new BigDecimal("7.5")), + "double", + true); + assertAdmittedScalar( + admission, + BexValues.scalar("text"), + "text", + true); + + assertEquals(4, boundary.calls); + } + + @Test + void integralDecimalCrossesTheHostBoundaryAsBlueDouble() { + RecordingBoundary boundary = new RecordingBoundary(); + BexAdmittedValue admitted = admission(boundary).admit( + BexValues.scalar(new BigDecimal("1.0")), + BexOutputKind.ROOT_RESULT); + + assertEquals(1, boundary.calls); + Node supplied = boundary.inputs.get(0); + assertEquals( + new BigDecimal("1.0"), + supplied.getRawValue()); + assertEquals( + blue.language.utils.Properties + .DOUBLE_TYPE_BLUE_ID, + supplied.getType().getBlueId()); + assertEquals( + blue.language.utils.Properties + .DOUBLE_TYPE_BLUE_ID, + admitted.node().getType().getBlueId()); + assertEquals( + "double", + BexValues.kind(admitted.value())); + } + + @Test + void admittedEmptyObjectsRetainTheirShapeWithoutOverridingHostFields() { + RecordingBoundary boundary = new RecordingBoundary(); + BexOutputAdmission admission = admission(boundary); + BexValue supplied = BexValues.fromSimple(map( + "empty", Collections.emptyMap(), + "status", "kept")); + + BexValue admitted = admission.admit( + supplied, + BexOutputKind.ROOT_RESULT).value(); + BexValue empty = admitted.get("empty"); + + assertTrue(empty.isExact()); + assertTrue(empty.isObject()); + assertFalse(empty.isNull()); + assertEquals(Collections.emptyList(), empty.keys()); + assertEquals(Collections.emptyMap(), empty.toSimple()); + assertEquals("kept", admitted.get("status").asText()); + assertEquals(1, boundary.calls); + } + + @Test + void hostNormalizationWinsWhileMatchingExactDescendantsStayLocal() { + Node exactContent = obj( + "deep", "already-resolved"); + String exactChildBlueId = + BlueIdCalculator.calculateBlueId( + exactContent); + FrozenNode exactChildFrozen = + FrozenNode.fromResolvedNode( + exactContent); + BexValue exactChild = BexValues.exact( + exactChildFrozen, + exactChildFrozen, + exactChildBlueId); + Node mismatchedSourceContent = obj( + "source", "different-identity"); + FrozenNode mismatchedSourceFrozen = + FrozenNode.fromResolvedNode( + mismatchedSourceContent); + String mismatchedSourceBlueId = + BlueIdCalculator.calculateBlueId( + mismatchedSourceContent); + BexValue mismatchedSourceExact = + BexValues.exact( + mismatchedSourceFrozen, + mismatchedSourceFrozen, + mismatchedSourceBlueId); + String hostMismatchBlueId = + BlueIdCalculator.calculateBlueId( + obj("host", "authoritative")); + + String authoredReferenceBlueId = + BlueIdCalculator.calculateBlueId( + obj("remote", "content")); + BexValue supplied = BexValues.fromSimple( + map( + "status", "before-host", + "removed", true, + "outer", map( + "exactChild", exactChild, + "mismatchedExact", + mismatchedSourceExact, + "authoredReference", map( + "blueId", + authoredReferenceBlueId)))); + Node hostNormalized = obj( + "status", "after-host", + "added", "by-host", + "outer", obj( + "exactChild", + new Node().blueId( + exactChildBlueId), + "mismatchedExact", + new Node().blueId( + hostMismatchBlueId), + "authoredReference", + new Node().blueId( + authoredReferenceBlueId))); + FrozenNode hostFrozen = + FrozenNode.fromResolvedNode( + hostNormalized); + String hostBlueId = + BlueIdCalculator.calculateBlueId( + hostNormalized); + + BexValue admitted = admission(ignored -> + new BexEstablishedIdentity( + hostBlueId, + hostFrozen)) + .admit( + supplied, + BexOutputKind.ROOT_RESULT) + .value(); + + assertEquals( + Arrays.asList( + "added", "outer", "status"), + admitted.keys()); + assertEquals("after-host", + admitted.get("status").asText()); + assertEquals("by-host", + admitted.get("added").asText()); + assertTrue(admitted.get("removed") + .isUndefined()); + + BexValue outer = admitted.get("outer"); + assertTrue(outer.isExact()); + assertSame( + exactChild, + outer.get("exactChild")); + assertEquals( + "already-resolved", + outer.get("exactChild") + .get("deep") + .asText()); + BexValue mismatchedExact = + outer.get("mismatchedExact"); + assertNotSame( + mismatchedSourceExact, + mismatchedExact); + assertEquals( + hostMismatchBlueId, + mismatchedExact.exactBlueId()); + + BexValue authoredReference = + outer.get("authoredReference"); + assertTrue(authoredReference.isExact()); + assertEquals( + authoredReferenceBlueId, + authoredReference.exactBlueId()); + assertThrows( + BexException.class, + () -> authoredReference.get("blueId")); + } + + @Test + void invokesBoundaryExactlyOnceForEveryDistinctTransientSemanticValue() { + RecordingBoundary boundary = new RecordingBoundary(); + BexOutputAdmission admission = admission(boundary); + List values = Arrays.asList( + BexValues.scalar("text"), + BexValues.scalar(BigInteger.valueOf(7L)), + BexValues.scalar(new BigDecimal("7.5")), + BexValues.scalar(true), + BexValues.nullValue(), + BexValues.fromSimple(Arrays.asList("x", 1)), + BexValues.fromSimple(map( + "outer", map("inner", 1), + "entries", Arrays.asList(true, "x")))); + + for (BexValue value : values) { + assertTrue(admission.admit( + value, BexOutputKind.ROOT_RESULT).value().isExact()); + } + + assertEquals(values.size(), boundary.calls); + assertEquals(values.size(), boundary.inputs.size()); + assertEquals(values.size(), admission.semanticIdentityMergeCount()); + } + + @Test + void reusesAliasesButAdmitsEveryRecreatedTransientStructure() { + RecordingBoundary boundary = new RecordingBoundary(); + BexOutputAdmission admission = admission(boundary); + BexValue first = BexValues.fromSimple(map( + "b", Arrays.asList(2, 3), + "a", 1)); + BexValue sameStructureDifferentOrder = BexValues.fromSimple(map( + "a", 1, + "b", Arrays.asList(2, 3))); + + BexAdmittedValue initial = admission.admit( + first, BexOutputKind.NODE_IDENTITY); + assertSame(initial, admission.admit( + first, BexOutputKind.PATCH_VALUE)); + BexAdmittedValue recreated = admission.admit( + sameStructureDifferentOrder, + BexOutputKind.EVENT); + assertNotSame(initial, recreated); + assertTrue(BexValues.equal( + initial.value(), + recreated.value())); + assertEquals( + initial.nodeBlueId(), + recreated.nodeBlueId()); + + assertEquals(2, boundary.calls); + assertEquals(2L, admission.semanticIdentityMergeCount()); + } + + @Test + void memoizedHostIdentityPreservesNullThenEmptyObjectShapes() { + assertMemoizedNullAndEmptyObjectShapes(true); + } + + @Test + void memoizedHostIdentityPreservesEmptyObjectThenNullShapes() { + assertMemoizedNullAndEmptyObjectShapes(false); + } + + @Test + void nodeIdentityAdmissionIsReusedByPatchEventOverlayAndRootOutput() { + RecordingBoundary boundary = new RecordingBoundary(); + BexExecutionContext context = BexExecutionContext.builder() + .document(defaultDocumentView()) + .semanticIdentityBoundary(boundary) + .gasLimit(1_000_000L) + .build(); + Node program = stepDo(list( + op("$let", obj( + "name", "shared", + "expr", obj("nested", obj("count", 7)))), + op("$let", obj( + "name", "identity", + "expr", op("$nodeBlueId", + op("$var", "shared")))), + op("$appendChange", obj( + "op", "replace", + "path", "/state", + "val", op("$var", "shared"))), + op("$appendEvent", op("$var", "shared")), + op("$return", op("$var", "shared")))); + + BexExecutionResult result = BexEngine.builder().build() + .compileAndExecute( + BexProgramSource.inline(frozen(program)), + context); + BexPatchEntry patch = + result.changeset().entries().get(0); + BexAdmittedValue event = + result.events().admittedEvents().get(0); + + assertEquals(1, boundary.calls); + assertEquals(1L, result.metrics() + .compiledExecutions()); + assertSame(result.output(), patch.admittedValue()); + assertSame(result.output(), event); + assertSame(result.output().value(), patch.val()); + assertSame(result.output().value(), + result.events().events().get(0)); + assertTrue(patch.val().isExact()); + assertTrue(result.events().events().get(0).isExact()); + assertEquals(result.output().nodeBlueId(), + patch.val().exactBlueId()); + assertEquals(result.output().nodeBlueId(), + result.events().events().get(0).exactBlueId()); + assertEquals( + Collections.singletonMap( + "nested", + Collections.singletonMap( + "count", + BigInteger.valueOf(7L))), + result.events().events().get(0).toSimple()); + } + + @Test + void outputAdmissionIsReusedByALaterNodeIdentityRequest() { + RecordingBoundary boundary = new RecordingBoundary(); + BexExecutionContext context = BexExecutionContext.builder() + .document(defaultDocumentView()) + .semanticIdentityBoundary(boundary) + .gasLimit(1_000_000L) + .build(); + Node program = stepDo(list( + op("$let", obj( + "name", "shared", + "expr", obj( + "nested", + obj("count", 7)))), + op("$appendEvent", + op("$var", "shared")), + op("$let", obj( + "name", "identity", + "expr", op("$nodeBlueId", + op("$var", "shared")))), + op("$return", + op("$var", "shared")))); + + BexExecutionResult result = + BexEngine.builder().build() + .compileAndExecute( + BexProgramSource.inline( + frozen(program)), + context); + BexAdmittedValue event = + result.events() + .admittedEvents() + .get(0); + + assertEquals(1, boundary.calls); + assertSame(result.output(), event); + assertSame( + result.output().value(), + result.events().events().get(0)); + assertEquals( + result.output().nodeBlueId(), + event.nodeBlueId()); + } + + @Test + void preservesHostFailureClassificationWithoutBexWrapping() { + ExecutionEvidenceUnavailableException unavailable = + new ExecutionEvidenceUnavailableException( + "missing exact evidence", + Collections.singleton("exact-id")); + InvalidExecutionEvidenceException invalid = + new InvalidExecutionEvidenceException("invalid evidence"); + ProcessorFailureException processor = + new ProcessorFailureException( + ProcessorErrorCategory + .InvalidProcessingDocument, + "invalid output"); + PortableLimitExceededException portable = + new PortableLimitExceededException( + "semanticNodes", 11L, 10L); + GasLimitExceededException gas = gasFailure(); + + assertBoundaryFailureIsSame(unavailable); + assertBoundaryFailureIsSame(invalid); + assertBoundaryFailureIsSame(processor); + assertBoundaryFailureIsSame(portable); + assertBoundaryFailureIsSame(gas); + + IllegalStateException unexpected = + new IllegalStateException("adapter defect"); + BexException wrapped = assertThrows( + BexException.class, + () -> admission(node -> { + throw unexpected; + }).admit( + BexValues.scalar("value"), + BexOutputKind.ROOT_RESULT)); + assertSame(unexpected, wrapped.getCause()); + } + + @Test + void failedAdmissionMutatesNeitherPatchNorEventBuffers() { + ExecutionEvidenceUnavailableException expected = + new ExecutionEvidenceUnavailableException( + "semantic output evidence unavailable"); + BexOutputAdmission admission = admission(node -> { + throw expected; + }); + BexExecutionAccumulator accumulator = + new BexExecutionAccumulator( + new BexResultOverlay( + defaultDocumentView(), + new BexMetrics()), + admission); + + ExecutionEvidenceUnavailableException patchFailure = + assertThrows( + ExecutionEvidenceUnavailableException.class, + () -> accumulator.appendChange( + new BexPatchEntry( + "replace", + "/state", + "/state", + BexValues.scalar( + "rejected")))); + assertSame(expected, patchFailure); + assertTrue( + accumulator.changeset() + .entries() + .isEmpty()); + + ExecutionEvidenceUnavailableException eventFailure = + assertThrows( + ExecutionEvidenceUnavailableException.class, + () -> accumulator.appendEvent( + BexValues.scalar( + "rejected-event"))); + assertSame(expected, eventFailure); + assertTrue( + accumulator.events() + .events() + .isEmpty()); + assertTrue( + accumulator.events() + .admittedEvents() + .isEmpty()); + } + + @Test + void exactCyclicMemberStaysOpaqueAndTransientCounterfeitsFailClosed() { + String cyclicMember = + BlueIdCalculator.calculateBlueId( + new Node().value("cyclic-set")) + + "#0"; + FrozenNode reference = FrozenNode.fromResolvedNode( + new Node().blueId(cyclicMember)); + BexValue exact = BexValues.exact( + reference, reference, cyclicMember); + RecordingBoundary boundary = new RecordingBoundary(); + BexAdmittedValue admitted = admission(boundary).admit( + exact, BexOutputKind.ROOT_RESULT); + + assertEquals(0, boundary.calls); + assertFalse(admitted.reconstructed()); + assertEquals(cyclicMember, admitted.nodeBlueId()); + assertEquals(cyclicMember, admitted.node().getBlueId()); + + BexValue transientCounterfeit = BexValues.fromSimple( + Collections.singletonMap( + "blueId", cyclicMember)); + BexException beforeBoundary = assertThrows( + BexException.class, + () -> admission(boundary).admit( + transientCounterfeit, + BexOutputKind.ROOT_RESULT)); + assertTrue(beforeBoundary.getMessage().contains( + "cannot counterfeit")); + assertEquals(0, boundary.calls); + + BexValue nestedCounterfeit = BexValues.fromSimple( + Collections.singletonMap( + "nested", + Collections.singletonMap( + "blueId", cyclicMember))); + BexException nestedFailure = assertThrows( + BexException.class, + () -> admission(boundary).admit( + nestedCounterfeit, + BexOutputKind.ROOT_RESULT)); + assertTrue(nestedFailure.getMessage().contains( + "cannot counterfeit")); + assertEquals(0, boundary.calls); + + BexValue transientWithExactCyclicChild = + BexValues.fromSimple( + Collections.singletonMap( + "nested", exact)); + BexAdmittedValue admittedParent = + admission(boundary).admit( + transientWithExactCyclicChild, + BexOutputKind.ROOT_RESULT); + assertEquals(1, boundary.calls); + assertSame( + exact, + admittedParent.value().get("nested")); + assertEquals( + cyclicMember, + admittedParent.value() + .get("nested") + .exactBlueId()); + + BexException afterBoundary = assertThrows( + BexException.class, + () -> admission(node -> + new BexEstablishedIdentity( + cyclicMember, + reference)) + .admit( + BexValues.scalar("ordinary"), + BexOutputKind.ROOT_RESULT)); + assertTrue(afterBoundary.getMessage().contains( + "cannot establish a cyclic-set")); + } + + private static void assertBoundaryFailureIsSame( + RuntimeException failure) { + RuntimeException observed = assertThrows( + failure.getClass(), + () -> admission(node -> { + throw failure; + }).admit( + BexValues.scalar("value"), + BexOutputKind.ROOT_RESULT)); + assertSame(failure, observed); + } + + private static void assertMemoizedNullAndEmptyObjectShapes( + boolean nullFirst) { + RecordingBoundary boundary = new RecordingBoundary(); + BexOutputAdmission admission = admission(boundary); + BexValue suppliedNull = BexValues.nullValue(); + BexValue suppliedEmptyObject = + BexValues.fromSimple(Collections.emptyMap()); + + BexAdmittedValue admittedNull; + BexAdmittedValue admittedEmptyObject; + if (nullFirst) { + admittedNull = admission.admit( + suppliedNull, + BexOutputKind.ROOT_RESULT); + admittedEmptyObject = admission.admit( + suppliedEmptyObject, + BexOutputKind.ROOT_RESULT); + } else { + admittedEmptyObject = admission.admit( + suppliedEmptyObject, + BexOutputKind.ROOT_RESULT); + admittedNull = admission.admit( + suppliedNull, + BexOutputKind.ROOT_RESULT); + } + + assertEquals(2, boundary.calls); + assertEquals(2L, admission.semanticIdentityMergeCount()); + assertNotSame(admittedNull, admittedEmptyObject); + assertTrue(admittedNull.value().isExact()); + assertEquals("null", BexValues.kind( + admittedNull.value())); + assertNull(admittedNull.value().toSimple()); + assertTrue(admittedEmptyObject.value().isExact()); + assertEquals("object", BexValues.kind( + admittedEmptyObject.value())); + assertEquals( + Collections.emptyMap(), + admittedEmptyObject.value().toSimple()); + assertEquals( + admittedNull.nodeBlueId(), + admittedEmptyObject.nodeBlueId()); + assertEquals( + admittedNull.value().exactBlueId(), + admittedEmptyObject.value().exactBlueId()); + } + + private static void assertAdmittedScalar( + BexOutputAdmission admission, + BexValue source, + String expectedKind, + boolean expectedTruthiness) { + BexValue admitted = admission.admit( + source, + BexOutputKind.ROOT_RESULT) + .value(); + + assertEquals( + expectedKind, + BexValues.kind(admitted)); + assertTrue(BexValues.equal( + admitted, + source)); + assertEquals( + expectedTruthiness, + BexValues.truthy(admitted)); + } + + private static GasLimitExceededException gasFailure() { + GasMeter meter = new GasMeter( + blue.language.processor.GasSchedule.contracts10(), + 0L); + GasMeter.ChildGasLedger ledger = + meter.childLedger( + "identity-test", + Collections.singletonMap( + "identity", 1L)); + return assertThrows( + GasLimitExceededException.class, + () -> ledger.charge("identity", 1L)); + } + + private static BexOutputAdmission admission( + BexSemanticIdentityBoundary boundary) { + return new BexOutputAdmission( + new BexGasMeter( + BexGasSchedule.defaults(), + 1_000_000L), + boundary); + } + + private static Map map( + Object... keysAndValues) { + Map result = + new LinkedHashMap<>(); + for (int index = 0; + index < keysAndValues.length; + index += 2) { + result.put( + (String) keysAndValues[index], + keysAndValues[index + 1]); + } + return result; + } + + private static final class RecordingBoundary + implements BexSemanticIdentityBoundary { + private final List inputs = new ArrayList<>(); + private int calls; + + @Override + public BexEstablishedIdentity establishIdentity( + Node node) { + calls++; + Node exact = node.clone(); + inputs.add(exact); + return new BexEstablishedIdentity( + BlueIdCalculator.calculateBlueId(exact), + FrozenNode.fromResolvedNode(exact)); + } + } +} diff --git a/src/test/java/blue/bex/test/BexTestFixtures.java b/src/test/java/blue/bex/test/BexTestFixtures.java index dcc22af..3b05770 100644 --- a/src/test/java/blue/bex/test/BexTestFixtures.java +++ b/src/test/java/blue/bex/test/BexTestFixtures.java @@ -73,8 +73,10 @@ public static Node op(String name, Object body) { return obj(name, body); } - public static Node emptyStatement() { - return obj("$empty", true); + public static Node noOpStatement() { + return obj("$returnIf", obj( + "cond", false, + "expr", null)); } public static Node obj(Object... keysAndValues) { diff --git a/src/test/java/blue/bex/value/BexUnicodeOrderTest.java b/src/test/java/blue/bex/value/BexUnicodeOrderTest.java new file mode 100644 index 0000000..36f0db3 --- /dev/null +++ b/src/test/java/blue/bex/value/BexUnicodeOrderTest.java @@ -0,0 +1,154 @@ +package blue.bex.value; + +import blue.bex.gas.BexGasCounter; +import blue.bex.gas.BexGasLimitExceededException; +import blue.bex.gas.BexGasMeter; +import blue.bex.gas.BexGasSchedule; +import blue.language.model.Node; +import blue.language.snapshot.FrozenNode; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class BexUnicodeOrderTest { + @Test + void usesTheCanonicalStableBottomUpMergeTrace() { + List trace = new ArrayList<>(); + + List sorted = BexUnicodeOrder.sortedCopy( + Arrays.asList("d", "b", "c", "a"), + (left, right) -> { + trace.add(left + ":" + right); + return BexUnicodeOrder.compareCodePoints( + left, right); + }); + + assertEquals( + Arrays.asList("a", "b", "c", "d"), + sorted); + assertEquals( + Arrays.asList( + "d:b", + "c:a", + "b:a", + "b:c", + "d:c"), + trace); + + String first = new String("same"); + String second = new String("same"); + List equal = BexUnicodeOrder.sortedCopy( + Arrays.asList(first, second), + BexUnicodeOrder::compareCodePoints); + assertSame(first, equal.get(0)); + assertSame(second, equal.get(1)); + } + + @Test + void rejectedSortAdmissionPerformsNoComparatorWork() { + BexGasMeter meter = new BexGasMeter( + BexGasSchedule.defaults(), 4L); + AtomicInteger completedComparisons = + new AtomicInteger(); + + assertThrows( + BexGasLimitExceededException.class, + () -> BexUnicodeOrder.sortedCopy( + Arrays.asList( + "d", "b", "c", "a"), + (left, right) -> { + meter.charge( + BexGasCounter + .SORT_COMPARISON); + meter.charge( + BexGasCounter + .COMPARISON_NODE_VISITED); + meter.charge( + BexGasCounter + .TEXT_BLOCK_EXAMINED, + 2L); + completedComparisons + .incrementAndGet(); + return BexUnicodeOrder + .compareCodePoints( + left, right); + })); + + assertEquals(1, completedComparisons.get()); + assertEquals(1L, meter.ledger().quantity( + BexGasCounter.SORT_COMPARISON)); + assertEquals(1L, meter.ledger().quantity( + BexGasCounter.COMPARISON_NODE_VISITED)); + assertEquals(2L, meter.ledger().quantity( + BexGasCounter.TEXT_BLOCK_EXAMINED)); + assertEquals(3, meter.trace().size()); + } + + @Test + void valueImplementationsExposeOneEstablishedCanonicalCursor() { + String supplementary = "\uD800\uDC00"; + Map fields = + new LinkedHashMap<>(); + fields.put(supplementary, BexValues.scalar(3)); + fields.put("z", BexValues.scalar(1)); + fields.put("\uE000", BexValues.scalar(2)); + List expected = Arrays.asList( + "z", "\uE000", supplementary); + + BexValue map = BexValues.map(fields); + assertEquals(expected, map.keys()); + assertSame(map.keys(), map.keys()); + + BexValue overlay = BexValues.overlay( + map, "a", BexValues.scalar(0)); + assertEquals( + Arrays.asList( + "a", "z", "\uE000", + supplementary), + overlay.keys()); + assertSame(overlay.keys(), overlay.keys()); + + BexValue pointer = BexValues.pointerSet( + map, + Arrays.asList("a"), + BexValues.scalar(0), + "set"); + assertEquals( + Arrays.asList( + "a", "z", "\uE000", + supplementary), + pointer.keys()); + assertSame(pointer.keys(), pointer.keys()); + + Map nodeFields = + new LinkedHashMap<>(); + nodeFields.put( + supplementary, + new Node().value(3)); + nodeFields.put("z", new Node().value(1)); + nodeFields.put( + "\uE000", new Node().value(2)); + BexValue node = BexValues + .nodeCursorTrustedImmutable( + new Node().properties( + nodeFields)); + assertEquals(expected, node.keys()); + assertSame(node.keys(), node.keys()); + + BexValue frozen = BexValues.frozen( + FrozenNode.fromResolvedNode( + new Node().properties( + nodeFields))); + assertEquals(expected, frozen.keys()); + assertSame(frozen.keys(), frozen.keys()); + } +} diff --git a/src/test/java/blue/language/processor/BexHostedRuntimeWorkSessionTest.java b/src/test/java/blue/language/processor/BexHostedRuntimeWorkSessionTest.java new file mode 100644 index 0000000..8d00e8f --- /dev/null +++ b/src/test/java/blue/language/processor/BexHostedRuntimeWorkSessionTest.java @@ -0,0 +1,1557 @@ +package blue.language.processor; + +import blue.bex.BexException; +import blue.bex.api.BexEngine; +import blue.bex.api.BexExecutionContext; +import blue.bex.api.BexGasLedgerHost; +import blue.bex.api.BexIntrinsicRegistry; +import blue.bex.api.BexProgramSource; +import blue.bex.api.FrozenBexDocumentView; +import blue.bex.api.ProcessorExecutionContextBexGasLedgerHost; +import blue.bex.compile.BexCompiledProgram; +import blue.bex.gas.BexGasCounter; +import blue.bex.gas.BexGasLimitExceededException; +import blue.bex.gas.BexGasSchedule; +import blue.bex.output.BexSemanticIdentityBoundary; +import blue.bex.pointer.BexPointerCache; +import blue.bex.result.BexExecutionResult; +import blue.bex.result.BexMetrics; +import blue.bex.runtime.BexRuntime; +import blue.bex.value.BexValues; +import blue.language.Blue; +import blue.language.NodeProvider; +import blue.language.model.Node; +import blue.language.provider.NodeProviderOutcome; +import blue.language.provider.NodeProviderResult; +import blue.language.snapshot.FrozenNode; +import blue.language.utils.BlueIdCalculator; +import org.junit.jupiter.api.Test; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; + +import static blue.bex.test.BexTestFixtures.frozen; +import static blue.bex.test.BexTestFixtures.list; +import static blue.bex.test.BexTestFixtures.obj; +import static blue.bex.test.BexTestFixtures.op; +import static blue.bex.test.BexTestFixtures.stepDo; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class BexHostedRuntimeWorkSessionTest { + private static final String FAILURE_INTRINSIC = + "TestHostedFailureIntrinsic"; + private static final String FAILURE_REGISTRY = + "test-hosted-failure/1"; + + @Test + void hostedBexLongTraceCapabilityProbeRecordsObservedOutcome() + throws Exception { + final int requestedItems = 128; + final int requiredTraceEntries = 516; + writeLongTraceEvidence( + false, + requestedItems, + requiredTraceEntries, + 0, + 0, + false, + null, + null); + GasMeter parent = parentMeter(100_000L); + RuntimeWorkSession session = session(parent); + RecordingSessionHost host = + new RecordingSessionHost( + session, "bex:long-trace-probe"); + Node program = stepDo(list( + op("$forEach", obj( + "in", integerList(requestedItems), + "item", "item", + "index", "index", + "do", list())), + op("$return", true))); + + BexExecutionResult result = null; + RuntimeException failure = null; + try { + result = BexEngine.builder() + .build() + .compileAndExecute( + BexProgramSource.inline( + frozen(program)), + context( + host, + -1L, + BexSemanticIdentityBoundary + .STANDALONE)); + } catch (RuntimeException observed) { + failure = observed; + } + + List staged = + session.stagedTrace(); + boolean orderPreserved = true; + Set counters = + new LinkedHashSet(); + for (int index = 0; index < staged.size(); index++) { + GasTraceEntry entry = staged.get(index); + orderPreserved &= entry.sequence() == index; + counters.add(entry.counter()); + } + PortableLimitExceededException portable = + cause( + failure, + PortableLimitExceededException.class); + writeLongTraceEvidence( + false, + requestedItems, + requiredTraceEntries, + staged.size(), + counters.size(), + orderPreserved, + portable, + failure); + assertNull( + failure, + "the final host must admit an ordered BEX trace " + + "longer than 256 entries"); + assertNotNull(result); + assertTrue( + staged.size() >= requiredTraceEntries, + "the hosted trace must exercise more than 256 " + + "ordered counter occurrences"); + assertTrue(orderPreserved); + assertTrue( + counters.size() + <= GasSchedule.contracts10().portableLimit( + GasScheduleConstants.PortableLimit + .RUNTIME_CHILD_LEDGER_COUNTER_KINDS), + "the portable limit applies to distinct counter " + + "kinds, not trace occurrences"); + assertEquals( + staged.size(), + result.gasTrace().size()); + for (int index = 0; + index < staged.size(); + index++) { + GasTraceEntry hosted = staged.get(index); + blue.bex.gas.BexGasCharge bex = + result.gasTrace().get(index); + assertEquals( + bex.counterName(), + hosted.counter()); + assertEquals( + bex.quantity(), + hosted.quantity()); + assertEquals( + bex.weight(), + hosted.weight()); + assertEquals( + bex.gas(), + hosted.subtotal()); + } + session.complete(); + + assertEquals(staged.size(), parent.trace().size()); + assertTrue(orderPreserved); + writeLongTraceEvidence( + true, + requestedItems, + requiredTraceEntries, + staged.size(), + counters.size(), + orderPreserved, + null, + null); + } + + @Test + void physicalNamespacesShareOneBudgetAndSubmittedLedgersAreFinal() { + GasMeter parent = parentMeter(100L); + RuntimeWorkSession session = session(parent); + ProcessorExecutionContextBexGasLedgerHost host = + new ProcessorExecutionContextBexGasLedgerHost( + session, "bex:first"); + + GasMeter.ChildGasLedger bex = host.open( + "bex", + Collections.singletonMap("expressionEvaluated", 1L)); + GasMeter.ChildGasLedger intrinsic = host.open( + "intrinsic:test", + Collections.singletonMap("work", 2L)); + + assertEquals("bex:first", bex.namespace()); + assertEquals( + "bex:first/intrinsic:test", + intrinsic.namespace()); + assertEquals(100L, bex.effectiveBudget()); + assertEquals(100L, intrinsic.effectiveBudget()); + assertThrows( + UnsupportedOperationException.class, + () -> bex.counterWeights().put("other", 1L)); + ProcessorExecutionContextBexGasLedgerHost secondOwner = + new ProcessorExecutionContextBexGasLedgerHost( + session, "bex:first"); + assertThrows( + IllegalStateException.class, + () -> secondOwner.open( + "bex", + Collections.singletonMap( + "expressionEvaluated", 1L))); + + bex.charge("expressionEvaluated", 1L); + intrinsic.charge("work", 1L); + assertEquals(0L, parent.totalGas()); + assertEquals(97L, parent.remainingGas()); + + host.submit(bex); + host.submit(intrinsic); + assertThrows( + IllegalStateException.class, + () -> host.submit(bex)); + assertThrows( + IllegalStateException.class, + () -> bex.charge("expressionEvaluated", 1L)); + + session.complete(); + assertEquals(3L, parent.totalGas()); + assertEquals(2, parent.trace().size()); + assertEquals("bex:first", parent.trace().get(0).namespace()); + assertEquals( + "bex:first/intrinsic:test", + parent.trace().get(1).namespace()); + } + + @Test + void successfulRuntimeSubmitsEverySeparatedLedgerExactlyOnce() { + GasMeter parent = parentMeter(100L); + RuntimeWorkSession session = session(parent); + RecordingSessionHost host = + new RecordingSessionHost(session, "bex:success"); + BexIntrinsicRegistry intrinsics = BexIntrinsicRegistry.builder() + .register( + "intrinsic-type", + "test-registry/1", + "intrinsic:test", + Collections.singletonMap("work", 2L), + invocation -> { + invocation.charge( + "work", 1L, "hosted-intrinsic"); + return BexValues.scalar(true); + }) + .build(); + + BexExecutionResult result = BexEngine.builder() + .intrinsics(intrinsics) + .build() + .compileAndExecute( + BexProgramSource.expression( + frozen(op( + "$intrinsic", + obj( + "type", + obj( + "blueId", + "intrinsic-type"))))), + context(host, -1L, + BexSemanticIdentityBoundary.STANDALONE)); + + assertNotNull(result); + assertEquals(2, host.openLogicalNamespaces.size()); + assertEquals("bex", host.openLogicalNamespaces.get(0)); + assertEquals( + "intrinsic:test", + host.openLogicalNamespaces.get(1)); + assertEquals(2, host.submitCount); + assertEquals(0, host.deterministicFailureCount); + assertEquals(0, host.unavailableCount); + + session.complete(); + assertTrue(parent.totalGas() > 0L); + assertTrue(parent.trace().stream().anyMatch( + entry -> "bex:success/intrinsic:test".equals( + entry.namespace()) + && "work".equals(entry.counter()))); + } + + @Test + void twoCompleteProgramsShareOneParentReservationAndLifecycle() { + long parentBudget = 1_000L; + GasMeter parent = parentMeter(parentBudget); + RuntimeWorkSession session = session(parent); + RecordingSessionHost firstHost = + new RecordingSessionHost( + session, "bex:first-program"); + RecordingSessionHost secondHost = + new RecordingSessionHost( + session, "bex:second-program"); + BexEngine engine = BexEngine.builder().build(); + + BexExecutionResult first = engine.compileAndExecute( + BexProgramSource.expression( + FrozenNode.fromResolvedNode( + new Node().value(1L))), + context( + firstHost, + -1L, + BexSemanticIdentityBoundary.STANDALONE)); + + assertNotNull(first); + assertEquals(1, firstHost.submitCount); + assertEquals(1, firstHost.openedLedgers.size()); + assertEquals( + parentBudget, + firstHost.openedLedgers.get(0) + .effectiveBudget()); + long firstStaged = gasForNamespace( + session.stagedTrace(), + "bex:first-program"); + assertTrue(firstStaged > 0L); + assertEquals( + parentBudget - firstStaged, + parent.remainingGas()); + assertEquals(0L, parent.totalGas()); + + BexExecutionResult second = engine.compileAndExecute( + BexProgramSource.expression( + FrozenNode.fromResolvedNode( + new Node().value(2L))), + context( + secondHost, + -1L, + BexSemanticIdentityBoundary.STANDALONE)); + + assertNotNull(second); + assertEquals(1, secondHost.submitCount); + assertEquals(1, secondHost.openedLedgers.size()); + assertEquals( + parentBudget - firstStaged, + secondHost.openedLedgers.get(0) + .effectiveBudget(), + "the second execution must inherit the live shared " + + "reservation, not an independent parent budget"); + + List staged = session.stagedTrace(); + long secondStaged = gasForNamespace( + staged, "bex:second-program"); + long combinedStaged = firstStaged + secondStaged; + assertTrue(secondStaged > 0L); + assertTrue(staged.stream().allMatch(entry -> + "bex:first-program".equals(entry.namespace()) + || "bex:second-program".equals( + entry.namespace()))); + assertTrue(staged.stream().anyMatch(entry -> + "bex:first-program".equals(entry.namespace()))); + assertTrue(staged.stream().anyMatch(entry -> + "bex:second-program".equals(entry.namespace()))); + assertEquals( + combinedStaged, + parentBudget - parent.remainingGas()); + assertEquals(0L, parent.totalGas()); + assertEquals(0, firstHost.deterministicFailureCount); + assertEquals(0, firstHost.unavailableCount); + assertEquals(0, secondHost.deterministicFailureCount); + assertEquals(0, secondHost.unavailableCount); + + session.complete(); + + assertEquals(combinedStaged, parent.totalGas()); + assertEquals( + parentBudget - combinedStaged, + parent.remainingGas()); + assertEquals( + combinedStaged, + gasForNamespace( + parent.trace(), + "bex:first-program") + + gasForNamespace( + parent.trace(), + "bex:second-program")); + assertThrows( + IllegalStateException.class, + session::complete); + assertEquals( + combinedStaged, + parent.totalGas(), + "the shared session lifecycle must merge exactly once"); + } + + @Test + void deterministicFailureDiscardsBufferedOutputsAndLeavesPrefixForOwner() { + GasMeter parent = parentMeter(100L); + RuntimeWorkSession session = session(parent); + RecordingSessionHost host = + new RecordingSessionHost(session, "bex:failure"); + AtomicInteger semanticAdmissions = + new AtomicInteger(); + BexExecutionContext context = + context( + host, + -1L, + node -> { + semanticAdmissions.incrementAndGet(); + return BexSemanticIdentityBoundary + .STANDALONE + .establishIdentity(node); + }); + Node program = stepDo(list( + op("$appendChange", obj( + "op", "replace", + "path", "/staged", + "val", "change")), + op("$appendEvent", obj( + "kind", "staged-event")), + op("$let", obj( + "name", "failure", + "expr", op( + "$integer", + "not-an-integer"))))); + + BexProgramSource source = + BexProgramSource.inline(frozen(program)); + BexCompiledProgram compiled = + BexEngine.builder().build().compile(source); + BexMetrics metrics = new BexMetrics(); + try (Blue blue = new Blue()) { + BexRuntime runtime = new BexRuntime( + compiled, + context, + blue, + BexGasSchedule.defaults(), + metrics, + new BexPointerCache()); + + assertThrows(BexException.class, runtime::execute); + + assertEquals(0, host.submitCount); + assertEquals(1, host.deterministicFailureCount); + assertEquals( + 2, + semanticAdmissions.get(), + "each transient output is admitted once before " + + "the later deterministic failure"); + assertTrue(runtime.accumulator() + .changeset().entries().isEmpty()); + assertTrue(runtime.accumulator() + .events().events().isEmpty()); + assertTrue(runtime.accumulator() + .events().admittedEvents().isEmpty()); + assertTrue(runtime.accumulator() + .overlay() + .rootValue() + .get("staged") + .isUndefined()); + } + assertFalse(session.stagedTrace().isEmpty()); + assertEquals(0L, parent.totalGas()); + + session.failDeterministically(); + assertTrue(parent.totalGas() > 0L); + } + + @Test + void laterHostFailureMergesSuccessfulBexLedgerOnlyOnce() { + GasMeter parent = parentMeter(100L); + RuntimeWorkSession session = session(parent); + RecordingSessionHost host = + new RecordingSessionHost(session, "bex:later-failure"); + + BexEngine.builder() + .build() + .compileAndExecute( + literalExpression(), + context( + host, + -1L, + BexSemanticIdentityBoundary.STANDALONE)); + + long stagedTotal = 0L; + for (GasTraceEntry entry : session.stagedTrace()) { + stagedTotal += entry.subtotal(); + } + assertTrue(stagedTotal > 0L); + assertEquals(1, host.submitCount); + assertEquals(0L, parent.totalGas()); + + session.failDeterministically(); + assertEquals(stagedTotal, parent.totalGas()); + assertThrows( + IllegalStateException.class, + session::failDeterministically); + assertEquals(stagedTotal, parent.totalGas()); + } + + @Test + void unavailableOutputLeavesLedgerForSessionSuspensionAndDiscard() { + GasMeter parent = parentMeter(100L); + RuntimeWorkSession session = session(parent); + RecordingSessionHost host = + new RecordingSessionHost(session, "bex:unavailable"); + + assertThrows( + ExecutionEvidenceUnavailableException.class, + () -> BexEngine.builder() + .build() + .compileAndExecute( + literalExpression(), + context( + host, + -1L, + node -> { + throw new + ExecutionEvidenceUnavailableException( + "identity evidence unavailable"); + }))); + + assertEquals(0, host.submitCount); + assertEquals(0, host.deterministicFailureCount); + assertEquals(1, host.unavailableCount); + assertFalse(session.stagedTrace().isEmpty()); + + session.suspend(); + assertEquals(0L, parent.totalGas()); + assertEquals(100L, parent.remainingGas()); + } + + @Test + void providerUnavailableUsesHostedSuspensionAndRestoresParentBudget() { + Node exactContent = obj( + "value", "temporarily-offline"); + String exactBlueId = + BlueIdCalculator.calculateBlueId(exactContent); + AtomicInteger providerDemands = + new AtomicInteger(); + NodeProvider provider = new NodeProvider() { + @Override + public List fetchByBlueId( + String requestedBlueId) { + NodeProviderResult result = + fetchResultByBlueId( + requestedBlueId); + return result.outcome() + == NodeProviderOutcome.FOUND + ? result.nodes() + : Collections.emptyList(); + } + + @Override + public NodeProviderResult fetchResultByBlueId( + String requestedBlueId) { + providerDemands.incrementAndGet(); + return NodeProviderResult.unavailable( + "document evidence temporarily unavailable"); + } + }; + GasMeter parent = parentMeter(100L); + RuntimeWorkSession session = session(parent); + RecordingSessionHost host = + new RecordingSessionHost( + session, + "bex:provider-unavailable"); + FrozenNode document = FrozenNode.fromNode( + obj("subject", + new Node().blueId( + exactBlueId))); + BexExecutionContext context = + BexExecutionContext.builder() + .document( + new FrozenBexDocumentView( + document, + document, + "/")) + .gasLedgerHost(host) + .semanticIdentityBoundary( + BexSemanticIdentityBoundary + .STANDALONE) + .build(); + + try (Blue blue = new Blue(provider)) { + ExecutionEvidenceUnavailableException failure = + assertThrows( + ExecutionEvidenceUnavailableException.class, + () -> BexEngine.builder() + .blue(blue) + .build() + .compileAndExecute( + BexProgramSource.expression( + frozen(op( + "$kind", + op( + "$document", + "/subject")))), + context)); + + assertEquals( + "document evidence temporarily unavailable", + failure.getMessage()); + assertEquals( + Collections.singletonList( + exactBlueId), + failure.requiredExactBlueIds()); + assertTrue(providerDemands.get() > 0); + assertEquals(0, host.submitCount); + assertEquals( + 0, + host.deterministicFailureCount); + assertEquals(1, host.unavailableCount); + assertFalse(session.stagedTrace().isEmpty()); + assertTrue(parent.remainingGas() < 100L, + "the live session must hold the uncommitted reservation"); + assertEquals(0L, parent.totalGas()); + + session.suspend(); + + assertEquals(0L, parent.totalGas()); + assertEquals(100L, + parent.remainingGas()); + assertFalse(session.isOpen()); + } + } + + @Test + void hostRejectionPropagatesTheExactRecordedException() { + GasMeter parent = parentMeter(100L); + RuntimeWorkSession session = session(parent); + RecordingSessionHost host = + new RecordingSessionHost(session, "bex:exhaustion"); + AtomicInteger identityCalls = new AtomicInteger(); + + try (Blue blue = new Blue()) { + BexEngine engine = BexEngine.builder() + .blue(blue) + .build(); + BexCompiledProgram compiled = + engine.compile(literalExpression()); + BexRuntime runtime = new BexRuntime( + compiled, + context( + host, + -1L, + node -> { + identityCalls.incrementAndGet(); + return BexSemanticIdentityBoundary.STANDALONE + .establishIdentity(node); + }), + blue, + BexGasSchedule.defaults(), + new BexMetrics(), + new BexPointerCache(), + BexIntrinsicRegistry.empty()); + + GasMeter.ChildGasLedger competing = + session.openLedger( + "competing", + Collections.singletonMap("work", 1L)); + competing.charge("work", 100L); + + GasLimitExceededException exhausted = assertThrows( + GasLimitExceededException.class, + runtime::execute); + + assertSame(host.propagatedExhaustion, exhausted); + assertEquals("bex:exhaustion", exhausted.namespace()); + assertEquals( + BexGasCounter.FUNCTION_CALLED.canonicalName(), + exhausted.counter()); + assertEquals(1L, exhausted.quantity()); + assertEquals(2L, exhausted.weight()); + assertEquals(0L, exhausted.admittedGas()); + assertEquals(100L, exhausted.effectiveBudget()); + assertEquals(0, identityCalls.get()); + assertEquals(0, host.submitCount); + assertEquals(1, host.deterministicFailureCount); + assertFalse(session.isOpen()); + assertEquals(100L, parent.totalGas()); + assertEquals(1, parent.trace().size()); + assertEquals( + "competing", + parent.trace().get(0).namespace()); + } + } + + @Test + void hostedGasExhaustionRetainsExactBexPrefixAndNoOutput() { + BexEngine engine = BexEngine.builder().build(); + Node program = stepDo(list( + op("$forEach", obj( + "in", integerList(4), + "item", "item", + "index", "index", + "do", list())), + op("$return", true))); + BexCompiledProgram compiled = + engine.compile(BexProgramSource.inline( + frozen(program))); + FrozenNode document = FrozenNode.fromResolvedNode( + new Node().properties( + Collections.emptyMap())); + BexExecutionResult baseline = engine.execute( + compiled, + BexExecutionContext.builder() + .document( + new FrozenBexDocumentView( + document)) + .semanticIdentityBoundary( + BexSemanticIdentityBoundary + .STANDALONE) + .gasLimit(1_000_000L) + .build()); + int rejectedIndex = 3; + assertTrue( + baseline.gasTrace().size() + > rejectedIndex); + long prefixGas = 0L; + for (int index = 0; + index < rejectedIndex; + index++) { + prefixGas += baseline.gasTrace() + .get(index).gas(); + } + blue.bex.gas.BexGasCharge rejected = + baseline.gasTrace().get(rejectedIndex); + + GasMeter parent = parentMeter(prefixGas); + RuntimeWorkSession session = session(parent); + RecordingSessionHost host = + new RecordingSessionHost( + session, "bex:prefix-exhaustion"); + + GasLimitExceededException failure = + assertThrows( + GasLimitExceededException.class, + () -> engine.execute( + compiled, + context( + host, + -1L, + BexSemanticIdentityBoundary + .STANDALONE))); + + assertSame(host.propagatedExhaustion, failure); + assertEquals( + "bex:prefix-exhaustion", + failure.namespace()); + assertEquals( + rejected.counterName(), + failure.counter()); + assertEquals( + rejected.quantity(), + failure.quantity()); + assertEquals( + rejected.weight(), + failure.weight()); + assertEquals(prefixGas, + failure.admittedGas()); + assertEquals(prefixGas, + failure.effectiveBudget()); + assertEquals(0, host.submitCount); + assertEquals(1, + host.deterministicFailureCount); + assertFalse(session.isOpen()); + assertEquals(prefixGas, + parent.totalGas()); + assertEquals(rejectedIndex, + parent.trace().size()); + for (int index = 0; + index < rejectedIndex; + index++) { + blue.bex.gas.BexGasCharge expected = + baseline.gasTrace().get(index); + GasTraceEntry actual = + parent.trace().get(index); + assertEquals( + expected.counterName(), + actual.counter()); + assertEquals( + expected.quantity(), + actual.quantity()); + assertEquals( + expected.weight(), + actual.weight()); + assertEquals( + expected.gas(), + actual.subtotal()); + } + assertFalse(parent.trace().stream() + .anyMatch(entry -> + rejected.counterName().equals( + entry.counter()) + && entry.sequence() + >= rejectedIndex)); + } + + @Test + void localLimitMapsToProcessorGasCategoryWithoutInventingHostRejection() { + GasMeter parent = parentMeter(100L); + RuntimeWorkSession session = session(parent); + RecordingSessionHost host = + new RecordingSessionHost(session, "bex:local-limit"); + AtomicInteger identityCalls = new AtomicInteger(); + + ProcessorFailureException failure = assertThrows( + ProcessorFailureException.class, + () -> BexEngine.builder() + .build() + .compileAndExecute( + literalExpression(), + context( + host, + 2L, + node -> { + identityCalls.incrementAndGet(); + return BexSemanticIdentityBoundary + .STANDALONE + .establishIdentity(node); + }))); + + assertEquals( + ProcessorErrorCategory.GasLimitExceeded, + failure.errorCategory()); + assertTrue( + failure.getCause() + instanceof BexGasLimitExceededException); + BexGasLimitExceededException local = + (BexGasLimitExceededException) failure.getCause(); + assertNull(local.hostGasLimitExceeded()); + assertEquals( + BexGasCounter.EXPRESSION_EVALUATED, + local.counter()); + assertEquals(2L, local.admittedGas()); + assertEquals(2L, local.effectiveBudget()); + assertEquals(0, identityCalls.get()); + assertEquals(0, host.submitCount); + assertEquals(1, host.deterministicFailureCount); + assertEquals(1, session.stagedTrace().size()); + assertTrue(session.isOpen()); + + session.failDeterministically(); + assertEquals(2L, parent.totalGas()); + } + + @Test + void runtimeNamespaceSeparatorIsReservedAndIntrinsicFlatteningFailsClosed() { + RuntimeWorkSession session = session(parentMeter(100L)); + assertThrows( + IllegalArgumentException.class, + () -> new ProcessorExecutionContextBexGasLedgerHost( + session, "bex/run")); + + NonSeparatingHost host = new NonSeparatingHost(); + BexIntrinsicRegistry intrinsics = BexIntrinsicRegistry.builder() + .register( + "intrinsic-type", + "test-registry/1", + "intrinsic:test", + Collections.singletonMap("work", 1L), + invocation -> BexValues.scalar(true)) + .build(); + assertThrows( + IllegalArgumentException.class, + () -> BexEngine.builder() + .intrinsics(intrinsics) + .build() + .compileAndExecute( + BexProgramSource.expression( + frozen(op( + "$intrinsic", + obj( + "type", + obj( + "blueId", + "intrinsic-type"))))), + context( + host, + -1L, + BexSemanticIdentityBoundary + .STANDALONE))); + assertEquals(0, host.openCount); + } + + @Test + void laterNamespaceOpenFailureFinalizesEveryLedgerAlreadyOpened() { + FailingSecondOpenHost host = + new FailingSecondOpenHost(); + BexIntrinsicRegistry intrinsics = BexIntrinsicRegistry.builder() + .register( + "intrinsic-type", + "test-registry/1", + "intrinsic:test", + Collections.singletonMap("work", 1L), + invocation -> BexValues.scalar(true)) + .build(); + + IllegalStateException failure = assertThrows( + IllegalStateException.class, + () -> BexEngine.builder() + .intrinsics(intrinsics) + .build() + .compileAndExecute( + BexProgramSource.expression( + frozen(op( + "$intrinsic", + obj( + "type", + obj( + "blueId", + "intrinsic-type"))))), + context( + host, + -1L, + BexSemanticIdentityBoundary + .STANDALONE))); + + assertEquals("second open rejected", failure.getMessage()); + assertEquals(2, host.openCount); + assertEquals(1, host.deterministicFinalizations); + assertEquals(0, host.unavailableFinalizations); + assertEquals(0, host.successfulSubmissions); + } + + @Test + void constructionProcessorFailureWinsOverNestedUnavailability() { + ExecutionEvidenceUnavailableException nested = + new ExecutionEvidenceUnavailableException( + "nested open evidence detail"); + ProcessorFailureException expected = + new ProcessorFailureException( + ProcessorErrorCategory + .RuntimeExecutionFailure, + "second namespace rejected deterministically", + nested); + FailingSecondOpenHost host = + new FailingSecondOpenHost(expected); + BexIntrinsicRegistry intrinsics = BexIntrinsicRegistry.builder() + .register( + "intrinsic-type", + "test-registry/1", + "intrinsic:test", + Collections.singletonMap("work", 1L), + invocation -> BexValues.scalar(true)) + .build(); + + ProcessorFailureException observed = assertThrows( + ProcessorFailureException.class, + () -> BexEngine.builder() + .intrinsics(intrinsics) + .build() + .compileAndExecute( + BexProgramSource.expression( + frozen(op( + "$intrinsic", + obj( + "type", + obj( + "blueId", + "intrinsic-type"))))), + context( + host, + -1L, + BexSemanticIdentityBoundary + .STANDALONE))); + + assertSame(expected, observed); + assertEquals(2, host.openCount); + assertEquals(1, host.deterministicFinalizations); + assertEquals(0, host.unavailableFinalizations); + assertEquals(0, host.successfulSubmissions); + } + + @Test + void processorExecutionContextUsesItsInvocationSemanticOutputBoundary() { + try (Blue blue = new Blue()) { + ProcessorEngine.Execution execution = + new ProcessorEngine.Execution( + blue.getDocumentProcessor(), + new Node().properties( + Collections.emptyMap())); + execution.preflightScope("/"); + long identitiesBefore = gasQuantity( + execution.runtime().gasMeter(), + GasScheduleConstants.Namespace.SEMANTIC, + GasScheduleConstants.SemanticCounter + .NODE_IDENTITY_ESTABLISHED); + + BexExecutionResult result; + try (ProcessorExecutionContext processorContext = + execution.createContext( + "/", + execution.bundleForScope("/"), + new Node(), + false)) { + BexExecutionContext context = + BexExecutionContext.builder() + .processorExecutionContext( + processorContext, + "bex:semantic-adapter") + .build(); + result = BexEngine.builder() + .build() + .compileAndExecute( + BexProgramSource.expression( + FrozenNode.fromResolvedNode( + new Node().value( + "hosted-output"))), + context); + + assertTrue(result.output().reconstructed()); + assertEquals( + result.output().nodeBlueId(), + BlueIdCalculator.calculateBlueId( + result.output().node())); + processorContext.applyBufferedEffects(); + } + + long identitiesAfter = gasQuantity( + execution.runtime().gasMeter(), + GasScheduleConstants.Namespace.SEMANTIC, + GasScheduleConstants.SemanticCounter + .NODE_IDENTITY_ESTABLISHED); + assertEquals( + 1L, + identitiesAfter - identitiesBefore, + "the concrete adapter must cross the invocation-owned " + + "semantic boundary exactly once"); + assertTrue(execution.runtime().gasMeter().trace().stream() + .anyMatch(entry -> + "bex:semantic-adapter".equals( + entry.namespace()))); + } + } + + @Test + void hostedOpaqueCyclicMemberSupportsIdentityAndOutputWithoutProofDemand() { + String memberBlueId = + BlueIdCalculator.calculateBlueId( + new Node().value("hosted-cyclic-set")) + + "#0"; + FrozenNode document = FrozenNode.fromResolvedNode( + new Node().properties( + "member", + new Node().blueId(memberBlueId))); + GasMeter parent = parentMeter(1_000L); + RuntimeWorkSession session = session(parent); + RecordingSessionHost host = + new RecordingSessionHost( + session, "bex:hosted-cyclic"); + AtomicInteger boundaryCalls = new AtomicInteger(); + BexExecutionContext context = + BexExecutionContext.builder() + .document(new FrozenBexDocumentView(document)) + .gasLedgerHost(host) + .semanticIdentityBoundary(node -> { + boundaryCalls.incrementAndGet(); + throw new AssertionError( + "opaque exact cyclic output must not " + + "re-enter semantic admission"); + }) + .build(); + Node program = stepDo(list( + op("$let", obj( + "name", "member", + "expr", op("$document", "/member"))), + op("$appendEvent", op("$var", "member")), + op("$let", obj( + "name", "identity", + "expr", op( + "$nodeBlueId", + op("$var", "member")))), + op("$return", op("$var", "member")))); + + BexExecutionResult result = BexEngine.builder() + .build() + .compileAndExecute( + BexProgramSource.inline(frozen(program)), + context); + + assertEquals(0, boundaryCalls.get()); + assertEquals(memberBlueId, + result.output().nodeBlueId()); + assertFalse(result.output().reconstructed()); + assertEquals(memberBlueId, + result.events().admittedEvents().get(0) + .nodeBlueId()); + assertEquals(1, host.submitCount); + assertEquals(0, host.deterministicFailureCount); + assertEquals(0, host.unavailableCount); + + session.complete(); + assertTrue(parent.trace().stream().anyMatch( + entry -> "bex:hosted-cyclic".equals( + entry.namespace()) + && BexGasCounter.NODE_IDENTITY_REQUESTED + .canonicalName().equals( + entry.counter()))); + } + + @Test + void intrinsicUnavailableUsesHostedDiscardLifecycle() { + ExecutionEvidenceUnavailableException expected = + new ExecutionEvidenceUnavailableException( + "intrinsic evidence temporarily unavailable"); + IntrinsicFailureScenario scenario = + intrinsicFailureScenario(expected); + + assertSame(expected, scenario.observed); + assertEquals(0, scenario.host.submitCount); + assertEquals(0, + scenario.host.deterministicFailureCount); + assertEquals(2, scenario.host.unavailableCount); + assertFalse(scenario.session.stagedTrace().isEmpty()); + + scenario.session.suspend(); + assertEquals(0L, scenario.parent.totalGas()); + assertEquals(1_000L, + scenario.parent.remainingGas()); + } + + @Test + void processorFailureWinsOverNestedUnavailableExecutionCause() { + ExecutionEvidenceUnavailableException nested = + new ExecutionEvidenceUnavailableException( + "nested intrinsic evidence detail"); + ProcessorFailureException expected = + new ProcessorFailureException( + ProcessorErrorCategory + .RuntimeExecutionFailure, + "intrinsic rejected deterministically", + nested); + IntrinsicFailureScenario scenario = + intrinsicFailureScenario(expected); + + assertSame(expected, scenario.observed); + assertEquals(0, scenario.host.submitCount); + assertEquals(2, + scenario.host.deterministicFailureCount); + assertEquals(0, scenario.host.unavailableCount); + assertFalse(scenario.session.stagedTrace().isEmpty()); + + scenario.session.failDeterministically(); + assertTrue(scenario.parent.totalGas() > 0L); + } + + @Test + void directInvalidEvidenceWinsOverItsNestedUnavailableCause() { + ExecutionEvidenceUnavailableException nested = + new ExecutionEvidenceUnavailableException( + "nested invalid-evidence detail"); + InvalidExecutionEvidenceException expected = + new InvalidExecutionEvidenceException( + "direct invalid evidence"); + expected.initCause(nested); + IntrinsicFailureScenario scenario = + intrinsicFailureScenario(expected); + + assertSame(expected, scenario.observed); + assertEquals(0, scenario.host.submitCount); + assertEquals(2, + scenario.host.deterministicFailureCount); + assertEquals(0, scenario.host.unavailableCount); + assertFalse(scenario.session.stagedTrace().isEmpty()); + + scenario.session.failDeterministically(); + assertTrue(scenario.parent.totalGas() > 0L); + } + + @Test + void intrinsicInvalidEvidenceUsesHostedDeterministicLifecycle() { + InvalidExecutionEvidenceException expected = + new InvalidExecutionEvidenceException( + "intrinsic returned invalid evidence"); + IntrinsicFailureScenario scenario = + intrinsicFailureScenario(expected); + + assertSame(expected, scenario.observed); + assertEquals(0, scenario.host.submitCount); + assertEquals(2, + scenario.host.deterministicFailureCount); + assertEquals(0, scenario.host.unavailableCount); + assertFalse(scenario.session.stagedTrace().isEmpty()); + + scenario.session.failDeterministically(); + assertTrue(scenario.parent.totalGas() > 0L); + } + + @Test + void arbitraryIntrinsicFailureIsNotReclassifiedAsUnavailable() { + IllegalStateException expected = + new IllegalStateException( + "intrinsic implementation defect"); + IntrinsicFailureScenario scenario = + intrinsicFailureScenario(expected); + + assertSame(expected, scenario.observed); + assertEquals(0, scenario.host.submitCount); + assertEquals(2, + scenario.host.deterministicFailureCount); + assertEquals(0, scenario.host.unavailableCount); + assertFalse(scenario.session.stagedTrace().isEmpty()); + + scenario.session.failDeterministically(); + assertTrue(scenario.parent.totalGas() > 0L); + } + + private static IntrinsicFailureScenario intrinsicFailureScenario( + RuntimeException expected) { + GasMeter parent = parentMeter(1_000L); + RuntimeWorkSession session = session(parent); + RecordingSessionHost host = + new RecordingSessionHost( + session, "bex:intrinsic-failure"); + BexEngine engine = BexEngine.builder() + .intrinsic( + FAILURE_INTRINSIC, + FAILURE_REGISTRY, + Collections.singletonMap("work", 1L), + invocation -> { + throw expected; + }) + .build(); + BexProgramSource source = + BexProgramSource.expression( + frozen(op( + "$intrinsic", + obj( + "type", + obj( + "blueId", + FAILURE_INTRINSIC))))); + + RuntimeException observed = assertThrows( + RuntimeException.class, + () -> engine.compileAndExecute( + source, + context( + host, + -1L, + BexSemanticIdentityBoundary + .STANDALONE))); + return new IntrinsicFailureScenario( + parent, session, host, observed); + } + + private static long gasQuantity( + GasMeter meter, + String namespace, + String counter) { + long quantity = 0L; + for (GasTraceEntry entry : meter.trace()) { + if (namespace.equals(entry.namespace()) + && counter.equals(entry.counter())) { + quantity += entry.quantity(); + } + } + return quantity; + } + + private static long gasForNamespace( + List trace, + String namespace) { + long subtotal = 0L; + for (GasTraceEntry entry : trace) { + if (namespace.equals(entry.namespace())) { + subtotal += entry.subtotal(); + } + } + return subtotal; + } + + private static RuntimeWorkSession session(GasMeter parent) { + return new RuntimeWorkSession( + parent, RuntimeWorkSession.Mode.PROCESSING); + } + + private static GasMeter parentMeter(long budget) { + return new GasMeter(GasSchedule.contracts10(), budget); + } + + private static BexProgramSource literalExpression() { + return BexProgramSource.expression( + FrozenNode.fromResolvedNode( + new Node().value(1L))); + } + + private static Node integerList(int size) { + List items = new ArrayList(); + for (int index = 0; index < size; index++) { + items.add(new Node().value((long) index)); + } + return new Node().items(items); + } + + private static T cause( + Throwable failure, + Class type) { + Throwable current = failure; + while (current != null) { + if (type.isInstance(current)) { + return type.cast(current); + } + current = current.getCause(); + } + return null; + } + + private static void writeLongTraceEvidence( + boolean passed, + int requestedItems, + int requiredTraceEntries, + int observedTraceEntries, + int observedCounterKinds, + boolean orderPreserved, + PortableLimitExceededException portable, + RuntimeException failure) throws Exception { + Path output = Paths.get( + System.getProperty("user.dir"), + "build", + "reports", + "bex-release", + "host-long-trace.properties"); + Files.createDirectories(output.getParent()); + List lines = new ArrayList(); + lines.add( + "schema=blue-bex-host-long-trace-evidence/1.0"); + lines.add("status=" + (passed ? "passed" : "blocking")); + lines.add("requestedItems=" + requestedItems); + lines.add( + "requiredMinimumOrderedTraceEntries=" + + requiredTraceEntries); + lines.add( + "maximumObservedOrderedTraceEntries=" + + observedTraceEntries); + lines.add( + "observedDistinctCounterKinds=" + + observedCounterKinds); + lines.add("orderPreserved=" + orderPreserved); + lines.add( + "failure.class=" + + (failure == null + ? "none" + : failure.getClass().getName())); + lines.add( + "portableLimit.name=" + + (portable == null + ? "none" + : portable.limitName())); + lines.add( + "portableLimit.observed=" + + (portable == null + ? "none" + : String.valueOf(portable.observed()))); + lines.add( + "portableLimit.limit=" + + (portable == null + ? "none" + : String.valueOf(portable.limit()))); + Files.write( + output, + (String.join("\n", lines) + "\n") + .getBytes(StandardCharsets.UTF_8)); + } + + private static BexExecutionContext context( + BexGasLedgerHost host, + long localLimit, + BexSemanticIdentityBoundary identityBoundary) { + FrozenNode document = FrozenNode.fromResolvedNode( + new Node().properties( + Collections.emptyMap())); + BexExecutionContext.Builder builder = + BexExecutionContext.builder() + .document(new FrozenBexDocumentView(document)) + .gasLedgerHost(host) + .semanticIdentityBoundary(identityBoundary); + if (localLimit >= 0L) { + builder.gasLimit(localLimit); + } + return builder.build(); + } + + private static final class RecordingSessionHost + implements BexGasLedgerHost { + private final ProcessorExecutionContextBexGasLedgerHost delegate; + private final List openLogicalNamespaces = + new ArrayList<>(); + private final List openedLedgers = + new ArrayList<>(); + private int submitCount; + private int deterministicFailureCount; + private int unavailableCount; + private GasLimitExceededException propagatedExhaustion; + + private RecordingSessionHost( + RuntimeWorkSession session, + String runtimeNamespace) { + this.delegate = + new ProcessorExecutionContextBexGasLedgerHost( + session, runtimeNamespace); + } + + @Override + public GasMeter.ChildGasLedger open( + String namespace, + Map counterWeights) { + openLogicalNamespaces.add(namespace); + GasMeter.ChildGasLedger ledger = + delegate.open(namespace, counterWeights); + openedLedgers.add(ledger); + return ledger; + } + + @Override + public void submit(GasMeter.ChildGasLedger ledger) { + submitCount++; + delegate.submit(ledger); + } + + @Override + public boolean separatesRuntimeNamespaces() { + return delegate.separatesRuntimeNamespaces(); + } + + @Override + public void failedDeterministically( + GasMeter.ChildGasLedger ledger) { + deterministicFailureCount++; + delegate.failedDeterministically(ledger); + } + + @Override + public void evidenceUnavailable( + GasMeter.ChildGasLedger ledger) { + unavailableCount++; + delegate.evidenceUnavailable(ledger); + } + + @Override + public RuntimeException localGasLimitExceeded( + BexGasLimitExceededException exhaustion, + RuntimeException originalFailure) { + return delegate.localGasLimitExceeded( + exhaustion, originalFailure); + } + + @Override + public void propagateGasExhaustion( + GasMeter.ChildGasLedger ledger, + GasLimitExceededException exhaustion) { + propagatedExhaustion = exhaustion; + delegate.propagateGasExhaustion( + ledger, exhaustion); + } + } + + private static final class IntrinsicFailureScenario { + private final GasMeter parent; + private final RuntimeWorkSession session; + private final RecordingSessionHost host; + private final RuntimeException observed; + + private IntrinsicFailureScenario( + GasMeter parent, + RuntimeWorkSession session, + RecordingSessionHost host, + RuntimeException observed) { + this.parent = parent; + this.session = session; + this.host = host; + this.observed = observed; + } + } + + private static final class NonSeparatingHost + implements BexGasLedgerHost { + private int openCount; + + @Override + public GasMeter.ChildGasLedger open( + String namespace, + Map counterWeights) { + openCount++; + throw new AssertionError( + "flattening host must be rejected before open"); + } + + @Override + public boolean separatesRuntimeNamespaces() { + return false; + } + + @Override + public void submit(GasMeter.ChildGasLedger ledger) { + } + + @Override + public void failedDeterministically( + GasMeter.ChildGasLedger ledger) { + } + + @Override + public void evidenceUnavailable( + GasMeter.ChildGasLedger ledger) { + } + } + + private static final class FailingSecondOpenHost + implements BexGasLedgerHost { + private final GasMeter parent = parentMeter(100L); + private final RuntimeException secondOpenFailure; + private int openCount; + private int deterministicFinalizations; + private int unavailableFinalizations; + private int successfulSubmissions; + + private FailingSecondOpenHost() { + this(new IllegalStateException( + "second open rejected")); + } + + private FailingSecondOpenHost( + RuntimeException secondOpenFailure) { + this.secondOpenFailure = + secondOpenFailure; + } + + @Override + public GasMeter.ChildGasLedger open( + String namespace, + Map counterWeights) { + openCount++; + if (openCount == 2) { + throw secondOpenFailure; + } + return parent.childLedger( + namespace, counterWeights); + } + + @Override + public void submit(GasMeter.ChildGasLedger ledger) { + successfulSubmissions++; + } + + @Override + public void failedDeterministically( + GasMeter.ChildGasLedger ledger) { + deterministicFinalizations++; + } + + @Override + public void evidenceUnavailable( + GasMeter.ChildGasLedger ledger) { + unavailableFinalizations++; + } + } +} diff --git a/src/test/resources/conformance/bex/fixtures/HARNESS.md b/src/test/resources/conformance/bex/fixtures/HARNESS.md new file mode 100644 index 0000000..4d0fae7 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/HARNESS.md @@ -0,0 +1,133 @@ +# Blue BEX 2.0 fixture harness + +## 1. Purpose + +The harness executes `blue-bex-fixture/2.0`. Fixtures are executable BEX programs, not descriptions of hypothetical cases. Unknown fields, operators, variants, cases, assertions, or output projections fail closed. + +The runner MUST use: + +```text +fixture-schema.yaml +operator-coverage.yaml +projection-catalog.yaml +``` + +## 2. Compilation and program selection + +Compilation follows the exact BEX 2.0 rules. `entry`, `expr`, and `do` are selected in the specified precedence order. An unselected `do` block cannot declare a local used by a selected root `expr`. Compile-time failures occur before runtime counters or effects. + +`program`, `constants`, `functions`, function arguments, static patterns, and static intrinsic types are interpreted exactly as specified. Fixture-only metadata never creates a BEX variable or capability. + +## 3. Context + +The context supplies exact values for: + +```text +$document +$event +$processingEvent +$currentContract +$steps +$binding +``` + +`documentScope` is the current Contracts scope. Provider entries map exact BlueIds to verified Blue values. Context values may be inline or pure references, but portable BEX cannot observe representation, cache state, or provider segmentation. + +## 4. Exact and transient values + +Existing exact Blue values retain their Node BlueId across reads, variables, constants, functions, patch/event appenders, and output. + +Transient objects, lists, Text, Integer, and decimal values are BEX values until they cross a Blue output or `$nodeBlueId` boundary. At that boundary: + +- BEX Integer becomes Blue Integer; +- every BEX decimal, including `1.0`, becomes Blue Double using binary64 round-to-nearest, ties-to-even; +- a decimal is never changed to Blue Integer merely because its mathematical value is integral; +- invalid Blue output shapes fail deterministically. + +## 5. Variants + +A variant is an object with an explicit transformation. Bare labels are invalid. + +- `rootForm: inline` materializes the referenced target directly in `rootDocument`. +- `rootForm: reference` retains the pure reference and provider entry. +- `rootForm: eager` verifies and materializes the target before execution. +- `rootForm: lazy` retains the reference until the first semantic demand. +- `rootForm: materialized` is the already verified materialized form used by kind tests. +- `cache` and `batching` alter physical provider preparation only. +- `rawRootDocumentJson` replaces `rootDocument` by parsing exactly that JSON source, preserving numeric token class before Blue inference. +- `deliveryKind` changes only the current internal-delivery classification. The supplied `$event` and `$processingEvent` remain exact and must retain their documented bindings. + +Each variant runs from an independent copy of the base context. `sameAcrossVariants` compares both the semantic result and every requested canonical trace projection. + +## 6. Cases + +`expected.cases` is an explicit list of complete subcases. Each case replaces `program`, optionally replaces `context`, and asserts the named `errorClass` and optional reason. A case name alone has no semantics. + +## 7. Output admission + +Values crossing a Blue boundary are validated under Blue Language 1.0 before identity calculation. The runner MUST reject: + +- root or list `undefined`; +- mixed `blueId` forms; +- mixed scalar/list/object payloads; +- reserved-invalid fields; +- invalid schema vocabulary; +- computed `blue` directives; +- unconsumed list controls; +- invalid numeric conversion. + +The BlueId helper used for package generation MUST validate these rules before hashing. Hashability alone is not validity. + +## 8. Gas and child ledger + +`parentRemainingGas` is the exact budget offered by the Contracts host. `gasLimit` is an optional BEX-local sub-limit. The effective runtime budget is the smaller of the two when both are present. + +The runner maintains a live child budget bounded by parent remaining gas and the optional local sub-limit. Every counter increment is admitted before work. The failing charge is absent. Short-circuited expressions and unexecuted statements charge nothing. + +The child ledger contains named counters and quantities only. It is merged into the Contracts host exactly once. Opaque runtime gas integers, recursive value-size counters, UTF-16 length counters, cache discounts, and representation-dependent charges are nonconforming. + +## 9. Operator coverage + +`operator-coverage.yaml` is generated from the normative operator tables and fixture programs. Every required operator MUST have at least one direct executable behavior fixture. Grouped prose vectors do not substitute for direct operator occurrence. + +Compiler-error, lazy-evaluation, output-boundary, and representation variants supplement direct success coverage where relevant. + +## 10. Assertions and projections + +`projection-catalog.yaml` is the closed list of legal assertion paths. Supported operators are: + +```text +equals +notEquals +absent +present +contains +notContains +lessThan +greaterThan +sameAcrossVariants +all +none +``` + +A missing projection is a runner failure unless `absent` is asserted. + +## 11. Gas microfixtures + +A microfixture supplies `context.directCounterFixture`. The runner emits exactly one named trace entry with the bound namespace, counter, quantity, weight, subtotal, and total. It cannot replace that trace with an opaque total. + +## 12. Package integrity + +`manifest.yaml` is the authoritative inventory for this fixture package. It lists every behavior fixture, direct operator fixture, gas microfixture, and support file with its relative path, role, LF-normalized byte length, and SHA-256 digest. It binds the exact BEX runtime-registry package identity, gas-manifest package identity and file digest, vector-coverage map, and direct operator-coverage map. + +The fixture-package identity is calculated as: + +```text +sha256( + UTF-8 canonical JSON of manifest.yaml + with packageIdentity set to null + and object keys sorted lexicographically +) +``` + +A fixture, support file, gas schedule, registry dependency, vector map, or operator map change requires a new fixture-package identity. Disagreement among prose, registry, gas manifest, fixtures, or package identities is a release failure. The registry manifest's reverse fixture binding is excluded from the registry package identity to avoid an identity cycle. diff --git a/src/test/resources/conformance/bex/fixtures/README.md b/src/test/resources/conformance/bex/fixtures/README.md new file mode 100644 index 0000000..6e49dbd --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/README.md @@ -0,0 +1,5 @@ +# Blue BEX 2.0 conformance fixtures + +This directory is the machine-readable conformance package for Blue BEX 2.0. It contains one behavioral fixture for every prose vector and one exact gas microfixture for every named BEX counter. The fixture manifest is bound to `../gas-manifest.yaml`; the prose table, machine-readable schedule, and all microfixture weights must agree exactly. + +Read `HARNESS.md` before implementing a runner. Unknown operators, context bindings, assertion projections, fixture fields, or intrinsic types are errors and MUST NOT be skipped. diff --git a/src/test/resources/conformance/bex/fixtures/c/bex-c-01.yaml b/src/test/resources/conformance/bex/fixtures/c/bex-c-01.yaml new file mode 100644 index 0000000..14bf5b1 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/c/bex-c-01.yaml @@ -0,0 +1,23 @@ +schema: blue-bex-fixture/2.0 +id: bex-c-01 +vectors: +- BEX-C-01 +category: c +description: Exactly one `$` key denotes an expression operator; multiple keys denote an object expression. +program: + expr: + $x: 1 + data: 2 +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + assertions: [] + result: + $x: 1 + data: 2 diff --git a/src/test/resources/conformance/bex/fixtures/c/bex-c-02.yaml b/src/test/resources/conformance/bex/fixtures/c/bex-c-02.yaml new file mode 100644 index 0000000..83e7137 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/c/bex-c-02.yaml @@ -0,0 +1,20 @@ +schema: blue-bex-fixture/2.0 +id: bex-c-02 +vectors: +- BEX-C-02 +category: c +description: Unknown expression and statement operators fail compilation. +program: + expr: + $unknown: 1 +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + assertions: [] + errorClass: compile-error diff --git a/src/test/resources/conformance/bex/fixtures/c/bex-c-03.yaml b/src/test/resources/conformance/bex/fixtures/c/bex-c-03.yaml new file mode 100644 index 0000000..dd668dd --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/c/bex-c-03.yaml @@ -0,0 +1,29 @@ +schema: blue-bex-fixture/2.0 +id: bex-c-03 +vectors: +- BEX-C-03 +category: c +description: '`$literal` preserves nested unknown operators but cannot place expressions in static Blue fields.' +program: + expr: + $literal: + $unknown: 1 +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + assertions: [] + result: + $unknown: 1 + additionalCase: + program: + expr: + $literal: + type: + $const: T + errorClass: compile-error diff --git a/src/test/resources/conformance/bex/fixtures/c/bex-c-04.yaml b/src/test/resources/conformance/bex/fixtures/c/bex-c-04.yaml new file mode 100644 index 0000000..9cd89fc --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/c/bex-c-04.yaml @@ -0,0 +1,28 @@ +schema: blue-bex-fixture/2.0 +id: bex-c-04 +vectors: +- BEX-C-04 +category: c +description: Unknown constants/functions, missing/extra arguments, recursive calls, and entry functions with arguments fail. +program: + entry: f + functions: + f: + args: + x: {} + expr: + $call: + function: f + args: + x: 1 +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + assertions: [] + errorClass: compile-error diff --git a/src/test/resources/conformance/bex/fixtures/c/bex-c-05.yaml b/src/test/resources/conformance/bex/fixtures/c/bex-c-05.yaml new file mode 100644 index 0000000..8432d57 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/c/bex-c-05.yaml @@ -0,0 +1,24 @@ +schema: blue-bex-fixture/2.0 +id: bex-c-05 +vectors: +- BEX-C-05 +category: c +description: Reserved names in plain name containers fail. +program: + functions: + f: + args: + type: {} + expr: 1 + entry: f +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + assertions: [] + errorClass: compile-error diff --git a/src/test/resources/conformance/bex/fixtures/c/bex-c-06.yaml b/src/test/resources/conformance/bex/fixtures/c/bex-c-06.yaml new file mode 100644 index 0000000..c4ed3e3 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/c/bex-c-06.yaml @@ -0,0 +1,23 @@ +schema: blue-bex-fixture/2.0 +id: bex-c-06 +vectors: +- BEX-C-06 +category: c +description: Dynamic `$is.pattern` and `$intrinsic.type` fail. +program: + expr: + $is: + node: 1 + pattern: + $const: P +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + assertions: [] + errorClass: compile-error diff --git a/src/test/resources/conformance/bex/fixtures/c/bex-c-07.yaml b/src/test/resources/conformance/bex/fixtures/c/bex-c-07.yaml new file mode 100644 index 0000000..b4a547b --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/c/bex-c-07.yaml @@ -0,0 +1,22 @@ +schema: blue-bex-fixture/2.0 +id: bex-c-07 +vectors: +- BEX-C-07 +category: c +description: '`$set` of an undeclared local and invalid `$let.order` fail.' +program: + do: + - $set: + name: missing + expr: 1 +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + assertions: [] + errorClass: compile-error diff --git a/src/test/resources/conformance/bex/fixtures/c/bex-c-08.yaml b/src/test/resources/conformance/bex/fixtures/c/bex-c-08.yaml new file mode 100644 index 0000000..f1c740b --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/c/bex-c-08.yaml @@ -0,0 +1,27 @@ +schema: blue-bex-fixture/2.0 +id: bex-c-08 +vectors: +- BEX-C-08 +category: c +description: Compiler diagnostics include class, source path, and operator when known. +program: + expr: + $unknown: 1 +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + assertions: + - actual: diagnostic.errorClass + op: equals + expected: compile-error + - actual: diagnostic.sourcePath + op: present + - actual: diagnostic.operator + op: equals + expected: $unknown diff --git a/src/test/resources/conformance/bex/fixtures/c/bex-c-09.yaml b/src/test/resources/conformance/bex/fixtures/c/bex-c-09.yaml new file mode 100644 index 0000000..b835c24 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/c/bex-c-09.yaml @@ -0,0 +1,37 @@ +schema: blue-bex-fixture/2.0 +id: bex-c-09 +vectors: +- BEX-C-09 +category: c +description: The compiler rejects mutual recursion from the static function-call graph before runtime execution. +program: + functions: + f: + args: [] + expr: + $call: + function: g + args: [] + g: + args: [] + expr: + $call: + function: f + args: [] + entry: f +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + compileStatus: rejected + errorClass: compile-error + reason: recursive-call-graph + assertions: + - actual: runtime.started + op: equals + expected: false diff --git a/src/test/resources/conformance/bex/fixtures/e/bex-e-01.yaml b/src/test/resources/conformance/bex/fixtures/e/bex-e-01.yaml new file mode 100644 index 0000000..558a8af --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/e/bex-e-01.yaml @@ -0,0 +1,27 @@ +schema: blue-bex-fixture/2.0 +id: bex-e-01 +vectors: +- BEX-E-01 +category: e +description: '`$document` uses document-scope-relative and absolute pointers correctly.' +program: + expr: + relative: + $document: value + absolute: + $document: /child/value +context: + rootDocument: + child: + value: 2 + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: /child +expected: + assertions: [] + result: + relative: 2 + absolute: 2 diff --git a/src/test/resources/conformance/bex/fixtures/e/bex-e-02.yaml b/src/test/resources/conformance/bex/fixtures/e/bex-e-02.yaml new file mode 100644 index 0000000..7120748 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/e/bex-e-02.yaml @@ -0,0 +1,59 @@ +schema: blue-bex-fixture/2.0 +id: bex-e-02 +vectors: +- BEX-E-02 +category: e +description: '`$event`, `$processingEvent`, `$currentContract`, `$steps`, `$binding`, `$var`, and `$const` use value-local pointers.' +program: + constants: + c: + x: 7 + do: + - $let: + name: v + expr: + x: 6 + - $return: + event: + $event: /x + processing: + $processingEvent: /x + contract: + $currentContract: /x + step: + $steps: s.x + binding: + $binding: b/x + var: + $var: + name: v + path: /x + const: + $const: + name: c + path: /x +context: + rootDocument: {} + event: + x: 1 + processingEvent: + x: 2 + currentContract: + x: 3 + steps: + s: + x: 4 + bindings: + b: + x: 5 + documentScope: / +expected: + result: + event: 1 + processing: 2 + contract: 3 + step: 4 + binding: 5 + var: 6 + const: 7 + assertions: [] diff --git a/src/test/resources/conformance/bex/fixtures/e/bex-e-03.yaml b/src/test/resources/conformance/bex/fixtures/e/bex-e-03.yaml new file mode 100644 index 0000000..a14741a --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/e/bex-e-03.yaml @@ -0,0 +1,23 @@ +schema: blue-bex-fixture/2.0 +id: bex-e-03 +vectors: +- BEX-E-03 +category: e +description: Dynamic null/undefined pointers and dynamic null/undefined key/name operands fail. +program: + expr: + $pointerGet: + object: {} + path: + $null: true +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + assertions: [] + errorClass: runtime-error diff --git a/src/test/resources/conformance/bex/fixtures/e/bex-e-04.yaml b/src/test/resources/conformance/bex/fixtures/e/bex-e-04.yaml new file mode 100644 index 0000000..de9524e --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/e/bex-e-04.yaml @@ -0,0 +1,22 @@ +schema: blue-bex-fixture/2.0 +id: bex-e-04 +vectors: +- BEX-E-04 +category: e +description: '`$pointerJoin` escapes `~` and `/`.' +program: + expr: + $pointerJoin: + - a/b + - a~b +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + assertions: [] + result: /a~1b/a~0b diff --git a/src/test/resources/conformance/bex/fixtures/e/bex-e-05.yaml b/src/test/resources/conformance/bex/fixtures/e/bex-e-05.yaml new file mode 100644 index 0000000..176ca7a --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/e/bex-e-05.yaml @@ -0,0 +1,30 @@ +schema: blue-bex-fixture/2.0 +id: bex-e-05 +vectors: +- BEX-E-05 +category: e +description: '`$exists` is false only for semantic `undefined`; incomplete access is not absence.' +program: + expr: + missing: + $exists: + $document: /missing + 'null': + $exists: + $null: true +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + assertions: [] + result: + missing: false + 'null': true + additionalCase: + providerUnavailableAt: /x + mustNotReturn: false diff --git a/src/test/resources/conformance/bex/fixtures/e/bex-e-06.yaml b/src/test/resources/conformance/bex/fixtures/e/bex-e-06.yaml new file mode 100644 index 0000000..7fea0b0 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/e/bex-e-06.yaml @@ -0,0 +1,32 @@ +schema: blue-bex-fixture/2.0 +id: bex-e-06 +vectors: +- BEX-E-06 +category: e +description: Numeric zero is truthy; empty object/list and null are falsy. +program: + expr: + zero: + $truthy: 0 + object: + $truthy: {} + list: + $truthy: [] + 'null': + $truthy: + $null: true +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + assertions: [] + result: + zero: true + object: false + list: false + 'null': false diff --git a/src/test/resources/conformance/bex/fixtures/e/bex-e-07.yaml b/src/test/resources/conformance/bex/fixtures/e/bex-e-07.yaml new file mode 100644 index 0000000..714c426 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/e/bex-e-07.yaml @@ -0,0 +1,25 @@ +schema: blue-bex-fixture/2.0 +id: bex-e-07 +vectors: +- BEX-E-07 +category: e +description: '`$and`, `$or`, `$coalesce`, `$choose`, `$if`, and collection search short-circuit.' +program: + expr: + $or: + - true + - $fail: 'no' +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + assertions: + - actual: gas.skippedOperandCharges + op: equals + expected: 0 + result: true diff --git a/src/test/resources/conformance/bex/fixtures/e/bex-e-08.yaml b/src/test/resources/conformance/bex/fixtures/e/bex-e-08.yaml new file mode 100644 index 0000000..8d1b2af --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/e/bex-e-08.yaml @@ -0,0 +1,30 @@ +schema: blue-bex-fixture/2.0 +id: bex-e-08 +vectors: +- BEX-E-08 +category: e +description: Numeric conversions, exact division, Text rendering, and finite Double conversion are deterministic. +program: + expr: + integer: + $integer: '9007199254740992' + text: + $text: true + divide: + $divide: + - 6 + - 3 +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + assertions: [] + result: + integer: 9007199254740992 + text: 'true' + divide: 2 diff --git a/src/test/resources/conformance/bex/fixtures/e/bex-e-09.yaml b/src/test/resources/conformance/bex/fixtures/e/bex-e-09.yaml new file mode 100644 index 0000000..7cb7d38 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/e/bex-e-09.yaml @@ -0,0 +1,26 @@ +schema: blue-bex-fixture/2.0 +id: bex-e-09 +vectors: +- BEX-E-09 +category: e +description: Object keys are exposed in Unicode code-point order independent of host maps and locale. +program: + expr: + $keys: + 😀: 1 + a: 2 + : 3 +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + assertions: [] + result: + - a + -  + - 😀 diff --git a/src/test/resources/conformance/bex/fixtures/e/bex-e-10.yaml b/src/test/resources/conformance/bex/fixtures/e/bex-e-10.yaml new file mode 100644 index 0000000..a134ac5 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/e/bex-e-10.yaml @@ -0,0 +1,31 @@ +schema: blue-bex-fixture/2.0 +id: bex-e-10 +vectors: +- BEX-E-10 +category: e +description: '`$kind` and `$isKind` report semantic kinds, never reference/cursor classes.' +program: + expr: + $kind: + $document: /x +context: + rootDocument: + x: + blueId: BUCEaEysDw5JAAm62NMYi5bdKZK4JnEcJzZbXxkN3Dw2 + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / + provider: + BUCEaEysDw5JAAm62NMYi5bdKZK4JnEcJzZbXxkN3Dw2: + a: 1 +expected: + assertions: [] + result: object + variants: + - name: reference + rootForm: reference + - name: materialized + rootForm: materialized diff --git a/src/test/resources/conformance/bex/fixtures/e/bex-e-11.yaml b/src/test/resources/conformance/bex/fixtures/e/bex-e-11.yaml new file mode 100644 index 0000000..2427f98 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/e/bex-e-11.yaml @@ -0,0 +1,32 @@ +schema: blue-bex-fixture/2.0 +id: bex-e-11 +vectors: +- BEX-E-11 +category: e +description: Collection queries iterate in canonical order, restore bindings, and preserve short-circuiting. +program: + expr: + $map: + in: + b: 2 + a: 1 + item: v + key: k + expr: + $var: k +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + assertions: + - actual: locals.restored + op: equals + expected: true + result: + - a + - b diff --git a/src/test/resources/conformance/bex/fixtures/e/bex-e-12.yaml b/src/test/resources/conformance/bex/fixtures/e/bex-e-12.yaml new file mode 100644 index 0000000..7ccfcec --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/e/bex-e-12.yaml @@ -0,0 +1,25 @@ +schema: blue-bex-fixture/2.0 +id: bex-e-12 +vectors: +- BEX-E-12 +category: e +description: '`$objectSet`/`$pointerSet` use value-local semantics and create only permitted transient intermediates.' +program: + expr: + $pointerSet: + object: {} + path: /a/b + val: 1 +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + assertions: [] + result: + a: + b: 1 diff --git a/src/test/resources/conformance/bex/fixtures/e/bex-e-13.yaml b/src/test/resources/conformance/bex/fixtures/e/bex-e-13.yaml new file mode 100644 index 0000000..ee03393 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/e/bex-e-13.yaml @@ -0,0 +1,32 @@ +schema: blue-bex-fixture/2.0 +id: bex-e-13 +vectors: +- BEX-E-13 +category: e +description: Function frames isolate locals and validate static argument patterns. +program: + functions: + f: + args: + x: + type: + blueId: E2LM6qgzWG9ttagq2xTmiZkgYEAgkYedFCmU9v7NnVEq + do: + - $let: + name: local + expr: 1 + - $return: + $var: x + entry: f +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + assertions: [] + errorClass: compile-error + reason: entry-function-has-args diff --git a/src/test/resources/conformance/bex/fixtures/e/bex-e-14.yaml b/src/test/resources/conformance/bex/fixtures/e/bex-e-14.yaml new file mode 100644 index 0000000..be0e634 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/e/bex-e-14.yaml @@ -0,0 +1,32 @@ +schema: blue-bex-fixture/2.0 +id: bex-e-14 +vectors: +- BEX-E-14 +category: e +description: BEX numeric equality remains distinct from Blue identity. +program: + expr: + semantic: + $eq: + - 1 + - 1.0 + identityA: + $nodeBlueId: 1 + identityB: + $nodeBlueId: 1.0 +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + assertions: + - actual: result.semantic + op: equals + expected: true + - actual: result.identityA + op: notEquals + expected: result.identityB diff --git a/src/test/resources/conformance/bex/fixtures/fixture-schema.yaml b/src/test/resources/conformance/bex/fixtures/fixture-schema.yaml new file mode 100644 index 0000000..87edd87 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/fixture-schema.yaml @@ -0,0 +1,112 @@ +$schema: https://json-schema.org/draft/2020-12/schema +$id: blue-bex-fixture/2.0 +title: Blue BEX 2.0 conformance fixture +type: object +additionalProperties: false +required: [schema, id, vectors, category, program, context, expected] +properties: + schema: {const: blue-bex-fixture/2.0} + id: + type: string + pattern: '^[A-Za-z0-9][A-Za-z0-9-]*$' + vectors: + type: array + minItems: 1 + uniqueItems: true + items: + type: string + pattern: '^BEX-[A-Z]+-[0-9]{2}$' + category: + enum: [c, e, g, gas, h, operator, r, s] + description: {type: string} + program: {} + context: + $ref: '#/$defs/context' + expected: + $ref: '#/$defs/expected' +$defs: + context: + type: object + additionalProperties: false + properties: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {type: object} + bindings: {type: object} + documentScope: {type: string} + provider: + type: object + additionalProperties: {} + gasLimit: {type: integer, minimum: 0} + parentRemainingGas: {type: integer, minimum: 0} + directCounterFixture: + type: object + additionalProperties: false + required: [counter, quantity] + properties: + namespace: {enum: [runtime, semantic, processor]} + counter: {type: string} + quantity: {type: integer, minimum: 0} + weightManifest: {type: string} + assertion: + type: object + additionalProperties: false + required: [actual, op] + properties: + actual: {type: string} + op: + enum: [equals, notEquals, absent, present, contains, notContains, lessThan, greaterThan, sameAcrossVariants, all, none] + expected: {} + variant: + type: object + additionalProperties: false + required: [name] + properties: + name: {type: string} + rootForm: {enum: [inline, reference, eager, lazy, materialized]} + cache: {enum: [warm, cold]} + batching: {enum: [batched, unbatched]} + rawRootDocumentJson: {type: string} + deliveryKind: {enum: [document-update, triggered, lifecycle, embedded]} + case: + type: object + additionalProperties: false + required: [name, program, errorClass] + properties: + name: {type: string} + program: {} + context: {$ref: '#/$defs/context'} + errorClass: {type: string} + reason: {type: string} + expected: + type: object + additionalProperties: false + properties: + compileStatus: {type: string} + result: {} + changes: + type: array + items: {} + events: + type: array + items: {} + errorClass: {type: string} + gasTrace: + type: array + items: {} + totalGas: {type: integer, minimum: 0} + assertions: + type: array + items: {$ref: '#/$defs/assertion'} + additionalCase: {} + variants: + type: array + minItems: 1 + items: {$ref: '#/$defs/variant'} + cases: + type: array + minItems: 1 + items: {$ref: '#/$defs/case'} + reason: {type: string} diff --git a/src/test/resources/conformance/bex/fixtures/g/bex-g-01.yaml b/src/test/resources/conformance/bex/fixtures/g/bex-g-01.yaml new file mode 100644 index 0000000..774c32d --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/g/bex-g-01.yaml @@ -0,0 +1,21 @@ +schema: blue-bex-fixture/2.0 +id: bex-g-01 +vectors: +- BEX-G-01 +category: g +description: Every named counter has an exact microfixture and frozen weight. +program: + expr: 1 +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + assertions: + - actual: manifest.counterCoverage.complete + op: equals + expected: true diff --git a/src/test/resources/conformance/bex/fixtures/g/bex-g-02.yaml b/src/test/resources/conformance/bex/fixtures/g/bex-g-02.yaml new file mode 100644 index 0000000..67060cb --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/g/bex-g-02.yaml @@ -0,0 +1,26 @@ +schema: blue-bex-fixture/2.0 +id: bex-g-02 +vectors: +- BEX-G-02 +category: g +description: Charges are admitted before work; the failing charge is absent on exhaustion. +program: + expr: + $concat: + - a + - b +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / + gasLimit: 3 +expected: + assertions: + - actual: gas.failedChargePresent + op: equals + expected: false + errorClass: gas-exhaustion diff --git a/src/test/resources/conformance/bex/fixtures/g/bex-g-03.yaml b/src/test/resources/conformance/bex/fixtures/g/bex-g-03.yaml new file mode 100644 index 0000000..6c67ce1 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/g/bex-g-03.yaml @@ -0,0 +1,24 @@ +schema: blue-bex-fixture/2.0 +id: bex-g-03 +vectors: +- BEX-G-03 +category: g +description: Skipped operands and post-short-circuit items produce no charges. +program: + expr: + $or: + - true + - $fail: skip +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + assertions: + - actual: gas.skippedOperandCharges + op: equals + expected: 0 diff --git a/src/test/resources/conformance/bex/fixtures/g/bex-g-04.yaml b/src/test/resources/conformance/bex/fixtures/g/bex-g-04.yaml new file mode 100644 index 0000000..0c0e0b7 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/g/bex-g-04.yaml @@ -0,0 +1,25 @@ +schema: blue-bex-fixture/2.0 +id: bex-g-04 +vectors: +- BEX-G-04 +category: g +description: Pointer reads/writes charge exact segment and member/item work. +program: + expr: + $pointerSet: + object: {} + path: /a/b + val: 1 +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + assertions: + - actual: gas.pointerSegmentWritten + op: equals + expected: 2 diff --git a/src/test/resources/conformance/bex/fixtures/g/bex-g-05.yaml b/src/test/resources/conformance/bex/fixtures/g/bex-g-05.yaml new file mode 100644 index 0000000..72c54ca --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/g/bex-g-05.yaml @@ -0,0 +1,25 @@ +schema: blue-bex-fixture/2.0 +id: bex-g-05 +vectors: +- BEX-G-05 +category: g +description: Text uses 64-code-point blocks, never UTF-16 units. +program: + expr: + $concat: + - xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + assertions: + - actual: gas.textBlockExamined + op: equals + expected: 2 + - actual: gas.utf16Counter + op: absent diff --git a/src/test/resources/conformance/bex/fixtures/g/bex-g-06.yaml b/src/test/resources/conformance/bex/fixtures/g/bex-g-06.yaml new file mode 100644 index 0000000..9d30608 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/g/bex-g-06.yaml @@ -0,0 +1,24 @@ +schema: blue-bex-fixture/2.0 +id: bex-g-06 +vectors: +- BEX-G-06 +category: g +description: Integer operations use the portable limb formulas. +program: + expr: + $multiply: + - 4294967296 + - 4294967296 +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + assertions: + - actual: gas.integerLimbOperation + op: equals + expected: 4 diff --git a/src/test/resources/conformance/bex/fixtures/g/bex-g-07.yaml b/src/test/resources/conformance/bex/fixtures/g/bex-g-07.yaml new file mode 100644 index 0000000..7d0bae2 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/g/bex-g-07.yaml @@ -0,0 +1,31 @@ +schema: blue-bex-fixture/2.0 +id: bex-g-07 +vectors: +- BEX-G-07 +category: g +description: Collection visits and produced items are charged exactly once per semantic occurrence. +program: + expr: + $map: + in: + - 1 + - 2 + item: x + expr: + $var: x +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + assertions: + - actual: gas.collectionItemVisited + op: equals + expected: 2 + - actual: gas.collectionItemProduced + op: equals + expected: 2 diff --git a/src/test/resources/conformance/bex/fixtures/g/bex-g-08.yaml b/src/test/resources/conformance/bex/fixtures/g/bex-g-08.yaml new file mode 100644 index 0000000..bf2bcf2 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/g/bex-g-08.yaml @@ -0,0 +1,27 @@ +schema: blue-bex-fixture/2.0 +id: bex-g-08 +vectors: +- BEX-G-08 +category: g +description: Equality/matching charges comparison nodes and examined scalar work. +program: + expr: + $eq: + - a: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + - a: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + assertions: + - actual: gas.comparisonNodeVisited + op: greaterThan + expected: 0 + - actual: gas.textBlockExamined + op: greaterThan + expected: 0 diff --git a/src/test/resources/conformance/bex/fixtures/g/bex-g-09.yaml b/src/test/resources/conformance/bex/fixtures/g/bex-g-09.yaml new file mode 100644 index 0000000..e89dddb --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/g/bex-g-09.yaml @@ -0,0 +1,28 @@ +schema: blue-bex-fixture/2.0 +id: bex-g-09 +vectors: +- BEX-G-09 +category: g +description: Sorting reports canonical stable merge-sort comparisons. +program: + expr: + $intrinsic: + type: + blueId: 2R1WaEk8LVwFRMEGnsZ8HTj15QTz3tQEj9LDYYjGFJJG + values: + - 3 + - 1 + - 2 +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + assertions: + - actual: gas.sortComparison + op: equals + expected: canonical-merge-sort diff --git a/src/test/resources/conformance/bex/fixtures/g/bex-g-10.yaml b/src/test/resources/conformance/bex/fixtures/g/bex-g-10.yaml new file mode 100644 index 0000000..18aa5ca --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/g/bex-g-10.yaml @@ -0,0 +1,22 @@ +schema: blue-bex-fixture/2.0 +id: bex-g-10 +vectors: +- BEX-G-10 +category: g +description: '`$pointerSet`, `$objectSet`, `$appendChange(s)`, and `$appendEvent(s)` contain no recursive `estimatedSize` term.' +program: + do: + - $appendEvent: + a: 1 +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + assertions: + - actual: gas.estimatedSize + op: absent diff --git a/src/test/resources/conformance/bex/fixtures/g/bex-g-11.yaml b/src/test/resources/conformance/bex/fixtures/g/bex-g-11.yaml new file mode 100644 index 0000000..cbd086b --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/g/bex-g-11.yaml @@ -0,0 +1,33 @@ +schema: blue-bex-fixture/2.0 +id: bex-g-11 +vectors: +- BEX-G-11 +category: g +description: Exact values and transient values differ only by actual construction/identity-boundary work, not by serialization shape. +program: + expr: + exact: + $document: /x + transient: + a: 1 +context: + rootDocument: + x: + blueId: BUCEaEysDw5JAAm62NMYi5bdKZK4JnEcJzZbXxkN3Dw2 + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / + provider: + BUCEaEysDw5JAAm62NMYi5bdKZK4JnEcJzZbXxkN3Dw2: + a: 1 +expected: + assertions: + - actual: gas.exact.recursiveConstruction + op: equals + expected: 0 + - actual: gas.transient.transientObjectMemberProduced + op: equals + expected: 1 diff --git a/src/test/resources/conformance/bex/fixtures/g/bex-g-12.yaml b/src/test/resources/conformance/bex/fixtures/g/bex-g-12.yaml new file mode 100644 index 0000000..c7f3c0e --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/g/bex-g-12.yaml @@ -0,0 +1,24 @@ +schema: blue-bex-fixture/2.0 +id: bex-g-12 +vectors: +- BEX-G-12 +category: g +description: The BEX child ledger is live-bounded and merged into Contracts exactly once. +program: + expr: 1 +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + assertions: + - actual: host.runtimeChildMergeCount + op: equals + expected: 1 + - actual: host.liveBounded + op: equals + expected: true diff --git a/src/test/resources/conformance/bex/fixtures/g/bex-g-13.yaml b/src/test/resources/conformance/bex/fixtures/g/bex-g-13.yaml new file mode 100644 index 0000000..95078b0 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/g/bex-g-13.yaml @@ -0,0 +1,26 @@ +schema: blue-bex-fixture/2.0 +id: bex-g-13 +vectors: +- BEX-G-13 +category: g +description: A BEX-local gas limit can reduce but never replenish the parent budget. +program: + expr: 1 +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / + parentRemainingGas: 100 + gasLimit: 10 +expected: + assertions: + - actual: effectiveRuntimeBudget + op: equals + expected: 10 + - actual: parentBudgetAfter + op: lessThan + expected: 100 diff --git a/src/test/resources/conformance/bex/fixtures/g/bex-g-14.yaml b/src/test/resources/conformance/bex/fixtures/g/bex-g-14.yaml new file mode 100644 index 0000000..67a12c9 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/g/bex-g-14.yaml @@ -0,0 +1,27 @@ +schema: blue-bex-fixture/2.0 +id: bex-g-14 +vectors: +- BEX-G-14 +category: g +description: Intrinsics return named deterministic child counters rather than opaque gas. +program: + expr: + $intrinsic: + type: + blueId: 5Zbnaiu1hzRNEpuQmKNHuSmkiq5VqdZ5ros49gwGB674 + x: 1 +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + assertions: + - actual: intrinsic.ledger.namedCounters + op: equals + expected: true + - actual: intrinsic.opaqueGas + op: absent diff --git a/src/test/resources/conformance/bex/fixtures/g/bex-g-15.yaml b/src/test/resources/conformance/bex/fixtures/g/bex-g-15.yaml new file mode 100644 index 0000000..14a5867 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/g/bex-g-15.yaml @@ -0,0 +1,108 @@ +schema: blue-bex-fixture/2.0 +id: bex-g-15 +vectors: +- BEX-G-15 +category: g +description: A finite but deliberately oversized iteration is bounded by live gas admission; the failing charge is absent and all buffered effects are discarded. +program: + do: + - $forEach: + in: + - 0 + - 1 + - 2 + - 3 + - 4 + - 5 + - 6 + - 7 + - 8 + - 9 + - 10 + - 11 + - 12 + - 13 + - 14 + - 15 + - 16 + - 17 + - 18 + - 19 + - 20 + - 21 + - 22 + - 23 + - 24 + - 25 + - 26 + - 27 + - 28 + - 29 + - 30 + - 31 + - 32 + - 33 + - 34 + - 35 + - 36 + - 37 + - 38 + - 39 + - 40 + - 41 + - 42 + - 43 + - 44 + - 45 + - 46 + - 47 + - 48 + - 49 + - 50 + - 51 + - 52 + - 53 + - 54 + - 55 + - 56 + - 57 + - 58 + - 59 + - 60 + - 61 + - 62 + - 63 + item: x + index: i + do: + - $appendEvent: + index: + $var: i + value: + $var: x +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / + gasLimit: 40 + parentRemainingGas: 40 +expected: + errorClass: gas-limit-exceeded + changes: [] + events: [] + assertions: + - actual: gas.failedChargePresent + op: equals + expected: false + - actual: gas.totalAdmitted + op: lessThan + expected: 41 + - actual: gas.trace + op: present + - actual: runtime.bufferedEffectsCommitted + op: equals + expected: false diff --git a/src/test/resources/conformance/bex/fixtures/gas-micro/bindingRead.yaml b/src/test/resources/conformance/bex/fixtures/gas-micro/bindingRead.yaml new file mode 100644 index 0000000..3858d29 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/gas-micro/bindingRead.yaml @@ -0,0 +1,19 @@ +schema: blue-bex-fixture/2.0 +id: gas-bindingRead +vectors: +- BEX-G-01 +category: gas +program: + expr: 0 +context: + directCounterFixture: + counter: bindingRead + quantity: 3 +expected: + gasTrace: + - sequence: 0 + counter: bindingRead + quantity: 3 + weight: 1 + gas: 3 + totalGas: 3 diff --git a/src/test/resources/conformance/bex/fixtures/gas-micro/blueOutputBoundary.yaml b/src/test/resources/conformance/bex/fixtures/gas-micro/blueOutputBoundary.yaml new file mode 100644 index 0000000..e713404 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/gas-micro/blueOutputBoundary.yaml @@ -0,0 +1,19 @@ +schema: blue-bex-fixture/2.0 +id: gas-blueOutputBoundary +vectors: +- BEX-G-01 +category: gas +program: + expr: 0 +context: + directCounterFixture: + counter: blueOutputBoundary + quantity: 3 +expected: + gasTrace: + - sequence: 0 + counter: blueOutputBoundary + quantity: 3 + weight: 5 + gas: 15 + totalGas: 15 diff --git a/src/test/resources/conformance/bex/fixtures/gas-micro/collectionItemProduced.yaml b/src/test/resources/conformance/bex/fixtures/gas-micro/collectionItemProduced.yaml new file mode 100644 index 0000000..86bb0cf --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/gas-micro/collectionItemProduced.yaml @@ -0,0 +1,19 @@ +schema: blue-bex-fixture/2.0 +id: gas-collectionItemProduced +vectors: +- BEX-G-01 +category: gas +program: + expr: 0 +context: + directCounterFixture: + counter: collectionItemProduced + quantity: 3 +expected: + gasTrace: + - sequence: 0 + counter: collectionItemProduced + quantity: 3 + weight: 1 + gas: 3 + totalGas: 3 diff --git a/src/test/resources/conformance/bex/fixtures/gas-micro/collectionItemVisited.yaml b/src/test/resources/conformance/bex/fixtures/gas-micro/collectionItemVisited.yaml new file mode 100644 index 0000000..b1ea002 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/gas-micro/collectionItemVisited.yaml @@ -0,0 +1,19 @@ +schema: blue-bex-fixture/2.0 +id: gas-collectionItemVisited +vectors: +- BEX-G-01 +category: gas +program: + expr: 0 +context: + directCounterFixture: + counter: collectionItemVisited + quantity: 3 +expected: + gasTrace: + - sequence: 0 + counter: collectionItemVisited + quantity: 3 + weight: 1 + gas: 3 + totalGas: 3 diff --git a/src/test/resources/conformance/bex/fixtures/gas-micro/comparisonNodeVisited.yaml b/src/test/resources/conformance/bex/fixtures/gas-micro/comparisonNodeVisited.yaml new file mode 100644 index 0000000..829703b --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/gas-micro/comparisonNodeVisited.yaml @@ -0,0 +1,19 @@ +schema: blue-bex-fixture/2.0 +id: gas-comparisonNodeVisited +vectors: +- BEX-G-01 +category: gas +program: + expr: 0 +context: + directCounterFixture: + counter: comparisonNodeVisited + quantity: 3 +expected: + gasTrace: + - sequence: 0 + counter: comparisonNodeVisited + quantity: 3 + weight: 1 + gas: 3 + totalGas: 3 diff --git a/src/test/resources/conformance/bex/fixtures/gas-micro/constantRead.yaml b/src/test/resources/conformance/bex/fixtures/gas-micro/constantRead.yaml new file mode 100644 index 0000000..5fad466 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/gas-micro/constantRead.yaml @@ -0,0 +1,19 @@ +schema: blue-bex-fixture/2.0 +id: gas-constantRead +vectors: +- BEX-G-01 +category: gas +program: + expr: 0 +context: + directCounterFixture: + counter: constantRead + quantity: 3 +expected: + gasTrace: + - sequence: 0 + counter: constantRead + quantity: 3 + weight: 1 + gas: 3 + totalGas: 3 diff --git a/src/test/resources/conformance/bex/fixtures/gas-micro/currentContractRead.yaml b/src/test/resources/conformance/bex/fixtures/gas-micro/currentContractRead.yaml new file mode 100644 index 0000000..daebe44 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/gas-micro/currentContractRead.yaml @@ -0,0 +1,19 @@ +schema: blue-bex-fixture/2.0 +id: gas-currentContractRead +vectors: +- BEX-G-01 +category: gas +program: + expr: 0 +context: + directCounterFixture: + counter: currentContractRead + quantity: 3 +expected: + gasTrace: + - sequence: 0 + counter: currentContractRead + quantity: 3 + weight: 1 + gas: 3 + totalGas: 3 diff --git a/src/test/resources/conformance/bex/fixtures/gas-micro/documentRead.yaml b/src/test/resources/conformance/bex/fixtures/gas-micro/documentRead.yaml new file mode 100644 index 0000000..40ba52f --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/gas-micro/documentRead.yaml @@ -0,0 +1,19 @@ +schema: blue-bex-fixture/2.0 +id: gas-documentRead +vectors: +- BEX-G-01 +category: gas +program: + expr: 0 +context: + directCounterFixture: + counter: documentRead + quantity: 3 +expected: + gasTrace: + - sequence: 0 + counter: documentRead + quantity: 3 + weight: 2 + gas: 6 + totalGas: 6 diff --git a/src/test/resources/conformance/bex/fixtures/gas-micro/eventAppended.yaml b/src/test/resources/conformance/bex/fixtures/gas-micro/eventAppended.yaml new file mode 100644 index 0000000..0d790fb --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/gas-micro/eventAppended.yaml @@ -0,0 +1,19 @@ +schema: blue-bex-fixture/2.0 +id: gas-eventAppended +vectors: +- BEX-G-01 +category: gas +program: + expr: 0 +context: + directCounterFixture: + counter: eventAppended + quantity: 3 +expected: + gasTrace: + - sequence: 0 + counter: eventAppended + quantity: 3 + weight: 5 + gas: 15 + totalGas: 15 diff --git a/src/test/resources/conformance/bex/fixtures/gas-micro/eventRead.yaml b/src/test/resources/conformance/bex/fixtures/gas-micro/eventRead.yaml new file mode 100644 index 0000000..ec2b6e3 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/gas-micro/eventRead.yaml @@ -0,0 +1,19 @@ +schema: blue-bex-fixture/2.0 +id: gas-eventRead +vectors: +- BEX-G-01 +category: gas +program: + expr: 0 +context: + directCounterFixture: + counter: eventRead + quantity: 3 +expected: + gasTrace: + - sequence: 0 + counter: eventRead + quantity: 3 + weight: 1 + gas: 3 + totalGas: 3 diff --git a/src/test/resources/conformance/bex/fixtures/gas-micro/expressionEvaluated.yaml b/src/test/resources/conformance/bex/fixtures/gas-micro/expressionEvaluated.yaml new file mode 100644 index 0000000..7eec189 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/gas-micro/expressionEvaluated.yaml @@ -0,0 +1,19 @@ +schema: blue-bex-fixture/2.0 +id: gas-expressionEvaluated +vectors: +- BEX-G-01 +category: gas +program: + expr: 0 +context: + directCounterFixture: + counter: expressionEvaluated + quantity: 3 +expected: + gasTrace: + - sequence: 0 + counter: expressionEvaluated + quantity: 3 + weight: 1 + gas: 3 + totalGas: 3 diff --git a/src/test/resources/conformance/bex/fixtures/gas-micro/functionCalled.yaml b/src/test/resources/conformance/bex/fixtures/gas-micro/functionCalled.yaml new file mode 100644 index 0000000..bd8c41e --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/gas-micro/functionCalled.yaml @@ -0,0 +1,19 @@ +schema: blue-bex-fixture/2.0 +id: gas-functionCalled +vectors: +- BEX-G-01 +category: gas +program: + expr: 0 +context: + directCounterFixture: + counter: functionCalled + quantity: 3 +expected: + gasTrace: + - sequence: 0 + counter: functionCalled + quantity: 3 + weight: 2 + gas: 6 + totalGas: 6 diff --git a/src/test/resources/conformance/bex/fixtures/gas-micro/integerLimbOperation.yaml b/src/test/resources/conformance/bex/fixtures/gas-micro/integerLimbOperation.yaml new file mode 100644 index 0000000..414e4c8 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/gas-micro/integerLimbOperation.yaml @@ -0,0 +1,19 @@ +schema: blue-bex-fixture/2.0 +id: gas-integerLimbOperation +vectors: +- BEX-G-01 +category: gas +program: + expr: 0 +context: + directCounterFixture: + counter: integerLimbOperation + quantity: 3 +expected: + gasTrace: + - sequence: 0 + counter: integerLimbOperation + quantity: 3 + weight: 1 + gas: 3 + totalGas: 3 diff --git a/src/test/resources/conformance/bex/fixtures/gas-micro/intrinsicCalled.yaml b/src/test/resources/conformance/bex/fixtures/gas-micro/intrinsicCalled.yaml new file mode 100644 index 0000000..c589913 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/gas-micro/intrinsicCalled.yaml @@ -0,0 +1,19 @@ +schema: blue-bex-fixture/2.0 +id: gas-intrinsicCalled +vectors: +- BEX-G-01 +category: gas +program: + expr: 0 +context: + directCounterFixture: + counter: intrinsicCalled + quantity: 3 +expected: + gasTrace: + - sequence: 0 + counter: intrinsicCalled + quantity: 3 + weight: 5 + gas: 15 + totalGas: 15 diff --git a/src/test/resources/conformance/bex/fixtures/gas-micro/listItemRead.yaml b/src/test/resources/conformance/bex/fixtures/gas-micro/listItemRead.yaml new file mode 100644 index 0000000..03813d3 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/gas-micro/listItemRead.yaml @@ -0,0 +1,19 @@ +schema: blue-bex-fixture/2.0 +id: gas-listItemRead +vectors: +- BEX-G-01 +category: gas +program: + expr: 0 +context: + directCounterFixture: + counter: listItemRead + quantity: 3 +expected: + gasTrace: + - sequence: 0 + counter: listItemRead + quantity: 3 + weight: 1 + gas: 3 + totalGas: 3 diff --git a/src/test/resources/conformance/bex/fixtures/gas-micro/nodeIdentityRequested.yaml b/src/test/resources/conformance/bex/fixtures/gas-micro/nodeIdentityRequested.yaml new file mode 100644 index 0000000..c045722 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/gas-micro/nodeIdentityRequested.yaml @@ -0,0 +1,19 @@ +schema: blue-bex-fixture/2.0 +id: gas-nodeIdentityRequested +vectors: +- BEX-G-01 +category: gas +program: + expr: 0 +context: + directCounterFixture: + counter: nodeIdentityRequested + quantity: 3 +expected: + gasTrace: + - sequence: 0 + counter: nodeIdentityRequested + quantity: 3 + weight: 5 + gas: 15 + totalGas: 15 diff --git a/src/test/resources/conformance/bex/fixtures/gas-micro/objectMemberRead.yaml b/src/test/resources/conformance/bex/fixtures/gas-micro/objectMemberRead.yaml new file mode 100644 index 0000000..2ae7a30 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/gas-micro/objectMemberRead.yaml @@ -0,0 +1,19 @@ +schema: blue-bex-fixture/2.0 +id: gas-objectMemberRead +vectors: +- BEX-G-01 +category: gas +program: + expr: 0 +context: + directCounterFixture: + counter: objectMemberRead + quantity: 3 +expected: + gasTrace: + - sequence: 0 + counter: objectMemberRead + quantity: 3 + weight: 1 + gas: 3 + totalGas: 3 diff --git a/src/test/resources/conformance/bex/fixtures/gas-micro/patchAppended.yaml b/src/test/resources/conformance/bex/fixtures/gas-micro/patchAppended.yaml new file mode 100644 index 0000000..6de4234 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/gas-micro/patchAppended.yaml @@ -0,0 +1,19 @@ +schema: blue-bex-fixture/2.0 +id: gas-patchAppended +vectors: +- BEX-G-01 +category: gas +program: + expr: 0 +context: + directCounterFixture: + counter: patchAppended + quantity: 3 +expected: + gasTrace: + - sequence: 0 + counter: patchAppended + quantity: 3 + weight: 5 + gas: 15 + totalGas: 15 diff --git a/src/test/resources/conformance/bex/fixtures/gas-micro/pointerSegmentRead.yaml b/src/test/resources/conformance/bex/fixtures/gas-micro/pointerSegmentRead.yaml new file mode 100644 index 0000000..50058d6 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/gas-micro/pointerSegmentRead.yaml @@ -0,0 +1,19 @@ +schema: blue-bex-fixture/2.0 +id: gas-pointerSegmentRead +vectors: +- BEX-G-01 +category: gas +program: + expr: 0 +context: + directCounterFixture: + counter: pointerSegmentRead + quantity: 3 +expected: + gasTrace: + - sequence: 0 + counter: pointerSegmentRead + quantity: 3 + weight: 1 + gas: 3 + totalGas: 3 diff --git a/src/test/resources/conformance/bex/fixtures/gas-micro/pointerSegmentWritten.yaml b/src/test/resources/conformance/bex/fixtures/gas-micro/pointerSegmentWritten.yaml new file mode 100644 index 0000000..4d3bbbf --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/gas-micro/pointerSegmentWritten.yaml @@ -0,0 +1,19 @@ +schema: blue-bex-fixture/2.0 +id: gas-pointerSegmentWritten +vectors: +- BEX-G-01 +category: gas +program: + expr: 0 +context: + directCounterFixture: + counter: pointerSegmentWritten + quantity: 3 +expected: + gasTrace: + - sequence: 0 + counter: pointerSegmentWritten + quantity: 3 + weight: 1 + gas: 3 + totalGas: 3 diff --git a/src/test/resources/conformance/bex/fixtures/gas-micro/processingEventRead.yaml b/src/test/resources/conformance/bex/fixtures/gas-micro/processingEventRead.yaml new file mode 100644 index 0000000..cc23374 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/gas-micro/processingEventRead.yaml @@ -0,0 +1,19 @@ +schema: blue-bex-fixture/2.0 +id: gas-processingEventRead +vectors: +- BEX-G-01 +category: gas +program: + expr: 0 +context: + directCounterFixture: + counter: processingEventRead + quantity: 3 +expected: + gasTrace: + - sequence: 0 + counter: processingEventRead + quantity: 3 + weight: 1 + gas: 3 + totalGas: 3 diff --git a/src/test/resources/conformance/bex/fixtures/gas-micro/resultValueRead.yaml b/src/test/resources/conformance/bex/fixtures/gas-micro/resultValueRead.yaml new file mode 100644 index 0000000..96c8a7b --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/gas-micro/resultValueRead.yaml @@ -0,0 +1,19 @@ +schema: blue-bex-fixture/2.0 +id: gas-resultValueRead +vectors: +- BEX-G-01 +category: gas +program: + expr: 0 +context: + directCounterFixture: + counter: resultValueRead + quantity: 3 +expected: + gasTrace: + - sequence: 0 + counter: resultValueRead + quantity: 3 + weight: 2 + gas: 6 + totalGas: 6 diff --git a/src/test/resources/conformance/bex/fixtures/gas-micro/sortComparison.yaml b/src/test/resources/conformance/bex/fixtures/gas-micro/sortComparison.yaml new file mode 100644 index 0000000..efa9fde --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/gas-micro/sortComparison.yaml @@ -0,0 +1,19 @@ +schema: blue-bex-fixture/2.0 +id: gas-sortComparison +vectors: +- BEX-G-01 +category: gas +program: + expr: 0 +context: + directCounterFixture: + counter: sortComparison + quantity: 3 +expected: + gasTrace: + - sequence: 0 + counter: sortComparison + quantity: 3 + weight: 1 + gas: 3 + totalGas: 3 diff --git a/src/test/resources/conformance/bex/fixtures/gas-micro/statementExecuted.yaml b/src/test/resources/conformance/bex/fixtures/gas-micro/statementExecuted.yaml new file mode 100644 index 0000000..8d03944 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/gas-micro/statementExecuted.yaml @@ -0,0 +1,19 @@ +schema: blue-bex-fixture/2.0 +id: gas-statementExecuted +vectors: +- BEX-G-01 +category: gas +program: + expr: 0 +context: + directCounterFixture: + counter: statementExecuted + quantity: 3 +expected: + gasTrace: + - sequence: 0 + counter: statementExecuted + quantity: 3 + weight: 1 + gas: 3 + totalGas: 3 diff --git a/src/test/resources/conformance/bex/fixtures/gas-micro/stepsRead.yaml b/src/test/resources/conformance/bex/fixtures/gas-micro/stepsRead.yaml new file mode 100644 index 0000000..c9b922c --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/gas-micro/stepsRead.yaml @@ -0,0 +1,19 @@ +schema: blue-bex-fixture/2.0 +id: gas-stepsRead +vectors: +- BEX-G-01 +category: gas +program: + expr: 0 +context: + directCounterFixture: + counter: stepsRead + quantity: 3 +expected: + gasTrace: + - sequence: 0 + counter: stepsRead + quantity: 3 + weight: 1 + gas: 3 + totalGas: 3 diff --git a/src/test/resources/conformance/bex/fixtures/gas-micro/textBlockConstructed.yaml b/src/test/resources/conformance/bex/fixtures/gas-micro/textBlockConstructed.yaml new file mode 100644 index 0000000..232711a --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/gas-micro/textBlockConstructed.yaml @@ -0,0 +1,19 @@ +schema: blue-bex-fixture/2.0 +id: gas-textBlockConstructed +vectors: +- BEX-G-01 +category: gas +program: + expr: 0 +context: + directCounterFixture: + counter: textBlockConstructed + quantity: 3 +expected: + gasTrace: + - sequence: 0 + counter: textBlockConstructed + quantity: 3 + weight: 1 + gas: 3 + totalGas: 3 diff --git a/src/test/resources/conformance/bex/fixtures/gas-micro/textBlockExamined.yaml b/src/test/resources/conformance/bex/fixtures/gas-micro/textBlockExamined.yaml new file mode 100644 index 0000000..bc31ec8 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/gas-micro/textBlockExamined.yaml @@ -0,0 +1,19 @@ +schema: blue-bex-fixture/2.0 +id: gas-textBlockExamined +vectors: +- BEX-G-01 +category: gas +program: + expr: 0 +context: + directCounterFixture: + counter: textBlockExamined + quantity: 3 +expected: + gasTrace: + - sequence: 0 + counter: textBlockExamined + quantity: 3 + weight: 1 + gas: 3 + totalGas: 3 diff --git a/src/test/resources/conformance/bex/fixtures/gas-micro/transientListItemProduced.yaml b/src/test/resources/conformance/bex/fixtures/gas-micro/transientListItemProduced.yaml new file mode 100644 index 0000000..e973370 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/gas-micro/transientListItemProduced.yaml @@ -0,0 +1,19 @@ +schema: blue-bex-fixture/2.0 +id: gas-transientListItemProduced +vectors: +- BEX-G-01 +category: gas +program: + expr: 0 +context: + directCounterFixture: + counter: transientListItemProduced + quantity: 3 +expected: + gasTrace: + - sequence: 0 + counter: transientListItemProduced + quantity: 3 + weight: 1 + gas: 3 + totalGas: 3 diff --git a/src/test/resources/conformance/bex/fixtures/gas-micro/transientObjectMemberProduced.yaml b/src/test/resources/conformance/bex/fixtures/gas-micro/transientObjectMemberProduced.yaml new file mode 100644 index 0000000..98b85d5 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/gas-micro/transientObjectMemberProduced.yaml @@ -0,0 +1,19 @@ +schema: blue-bex-fixture/2.0 +id: gas-transientObjectMemberProduced +vectors: +- BEX-G-01 +category: gas +program: + expr: 0 +context: + directCounterFixture: + counter: transientObjectMemberProduced + quantity: 3 +expected: + gasTrace: + - sequence: 0 + counter: transientObjectMemberProduced + quantity: 3 + weight: 1 + gas: 3 + totalGas: 3 diff --git a/src/test/resources/conformance/bex/fixtures/gas-micro/variableRead.yaml b/src/test/resources/conformance/bex/fixtures/gas-micro/variableRead.yaml new file mode 100644 index 0000000..ccc223f --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/gas-micro/variableRead.yaml @@ -0,0 +1,19 @@ +schema: blue-bex-fixture/2.0 +id: gas-variableRead +vectors: +- BEX-G-01 +category: gas +program: + expr: 0 +context: + directCounterFixture: + counter: variableRead + quantity: 3 +expected: + gasTrace: + - sequence: 0 + counter: variableRead + quantity: 3 + weight: 1 + gas: 3 + totalGas: 3 diff --git a/src/test/resources/conformance/bex/fixtures/h/bex-h-01.yaml b/src/test/resources/conformance/bex/fixtures/h/bex-h-01.yaml new file mode 100644 index 0000000..c0ce75f --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/h/bex-h-01.yaml @@ -0,0 +1,67 @@ +schema: blue-bex-fixture/2.0 +id: bex-h-01 +vectors: +- BEX-H-01 +category: h +description: Root `undefined`, list `undefined`, mixed `blueId`, mixed payload kinds, `properties`, invalid schema keys, computed `blue`, and unsupported list controls fail conversion. +program: + expr: + blueId: BUCEaEysDw5JAAm62NMYi5bdKZK4JnEcJzZbXxkN3Dw2 + a: 1 +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + assertions: [] + errorClass: output-conversion-error + cases: + - name: root-undefined + program: + expr: + $document: /missing + errorClass: output-conversion-error + - name: list-undefined + program: + expr: + - $document: /missing + errorClass: output-conversion-error + - name: mixed-blueid + program: + expr: + blueId: BUCEaEysDw5JAAm62NMYi5bdKZK4JnEcJzZbXxkN3Dw2 + a: 1 + errorClass: output-conversion-error + - name: mixed-payload + program: + expr: + value: 1 + child: 2 + errorClass: output-conversion-error + - name: properties + program: + expr: + properties: + a: 1 + errorClass: output-conversion-error + - name: bad-schema + program: + expr: + schema: + unsupported: true + errorClass: output-conversion-error + - name: blue + program: + expr: + blue: forbidden + errorClass: output-conversion-error + - name: list-control + program: + expr: + - $pos: 0 + value: 1 + errorClass: output-conversion-error diff --git a/src/test/resources/conformance/bex/fixtures/h/bex-h-02.yaml b/src/test/resources/conformance/bex/fixtures/h/bex-h-02.yaml new file mode 100644 index 0000000..a9d1a88 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/h/bex-h-02.yaml @@ -0,0 +1,30 @@ +schema: blue-bex-fixture/2.0 +id: bex-h-02 +vectors: +- BEX-H-02 +category: h +description: Existing exact nodes cross output by identity; transient aggregates convert deterministically. +program: + expr: + $document: /x +context: + rootDocument: + x: + blueId: BUCEaEysDw5JAAm62NMYi5bdKZK4JnEcJzZbXxkN3Dw2 + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / + provider: + BUCEaEysDw5JAAm62NMYi5bdKZK4JnEcJzZbXxkN3Dw2: + a: 1 +expected: + assertions: + - actual: output.nodeBlueId + op: equals + expected: BUCEaEysDw5JAAm62NMYi5bdKZK4JnEcJzZbXxkN3Dw2 + - actual: output.reconstructed + op: equals + expected: false diff --git a/src/test/resources/conformance/bex/fixtures/h/bex-h-03.yaml b/src/test/resources/conformance/bex/fixtures/h/bex-h-03.yaml new file mode 100644 index 0000000..45a402a --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/h/bex-h-03.yaml @@ -0,0 +1,24 @@ +schema: blue-bex-fixture/2.0 +id: bex-h-03 +vectors: +- BEX-H-03 +category: h +description: Computed Blue language fields retain their reserved meaning. +program: + expr: + type: + blueId: E2LM6qgzWG9ttagq2xTmiZkgYEAgkYedFCmU9v7NnVEq + value: 1 +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + assertions: + - actual: output.type.blueId + op: equals + expected: E2LM6qgzWG9ttagq2xTmiZkgYEAgkYedFCmU9v7NnVEq diff --git a/src/test/resources/conformance/bex/fixtures/h/bex-h-04.yaml b/src/test/resources/conformance/bex/fixtures/h/bex-h-04.yaml new file mode 100644 index 0000000..f33a762 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/h/bex-h-04.yaml @@ -0,0 +1,27 @@ +schema: blue-bex-fixture/2.0 +id: bex-h-04 +vectors: +- BEX-H-04 +category: h +description: Sparse overlay lists cannot cross as ordinary Blue lists. +program: + do: + - $appendChange: + op: remove + path: /items/0 + - $return: + $resultValue: /items +context: + rootDocument: + items: + - 1 + - 2 + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + assertions: [] + errorClass: output-conversion-error diff --git a/src/test/resources/conformance/bex/fixtures/h/bex-h-05.yaml b/src/test/resources/conformance/bex/fixtures/h/bex-h-05.yaml new file mode 100644 index 0000000..04ac1eb --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/h/bex-h-05.yaml @@ -0,0 +1,28 @@ +schema: blue-bex-fixture/2.0 +id: bex-h-05 +vectors: +- BEX-H-05 +category: h +description: Blue Double input is source-token independent and output rounding is deterministic. +program: + expr: + $number: + $document: /d +context: + rootDocument: + d: 1.0 + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + assertions: + - actual: result + op: sameAcrossVariants + variants: + - name: source-1.0 + rawRootDocumentJson: '{"d":1.0}' + - name: source-1e0 + rawRootDocumentJson: '{"d":1e0}' diff --git a/src/test/resources/conformance/bex/fixtures/h/bex-h-06.yaml b/src/test/resources/conformance/bex/fixtures/h/bex-h-06.yaml new file mode 100644 index 0000000..055a120 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/h/bex-h-06.yaml @@ -0,0 +1,36 @@ +schema: blue-bex-fixture/2.0 +id: bex-h-06 +vectors: +- BEX-H-06 +category: h +description: '`$processingEvent` remains the original external event in Document Update, Triggered, Lifecycle, and Embedded deliveries.' +program: + expr: + current: + $event: /id + causal: + $processingEvent: /id +context: + rootDocument: {} + event: + id: internal + processingEvent: + id: external + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + assertions: [] + result: + current: internal + causal: external + variants: + - name: document-update + deliveryKind: document-update + - name: triggered + deliveryKind: triggered + - name: lifecycle + deliveryKind: lifecycle + - name: embedded + deliveryKind: embedded diff --git a/src/test/resources/conformance/bex/fixtures/manifest.yaml b/src/test/resources/conformance/bex/fixtures/manifest.yaml new file mode 100644 index 0000000..d05b3ae --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/manifest.yaml @@ -0,0 +1,581 @@ +fixturePackage: blue-bex-conformance +specificationVersion: '2.0' +schemaVersion: blue-bex-fixture/2.0 +registryPackageIdentity: sha256:23d282ec1c0bb016263922b1b49c369fdd537efdcf23e005eceeb888d7763fe1 +vectorCount: 60 +behaviorFixtureCount: 105 +gasFixtureCount: 30 +files: +- path: HARNESS.md + role: support + sha256: 13d8124d59aa495f41cdc40f68d38be0299dc61765b4b7271978d728e8376b03 + bytes: 6357 +- path: README.md + role: support + sha256: a0776a0e1f3c21b388369a7ac92506d2ceca34d3d813cced09f627d253cb42f9 + bytes: 562 +- path: c/bex-c-01.yaml + role: behavior-fixture + sha256: ee53d235ef84975ebfa07522703376ddec0a1596510e90704e923c91122a7208 + bytes: 410 +- path: c/bex-c-02.yaml + role: behavior-fixture + sha256: 7048150c8ada99b6ca8b0aec03207c64ce36dc0467f4dd5cebc1e5ffe4a8f32d + bytes: 366 +- path: c/bex-c-03.yaml + role: behavior-fixture + sha256: 09ea76a45dcb6050f6d37583cc0e5150fd558579d7437d4d54d326a5dea344ed + bytes: 548 +- path: c/bex-c-04.yaml + role: behavior-fixture + sha256: a35e4bc4085fac1b9165405d99bbabbac99072c04285617d1c4c78d669227da9 + bytes: 532 +- path: c/bex-c-05.yaml + role: behavior-fixture + sha256: 4cd913396f64323b143c215c4bd6c1128eb010cb351d5f032f7ae80392f3e70e + bytes: 401 +- path: c/bex-c-06.yaml + role: behavior-fixture + sha256: 8a33c2f91a2c60b73c14fc56d30bf409e92b4534352265b7e56e3d1fbbb74da1 + bytes: 395 +- path: c/bex-c-07.yaml + role: behavior-fixture + sha256: f54f939c684f63b4671a02513ac41bfc89cbabcb78e47a4047c159a12d07bcc0 + bytes: 394 +- path: c/bex-c-08.yaml + role: behavior-fixture + sha256: d5bc2709eccc3c563268947024f56ecaf57fa460ca8339d3941d101da9437e71 + bytes: 545 +- path: c/bex-c-09.yaml + role: behavior-fixture + sha256: eebecf5d37c7ae74864d02253cd9b10c3b11b51e81efe431cd2616b934b0e745 + bytes: 702 +- path: e/bex-e-01.yaml + role: behavior-fixture + sha256: 836858985f3f236ff0991e9e2ef3474bad63b76cfd205a4c3410014a95579b62 + bytes: 488 +- path: e/bex-e-02.yaml + role: behavior-fixture + sha256: 3d4b27d8c14ebc7d3140b01a5cd7639f9ec58d89674d7703bc9c724722c1a115 + bytes: 950 +- path: e/bex-e-03.yaml + role: behavior-fixture + sha256: 61e1a9a5bf0e94ec2e185c0c6209f14fa5730cb3edbcd1e2dc84b3e5358c442b + bytes: 438 +- path: e/bex-e-04.yaml + role: behavior-fixture + sha256: f87ab28a19edc630a9a1344e90490679b629bc1c5d2bff9d7bef5ceda4401192 + bytes: 358 +- path: e/bex-e-05.yaml + role: behavior-fixture + sha256: f3950c865c676c86fc79570dec67f89a9da856929319ad26f6bdd06a61d2927a + bytes: 569 +- path: e/bex-e-06.yaml + role: behavior-fixture + sha256: eaf1fd4df0d4c1ff612d1dd0c769dd2954388fa7437e9886ca6dc36286964862 + bytes: 532 +- path: e/bex-e-07.yaml + role: behavior-fixture + sha256: 9af6cb4552f1fd4bb12131c572dcec702caa90c39ce22d41ec269306081aca2e + bytes: 465 +- path: e/bex-e-08.yaml + role: behavior-fixture + sha256: e0871e785c2e8db89e43a5aa3524659b7d548c2d1fce00712f4366b1d3e9f991 + bytes: 556 +- path: e/bex-e-09.yaml + role: behavior-fixture + sha256: ce8d3c066e4b4767488a28bae0f276bde1736e09a73a9ac62f957d787351f18d + bytes: 432 +- path: e/bex-e-10.yaml + role: behavior-fixture + sha256: e3c138103cb2aa3911a1f0385851c3c085ce8ec7a817cc8ed00645b5e4b21022 + bytes: 630 +- path: e/bex-e-11.yaml + role: behavior-fixture + sha256: 75be7b11c8d80cc6c93096eb26c4c77ee4786dcc5f7fc9322a0a1e9e200e9a51 + bytes: 539 +- path: e/bex-e-12.yaml + role: behavior-fixture + sha256: d783cb9f47b7abe6e509646b8d42eaac3c8c4d6392811b000aa82a8be86ec34b + bytes: 459 +- path: e/bex-e-13.yaml + role: behavior-fixture + sha256: 76233a657a1d4f54e0263d6e8e8ad14c4ffb902496791643dc4b50fc08ffbad1 + bytes: 619 +- path: e/bex-e-14.yaml + role: behavior-fixture + sha256: 92f074b34268640dba1c9e1572b8f14acab6c3199afa2ad3da43f8072f60cd04 + bytes: 577 +- path: fixture-schema.yaml + role: support + sha256: a808d5fb6fe7f7596fd12b17b8c10873f01c0b8d731c845c3bfda2c0e570b6dc + bytes: 3062 +- path: g/bex-g-01.yaml + role: behavior-fixture + sha256: fecea8ce022312ad088d896a2a74ecebe65eb98812bb65c94e9178e76b73a8fe + bytes: 405 +- path: g/bex-g-02.yaml + role: behavior-fixture + sha256: ea3d4ccb6875b028f0450ba544a5ff3fdafc5f61a395134aa0aa99f07ee15f58 + bytes: 479 +- path: g/bex-g-03.yaml + role: behavior-fixture + sha256: 12c4988e7c1fe5f6192cab1fb1edbd141d589d7e2ad3272a4c366c4bb46f9e55 + bytes: 431 +- path: g/bex-g-04.yaml + role: behavior-fixture + sha256: 13431d270811c4bbffe5e97a81efb9b2d7039618f6696493972bb2a8dcdcae42 + bytes: 455 +- path: g/bex-g-05.yaml + role: behavior-fixture + sha256: 551bd8e30d73ab0ac1648f7cd454db8d59a4f5a9fc7f330802eca42b62d2fd97 + bytes: 504 +- path: g/bex-g-06.yaml + role: behavior-fixture + sha256: 11d24cbe5ef1638467c6fdf7894642ae35cba21401594914d486af2ce14ab09c + bytes: 426 +- path: g/bex-g-07.yaml + role: behavior-fixture + sha256: d8a573d9da4908d4ee31de5c9f42125cc0cacbf6cae8eff4ea52809966c04fb9 + bytes: 566 +- path: g/bex-g-08.yaml + role: behavior-fixture + sha256: 42c1233c24302a8bf024bcab2fcb928326642bc630efb9020981044e80ba92d9 + bytes: 630 +- path: g/bex-g-09.yaml + role: behavior-fixture + sha256: 6714c4d282e84f519c79184d5fd107c079b2292350a4ad0c7503dd9a0e5269fc + bytes: 529 +- path: g/bex-g-10.yaml + role: behavior-fixture + sha256: 3acc46272cdf689fc8a056b076a983f2127574e29642ae389e46800ed7c5c952 + bytes: 446 +- path: g/bex-g-11.yaml + role: behavior-fixture + sha256: 9aabc381fcaa664a604677ed9443468e9064c1c7d11e3bce4a43602aa5c7d074 + bytes: 734 +- path: g/bex-g-12.yaml + role: behavior-fixture + sha256: 9786cf7d092d88301d07bc894cbc662fdc28c140c9dbe753a55a7a803e190e45 + bytes: 471 +- path: g/bex-g-13.yaml + role: behavior-fixture + sha256: d0e220e81a34507de62c6158acd704b3ca94153d888dfe749416220a54f07b61 + bytes: 505 +- path: g/bex-g-14.yaml + role: behavior-fixture + sha256: e06fcda179f2328eb572b4f219ffcb8cb7b83ba1d562e51b9efbac9a7802c7b5 + bytes: 559 +- path: g/bex-g-15.yaml + role: behavior-fixture + sha256: 6c8ac6135a3e084b565d7b5a95ed25f40b264ee4067431373b6fd58345aafae9 + bytes: 1611 +- path: gas-micro/bindingRead.yaml + role: gas-fixture + sha256: 48c74333edf208948399697fd2cf9f2756a7214638184d3d1a59f01f3ef91396 + bytes: 294 +- path: gas-micro/blueOutputBoundary.yaml + role: gas-fixture + sha256: 856d77bbad92e0e7a2cd127ee9383623ccc6bc1bd6367c246046ae4c62c7f399 + bytes: 317 +- path: gas-micro/collectionItemProduced.yaml + role: gas-fixture + sha256: 2423546ea05fdddb8c3accc6bd1e7741c3ca45e07d82e24861428de6fad458ac + bytes: 327 +- path: gas-micro/collectionItemVisited.yaml + role: gas-fixture + sha256: 96b841734fcd7297ac9afe59142c1ce1938a03ef124201d63ad8d5abbf655fbb + bytes: 324 +- path: gas-micro/comparisonNodeVisited.yaml + role: gas-fixture + sha256: dd9883f1822d0c16a468d158baa1374222856469d61342d17969cc9e0e154f8f + bytes: 324 +- path: gas-micro/constantRead.yaml + role: gas-fixture + sha256: c0ad6db3ae4b5e8d09632b07ab5cce216ac70029227938a38301d6c88dc111d9 + bytes: 297 +- path: gas-micro/currentContractRead.yaml + role: gas-fixture + sha256: b47c2984e10c9b375ec113f1764327207e3559fa0c2eb6cc64ee45a967f103b5 + bytes: 318 +- path: gas-micro/documentRead.yaml + role: gas-fixture + sha256: a78abe1756f3310c13954645e6bdd07efc8854adeab5429b057798417cbda087 + bytes: 297 +- path: gas-micro/eventAppended.yaml + role: gas-fixture + sha256: 804e72243b0feedf1cd25593236f5bdbfb036b7725b969e29d4c3d42ca0c83b6 + bytes: 302 +- path: gas-micro/eventRead.yaml + role: gas-fixture + sha256: d3b632611e719f744d33276db44bf5e3a0425282d202e9f5874f40ee65136bb8 + bytes: 288 +- path: gas-micro/expressionEvaluated.yaml + role: gas-fixture + sha256: 9e7c410124a827f3db400df5c0f698ad3cde21b8497c6046dd371a99e141ae2b + bytes: 318 +- path: gas-micro/functionCalled.yaml + role: gas-fixture + sha256: a869fc9365e7a6c051807ef5060fd5cc0cb8a226af1b88d5f2b9755f16da8cee + bytes: 303 +- path: gas-micro/integerLimbOperation.yaml + role: gas-fixture + sha256: a81c5f5a9bb88f555cc1af3dc42c86d822e3fd92168f85390d42810fb8ea875e + bytes: 321 +- path: gas-micro/intrinsicCalled.yaml + role: gas-fixture + sha256: 6496f03c3371fde3c35f8229bb974ba1b445aaca8f501ad6a7c549c08d58f887 + bytes: 308 +- path: gas-micro/listItemRead.yaml + role: gas-fixture + sha256: 9053b1b4cf7276cf8b61cfdf7a246289dcada9c4865002dc022f8f519f7f28c7 + bytes: 297 +- path: gas-micro/nodeIdentityRequested.yaml + role: gas-fixture + sha256: 811c14244cd6229fb3e74cf3f2f85cd10962f50c5b55fa5c474549e6b19e38b7 + bytes: 326 +- path: gas-micro/objectMemberRead.yaml + role: gas-fixture + sha256: 5f78e274b30ab1ffbd42798ffd729e597a60f97dd34c29ab4d554afe69061f10 + bytes: 309 +- path: gas-micro/patchAppended.yaml + role: gas-fixture + sha256: 39d96690e7a9a74cf773386d586dd9304cd4e3a475665e618546ef3e8b831b52 + bytes: 302 +- path: gas-micro/pointerSegmentRead.yaml + role: gas-fixture + sha256: 6aa9d690cc679825796435fbb1400dbfb181026c2fcda59e4b84b618fd931bb0 + bytes: 315 +- path: gas-micro/pointerSegmentWritten.yaml + role: gas-fixture + sha256: af83965183b70e61e79c2ad85c81e927fee225ccbf5e90a40ad06f9bf2027b12 + bytes: 324 +- path: gas-micro/processingEventRead.yaml + role: gas-fixture + sha256: faf6ee6581f3af0c4e619c2c7f98fee4f3d271ad2c08503c090a91275835034b + bytes: 318 +- path: gas-micro/resultValueRead.yaml + role: gas-fixture + sha256: c8e28ab6fc2e25a77325ded00233a3e22552868b20f24207da24d2128b4eccd1 + bytes: 306 +- path: gas-micro/sortComparison.yaml + role: gas-fixture + sha256: a6c017e9782ebf33d665452a6e765c1357f2f95d786826fc04b5598b69fe8f99 + bytes: 303 +- path: gas-micro/statementExecuted.yaml + role: gas-fixture + sha256: 4a6f3328dc3054266af6f3fd9bbf7cdda4bbd4c7d8af2da661979d363731e2ea + bytes: 312 +- path: gas-micro/stepsRead.yaml + role: gas-fixture + sha256: dbb8b6fd022d2552466f4f19f0d854fd4aae21cc4521a64f8d4b4b462d241904 + bytes: 288 +- path: gas-micro/textBlockConstructed.yaml + role: gas-fixture + sha256: 19af7af1641a13f9c861ec3125c1ac02631af263773cac338b7c1a0501227bde + bytes: 321 +- path: gas-micro/textBlockExamined.yaml + role: gas-fixture + sha256: 45add971e3c0193615050e6fd2bb4c759e6dc3fe0cbf6e9b4c70b5ff21c8f7af + bytes: 312 +- path: gas-micro/transientListItemProduced.yaml + role: gas-fixture + sha256: e709abb607253b9d669e204580a7a4441bc4cf8559fee932a75ce88a5048c28f + bytes: 336 +- path: gas-micro/transientObjectMemberProduced.yaml + role: gas-fixture + sha256: 87dd0f408f5fde60f555aa04d769e533e30e744fd6aac65c06077083ba059e9e + bytes: 348 +- path: gas-micro/variableRead.yaml + role: gas-fixture + sha256: fa36b05b04d05b44baa3e5e19ab0e25383a32d56c14d6749e1bc6502e865dc60 + bytes: 297 +- path: h/bex-h-01.yaml + role: behavior-fixture + sha256: dbcbff932acb97e02345430fac468a6e4cfd4f561f1e1af66afb70aa676a44d0 + bytes: 1543 +- path: h/bex-h-02.yaml + role: behavior-fixture + sha256: e973d8bbf330beebd653544e5f17fea45939028075ac3475c6481886c7450384 + bytes: 679 +- path: h/bex-h-03.yaml + role: behavior-fixture + sha256: 348ab512dfc061b5ddd98b1d709bfd2ea41e7cb5f238d270bf91d06c4c423c59 + bytes: 506 +- path: h/bex-h-04.yaml + role: behavior-fixture + sha256: 2c9718b445a5b538cddbeb1b5fb7207ffa5dc974321987ec3cd361e91544778f + bytes: 476 +- path: h/bex-h-05.yaml + role: behavior-fixture + sha256: e95611f3f5f3add44c305d4ae5d55d9f96273ee9171e3e11b429733b3e98622b + bytes: 557 +- path: h/bex-h-06.yaml + role: behavior-fixture + sha256: ae59f52d1d227e16472c86df67707a0f333145fe7c1101eb88c9749bb81f72c6 + bytes: 751 +- path: operator-coverage.yaml + role: support + sha256: 98219b5987057767e498096d42242e9b8326fb631aa429efc820126d3ba216fd + bytes: 6859 +- path: operators/bex-op-add.yaml + role: behavior-fixture + sha256: fced17f3d8f26a4166932871348c6b7486c9732cbf8fd7fefd9047af21edc99f + bytes: 355 +- path: operators/bex-op-and.yaml + role: behavior-fixture + sha256: f80d2d38e47689029ebd536897c312754cad597c8d6849a8eafc04075415b4c2 + bytes: 384 +- path: operators/bex-op-appendchanges.yaml + role: behavior-fixture + sha256: da672a99563b05a76becb74e0fe082f1ff3da5ba75d73d368b679750986084c1 + bytes: 496 +- path: operators/bex-op-appendevents.yaml + role: behavior-fixture + sha256: 33bf1626ed3ffe4abe0eeeb189ac0acbfc3a4585bf91ed2b2876373ee4f877a0 + bytes: 398 +- path: operators/bex-op-boolean.yaml + role: behavior-fixture + sha256: 04bad12d51f721d82e8da635d76240ff40ed59b5fc8dacc9526b3675c156f842 + bytes: 353 +- path: operators/bex-op-changeset.yaml + role: behavior-fixture + sha256: a6da9c756f899d0c61ae7b38794cd46c32872a1dc3112319c41cba68fe8f9e46 + bytes: 509 +- path: operators/bex-op-choose.yaml + role: behavior-fixture + sha256: 826db21a5d434a50e3cd84f5a125b0e19450e6994aa8e0681d4096fd903519b2 + bytes: 411 +- path: operators/bex-op-coalesce.yaml + role: behavior-fixture + sha256: 67f020de02ef65ec9bc54fa2de01816a3be7043600e72e37e5aa5abbfe20c513 + bytes: 415 +- path: operators/bex-op-default.yaml + role: behavior-fixture + sha256: 450a01b05f017a987c8b47eeeb3f3005974a53fa8e74749fabd6b1357495b1c6 + bytes: 391 +- path: operators/bex-op-empty.yaml + role: behavior-fixture + sha256: 96e6e2b756cd1800782c47bbd0d0dd0687ca3ab94f3fc8fa675814f81d9fd7b2 + bytes: 343 +- path: operators/bex-op-emptylist.yaml + role: behavior-fixture + sha256: 7d4df963252a887e354b640c70a97e04da1ef6bd2ab391111df4480e13df6f31 + bytes: 355 +- path: operators/bex-op-emptyobject.yaml + role: behavior-fixture + sha256: 460fe85577a38d572db7043cbb87a43dd7dc52cd261935e8d4340783e09e0947 + bytes: 361 +- path: operators/bex-op-entries.yaml + role: behavior-fixture + sha256: 9bd4d453c675f7d60d5f9ebc0bf5aeff020d172e82c7e53b401a50e48d2bef9a + bytes: 407 +- path: operators/bex-op-events.yaml + role: behavior-fixture + sha256: 2a5aa1561eca672137f752a983fd9f1ef266a0f9cb4b0681859774a2dc28a1b2 + bytes: 416 +- path: operators/bex-op-failif.yaml + role: behavior-fixture + sha256: c10947dec63fd1021b4bd33f4ee4a5c204331c23d25b897411707bee7094c3ce + bytes: 414 +- path: operators/bex-op-filter.yaml + role: behavior-fixture + sha256: 28ac4fdbcaafe00fdd582a8c3f419462cae8952caf94644b573b3b7b8d13e4fd + bytes: 460 +- path: operators/bex-op-find.yaml + role: behavior-fixture + sha256: 8e1f1509c0a7b3dfbbce4b65aaa6eae7d89e09414e655703603c957cbad12375 + bytes: 444 +- path: operators/bex-op-findentry.yaml + role: behavior-fixture + sha256: e9edd0158af743933b37d1659702bd2ae9e3a3d0f4fb217f6786819e45c24420 + bytes: 488 +- path: operators/bex-op-flatmap.yaml + role: behavior-fixture + sha256: 3adb2b973f48c30d2c551f3c5ffe2beed4a284b2840e09c3f1f2ba5d9f0fde3f + bytes: 453 +- path: operators/bex-op-get.yaml + role: behavior-fixture + sha256: a7b1fcf6fb15040863c1b29f0255714c049d22a6d63eeb910147d839c82742f5 + bytes: 371 +- path: operators/bex-op-gt.yaml + role: behavior-fixture + sha256: bada572ba6be2d1cd0fe73f2d67bb830335b0e65a504e462ef722bd1cdb65d25 + bytes: 347 +- path: operators/bex-op-gte.yaml + role: behavior-fixture + sha256: 15f8d2a09dc28063cae4064a72c335b749ba208551bb0b2ed526d85b4af98db3 + bytes: 350 +- path: operators/bex-op-haskey.yaml + role: behavior-fixture + sha256: d06db2bbcb4eaddbe574eab9be04179323173933590d43c5fa1aa83b44fc30c3 + bytes: 383 +- path: operators/bex-op-includes.yaml + role: behavior-fixture + sha256: 5c525b7a3969c0990b438e9118f58e70a34856a1595c7fa4152e441e95060165 + bytes: 394 +- path: operators/bex-op-isempty.yaml + role: behavior-fixture + sha256: 5f7bc6d8662c2d85f1fb3c416540c41bf16e288a847a2ef3ec8d26d90aadb8ba + bytes: 349 +- path: operators/bex-op-iskind.yaml + role: behavior-fixture + sha256: 06d9522c7c12fe1442e8742402dbb494aa8b6d7414fc19e5bdd6fda72c4eba03 + bytes: 399 +- path: operators/bex-op-join.yaml + role: behavior-fixture + sha256: 36696057002ba4324d5c6b4ccdb4e218696fbee7a9ce27042767c9e83d467a36 + bytes: 401 +- path: operators/bex-op-list.yaml + role: behavior-fixture + sha256: 52537399e1697d3f00994e5af99f8e09ef0c3ae246e56b5cb976d2dfd38b31a6 + bytes: 340 +- path: operators/bex-op-listconcat.yaml + role: behavior-fixture + sha256: f366feac444369710b7a64bed32f0d9724f1d0edea4dbcc0300d32e40df10b98 + bytes: 398 +- path: operators/bex-op-listget.yaml + role: behavior-fixture + sha256: b3e9d08b660837df638801faed738e226d3daaf822c1aa79c8763efd2a762c82 + bytes: 390 +- path: operators/bex-op-lt.yaml + role: behavior-fixture + sha256: 625dd2caee9465bd9b75312676baa0be547192ee035fbf4ec936f2d074e42e4e + bytes: 347 +- path: operators/bex-op-lte.yaml + role: behavior-fixture + sha256: 66a58976577f9fe940e630c0fe777c2cab25858eb47b08071b2828c78789361a + bytes: 350 +- path: operators/bex-op-merge.yaml + role: behavior-fixture + sha256: 994845cc53b16858fbd0b73373583e85a3f0bfa921ca3317ebde3704b951530f + bytes: 386 +- path: operators/bex-op-ne.yaml + role: behavior-fixture + sha256: d9224e5dffe307ed67d1fac7e483ca07e302b4446789da0eceb6e161a3127b31 + bytes: 347 +- path: operators/bex-op-not.yaml + role: behavior-fixture + sha256: 44128f91ab89d1fc80b2a3b6d68ed2b313f017758990dadb87e078ca5a7f4f9c + bytes: 340 +- path: operators/bex-op-object.yaml + role: behavior-fixture + sha256: ea2d7f40ef32b88be59cf8a752c08f4af41418fc2ec1166319c854609ed934b5 + bytes: 346 +- path: operators/bex-op-objectfromentries.yaml + role: behavior-fixture + sha256: b1a9d4bf2a0abaf162709525028c8b726bb78d17e43780f034d7ce4ba78a064d + bytes: 441 +- path: operators/bex-op-objectset.yaml + role: behavior-fixture + sha256: 51a95b9b91a448ee9487e2561b373b294cadeb7c02073c10034bb6f8758b7964 + bytes: 418 +- path: operators/bex-op-reduce.yaml + role: behavior-fixture + sha256: f8e1c91188e86bec596c6b681fd951f531b004bc78fa985a9d03d255615f9a50 + bytes: 487 +- path: operators/bex-op-sliceafter.yaml + role: behavior-fixture + sha256: 93a5977dcd6694331769d11ff975965f77f668dd2210fe21854781ac30036fde + bytes: 389 +- path: operators/bex-op-some.yaml + role: behavior-fixture + sha256: 5839dedf9932e3a54ffeb8e932df29b29269d44f5aacc647ff9c241ed8d2fa01 + bytes: 447 +- path: operators/bex-op-split.yaml + role: behavior-fixture + sha256: 631c17b4399f4d532a23e0c09a70eafceaf2bf251c4ee8c97521ca60cdab6d54 + bytes: 403 +- path: operators/bex-op-startswith.yaml + role: behavior-fixture + sha256: 0d26efdfa9e52d248cfbd9b4e93c5d0e2672f4e73ebf045dba3811b6d0802a8a + bytes: 388 +- path: operators/bex-op-subtract.yaml + role: behavior-fixture + sha256: cbb311a97da9125691eeac75025fff6b878e158a50db92ab99008b6099c319b7 + bytes: 370 +- path: operators/bex-op-unwrap.yaml + role: behavior-fixture + sha256: c3444cd7ec3c4b49787f811d4456b85603cda485d8bd0ab72dafe7862a5d8d5f + bytes: 370 +- path: projection-catalog.yaml + role: support + sha256: d562daf891e62840596cd5d51fe41fda05ed428d67315cce0ddd9d49d883c8ea + bytes: 3789 +- path: r/bex-r-01.yaml + role: behavior-fixture + sha256: 5f5466a3d0cbefb69d1c5820cf98066ff84edc780fdac1828f62a979b6c88a1a + bytes: 839 +- path: r/bex-r-02.yaml + role: behavior-fixture + sha256: aa63d39ddd63cd14bb812a655ba8b9776c34de611fe167490f6c833c4ac93f40 + bytes: 1661 +- path: r/bex-r-03.yaml + role: behavior-fixture + sha256: 927b09a71bc1995c75fb4af5cc5e0718ee172926f7bb1acde45217750d4cb40f + bytes: 561 +- path: r/bex-r-04.yaml + role: behavior-fixture + sha256: caee85a57652734e0b95e5f1921e8ff7bef2a64f280d083f22665e5524615a15 + bytes: 700 +- path: r/bex-r-05.yaml + role: behavior-fixture + sha256: 037b46f50d8587961f9d5ae5a698198811d1bcdd0e5979e1f9271e1c485579cb + bytes: 684 +- path: r/bex-r-06.yaml + role: behavior-fixture + sha256: 522744de0d426baf09e35ab869cbcb9f19c1aaec7948f6001e4ffa48b2f3eecb + bytes: 812 +- path: r/bex-r-07.yaml + role: behavior-fixture + sha256: 20decce24be6a9c433cc4a0982c6eb71ebec507d2843049f1fb56a2af8d8cf27 + bytes: 798 +- path: r/bex-r-08.yaml + role: behavior-fixture + sha256: 424e372a1ac81fb897ced8f1a404038e028129ee802e8442ca36ec1a8de54410 + bytes: 819 +- path: r/bex-r-09.yaml + role: behavior-fixture + sha256: 4caeb4145d4632ebc6524d7c26316f63ded2ce17309dd5a894902ecc7d28c218 + bytes: 660 +- path: s/bex-s-01.yaml + role: behavior-fixture + sha256: 22951c3b8f7cecab07a5e65333c348cd6e4ac0fcfe9be46965fd88accc88e076 + bytes: 651 +- path: s/bex-s-02.yaml + role: behavior-fixture + sha256: a5aa0e581d57f7fa8c3cc0e7fb5a4a34ba6dc7396b11da9f73491ba019e2874b + bytes: 504 +- path: s/bex-s-03.yaml + role: behavior-fixture + sha256: 504737bcab22cc6978a79c3c9fd47f34c6830a5630715d23b4a7a61163fbb1fb + bytes: 533 +- path: s/bex-s-04.yaml + role: behavior-fixture + sha256: 719b1fcad2b39ae79b0d3767f7b90a82dc82c505d45a01152155e07c9690ad4d + bytes: 427 +- path: s/bex-s-05.yaml + role: behavior-fixture + sha256: f8821096153f36159f357bf20209274e5e9c06080c6aa4ccddcdee97df5f852d + bytes: 600 +- path: s/bex-s-06.yaml + role: behavior-fixture + sha256: 628269f621348f4609794b3c79a2c821312ef408d65041f2caecd749613171e8 + bytes: 429 +- path: s/bex-s-07.yaml + role: behavior-fixture + sha256: 3d02069db067dd2d615d0d4c00f4630dda1a0a9faf8c0b281add51472fb91b72 + bytes: 579 +- path: vector-coverage.yaml + role: support + sha256: ded406cbf28e659ff2b0b7ab37bb839b1b64a2595555a3682cdbb978d922f8d0 + bytes: 4570 +packageIdentityAlgorithm: + digest: sha256 + encoding: UTF-8 canonical JSON with sorted keys + normalization: packageIdentity is null before hashing + lineEndings: LF +packageIdentity: sha256:a1b7bb2b3687389409bc9d0aa450c734f7856d2bcb818c95f4d7ecb19095d20e +gasSchedule: blue-bex/gas/2.0 +gasManifestPackageIdentity: sha256:41247c820d91a12fdfc17fd9e787a5d8d668d8acc5954fdcb131715bf9e6147d +gasManifestSha256: 1f689e0cf51b0f9afa6b18a640e0c755470921a7b0d66f62bfc2206679de640d diff --git a/src/test/resources/conformance/bex/fixtures/operator-coverage.yaml b/src/test/resources/conformance/bex/fixtures/operator-coverage.yaml new file mode 100644 index 0000000..cde658d --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/operator-coverage.yaml @@ -0,0 +1,331 @@ +schema: blue-bex-operator-coverage/2.0 +operatorCount: 86 +operators: +- operator: $add + fixtures: + - operators/bex-op-add.yaml + - operators/bex-op-reduce.yaml +- operator: $and + fixtures: + - operators/bex-op-and.yaml +- operator: $appendChange + fixtures: + - h/bex-h-04.yaml + - operators/bex-op-changeset.yaml + - s/bex-s-02.yaml + - s/bex-s-03.yaml + - s/bex-s-05.yaml +- operator: $appendChanges + fixtures: + - operators/bex-op-appendchanges.yaml +- operator: $appendEvent + fixtures: + - g/bex-g-10.yaml + - operators/bex-op-events.yaml + - r/bex-r-06.yaml + - s/bex-s-01.yaml + - s/bex-s-04.yaml +- operator: $appendEvents + fixtures: + - operators/bex-op-appendevents.yaml +- operator: $binding + fixtures: + - e/bex-e-02.yaml +- operator: $boolean + fixtures: + - operators/bex-op-boolean.yaml +- operator: $call + fixtures: + - c/bex-c-04.yaml +- operator: $changeset + fixtures: + - operators/bex-op-changeset.yaml +- operator: $choose + fixtures: + - operators/bex-op-choose.yaml +- operator: $coalesce + fixtures: + - operators/bex-op-coalesce.yaml +- operator: $concat + fixtures: + - g/bex-g-02.yaml + - g/bex-g-05.yaml + - r/bex-r-07.yaml +- operator: $const + fixtures: + - c/bex-c-06.yaml + - e/bex-e-02.yaml +- operator: $currentContract + fixtures: + - e/bex-e-02.yaml +- operator: $default + fixtures: + - operators/bex-op-default.yaml +- operator: $divide + fixtures: + - e/bex-e-08.yaml +- operator: $document + fixtures: + - e/bex-e-01.yaml + - e/bex-e-05.yaml + - e/bex-e-10.yaml + - g/bex-g-11.yaml + - h/bex-h-01.yaml + - h/bex-h-02.yaml + - h/bex-h-05.yaml + - operators/bex-op-coalesce.yaml + - operators/bex-op-default.yaml + - r/bex-r-01.yaml + - r/bex-r-02.yaml + - r/bex-r-03.yaml + - r/bex-r-04.yaml + - r/bex-r-06.yaml + - r/bex-r-08.yaml +- operator: $empty + fixtures: + - operators/bex-op-empty.yaml +- operator: $emptyList + fixtures: + - operators/bex-op-emptylist.yaml +- operator: $emptyObject + fixtures: + - operators/bex-op-emptyobject.yaml +- operator: $entries + fixtures: + - operators/bex-op-entries.yaml +- operator: $eq + fixtures: + - e/bex-e-14.yaml + - g/bex-g-08.yaml +- operator: $event + fixtures: + - e/bex-e-02.yaml + - h/bex-h-06.yaml +- operator: $events + fixtures: + - operators/bex-op-events.yaml +- operator: $exists + fixtures: + - e/bex-e-05.yaml + - r/bex-r-02.yaml + - r/bex-r-03.yaml +- operator: $fail + fixtures: + - e/bex-e-07.yaml + - g/bex-g-03.yaml + - operators/bex-op-and.yaml + - operators/bex-op-choose.yaml + - operators/bex-op-coalesce.yaml + - s/bex-s-02.yaml + - s/bex-s-06.yaml +- operator: $failIf + fixtures: + - operators/bex-op-failif.yaml +- operator: $filter + fixtures: + - operators/bex-op-filter.yaml +- operator: $find + fixtures: + - operators/bex-op-find.yaml +- operator: $findEntry + fixtures: + - operators/bex-op-findentry.yaml +- operator: $flatMap + fixtures: + - operators/bex-op-flatmap.yaml +- operator: $forEach + fixtures: + - s/bex-s-01.yaml +- operator: $get + fixtures: + - operators/bex-op-get.yaml +- operator: $gt + fixtures: + - operators/bex-op-filter.yaml + - operators/bex-op-find.yaml + - operators/bex-op-findentry.yaml + - operators/bex-op-gt.yaml + - operators/bex-op-some.yaml +- operator: $gte + fixtures: + - operators/bex-op-gte.yaml +- operator: $hasKey + fixtures: + - operators/bex-op-haskey.yaml +- operator: $if + fixtures: + - s/bex-s-01.yaml +- operator: $includes + fixtures: + - operators/bex-op-includes.yaml +- operator: $integer + fixtures: + - e/bex-e-08.yaml +- operator: $intrinsic + fixtures: + - g/bex-g-09.yaml + - g/bex-g-14.yaml +- operator: $is + fixtures: + - c/bex-c-06.yaml +- operator: $isEmpty + fixtures: + - operators/bex-op-isempty.yaml +- operator: $isKind + fixtures: + - operators/bex-op-iskind.yaml +- operator: $join + fixtures: + - operators/bex-op-join.yaml +- operator: $keys + fixtures: + - e/bex-e-09.yaml + - r/bex-r-02.yaml +- operator: $kind + fixtures: + - e/bex-e-10.yaml + - r/bex-r-02.yaml +- operator: $let + fixtures: + - e/bex-e-02.yaml + - e/bex-e-13.yaml + - s/bex-s-07.yaml +- operator: $list + fixtures: + - operators/bex-op-list.yaml +- operator: $listConcat + fixtures: + - operators/bex-op-listconcat.yaml +- operator: $listGet + fixtures: + - operators/bex-op-listget.yaml +- operator: $literal + fixtures: + - c/bex-c-03.yaml +- operator: $lt + fixtures: + - operators/bex-op-lt.yaml +- operator: $lte + fixtures: + - operators/bex-op-lte.yaml +- operator: $map + fixtures: + - e/bex-e-11.yaml + - g/bex-g-07.yaml +- operator: $merge + fixtures: + - operators/bex-op-merge.yaml +- operator: $multiply + fixtures: + - g/bex-g-06.yaml +- operator: $ne + fixtures: + - operators/bex-op-ne.yaml +- operator: $nodeBlueId + fixtures: + - e/bex-e-14.yaml + - r/bex-r-04.yaml + - r/bex-r-05.yaml +- operator: $not + fixtures: + - operators/bex-op-not.yaml +- operator: $null + fixtures: + - e/bex-e-03.yaml + - e/bex-e-05.yaml + - e/bex-e-06.yaml +- operator: $number + fixtures: + - h/bex-h-05.yaml +- operator: $object + fixtures: + - operators/bex-op-object.yaml +- operator: $objectFromEntries + fixtures: + - operators/bex-op-objectfromentries.yaml +- operator: $objectSet + fixtures: + - operators/bex-op-objectset.yaml +- operator: $or + fixtures: + - e/bex-e-07.yaml + - g/bex-g-03.yaml +- operator: $pointerGet + fixtures: + - e/bex-e-03.yaml +- operator: $pointerJoin + fixtures: + - e/bex-e-04.yaml +- operator: $pointerSet + fixtures: + - e/bex-e-12.yaml + - g/bex-g-04.yaml +- operator: $processingEvent + fixtures: + - e/bex-e-02.yaml + - h/bex-h-06.yaml +- operator: $reduce + fixtures: + - operators/bex-op-reduce.yaml +- operator: $resultValue + fixtures: + - h/bex-h-04.yaml + - s/bex-s-05.yaml +- operator: $return + fixtures: + - e/bex-e-02.yaml + - e/bex-e-13.yaml + - h/bex-h-04.yaml + - operators/bex-op-changeset.yaml + - operators/bex-op-events.yaml + - s/bex-s-05.yaml +- operator: $returnIf + fixtures: + - s/bex-s-06.yaml +- operator: $set + fixtures: + - c/bex-c-07.yaml +- operator: $size + fixtures: + - r/bex-r-02.yaml +- operator: $sliceAfter + fixtures: + - operators/bex-op-sliceafter.yaml +- operator: $some + fixtures: + - operators/bex-op-some.yaml +- operator: $split + fixtures: + - operators/bex-op-split.yaml +- operator: $startsWith + fixtures: + - operators/bex-op-startswith.yaml +- operator: $steps + fixtures: + - e/bex-e-02.yaml +- operator: $subtract + fixtures: + - operators/bex-op-subtract.yaml +- operator: $text + fixtures: + - e/bex-e-08.yaml +- operator: $truthy + fixtures: + - e/bex-e-06.yaml +- operator: $unwrap + fixtures: + - operators/bex-op-unwrap.yaml +- operator: $var + fixtures: + - e/bex-e-02.yaml + - e/bex-e-11.yaml + - e/bex-e-13.yaml + - g/bex-g-07.yaml + - operators/bex-op-filter.yaml + - operators/bex-op-find.yaml + - operators/bex-op-findentry.yaml + - operators/bex-op-flatmap.yaml + - operators/bex-op-reduce.yaml + - operators/bex-op-some.yaml + - s/bex-s-01.yaml + - s/bex-s-07.yaml diff --git a/src/test/resources/conformance/bex/fixtures/operators/bex-op-add.yaml b/src/test/resources/conformance/bex/fixtures/operators/bex-op-add.yaml new file mode 100644 index 0000000..c3c4b01 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/operators/bex-op-add.yaml @@ -0,0 +1,23 @@ +schema: blue-bex-fixture/2.0 +id: bex-op-add +vectors: +- BEX-E-08 +category: operator +description: Direct executable coverage for `$add`. +program: + expr: + $add: + - 1 + - 2 + - 3 +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + result: 6 + assertions: [] diff --git a/src/test/resources/conformance/bex/fixtures/operators/bex-op-and.yaml b/src/test/resources/conformance/bex/fixtures/operators/bex-op-and.yaml new file mode 100644 index 0000000..0894843 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/operators/bex-op-and.yaml @@ -0,0 +1,23 @@ +schema: blue-bex-fixture/2.0 +id: bex-op-and +vectors: +- BEX-E-07 +category: operator +description: Direct executable coverage for `$and`. +program: + expr: + $and: + - true + - false + - $fail: must-not-run +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + result: false + assertions: [] diff --git a/src/test/resources/conformance/bex/fixtures/operators/bex-op-appendchanges.yaml b/src/test/resources/conformance/bex/fixtures/operators/bex-op-appendchanges.yaml new file mode 100644 index 0000000..f777e78 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/operators/bex-op-appendchanges.yaml @@ -0,0 +1,30 @@ +schema: blue-bex-fixture/2.0 +id: bex-op-appendchanges +vectors: +- BEX-S-02 +category: operator +description: Direct executable coverage for `$appendChanges`. +program: + do: + - $appendChanges: + - op: add + path: /a + val: 1 + - op: remove + path: /b +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + changes: + - op: add + path: /a + val: 1 + - op: remove + path: /b + assertions: [] diff --git a/src/test/resources/conformance/bex/fixtures/operators/bex-op-appendevents.yaml b/src/test/resources/conformance/bex/fixtures/operators/bex-op-appendevents.yaml new file mode 100644 index 0000000..4f2aae5 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/operators/bex-op-appendevents.yaml @@ -0,0 +1,24 @@ +schema: blue-bex-fixture/2.0 +id: bex-op-appendevents +vectors: +- BEX-S-04 +category: operator +description: Direct executable coverage for `$appendEvents`. +program: + do: + - $appendEvents: + - id: A + - id: A +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + events: + - id: A + - id: A + assertions: [] diff --git a/src/test/resources/conformance/bex/fixtures/operators/bex-op-boolean.yaml b/src/test/resources/conformance/bex/fixtures/operators/bex-op-boolean.yaml new file mode 100644 index 0000000..c2c17d7 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/operators/bex-op-boolean.yaml @@ -0,0 +1,20 @@ +schema: blue-bex-fixture/2.0 +id: bex-op-boolean +vectors: +- BEX-E-08 +category: operator +description: Direct executable coverage for `$boolean`. +program: + expr: + $boolean: 'true' +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + result: true + assertions: [] diff --git a/src/test/resources/conformance/bex/fixtures/operators/bex-op-changeset.yaml b/src/test/resources/conformance/bex/fixtures/operators/bex-op-changeset.yaml new file mode 100644 index 0000000..26a2666 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/operators/bex-op-changeset.yaml @@ -0,0 +1,32 @@ +schema: blue-bex-fixture/2.0 +id: bex-op-changeset +vectors: +- BEX-S-05 +category: operator +description: Direct executable coverage for `$changeset`. +program: + do: + - $appendChange: + op: add + path: /a + val: 1 + - $return: + $changeset: null +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + result: + - op: add + path: /a + val: 1 + changes: + - op: add + path: /a + val: 1 + assertions: [] diff --git a/src/test/resources/conformance/bex/fixtures/operators/bex-op-choose.yaml b/src/test/resources/conformance/bex/fixtures/operators/bex-op-choose.yaml new file mode 100644 index 0000000..9509890 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/operators/bex-op-choose.yaml @@ -0,0 +1,24 @@ +schema: blue-bex-fixture/2.0 +id: bex-op-choose +vectors: +- BEX-E-07 +category: operator +description: Direct executable coverage for `$choose`. +program: + expr: + $choose: + cond: true + then: 1 + else: + $fail: must-not-run +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + result: 1 + assertions: [] diff --git a/src/test/resources/conformance/bex/fixtures/operators/bex-op-coalesce.yaml b/src/test/resources/conformance/bex/fixtures/operators/bex-op-coalesce.yaml new file mode 100644 index 0000000..9c3f5d0 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/operators/bex-op-coalesce.yaml @@ -0,0 +1,24 @@ +schema: blue-bex-fixture/2.0 +id: bex-op-coalesce +vectors: +- BEX-E-07 +category: operator +description: Direct executable coverage for `$coalesce`. +program: + expr: + $coalesce: + - $document: /missing + - '' + - 7 + - $fail: must-not-run +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + result: 7 + assertions: [] diff --git a/src/test/resources/conformance/bex/fixtures/operators/bex-op-default.yaml b/src/test/resources/conformance/bex/fixtures/operators/bex-op-default.yaml new file mode 100644 index 0000000..693c20c --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/operators/bex-op-default.yaml @@ -0,0 +1,22 @@ +schema: blue-bex-fixture/2.0 +id: bex-op-default +vectors: +- BEX-E-07 +category: operator +description: Direct executable coverage for `$default`. +program: + expr: + $default: + - $document: /missing + - fallback +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + result: fallback + assertions: [] diff --git a/src/test/resources/conformance/bex/fixtures/operators/bex-op-empty.yaml b/src/test/resources/conformance/bex/fixtures/operators/bex-op-empty.yaml new file mode 100644 index 0000000..1b5baa3 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/operators/bex-op-empty.yaml @@ -0,0 +1,20 @@ +schema: blue-bex-fixture/2.0 +id: bex-op-empty +vectors: +- BEX-E-06 +category: operator +description: Direct executable coverage for `$empty`. +program: + expr: + $empty: [] +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + result: true + assertions: [] diff --git a/src/test/resources/conformance/bex/fixtures/operators/bex-op-emptylist.yaml b/src/test/resources/conformance/bex/fixtures/operators/bex-op-emptylist.yaml new file mode 100644 index 0000000..48da152 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/operators/bex-op-emptylist.yaml @@ -0,0 +1,20 @@ +schema: blue-bex-fixture/2.0 +id: bex-op-emptylist +vectors: +- BEX-E-06 +category: operator +description: Direct executable coverage for `$emptyList`. +program: + expr: + $emptyList: null +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + result: [] + assertions: [] diff --git a/src/test/resources/conformance/bex/fixtures/operators/bex-op-emptyobject.yaml b/src/test/resources/conformance/bex/fixtures/operators/bex-op-emptyobject.yaml new file mode 100644 index 0000000..459ca0e --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/operators/bex-op-emptyobject.yaml @@ -0,0 +1,20 @@ +schema: blue-bex-fixture/2.0 +id: bex-op-emptyobject +vectors: +- BEX-E-06 +category: operator +description: Direct executable coverage for `$emptyObject`. +program: + expr: + $emptyObject: null +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + result: {} + assertions: [] diff --git a/src/test/resources/conformance/bex/fixtures/operators/bex-op-entries.yaml b/src/test/resources/conformance/bex/fixtures/operators/bex-op-entries.yaml new file mode 100644 index 0000000..e3d9972 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/operators/bex-op-entries.yaml @@ -0,0 +1,26 @@ +schema: blue-bex-fixture/2.0 +id: bex-op-entries +vectors: +- BEX-E-09 +category: operator +description: Direct executable coverage for `$entries`. +program: + expr: + $entries: + b: 2 + a: 1 +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + result: + - key: a + val: 1 + - key: b + val: 2 + assertions: [] diff --git a/src/test/resources/conformance/bex/fixtures/operators/bex-op-events.yaml b/src/test/resources/conformance/bex/fixtures/operators/bex-op-events.yaml new file mode 100644 index 0000000..e1ee181 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/operators/bex-op-events.yaml @@ -0,0 +1,26 @@ +schema: blue-bex-fixture/2.0 +id: bex-op-events +vectors: +- BEX-S-04 +category: operator +description: Direct executable coverage for `$events`. +program: + do: + - $appendEvent: + id: A + - $return: + $events: null +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + result: + - id: A + events: + - id: A + assertions: [] diff --git a/src/test/resources/conformance/bex/fixtures/operators/bex-op-failif.yaml b/src/test/resources/conformance/bex/fixtures/operators/bex-op-failif.yaml new file mode 100644 index 0000000..7b8e17c --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/operators/bex-op-failif.yaml @@ -0,0 +1,23 @@ +schema: blue-bex-fixture/2.0 +id: bex-op-failif +vectors: +- BEX-S-06 +category: operator +description: Direct executable coverage for `$failIf`. +program: + do: + - $failIf: + cond: true + message: expected +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + errorClass: runtime-error + reason: expected + assertions: [] diff --git a/src/test/resources/conformance/bex/fixtures/operators/bex-op-filter.yaml b/src/test/resources/conformance/bex/fixtures/operators/bex-op-filter.yaml new file mode 100644 index 0000000..1a7fb60 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/operators/bex-op-filter.yaml @@ -0,0 +1,31 @@ +schema: blue-bex-fixture/2.0 +id: bex-op-filter +vectors: +- BEX-E-11 +category: operator +description: Direct executable coverage for `$filter`. +program: + expr: + $filter: + in: + - 1 + - 2 + - 3 + item: x + where: + $gt: + - $var: x + - 1 +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + result: + - 2 + - 3 + assertions: [] diff --git a/src/test/resources/conformance/bex/fixtures/operators/bex-op-find.yaml b/src/test/resources/conformance/bex/fixtures/operators/bex-op-find.yaml new file mode 100644 index 0000000..1cc9c70 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/operators/bex-op-find.yaml @@ -0,0 +1,29 @@ +schema: blue-bex-fixture/2.0 +id: bex-op-find +vectors: +- BEX-E-11 +category: operator +description: Direct executable coverage for `$find`. +program: + expr: + $find: + in: + - 1 + - 2 + - 3 + item: x + where: + $gt: + - $var: x + - 1 +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + result: 2 + assertions: [] diff --git a/src/test/resources/conformance/bex/fixtures/operators/bex-op-findentry.yaml b/src/test/resources/conformance/bex/fixtures/operators/bex-op-findentry.yaml new file mode 100644 index 0000000..f4ee098 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/operators/bex-op-findentry.yaml @@ -0,0 +1,31 @@ +schema: blue-bex-fixture/2.0 +id: bex-op-findentry +vectors: +- BEX-E-11 +category: operator +description: Direct executable coverage for `$findEntry`. +program: + expr: + $findEntry: + in: + b: 2 + a: 1 + item: x + key: k + where: + $gt: + - $var: x + - 1 +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + result: + key: b + val: 2 + assertions: [] diff --git a/src/test/resources/conformance/bex/fixtures/operators/bex-op-flatmap.yaml b/src/test/resources/conformance/bex/fixtures/operators/bex-op-flatmap.yaml new file mode 100644 index 0000000..3223fa7 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/operators/bex-op-flatmap.yaml @@ -0,0 +1,31 @@ +schema: blue-bex-fixture/2.0 +id: bex-op-flatmap +vectors: +- BEX-E-11 +category: operator +description: Direct executable coverage for `$flatMap`. +program: + expr: + $flatMap: + in: + - 1 + - 2 + item: x + expr: + - $var: x + - $var: x +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + result: + - 1 + - 1 + - 2 + - 2 + assertions: [] diff --git a/src/test/resources/conformance/bex/fixtures/operators/bex-op-get.yaml b/src/test/resources/conformance/bex/fixtures/operators/bex-op-get.yaml new file mode 100644 index 0000000..4ad0bfc --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/operators/bex-op-get.yaml @@ -0,0 +1,23 @@ +schema: blue-bex-fixture/2.0 +id: bex-op-get +vectors: +- BEX-E-02 +category: operator +description: Direct executable coverage for `$get`. +program: + expr: + $get: + object: + a: 1 + key: a +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + result: 1 + assertions: [] diff --git a/src/test/resources/conformance/bex/fixtures/operators/bex-op-gt.yaml b/src/test/resources/conformance/bex/fixtures/operators/bex-op-gt.yaml new file mode 100644 index 0000000..3cc85ee --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/operators/bex-op-gt.yaml @@ -0,0 +1,22 @@ +schema: blue-bex-fixture/2.0 +id: bex-op-gt +vectors: +- BEX-E-08 +category: operator +description: Direct executable coverage for `$gt`. +program: + expr: + $gt: + - 2 + - 1 +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + result: true + assertions: [] diff --git a/src/test/resources/conformance/bex/fixtures/operators/bex-op-gte.yaml b/src/test/resources/conformance/bex/fixtures/operators/bex-op-gte.yaml new file mode 100644 index 0000000..db85fda --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/operators/bex-op-gte.yaml @@ -0,0 +1,22 @@ +schema: blue-bex-fixture/2.0 +id: bex-op-gte +vectors: +- BEX-E-08 +category: operator +description: Direct executable coverage for `$gte`. +program: + expr: + $gte: + - 2 + - 2 +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + result: true + assertions: [] diff --git a/src/test/resources/conformance/bex/fixtures/operators/bex-op-haskey.yaml b/src/test/resources/conformance/bex/fixtures/operators/bex-op-haskey.yaml new file mode 100644 index 0000000..96746a8 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/operators/bex-op-haskey.yaml @@ -0,0 +1,23 @@ +schema: blue-bex-fixture/2.0 +id: bex-op-haskey +vectors: +- BEX-E-11 +category: operator +description: Direct executable coverage for `$hasKey`. +program: + expr: + $hasKey: + object: + a: 1 + key: a +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + result: true + assertions: [] diff --git a/src/test/resources/conformance/bex/fixtures/operators/bex-op-includes.yaml b/src/test/resources/conformance/bex/fixtures/operators/bex-op-includes.yaml new file mode 100644 index 0000000..06b612d --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/operators/bex-op-includes.yaml @@ -0,0 +1,24 @@ +schema: blue-bex-fixture/2.0 +id: bex-op-includes +vectors: +- BEX-E-11 +category: operator +description: Direct executable coverage for `$includes`. +program: + expr: + $includes: + list: + - 1 + - 2 + val: 2 +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + result: true + assertions: [] diff --git a/src/test/resources/conformance/bex/fixtures/operators/bex-op-isempty.yaml b/src/test/resources/conformance/bex/fixtures/operators/bex-op-isempty.yaml new file mode 100644 index 0000000..3681e0e --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/operators/bex-op-isempty.yaml @@ -0,0 +1,20 @@ +schema: blue-bex-fixture/2.0 +id: bex-op-isempty +vectors: +- BEX-E-06 +category: operator +description: Direct executable coverage for `$isEmpty`. +program: + expr: + $isEmpty: {} +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + result: true + assertions: [] diff --git a/src/test/resources/conformance/bex/fixtures/operators/bex-op-iskind.yaml b/src/test/resources/conformance/bex/fixtures/operators/bex-op-iskind.yaml new file mode 100644 index 0000000..92dc2e8 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/operators/bex-op-iskind.yaml @@ -0,0 +1,24 @@ +schema: blue-bex-fixture/2.0 +id: bex-op-iskind +vectors: +- BEX-E-10 +category: operator +description: Direct executable coverage for `$isKind`. +program: + expr: + $isKind: + val: 1 + kind: + - integer + - double +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + result: true + assertions: [] diff --git a/src/test/resources/conformance/bex/fixtures/operators/bex-op-join.yaml b/src/test/resources/conformance/bex/fixtures/operators/bex-op-join.yaml new file mode 100644 index 0000000..6a29025 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/operators/bex-op-join.yaml @@ -0,0 +1,25 @@ +schema: blue-bex-fixture/2.0 +id: bex-op-join +vectors: +- BEX-E-08 +category: operator +description: Direct executable coverage for `$join`. +program: + expr: + $join: + list: + - a + - b + - c + separator: '-' +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + result: a-b-c + assertions: [] diff --git a/src/test/resources/conformance/bex/fixtures/operators/bex-op-list.yaml b/src/test/resources/conformance/bex/fixtures/operators/bex-op-list.yaml new file mode 100644 index 0000000..8211d5a --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/operators/bex-op-list.yaml @@ -0,0 +1,20 @@ +schema: blue-bex-fixture/2.0 +id: bex-op-list +vectors: +- BEX-E-08 +category: operator +description: Direct executable coverage for `$list`. +program: + expr: + $list: null +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + result: [] + assertions: [] diff --git a/src/test/resources/conformance/bex/fixtures/operators/bex-op-listconcat.yaml b/src/test/resources/conformance/bex/fixtures/operators/bex-op-listconcat.yaml new file mode 100644 index 0000000..a5880a3 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/operators/bex-op-listconcat.yaml @@ -0,0 +1,26 @@ +schema: blue-bex-fixture/2.0 +id: bex-op-listconcat +vectors: +- BEX-E-11 +category: operator +description: Direct executable coverage for `$listConcat`. +program: + expr: + $listConcat: + - - 1 + - 2 + - - 3 +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + result: + - 1 + - 2 + - 3 + assertions: [] diff --git a/src/test/resources/conformance/bex/fixtures/operators/bex-op-listget.yaml b/src/test/resources/conformance/bex/fixtures/operators/bex-op-listget.yaml new file mode 100644 index 0000000..368f39f --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/operators/bex-op-listget.yaml @@ -0,0 +1,24 @@ +schema: blue-bex-fixture/2.0 +id: bex-op-listget +vectors: +- BEX-E-11 +category: operator +description: Direct executable coverage for `$listGet`. +program: + expr: + $listGet: + list: + - 1 + - 2 + index: 1 +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + result: 2 + assertions: [] diff --git a/src/test/resources/conformance/bex/fixtures/operators/bex-op-lt.yaml b/src/test/resources/conformance/bex/fixtures/operators/bex-op-lt.yaml new file mode 100644 index 0000000..fbbd6cf --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/operators/bex-op-lt.yaml @@ -0,0 +1,22 @@ +schema: blue-bex-fixture/2.0 +id: bex-op-lt +vectors: +- BEX-E-08 +category: operator +description: Direct executable coverage for `$lt`. +program: + expr: + $lt: + - 1 + - 2 +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + result: true + assertions: [] diff --git a/src/test/resources/conformance/bex/fixtures/operators/bex-op-lte.yaml b/src/test/resources/conformance/bex/fixtures/operators/bex-op-lte.yaml new file mode 100644 index 0000000..9321354 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/operators/bex-op-lte.yaml @@ -0,0 +1,22 @@ +schema: blue-bex-fixture/2.0 +id: bex-op-lte +vectors: +- BEX-E-08 +category: operator +description: Direct executable coverage for `$lte`. +program: + expr: + $lte: + - 2 + - 2 +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + result: true + assertions: [] diff --git a/src/test/resources/conformance/bex/fixtures/operators/bex-op-merge.yaml b/src/test/resources/conformance/bex/fixtures/operators/bex-op-merge.yaml new file mode 100644 index 0000000..504092d --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/operators/bex-op-merge.yaml @@ -0,0 +1,25 @@ +schema: blue-bex-fixture/2.0 +id: bex-op-merge +vectors: +- BEX-E-11 +category: operator +description: Direct executable coverage for `$merge`. +program: + expr: + $merge: + - a: 1 + - a: 2 + b: 3 +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + result: + a: 2 + b: 3 + assertions: [] diff --git a/src/test/resources/conformance/bex/fixtures/operators/bex-op-ne.yaml b/src/test/resources/conformance/bex/fixtures/operators/bex-op-ne.yaml new file mode 100644 index 0000000..eaf8e28 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/operators/bex-op-ne.yaml @@ -0,0 +1,22 @@ +schema: blue-bex-fixture/2.0 +id: bex-op-ne +vectors: +- BEX-E-14 +category: operator +description: Direct executable coverage for `$ne`. +program: + expr: + $ne: + - 1 + - 2 +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + result: true + assertions: [] diff --git a/src/test/resources/conformance/bex/fixtures/operators/bex-op-not.yaml b/src/test/resources/conformance/bex/fixtures/operators/bex-op-not.yaml new file mode 100644 index 0000000..ed954d8 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/operators/bex-op-not.yaml @@ -0,0 +1,20 @@ +schema: blue-bex-fixture/2.0 +id: bex-op-not +vectors: +- BEX-E-06 +category: operator +description: Direct executable coverage for `$not`. +program: + expr: + $not: false +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + result: true + assertions: [] diff --git a/src/test/resources/conformance/bex/fixtures/operators/bex-op-object.yaml b/src/test/resources/conformance/bex/fixtures/operators/bex-op-object.yaml new file mode 100644 index 0000000..e32f421 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/operators/bex-op-object.yaml @@ -0,0 +1,20 @@ +schema: blue-bex-fixture/2.0 +id: bex-op-object +vectors: +- BEX-E-08 +category: operator +description: Direct executable coverage for `$object`. +program: + expr: + $object: null +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + result: {} + assertions: [] diff --git a/src/test/resources/conformance/bex/fixtures/operators/bex-op-objectfromentries.yaml b/src/test/resources/conformance/bex/fixtures/operators/bex-op-objectfromentries.yaml new file mode 100644 index 0000000..840da20 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/operators/bex-op-objectfromentries.yaml @@ -0,0 +1,26 @@ +schema: blue-bex-fixture/2.0 +id: bex-op-objectfromentries +vectors: +- BEX-E-11 +category: operator +description: Direct executable coverage for `$objectFromEntries`. +program: + expr: + $objectFromEntries: + - key: a + val: 1 + - key: b + val: 2 +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + result: + a: 1 + b: 2 + assertions: [] diff --git a/src/test/resources/conformance/bex/fixtures/operators/bex-op-objectset.yaml b/src/test/resources/conformance/bex/fixtures/operators/bex-op-objectset.yaml new file mode 100644 index 0000000..728b857 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/operators/bex-op-objectset.yaml @@ -0,0 +1,26 @@ +schema: blue-bex-fixture/2.0 +id: bex-op-objectset +vectors: +- BEX-E-12 +category: operator +description: Direct executable coverage for `$objectSet`. +program: + expr: + $objectSet: + object: + a: 1 + key: b + val: 2 +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + result: + a: 1 + b: 2 + assertions: [] diff --git a/src/test/resources/conformance/bex/fixtures/operators/bex-op-reduce.yaml b/src/test/resources/conformance/bex/fixtures/operators/bex-op-reduce.yaml new file mode 100644 index 0000000..f87396c --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/operators/bex-op-reduce.yaml @@ -0,0 +1,31 @@ +schema: blue-bex-fixture/2.0 +id: bex-op-reduce +vectors: +- BEX-E-11 +category: operator +description: Direct executable coverage for `$reduce`. +program: + expr: + $reduce: + in: + - 1 + - 2 + - 3 + acc: sum + init: 0 + item: x + expr: + $add: + - $var: sum + - $var: x +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + result: 6 + assertions: [] diff --git a/src/test/resources/conformance/bex/fixtures/operators/bex-op-sliceafter.yaml b/src/test/resources/conformance/bex/fixtures/operators/bex-op-sliceafter.yaml new file mode 100644 index 0000000..3ac8b18 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/operators/bex-op-sliceafter.yaml @@ -0,0 +1,22 @@ +schema: blue-bex-fixture/2.0 +id: bex-op-sliceafter +vectors: +- BEX-E-08 +category: operator +description: Direct executable coverage for `$sliceAfter`. +program: + expr: + $sliceAfter: + - prefix-value + - prefix- +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + result: value + assertions: [] diff --git a/src/test/resources/conformance/bex/fixtures/operators/bex-op-some.yaml b/src/test/resources/conformance/bex/fixtures/operators/bex-op-some.yaml new file mode 100644 index 0000000..f9e4e0d --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/operators/bex-op-some.yaml @@ -0,0 +1,29 @@ +schema: blue-bex-fixture/2.0 +id: bex-op-some +vectors: +- BEX-E-11 +category: operator +description: Direct executable coverage for `$some`. +program: + expr: + $some: + in: + - 1 + - 2 + - 3 + item: x + where: + $gt: + - $var: x + - 2 +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + result: true + assertions: [] diff --git a/src/test/resources/conformance/bex/fixtures/operators/bex-op-split.yaml b/src/test/resources/conformance/bex/fixtures/operators/bex-op-split.yaml new file mode 100644 index 0000000..5a478e5 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/operators/bex-op-split.yaml @@ -0,0 +1,25 @@ +schema: blue-bex-fixture/2.0 +id: bex-op-split +vectors: +- BEX-E-08 +category: operator +description: Direct executable coverage for `$split`. +program: + expr: + $split: + text: a,b,c + separator: ',' + limit: 2 +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + result: + - a + - b,c + assertions: [] diff --git a/src/test/resources/conformance/bex/fixtures/operators/bex-op-startswith.yaml b/src/test/resources/conformance/bex/fixtures/operators/bex-op-startswith.yaml new file mode 100644 index 0000000..6d6e722 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/operators/bex-op-startswith.yaml @@ -0,0 +1,22 @@ +schema: blue-bex-fixture/2.0 +id: bex-op-startswith +vectors: +- BEX-E-08 +category: operator +description: Direct executable coverage for `$startsWith`. +program: + expr: + $startsWith: + - prefix-value + - prefix- +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + result: true + assertions: [] diff --git a/src/test/resources/conformance/bex/fixtures/operators/bex-op-subtract.yaml b/src/test/resources/conformance/bex/fixtures/operators/bex-op-subtract.yaml new file mode 100644 index 0000000..24a85aa --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/operators/bex-op-subtract.yaml @@ -0,0 +1,23 @@ +schema: blue-bex-fixture/2.0 +id: bex-op-subtract +vectors: +- BEX-E-08 +category: operator +description: Direct executable coverage for `$subtract`. +program: + expr: + $subtract: + - 7 + - 2 + - 1 +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + result: 4 + assertions: [] diff --git a/src/test/resources/conformance/bex/fixtures/operators/bex-op-unwrap.yaml b/src/test/resources/conformance/bex/fixtures/operators/bex-op-unwrap.yaml new file mode 100644 index 0000000..f97a53c --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/operators/bex-op-unwrap.yaml @@ -0,0 +1,22 @@ +schema: blue-bex-fixture/2.0 +id: bex-op-unwrap +vectors: +- BEX-E-08 +category: operator +description: Direct executable coverage for `$unwrap`. +program: + expr: + $unwrap: + value: + value: 3 +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + result: 3 + assertions: [] diff --git a/src/test/resources/conformance/bex/fixtures/projection-catalog.yaml b/src/test/resources/conformance/bex/fixtures/projection-catalog.yaml new file mode 100644 index 0000000..add4ba7 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/projection-catalog.yaml @@ -0,0 +1,80 @@ +schema: blue-bex-projection-catalog/2.0 +entries: +- path: demands + definition: Ordered logical semantic demands made by BEX. +- path: diagnostic.errorClass + definition: Portable compile/runtime diagnostic field. +- path: diagnostic.operator + definition: Portable compile/runtime diagnostic field. +- path: diagnostic.sourcePath + definition: Portable compile/runtime diagnostic field. +- path: effectiveRuntimeBudget + definition: Minimum of parent remaining gas and optional BEX-local sub-limit. +- path: gas.collectionItemProduced + definition: Exact quantity of the named BEX or semantic counter. +- path: gas.collectionItemVisited + definition: Exact quantity of the named BEX or semantic counter. +- path: gas.comparisonNodeVisited + definition: Exact quantity of the named BEX or semantic counter. +- path: gas.estimatedSize + definition: Exact quantity of the named BEX or semantic counter. +- path: gas.exact.recursiveConstruction + definition: Exact quantity of the named BEX or semantic counter. +- path: gas.failedChargePresent + definition: Exact quantity of the named BEX or semantic counter. +- path: gas.integerLimbOperation + definition: Exact quantity of the named BEX or semantic counter. +- path: gas.pointerSegmentWritten + definition: Exact quantity of the named BEX or semantic counter. +- path: gas.recursiveSizeCounter + definition: Exact quantity of the named BEX or semantic counter. +- path: gas.semanticIdentityMergeCount + definition: Exact quantity of the named BEX or semantic counter. +- path: gas.skippedOperandCharges + definition: Exact quantity of the named BEX or semantic counter. +- path: gas.sortComparison + definition: Exact quantity of the named BEX or semantic counter. +- path: gas.textBlockConstructed + definition: Exact quantity of the named BEX or semantic counter. +- path: gas.textBlockExamined + definition: Exact quantity of the named BEX or semantic counter. +- path: gas.totalAdmitted + definition: Total gas admitted before the failing charge. +- path: gas.trace + definition: Canonical ordered gas trace prefix available on exhaustion. +- path: gas.transient.transientObjectMemberProduced + definition: Exact quantity of the named BEX or semantic counter. +- path: gas.utf16Counter + definition: Exact quantity of the named BEX or semantic counter. +- path: gasTrace + definition: Canonical ordered BEX child-ledger trace. +- path: host.liveBounded + definition: Generic host-ledger integration invariant. +- path: host.runtimeChildMergeCount + definition: Generic host-ledger integration invariant. +- path: intrinsic.ledger.namedCounters + definition: Registered intrinsic child-ledger conformance projection. +- path: intrinsic.opaqueGas + definition: Registered intrinsic child-ledger conformance projection. +- path: locals.restored + definition: Function or iterator local-frame restoration projection. +- path: manifest.counterCoverage.complete + definition: Bound package-manifest coverage projection. +- path: output.nodeBlueId + definition: Blue output-admission result projection. +- path: output.reconstructed + definition: Blue output-admission result projection. +- path: output.type.blueId + definition: Blue output-admission result projection. +- path: parentBudgetAfter + definition: Parent remaining budget after one exact child-ledger merge. +- path: result + definition: Exact BEX semantic result before or after the declared Blue output boundary, as specified by the fixture. +- path: result.identityA + definition: Closed BEX conformance projection. +- path: result.semantic + definition: Closed BEX conformance projection. +- path: runtime.bufferedEffectsCommitted + definition: Whether buffered BEX changes/events crossed the successful result boundary. +- path: runtime.started + definition: Whether runtime execution began after compilation. diff --git a/src/test/resources/conformance/bex/fixtures/r/bex-r-01.yaml b/src/test/resources/conformance/bex/fixtures/r/bex-r-01.yaml new file mode 100644 index 0000000..b538ada --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/r/bex-r-01.yaml @@ -0,0 +1,40 @@ +schema: blue-bex-fixture/2.0 +id: bex-r-01 +vectors: +- BEX-R-01 +category: r +description: Inline, pure-reference, eagerly materialized, and lazily materialized forms of the same exact value produce the same result and gas. +program: + expr: + $document: /x/a +context: + rootDocument: + x: + blueId: FbqPSGFDzJtRSYfjogURf8wkzdDdoWJ7oaLVmTYKWE6v + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / + provider: + FbqPSGFDzJtRSYfjogURf8wkzdDdoWJ7oaLVmTYKWE6v: + a: 1 + values: + - 1 + - 2 +expected: + assertions: + - actual: result + op: sameAcrossVariants + - actual: gasTrace + op: sameAcrossVariants + variants: + - name: inline + rootForm: inline + - name: reference + rootForm: reference + - name: eager + rootForm: eager + - name: lazy + rootForm: lazy diff --git a/src/test/resources/conformance/bex/fixtures/r/bex-r-02.yaml b/src/test/resources/conformance/bex/fixtures/r/bex-r-02.yaml new file mode 100644 index 0000000..8704867 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/r/bex-r-02.yaml @@ -0,0 +1,85 @@ +schema: blue-bex-fixture/2.0 +id: bex-r-02 +vectors: +- BEX-R-02 +category: r +description: '`$kind`, `$exists`, `$keys`, `$entries`, `$size`, truthiness, exact/deep equality, matching, iteration, pointer reads, and explicit identity do not reveal reference state.' +program: + expr: + kind: + $kind: + $document: /x + exists: + $exists: + $document: /x/a + keys: + $keys: + $document: /x + entries: + $entries: + $document: /x + size: + $size: + $document: /x + truthiness: + $boolean: + $document: /x + identity: + $nodeBlueId: + $document: /x + exactEquality: + $eq: + - $document: /x + - $document: /same + deepEquality: + $eq: + - $document: /x + - a: 1 + values: + - 1 + - 2 + matching: + $is: + node: + $document: /x/a + pattern: + type: Integer + iteration: + $map: + in: + $document: /x/values + item: item + expr: + $var: item + pointerRead: + $pointerGet: + object: + $document: /x + path: /a +context: + rootDocument: + x: + blueId: FbqPSGFDzJtRSYfjogURf8wkzdDdoWJ7oaLVmTYKWE6v + same: + blueId: FbqPSGFDzJtRSYfjogURf8wkzdDdoWJ7oaLVmTYKWE6v + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / + provider: + FbqPSGFDzJtRSYfjogURf8wkzdDdoWJ7oaLVmTYKWE6v: + a: 1 + values: + - 1 + - 2 +expected: + assertions: + - actual: result + op: sameAcrossVariants + variants: + - name: inline + rootForm: inline + - name: reference + rootForm: reference diff --git a/src/test/resources/conformance/bex/fixtures/r/bex-r-03.yaml b/src/test/resources/conformance/bex/fixtures/r/bex-r-03.yaml new file mode 100644 index 0000000..0f4762c --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/r/bex-r-03.yaml @@ -0,0 +1,29 @@ +schema: blue-bex-fixture/2.0 +id: bex-r-03 +vectors: +- BEX-R-03 +category: r +description: A pure reference wrapper does not create a semantic child named `blueId`. +program: + expr: + $exists: + $document: /x/blueId +context: + rootDocument: + x: + blueId: FbqPSGFDzJtRSYfjogURf8wkzdDdoWJ7oaLVmTYKWE6v + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / + provider: + FbqPSGFDzJtRSYfjogURf8wkzdDdoWJ7oaLVmTYKWE6v: + a: 1 + values: + - 1 + - 2 +expected: + assertions: [] + result: false diff --git a/src/test/resources/conformance/bex/fixtures/r/bex-r-04.yaml b/src/test/resources/conformance/bex/fixtures/r/bex-r-04.yaml new file mode 100644 index 0000000..136b8a1 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/r/bex-r-04.yaml @@ -0,0 +1,32 @@ +schema: blue-bex-fixture/2.0 +id: bex-r-04 +vectors: +- BEX-R-04 +category: r +description: '`$nodeBlueId` returns exact identity without transitive expansion.' +program: + expr: + $nodeBlueId: + $document: /x +context: + rootDocument: + x: + blueId: FbqPSGFDzJtRSYfjogURf8wkzdDdoWJ7oaLVmTYKWE6v + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / + provider: + FbqPSGFDzJtRSYfjogURf8wkzdDdoWJ7oaLVmTYKWE6v: + a: 1 + values: + - 1 + - 2 +expected: + assertions: + - actual: demands + op: notContains + expected: FbqPSGFDzJtRSYfjogURf8wkzdDdoWJ7oaLVmTYKWE6v/descendants + result: FbqPSGFDzJtRSYfjogURf8wkzdDdoWJ7oaLVmTYKWE6v diff --git a/src/test/resources/conformance/bex/fixtures/r/bex-r-05.yaml b/src/test/resources/conformance/bex/fixtures/r/bex-r-05.yaml new file mode 100644 index 0000000..c3d124f --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/r/bex-r-05.yaml @@ -0,0 +1,34 @@ +schema: blue-bex-fixture/2.0 +id: bex-r-05 +vectors: +- BEX-R-05 +category: r +description: '`$nodeBlueId` on a transient value establishes one exact identity and merges semantic identity work once.' +program: + expr: + $nodeBlueId: + a: 1 + b: 2 +context: + rootDocument: + x: + blueId: FbqPSGFDzJtRSYfjogURf8wkzdDdoWJ7oaLVmTYKWE6v + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / + provider: + FbqPSGFDzJtRSYfjogURf8wkzdDdoWJ7oaLVmTYKWE6v: + a: 1 + values: + - 1 + - 2 +expected: + assertions: + - actual: gas.semanticIdentityMergeCount + op: equals + expected: 1 + - actual: result + op: present diff --git a/src/test/resources/conformance/bex/fixtures/r/bex-r-06.yaml b/src/test/resources/conformance/bex/fixtures/r/bex-r-06.yaml new file mode 100644 index 0000000..d81e965 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/r/bex-r-06.yaml @@ -0,0 +1,30 @@ +schema: blue-bex-fixture/2.0 +id: bex-r-06 +vectors: +- BEX-R-06 +category: r +description: Passing an existing large exact node through patches, events, constants, functions, and output has no recursive size charge. +program: + do: + - $appendEvent: + $document: /x +context: + rootDocument: + x: + blueId: 4Y2G5fYdCn2jELXoZj9yk6UXUbt54U8W2iYKfKg5CQzi + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / + provider: + 4Y2G5fYdCn2jELXoZj9yk6UXUbt54U8W2iYKfKg5CQzi: + largeExactText: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +expected: + assertions: + - actual: gas.textBlockExamined + op: equals + expected: 0 + - actual: gas.recursiveSizeCounter + op: absent diff --git a/src/test/resources/conformance/bex/fixtures/r/bex-r-07.yaml b/src/test/resources/conformance/bex/fixtures/r/bex-r-07.yaml new file mode 100644 index 0000000..2ae3eac --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/r/bex-r-07.yaml @@ -0,0 +1,35 @@ +schema: blue-bex-fixture/2.0 +id: bex-r-07 +vectors: +- BEX-R-07 +category: r +description: Constructing or scanning a large value pays member/Text/numeric work. +program: + expr: + $concat: + - xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + - yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy +context: + rootDocument: + x: + blueId: FbqPSGFDzJtRSYfjogURf8wkzdDdoWJ7oaLVmTYKWE6v + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / + provider: + FbqPSGFDzJtRSYfjogURf8wkzdDdoWJ7oaLVmTYKWE6v: + a: 1 + values: + - 1 + - 2 +expected: + assertions: + - actual: gas.textBlockExamined + op: greaterThan + expected: 0 + - actual: gas.textBlockConstructed + op: greaterThan + expected: 0 diff --git a/src/test/resources/conformance/bex/fixtures/r/bex-r-08.yaml b/src/test/resources/conformance/bex/fixtures/r/bex-r-08.yaml new file mode 100644 index 0000000..3f4deee --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/r/bex-r-08.yaml @@ -0,0 +1,40 @@ +schema: blue-bex-fixture/2.0 +id: bex-r-08 +vectors: +- BEX-R-08 +category: r +description: Warm/cold cache, provider batching, and physical segmentation do not change result or gas. +program: + expr: + $document: /x/a +context: + rootDocument: + x: + blueId: FbqPSGFDzJtRSYfjogURf8wkzdDdoWJ7oaLVmTYKWE6v + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / + provider: + FbqPSGFDzJtRSYfjogURf8wkzdDdoWJ7oaLVmTYKWE6v: + a: 1 + values: + - 1 + - 2 +expected: + assertions: + - actual: result + op: sameAcrossVariants + - actual: gasTrace + op: sameAcrossVariants + variants: + - name: cold-unbatched + rootForm: reference + cache: cold + batching: unbatched + - name: warm-batched + rootForm: reference + cache: warm + batching: batched diff --git a/src/test/resources/conformance/bex/fixtures/r/bex-r-09.yaml b/src/test/resources/conformance/bex/fixtures/r/bex-r-09.yaml new file mode 100644 index 0000000..b8cf9a3 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/r/bex-r-09.yaml @@ -0,0 +1,26 @@ +schema: blue-bex-fixture/2.0 +id: bex-r-09 +vectors: +- BEX-R-09 +category: r +description: 'A final cyclic-set member is an exact opaque value: $nodeBlueId returns MASTER#index without independent member hashing or provider demand.' +program: + expr: + $nodeBlueId: + $document: /x +context: + rootDocument: + x: + blueId: GX7CFU287wrZ7qw3LQG7gQi6UUoy1FFpM3tzupQJKi3N#0 + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + assertions: + - actual: demands + op: notContains + expected: GX7CFU287wrZ7qw3LQG7gQi6UUoy1FFpM3tzupQJKi3N#0 + result: GX7CFU287wrZ7qw3LQG7gQi6UUoy1FFpM3tzupQJKi3N#0 diff --git a/src/test/resources/conformance/bex/fixtures/s/bex-s-01.yaml b/src/test/resources/conformance/bex/fixtures/s/bex-s-01.yaml new file mode 100644 index 0000000..b75999a --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/s/bex-s-01.yaml @@ -0,0 +1,38 @@ +schema: blue-bex-fixture/2.0 +id: bex-s-01 +vectors: +- BEX-S-01 +category: s +description: '`$if` executes only one branch; `$forEach` binds item/key/index deterministically.' +program: + do: + - $if: + cond: true + then: + - $forEach: + in: + - 10 + - 20 + item: x + index: i + do: + - $appendEvent: + i: + $var: i + x: + $var: x +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + assertions: [] + events: + - i: 0 + x: 10 + - i: 1 + x: 20 diff --git a/src/test/resources/conformance/bex/fixtures/s/bex-s-02.yaml b/src/test/resources/conformance/bex/fixtures/s/bex-s-02.yaml new file mode 100644 index 0000000..56a1a32 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/s/bex-s-02.yaml @@ -0,0 +1,26 @@ +schema: blue-bex-fixture/2.0 +id: bex-s-02 +vectors: +- BEX-S-02 +category: s +description: '`$appendChange(s)` validates operations, requires `val` for add/replace, and does not evaluate remove `val`.' +program: + do: + - $appendChange: + op: remove + path: /x + val: + $fail: must-not-evaluate +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + assertions: [] + changes: + - op: remove + path: /x diff --git a/src/test/resources/conformance/bex/fixtures/s/bex-s-03.yaml b/src/test/resources/conformance/bex/fixtures/s/bex-s-03.yaml new file mode 100644 index 0000000..2dfad17 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/s/bex-s-03.yaml @@ -0,0 +1,33 @@ +schema: blue-bex-fixture/2.0 +id: bex-s-03 +vectors: +- BEX-S-03 +category: s +description: Duplicate patch paths are preserved in append order. +program: + do: + - $appendChange: + op: replace + path: /x + val: 1 + - $appendChange: + op: replace + path: /x + val: 2 +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + assertions: [] + changes: + - op: replace + path: /x + val: 1 + - op: replace + path: /x + val: 2 diff --git a/src/test/resources/conformance/bex/fixtures/s/bex-s-04.yaml b/src/test/resources/conformance/bex/fixtures/s/bex-s-04.yaml new file mode 100644 index 0000000..f66f10e --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/s/bex-s-04.yaml @@ -0,0 +1,25 @@ +schema: blue-bex-fixture/2.0 +id: bex-s-04 +vectors: +- BEX-S-04 +category: s +description: '`$appendEvent(s)` preserves order and multiplicity and rejects `undefined`.' +program: + do: + - $appendEvent: + id: A + - $appendEvent: + id: A +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + assertions: [] + events: + - id: A + - id: A diff --git a/src/test/resources/conformance/bex/fixtures/s/bex-s-05.yaml b/src/test/resources/conformance/bex/fixtures/s/bex-s-05.yaml new file mode 100644 index 0000000..720d541 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/s/bex-s-05.yaml @@ -0,0 +1,33 @@ +schema: blue-bex-fixture/2.0 +id: bex-s-05 +vectors: +- BEX-S-05 +category: s +description: '`$resultValue` applies accumulated patches in order, including parent/child replacement and sparse non-shifting list removal.' +program: + do: + - $appendChange: + op: replace + path: /a + val: + b: 1 + - $appendChange: + op: replace + path: /a/b + val: 2 + - $return: + $resultValue: /a +context: + rootDocument: + a: + b: 0 + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + assertions: [] + result: + b: 2 diff --git a/src/test/resources/conformance/bex/fixtures/s/bex-s-06.yaml b/src/test/resources/conformance/bex/fixtures/s/bex-s-06.yaml new file mode 100644 index 0000000..dee4340 --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/s/bex-s-06.yaml @@ -0,0 +1,23 @@ +schema: blue-bex-fixture/2.0 +id: bex-s-06 +vectors: +- BEX-S-06 +category: s +description: '`$return`, `$returnIf`, `$fail`, and `$failIf` have deterministic lazy exit behavior.' +program: + do: + - $returnIf: + cond: true + expr: 1 + - $fail: must-not-run +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + assertions: [] + result: 1 diff --git a/src/test/resources/conformance/bex/fixtures/s/bex-s-07.yaml b/src/test/resources/conformance/bex/fixtures/s/bex-s-07.yaml new file mode 100644 index 0000000..9dd1a9c --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/s/bex-s-07.yaml @@ -0,0 +1,35 @@ +schema: blue-bex-fixture/2.0 +id: bex-s-07 +vectors: +- BEX-S-07 +category: s +description: Parallel and ordered `$let` semantics are distinct and deterministic. +program: + do: + - $let: + vars: + a: 1 + b: + $var: a +context: + rootDocument: {} + event: {} + processingEvent: {} + currentContract: {} + steps: {} + bindings: {} + documentScope: / +expected: + assertions: [] + errorClass: runtime-error + additionalCase: + ordered: + $let: + order: + - a + - b + vars: + a: 1 + b: + $var: a + result: 1 diff --git a/src/test/resources/conformance/bex/fixtures/vector-coverage.yaml b/src/test/resources/conformance/bex/fixtures/vector-coverage.yaml new file mode 100644 index 0000000..4003a4c --- /dev/null +++ b/src/test/resources/conformance/bex/fixtures/vector-coverage.yaml @@ -0,0 +1,197 @@ +specification: blue-bex/2.0 +vectors: + BEX-C-01: + - c/bex-c-01.yaml + BEX-C-02: + - c/bex-c-02.yaml + BEX-C-03: + - c/bex-c-03.yaml + BEX-C-04: + - c/bex-c-04.yaml + BEX-C-05: + - c/bex-c-05.yaml + BEX-C-06: + - c/bex-c-06.yaml + BEX-C-07: + - c/bex-c-07.yaml + BEX-C-08: + - c/bex-c-08.yaml + BEX-C-09: + - c/bex-c-09.yaml + BEX-E-01: + - e/bex-e-01.yaml + BEX-E-02: + - e/bex-e-02.yaml + - operators/bex-op-get.yaml + BEX-E-03: + - e/bex-e-03.yaml + BEX-E-04: + - e/bex-e-04.yaml + BEX-E-05: + - e/bex-e-05.yaml + BEX-E-06: + - e/bex-e-06.yaml + - operators/bex-op-empty.yaml + - operators/bex-op-emptylist.yaml + - operators/bex-op-emptyobject.yaml + - operators/bex-op-isempty.yaml + - operators/bex-op-not.yaml + BEX-E-07: + - e/bex-e-07.yaml + - operators/bex-op-and.yaml + - operators/bex-op-choose.yaml + - operators/bex-op-coalesce.yaml + - operators/bex-op-default.yaml + BEX-E-08: + - e/bex-e-08.yaml + - operators/bex-op-add.yaml + - operators/bex-op-boolean.yaml + - operators/bex-op-gt.yaml + - operators/bex-op-gte.yaml + - operators/bex-op-join.yaml + - operators/bex-op-list.yaml + - operators/bex-op-lt.yaml + - operators/bex-op-lte.yaml + - operators/bex-op-object.yaml + - operators/bex-op-sliceafter.yaml + - operators/bex-op-split.yaml + - operators/bex-op-startswith.yaml + - operators/bex-op-subtract.yaml + - operators/bex-op-unwrap.yaml + BEX-E-09: + - e/bex-e-09.yaml + - operators/bex-op-entries.yaml + BEX-E-10: + - e/bex-e-10.yaml + - operators/bex-op-iskind.yaml + BEX-E-11: + - e/bex-e-11.yaml + - operators/bex-op-filter.yaml + - operators/bex-op-find.yaml + - operators/bex-op-findentry.yaml + - operators/bex-op-flatmap.yaml + - operators/bex-op-haskey.yaml + - operators/bex-op-includes.yaml + - operators/bex-op-listconcat.yaml + - operators/bex-op-listget.yaml + - operators/bex-op-merge.yaml + - operators/bex-op-objectfromentries.yaml + - operators/bex-op-reduce.yaml + - operators/bex-op-some.yaml + BEX-E-12: + - e/bex-e-12.yaml + - operators/bex-op-objectset.yaml + BEX-E-13: + - e/bex-e-13.yaml + BEX-E-14: + - e/bex-e-14.yaml + - operators/bex-op-ne.yaml + BEX-G-01: + - g/bex-g-01.yaml + - gas-micro/bindingRead.yaml + - gas-micro/blueOutputBoundary.yaml + - gas-micro/collectionItemProduced.yaml + - gas-micro/collectionItemVisited.yaml + - gas-micro/comparisonNodeVisited.yaml + - gas-micro/constantRead.yaml + - gas-micro/currentContractRead.yaml + - gas-micro/documentRead.yaml + - gas-micro/eventAppended.yaml + - gas-micro/eventRead.yaml + - gas-micro/expressionEvaluated.yaml + - gas-micro/functionCalled.yaml + - gas-micro/integerLimbOperation.yaml + - gas-micro/intrinsicCalled.yaml + - gas-micro/listItemRead.yaml + - gas-micro/nodeIdentityRequested.yaml + - gas-micro/objectMemberRead.yaml + - gas-micro/patchAppended.yaml + - gas-micro/pointerSegmentRead.yaml + - gas-micro/pointerSegmentWritten.yaml + - gas-micro/processingEventRead.yaml + - gas-micro/resultValueRead.yaml + - gas-micro/sortComparison.yaml + - gas-micro/statementExecuted.yaml + - gas-micro/stepsRead.yaml + - gas-micro/textBlockConstructed.yaml + - gas-micro/textBlockExamined.yaml + - gas-micro/transientListItemProduced.yaml + - gas-micro/transientObjectMemberProduced.yaml + - gas-micro/variableRead.yaml + BEX-G-02: + - g/bex-g-02.yaml + BEX-G-03: + - g/bex-g-03.yaml + BEX-G-04: + - g/bex-g-04.yaml + BEX-G-05: + - g/bex-g-05.yaml + BEX-G-06: + - g/bex-g-06.yaml + BEX-G-07: + - g/bex-g-07.yaml + BEX-G-08: + - g/bex-g-08.yaml + BEX-G-09: + - g/bex-g-09.yaml + BEX-G-10: + - g/bex-g-10.yaml + BEX-G-11: + - g/bex-g-11.yaml + BEX-G-12: + - g/bex-g-12.yaml + BEX-G-13: + - g/bex-g-13.yaml + BEX-G-14: + - g/bex-g-14.yaml + BEX-G-15: + - g/bex-g-15.yaml + BEX-H-01: + - h/bex-h-01.yaml + BEX-H-02: + - h/bex-h-02.yaml + BEX-H-03: + - h/bex-h-03.yaml + BEX-H-04: + - h/bex-h-04.yaml + BEX-H-05: + - h/bex-h-05.yaml + BEX-H-06: + - h/bex-h-06.yaml + BEX-R-01: + - r/bex-r-01.yaml + BEX-R-02: + - r/bex-r-02.yaml + BEX-R-03: + - r/bex-r-03.yaml + BEX-R-04: + - r/bex-r-04.yaml + BEX-R-05: + - r/bex-r-05.yaml + BEX-R-06: + - r/bex-r-06.yaml + BEX-R-07: + - r/bex-r-07.yaml + BEX-R-08: + - r/bex-r-08.yaml + BEX-R-09: + - r/bex-r-09.yaml + BEX-S-01: + - s/bex-s-01.yaml + BEX-S-02: + - operators/bex-op-appendchanges.yaml + - s/bex-s-02.yaml + BEX-S-03: + - s/bex-s-03.yaml + BEX-S-04: + - operators/bex-op-appendevents.yaml + - operators/bex-op-events.yaml + - s/bex-s-04.yaml + BEX-S-05: + - operators/bex-op-changeset.yaml + - s/bex-s-05.yaml + BEX-S-06: + - operators/bex-op-failif.yaml + - s/bex-s-06.yaml + BEX-S-07: + - s/bex-s-07.yaml diff --git a/src/test/resources/conformance/bex/gas-manifest.yaml b/src/test/resources/conformance/bex/gas-manifest.yaml new file mode 100644 index 0000000..0dc348f --- /dev/null +++ b/src/test/resources/conformance/bex/gas-manifest.yaml @@ -0,0 +1,91 @@ +manifestType: blue-bex-gas-manifest +schedule: blue-bex/gas/2.0 +specification: Blue BEX +specificationVersion: '2.0' +status: implementation-baseline-pending-calibration +unit: gas +hostSchedule: blue-contracts/gas/1.0 +counterCount: 30 +counters: + expressionEvaluated: 1 + statementExecuted: 1 + functionCalled: 2 + intrinsicCalled: 5 + documentRead: 2 + eventRead: 1 + processingEventRead: 1 + currentContractRead: 1 + stepsRead: 1 + bindingRead: 1 + variableRead: 1 + constantRead: 1 + resultValueRead: 2 + pointerSegmentRead: 1 + pointerSegmentWritten: 1 + objectMemberRead: 1 + listItemRead: 1 + collectionItemVisited: 1 + collectionItemProduced: 1 + textBlockExamined: 1 + textBlockConstructed: 1 + integerLimbOperation: 1 + comparisonNodeVisited: 1 + sortComparison: 1 + patchAppended: 5 + eventAppended: 5 + transientObjectMemberProduced: 1 + transientListItemProduced: 1 + blueOutputBoundary: 5 + nodeIdentityRequested: 5 +admissionRule: Admit quantity * weight before work into a child ledger bounded by the exact remaining Contracts budget. +mergeRule: Merge the ordered child ledger into the parent exactly once; a local limit may only reduce the remaining budget. +formulas: + textBlocks: + blockCodePoints: 64 + fullScan: ceil(codePointLength / 64) + construction: ceil(constructedCodePointLength / 64) + comparison: charge blocks actually read from each operand + integerLimbs: + radix: 2^32 + minimumLimbs: 1 + equalityOrOrdering: L(a) + L(b) + additionOrSubtraction: max(L(a), L(b)) + 1 + multiplication: L(a) * L(b) + divisionOrRemainder: L(a) * L(b) + decimalScaleAlignment: same unscaled Integer formula plus one operation + collectionIteration: + input: collectionItemVisited once per evaluated item + output: collectionItemProduced once per produced item + dynamicObject: transientObjectMemberProduced per retained field + dynamicList: transientListItemProduced per item + shortCircuit: no charges for skipped items + sorting: + algorithm: stable bottom-up merge sort + initialRunWidth: 1 + mergeOrder: left-to-right + equalSelection: left + widthProgression: double after each pass + comparisonCharges: + - sortComparison + - comparisonNodeVisited + - scalar content work + blueBoundary: + everyValue: blueOutputBoundary += 1 + existingExactValue: no recursive construction or size charge + transientValue: Contracts semantic identity establishment is merged once + nodeBlueIdTransient: nodeIdentityRequested + blueOutputBoundary + Contracts identity establishment +forbiddenPortableMetering: +- recursive estimatedSize +- serialized payload bytes at patch or event boundary +- UTF-16 length +- cache-dependent discounts +- opaque implementation-selected gasConsumed +fixtureRequirements: +- one exact microfixture per named counter +- operator behavior vectors +- representation-blind inline/reference variants +- shared-meter exhaustion prefix +- intrinsic named-ledger fixture +identityAlgorithm: sha256 of UTF-8 canonical JSON with packageIdentity set to null +packageIdentity: sha256:41247c820d91a12fdfc17fd9e787a5d8d668d8acc5954fdcb131715bf9e6147d +numericWeightsStatus: provisional pending calibration; counter names, ownership, formulas, and trace order are frozen for implementation diff --git a/src/test/resources/conformance/bex/registry/Compute2.blue b/src/test/resources/conformance/bex/registry/Compute2.blue new file mode 100644 index 0000000..7f3023f --- /dev/null +++ b/src/test/resources/conformance/bex/registry/Compute2.blue @@ -0,0 +1,16 @@ +name: Compute 2.0 +type: + blueId: 2Ag2NfcWpCfPqBAR7bFAEL9L3roX3UWGUkDq7nN3D4gV +description: Portable deterministic Handler runtime implementing Blue BEX Specification 2.0. It receives identity-preserving document, event, processingEvent, currentContract, steps, and bindings; returns a Contract Execution Result and one exact named child ledger. +program: + description: Required BEX 2.0 program or exact reference to it. + schema: + required: true +entry: + type: + blueId: GX7CFUmSDrE2MzptunLCCdZwnuwwrenRQqEnHL4x3uoC + description: Optional root function name. +gasLimit: + type: + blueId: E2LM6qgzWG9ttagq2xTmiZkgYEAgkYedFCmU9v7NnVEq + description: Optional runtime sub-limit that may only reduce the parent budget. diff --git a/src/test/resources/conformance/bex/registry/FixtureIntrinsic.blue b/src/test/resources/conformance/bex/registry/FixtureIntrinsic.blue new file mode 100644 index 0000000..ebe0572 --- /dev/null +++ b/src/test/resources/conformance/bex/registry/FixtureIntrinsic.blue @@ -0,0 +1,8 @@ +name: BEX Fixture Intrinsic 2.0 +description: Conformance-only deterministic intrinsic returning its payload and a declared named child counter ledger. +counter: + type: + blueId: GX7CFUmSDrE2MzptunLCCdZwnuwwrenRQqEnHL4x3uoC +quantity: + type: + blueId: E2LM6qgzWG9ttagq2xTmiZkgYEAgkYedFCmU9v7NnVEq diff --git a/src/test/resources/conformance/bex/registry/SortFixtureIntrinsic.blue b/src/test/resources/conformance/bex/registry/SortFixtureIntrinsic.blue new file mode 100644 index 0000000..21a1249 --- /dev/null +++ b/src/test/resources/conformance/bex/registry/SortFixtureIntrinsic.blue @@ -0,0 +1,5 @@ +name: BEX Sort Fixture Intrinsic 2.0 +description: Conformance-only deterministic intrinsic that sorts a supplied list using the canonical merge-sort schedule and reports named sortComparison counters. +values: + type: + blueId: 8DSFoWG9MqRSUhStqoPLrwVQiYByRh18NWbDEarN8MKF diff --git a/src/test/resources/conformance/bex/registry/manifest.yaml b/src/test/resources/conformance/bex/registry/manifest.yaml new file mode 100644 index 0000000..534791a --- /dev/null +++ b/src/test/resources/conformance/bex/registry/manifest.yaml @@ -0,0 +1,30 @@ +registry: blue-bex-runtime +registryKind: runtime-type +specificationVersion: '2.0' +languageVersion: '1.0' +contractsVersion: '1.0' +fixturePackageIdentity: sha256:a1b7bb2b3687389409bc9d0aa450c734f7856d2bcb818c95f4d7ecb19095d20e +entries: +- key: Compute2 + path: Compute2.blue + blueId: H7Z5Wyg8oxA63EdauGhw8nHtNysdmWh7CWdV1osKTKbb + sha256: b4f00a1f953e337982fdb668eee1e9fa987143c1fcfaba91cebd62a0a35956fa + semanticDescriptionIdentityBearing: true + fixtureOnly: false +- key: FixtureIntrinsic + path: FixtureIntrinsic.blue + blueId: 5Zbnaiu1hzRNEpuQmKNHuSmkiq5VqdZ5ros49gwGB674 + sha256: 8cfaf7cd9ff8bb4b833692001f2f68cda50179112d58e8f2e98fa2bfce72f4ea + semanticDescriptionIdentityBearing: true + fixtureOnly: true +- key: SortFixtureIntrinsic + path: SortFixtureIntrinsic.blue + blueId: 2R1WaEk8LVwFRMEGnsZ8HTj15QTz3tQEj9LDYYjGFJJG + sha256: 243833c0ac8a34a11c976c20199c6a89e815361447b1578e86e526b2942ea18a + semanticDescriptionIdentityBearing: true + fixtureOnly: true +packageIdentityAlgorithm: + digest: sha256 + encoding: UTF-8 canonical JSON with sorted keys + normalization: packageIdentity and fixturePackageIdentity are null before hashing +packageIdentity: sha256:23d282ec1c0bb016263922b1b49c369fdd537efdcf23e005eceeb888d7763fe1 diff --git a/src/test/resources/fixtures/customer-paynote-snapshot-bex-functions.yaml b/src/test/resources/fixtures/customer-paynote-snapshot-bex-functions.yaml index 96cbb71..ae0329c 100644 --- a/src/test/resources/fixtures/customer-paynote-snapshot-bex-functions.yaml +++ b/src/test/resources/fixtures/customer-paynote-snapshot-bex-functions.yaml @@ -108,7 +108,9 @@ contracts: $empty: $var: sessionId then: - - $empty: true + - $returnIf: + cond: false + expr: null - $let: name: pkg expr: @@ -136,7 +138,9 @@ contracts: args: sessionId: $var: sessionId - - $empty: true + - $returnIf: + cond: false + expr: null orderFieldRelativePath: args: key: @@ -199,7 +203,9 @@ contracts: $empty: $var: sessionId then: - - $empty: true + - $returnIf: + cond: false + expr: null - $call: function: ensureOrderLedger args: @@ -227,7 +233,9 @@ contracts: $var: pathText val: $var: val - - $empty: true + - $returnIf: + cond: false + expr: null setOrderField: args: sessionId: @@ -260,7 +268,9 @@ contracts: $var: relativePath val: $var: val - - $empty: true + - $returnIf: + cond: false + expr: null mergeOrderObjectField: args: sessionId: @@ -319,7 +329,9 @@ contracts: object: $var: entry path: /val - - $empty: true + - $returnIf: + cond: false + expr: null markPackagePayNoteSecured: args: packageOrderSessionId: @@ -348,7 +360,9 @@ contracts: - $var: normalizedAmount - $const: expectedPackageAmount then: - - $empty: true + - $returnIf: + cond: false + expr: null - $call: function: setOrderField args: @@ -369,7 +383,9 @@ contracts: args: sessionId: $var: packageOrderSessionId - - $empty: true + - $returnIf: + cond: false + expr: null markPackagePayNoteSecuredFromSnapshot: args: snapshot: @@ -384,7 +400,9 @@ contracts: snapshot: $var: snapshot then: - - $empty: true + - $returnIf: + cond: false + expr: null - $let: name: context expr: @@ -437,7 +455,9 @@ contracts: $var: amount path: /secured default: 0 - - $empty: true + - $returnIf: + cond: false + expr: null processCustomerPayNoteInitialSnapshot: args: payNoteSessionId: @@ -467,7 +487,9 @@ contracts: snapshot: $var: snapshot then: - - $empty: true + - $returnIf: + cond: false + expr: null - $let: name: context expr: @@ -501,7 +523,9 @@ contracts: $empty: $var: packageOrderSessionId then: - - $empty: true + - $returnIf: + cond: false + expr: null - $call: function: ensureOrderLedger args: @@ -575,7 +599,9 @@ contracts: - $var: packageOrderSessionId - /customerPayNote/attachedToPackageOrder then: - - $empty: true + - $returnIf: + cond: false + expr: null - $call: function: setOrderField args: @@ -597,7 +623,9 @@ contracts: $var: snapshot key: type val: MyOS/Call Operation Requested - - $empty: true + - $returnIf: + cond: false + expr: null placeResaleOrdersForOrder: args: sessionId: @@ -646,7 +674,9 @@ contracts: - $var: sessionId - /customerPayNote/secured then: - - $empty: true + - $returnIf: + cond: false + expr: null - $call: function: placeOneResaleOrder args: @@ -675,7 +705,9 @@ contracts: entitlement: title: Two-dish dinner with selected wines description: Dinner menu with selected wines. - - $empty: true + - $returnIf: + cond: false + expr: null placeOneResaleOrder: args: sessionId: @@ -734,7 +766,9 @@ contracts: - $var: requestId - /kind then: - - $empty: true + - $returnIf: + cond: false + expr: null - $appendChange: op: add path: @@ -810,7 +844,9 @@ contracts: $var: entitlement key: type val: MyOS/Call Operation Requested - - $empty: true + - $returnIf: + cond: false + expr: null processCustomerPayNoteSnapshotResolved: do: - $let: diff --git a/src/test/resources/hosted-release/baseline.properties b/src/test/resources/hosted-release/baseline.properties new file mode 100644 index 0000000..7920ab0 --- /dev/null +++ b/src/test/resources/hosted-release/baseline.properties @@ -0,0 +1,22 @@ +schema=blue-bex-hosted-release-baseline/1.0 +recordedDate=2026-07-28 +commit=6f15d637863f10c321098928fda85282f8ba16c0 +workspaceSha256=f043a09e997cda8706f786f59fc2f7beed645ba911669d68e6719699e6992ef8 +tests.executed=741 +tests.passed=741 +tests.failed=0 +tests.skipped=0 +behaviorFixtures=105 +gasMicrofixtures=30 +normativeVectors=60 +operators=86 +bexRegistryIdentity=sha256:23d282ec1c0bb016263922b1b49c369fdd537efdcf23e005eceeb888d7763fe1 +gasManifestIdentity=sha256:41247c820d91a12fdfc17fd9e787a5d8d668d8acc5954fdcb131715bf9e6147d +fixturePackageIdentity=sha256:a1b7bb2b3687389409bc9d0aa450c734f7856d2bcb818c95f4d7ecb19095d20e +mainJarSha256=c916f3b7d00f6a57d5f3e8106b9fc5bcdcd682760a058dca46f7a9dc2ab17efe +sourcesJarSha256=5694956b5bdf531fcc5216900a9e002a87596e999409d9461add02f5ed959143 +javadocJarSha256=7811d8ac0259b62e54ccc1cae182e5c95559d9577349ce0ac7df001e73dc1ab5 +blueLanguageCommit=f1f33ce30ab578bd6aedcdd81164fec85ad9fb87 +blueLanguageWorkspaceSha256=c496d34d0539b19ef3d343864348adab9dde62ac2dad0a5bb320cdf35319ba5e +czTomlSha256=2dd317fbe362561f0e9827c705ff6260fd6df26c16518e0acce99ecba595a4b1 +specificationSha256=b25d6d255f84c584ed7a484411430fab50c18142a1bb6c08cfb104acf09d6f69 diff --git a/src/test/resources/hosted-release/gas-exhaustion-trace-examples.properties b/src/test/resources/hosted-release/gas-exhaustion-trace-examples.properties new file mode 100644 index 0000000..b501d26 --- /dev/null +++ b/src/test/resources/hosted-release/gas-exhaustion-trace-examples.properties @@ -0,0 +1,45 @@ +schema=blue-bex-gas-exhaustion-trace-examples/1.0 +example.ids=statement-executed,function-called,event-appended,node-identity-requested +test.class=blue.bex.BexPrimitiveExhaustionEvidenceTest +admittedTrace.namespace=bex +admittedTrace.counterName=expressionEvaluated +admittedTrace.quantity=1 +admittedTrace.weight=1 +admittedTrace.size=1 +admittedTrace.totalGas=1 +example.statement-executed.testName=statementExecuted +example.statement-executed.namespace=bex +example.statement-executed.counterName=statementExecuted +example.statement-executed.quantity=1 +example.statement-executed.weight=1 +example.statement-executed.admittedGas=1 +example.statement-executed.effectiveBudget=1 +example.statement-executed.rejectedChargePresent=false +example.statement-executed.laterWorkCount=0 +example.function-called.testName=functionCalled +example.function-called.namespace=bex +example.function-called.counterName=functionCalled +example.function-called.quantity=1 +example.function-called.weight=2 +example.function-called.admittedGas=1 +example.function-called.effectiveBudget=1 +example.function-called.rejectedChargePresent=false +example.function-called.laterWorkCount=0 +example.event-appended.testName=eventAppended +example.event-appended.namespace=bex +example.event-appended.counterName=eventAppended +example.event-appended.quantity=1 +example.event-appended.weight=5 +example.event-appended.admittedGas=1 +example.event-appended.effectiveBudget=1 +example.event-appended.rejectedChargePresent=false +example.event-appended.laterWorkCount=0 +example.node-identity-requested.testName=nodeIdentityRequested +example.node-identity-requested.namespace=bex +example.node-identity-requested.counterName=nodeIdentityRequested +example.node-identity-requested.quantity=1 +example.node-identity-requested.weight=5 +example.node-identity-requested.admittedGas=1 +example.node-identity-requested.effectiveBudget=1 +example.node-identity-requested.rejectedChargePresent=false +example.node-identity-requested.laterWorkCount=0 diff --git a/src/test/resources/hosted-release/published-api-inspection.properties b/src/test/resources/hosted-release/published-api-inspection.properties new file mode 100644 index 0000000..de0e17e --- /dev/null +++ b/src/test/resources/hosted-release/published-api-inspection.properties @@ -0,0 +1,29 @@ +schema=blue-bex-published-host-api-inspection/1.0 +repository=https://repo1.maven.org/maven2 +metadata.lastUpdated=20260723002725 +metadata.latest=3.1.0-rc.19 +coordinate=blue.language:blue-language-java:3.1.0-rc.19 +artifact.sha256=e33e04065c6f9aa5189040a5816786953e1b5c58862f37a542e84ea2a3379235 +source.tag=v3.1.0-rc.19 +source.commit=8fd8af2ad90147336114774fb47c030a252682b3 +inspection=jar-tf-and-javap +standaloneCompile=failed-with-missing-symbols +standaloneCompile.minimumErrorCount=100 +class.blue.language.BlueOperationLimits=false +class.blue.language.BlueOperationOutcome=false +class.blue.language.BlueOperationResult=false +class.blue.language.processor.ExecutionEvidenceUnavailableException=false +class.blue.language.processor.RuntimeWorkSession=false +class.blue.language.processor.SemanticOutputBoundary=false +class.blue.language.processor.ExactBlueValue=false +class.blue.language.processor.GasChargeContext=false +class.blue.language.processor.GasLimitExceededException=false +class.blue.language.processor.InvalidExecutionEvidenceException=false +class.blue.language.processor.PortableLimitExceededException=false +visibility.blue.language.processor.GasMeter.public=false +method.blue.language.processor.ProcessorExecutionContext.runtimeWorkSession=false +method.blue.language.processor.ProcessorExecutionContext.semanticOutputBoundary=false +class.blue.language.processor.GasMeter.ChildGasLedger=false +method.blue.language.snapshot.ResolvedSnapshot.isResolutionComplete=false +method.blue.language.model.Schema.blueId=false +status=incompatible-with-current-hosted-adapter diff --git a/src/test/resources/hosted-release/required-public-api.txt b/src/test/resources/hosted-release/required-public-api.txt new file mode 100644 index 0000000..cfa4a6b --- /dev/null +++ b/src/test/resources/hosted-release/required-public-api.txt @@ -0,0 +1,789 @@ +schema=blue-bex-binary-api-manifest/1.0 +class public blue.bex.BexException extends java.lang.RuntimeException + constructor public (java.lang.String) + constructor public (java.lang.String,java.lang.Throwable) + method public sourcePath():java.util.Optional + method public static at(blue.bex.BexSourcePath,java.lang.String):blue.bex.BexException + method public static at(blue.bex.BexSourcePath,java.lang.String,java.lang.Throwable):blue.bex.BexException + method public withSourcePath(blue.bex.BexSourcePath):blue.bex.BexException +class public final blue.bex.BexSourcePath + constructor public (java.lang.String,java.lang.String,java.lang.String) + method public equals(java.lang.Object):boolean + method public functionName():java.lang.String + method public hashCode():int + method public operator():java.lang.String + method public pointer():java.lang.String + method public static of(java.lang.String,java.lang.String,java.lang.String):blue.bex.BexSourcePath + method public toString():java.lang.String +class public abstract interface blue.bex.api.BexDocumentView + method public abstract canonicalAt(java.lang.String):blue.bex.value.BexValue + method public abstract currentScopePath():java.lang.String + method public abstract resolvePointer(java.lang.String):java.lang.String + method public abstract resolvedAt(java.lang.String):blue.bex.value.BexValue +class public final blue.bex.api.BexEngine + method public compile(blue.bex.api.BexProgramSource):blue.bex.compile.BexCompiledProgram + method public compileAndExecute(blue.bex.api.BexProgramSource,blue.bex.api.BexExecutionContext):blue.bex.result.BexExecutionResult + method public execute(blue.bex.compile.BexCompiledProgram,blue.bex.api.BexExecutionContext):blue.bex.result.BexExecutionResult + method public static builder():blue.bex.api.BexEngine$Builder +class public static final blue.bex.api.BexEngine$Builder + constructor public () + method public blue(blue.language.Blue):blue.bex.api.BexEngine$Builder + method public build():blue.bex.api.BexEngine + method public cache(blue.bex.compile.BexCompiledProgramCache):blue.bex.api.BexEngine$Builder + method public gasSchedule(blue.bex.gas.BexGasSchedule):blue.bex.api.BexEngine$Builder + method public intrinsic(java.lang.Class,java.lang.String,java.util.Map,blue.bex.api.BexIntrinsicProcessor):blue.bex.api.BexEngine$Builder + method public intrinsic(java.lang.String,java.lang.String,java.util.Map,blue.bex.api.BexIntrinsicProcessor):blue.bex.api.BexEngine$Builder + method public intrinsics(blue.bex.api.BexIntrinsicRegistry):blue.bex.api.BexEngine$Builder + method public metrics(blue.bex.api.BexMetricsSink):blue.bex.api.BexEngine$Builder +class public final blue.bex.api.BexExecutionContext + method public binding(java.lang.String):blue.bex.value.BexValue + method public bindings():java.util.Map + method public currentContract():blue.bex.value.BexValue + method public currentScopePath():java.lang.String + method public document():blue.bex.api.BexDocumentView + method public event():blue.bex.value.BexValue + method public gasLedgerHost():blue.bex.api.BexGasLedgerHost + method public gasLimit():long + method public parentRemainingGas():long + method public processingEvent():blue.bex.value.BexValue + method public semanticIdentityBoundary():blue.bex.output.BexSemanticIdentityBoundary + method public static builder():blue.bex.api.BexExecutionContext$Builder + method public steps():blue.bex.api.BexStepResults +class public static final blue.bex.api.BexExecutionContext$Builder + constructor public () + method public binding(java.lang.String,blue.bex.value.BexValue):blue.bex.api.BexExecutionContext$Builder + method public bindings(java.util.Map):blue.bex.api.BexExecutionContext$Builder + method public build():blue.bex.api.BexExecutionContext + method public currentContract(blue.bex.value.BexValue):blue.bex.api.BexExecutionContext$Builder + method public document(blue.bex.api.BexDocumentView):blue.bex.api.BexExecutionContext$Builder + method public event(blue.bex.value.BexValue):blue.bex.api.BexExecutionContext$Builder + method public gasLedgerHost(blue.bex.api.BexGasLedgerHost):blue.bex.api.BexExecutionContext$Builder + method public gasLimit(long):blue.bex.api.BexExecutionContext$Builder + method public lazyBinding(java.lang.String,java.util.function.Supplier):blue.bex.api.BexExecutionContext$Builder + method public parentRemainingGas(long):blue.bex.api.BexExecutionContext$Builder + method public processingEvent(blue.bex.value.BexValue):blue.bex.api.BexExecutionContext$Builder + method public processorExecutionContext(blue.language.processor.ProcessorExecutionContext):blue.bex.api.BexExecutionContext$Builder + method public processorExecutionContext(blue.language.processor.ProcessorExecutionContext,java.lang.String):blue.bex.api.BexExecutionContext$Builder + method public semanticIdentityBoundary(blue.bex.output.BexSemanticIdentityBoundary):blue.bex.api.BexExecutionContext$Builder + method public steps(blue.bex.api.BexStepResults):blue.bex.api.BexExecutionContext$Builder +class public abstract interface blue.bex.api.BexGasLedgerHost + method public abstract evidenceUnavailable(blue.language.processor.GasMeter$ChildGasLedger):void + method public abstract failedDeterministically(blue.language.processor.GasMeter$ChildGasLedger):void + method public abstract open(java.lang.String,java.util.Map):blue.language.processor.GasMeter$ChildGasLedger + method public abstract submit(blue.language.processor.GasMeter$ChildGasLedger):void + method public localGasLimitExceeded(blue.bex.gas.BexGasLimitExceededException,java.lang.RuntimeException):java.lang.RuntimeException + method public propagateGasExhaustion(blue.language.processor.GasMeter$ChildGasLedger,blue.language.processor.GasLimitExceededException):void + method public separatesRuntimeNamespaces():boolean +class public final blue.bex.api.BexIntrinsicInvocation + method public blueId():java.lang.String + method public charge(java.lang.String,long):void + method public charge(java.lang.String,long,java.lang.String):void + method public exactField(java.lang.String):blue.bex.output.BexAdmittedValue + method public field(java.lang.String):blue.bex.value.BexValue + method public fields():java.util.Map + method public gasNamespace():java.lang.String + method public gasUsed():long + method public namedCounterWeights():java.util.Map + method public type():blue.bex.value.BexValue +class public abstract interface blue.bex.api.BexIntrinsicProcessor + method public abstract execute(blue.bex.api.BexIntrinsicInvocation):blue.bex.value.BexValue +class public final blue.bex.api.BexIntrinsicRegistry + method public identity():java.lang.String + method public invoke(java.lang.String,blue.bex.value.BexValue,java.util.Map,blue.bex.gas.BexGasMeter,blue.bex.output.BexOutputAdmission):blue.bex.value.BexValue + method public registeredNamedWeights():java.util.Map + method public registeredNamedWeights(java.util.Set):java.util.Map + method public registeredNamespaceWeights():java.util.Map + method public registeredNamespaceWeights(java.util.Set):java.util.Map + method public static builder():blue.bex.api.BexIntrinsicRegistry$Builder + method public static empty():blue.bex.api.BexIntrinsicRegistry + method public supportedBlueIds():java.util.Set + method public supports(java.lang.String):boolean + method public with(java.lang.Class,java.lang.String,java.util.Map,blue.bex.api.BexIntrinsicProcessor):blue.bex.api.BexIntrinsicRegistry + method public with(java.lang.String,java.lang.String,java.util.Map,blue.bex.api.BexIntrinsicProcessor):blue.bex.api.BexIntrinsicRegistry +class public static final blue.bex.api.BexIntrinsicRegistry$Builder + constructor public () + method public build():blue.bex.api.BexIntrinsicRegistry + method public register(java.lang.Class,java.lang.String,java.util.Map,blue.bex.api.BexIntrinsicProcessor):blue.bex.api.BexIntrinsicRegistry$Builder + method public register(java.lang.String,java.lang.String,java.lang.String,java.util.Map,blue.bex.api.BexIntrinsicProcessor):blue.bex.api.BexIntrinsicRegistry$Builder + method public register(java.lang.String,java.lang.String,java.util.Map,blue.bex.api.BexIntrinsicProcessor):blue.bex.api.BexIntrinsicRegistry$Builder +class public abstract interface blue.bex.api.BexMetricsSink + field public static final NOOP:blue.bex.api.BexMetricsSink + method public abstract accept(blue.bex.result.BexMetrics):void +class public final blue.bex.api.BexProgramSource + method public definitionNode():java.util.Optional + method public entry():java.util.Optional + method public isExpression():boolean + method public kind():blue.bex.api.BexProgramSource$Kind + method public programNode():blue.language.snapshot.FrozenNode + method public static expression(blue.language.snapshot.FrozenNode):blue.bex.api.BexProgramSource + method public static inline(blue.language.snapshot.FrozenNode):blue.bex.api.BexProgramSource + method public static withDefinition(blue.language.snapshot.FrozenNode,blue.language.snapshot.FrozenNode,java.lang.String):blue.bex.api.BexProgramSource +class public static final blue.bex.api.BexProgramSource$Kind extends java.lang.Enum + field public static final EXPRESSION:blue.bex.api.BexProgramSource$Kind + field public static final FULL_PROGRAM:blue.bex.api.BexProgramSource$Kind + method public static valueOf(java.lang.String):blue.bex.api.BexProgramSource$Kind + method public static values():blue.bex.api.BexProgramSource$Kind[] +class public final blue.bex.api.BexStepResults + method public asValue():blue.bex.value.BexValue + method public static builder():blue.bex.api.BexStepResults$Builder + method public static empty():blue.bex.api.BexStepResults + method public step(java.lang.String):blue.bex.value.BexValue +class public static final blue.bex.api.BexStepResults$Builder + constructor public () + method public build():blue.bex.api.BexStepResults + method public put(java.lang.String,blue.bex.result.BexExecutionResult):blue.bex.api.BexStepResults$Builder + method public put(java.lang.String,blue.bex.value.BexValue):blue.bex.api.BexStepResults$Builder +class public final blue.bex.api.FrozenBexDocumentView implements blue.bex.api.BexDocumentView + constructor public (blue.language.snapshot.FrozenNode) + constructor public (blue.language.snapshot.FrozenNode,blue.language.snapshot.FrozenNode,java.lang.String) + method public canonicalAt(java.lang.String):blue.bex.value.BexValue + method public currentScopePath():java.lang.String + method public resolvePointer(java.lang.String):java.lang.String + method public resolvedAt(java.lang.String):blue.bex.value.BexValue +class public final blue.bex.api.ProcessorExecutionContextBexDocumentView implements blue.bex.api.BexDocumentView + constructor public (blue.language.processor.ProcessorExecutionContext) + method public canonicalAt(java.lang.String):blue.bex.value.BexValue + method public currentScopePath():java.lang.String + method public resolvePointer(java.lang.String):java.lang.String + method public resolvedAt(java.lang.String):blue.bex.value.BexValue +class public final blue.bex.api.ProcessorExecutionContextBexGasLedgerHost implements blue.bex.api.BexGasLedgerHost + constructor public (blue.language.processor.ProcessorExecutionContext) + constructor public (blue.language.processor.ProcessorExecutionContext,java.lang.String) + constructor public (blue.language.processor.RuntimeWorkSession,java.lang.String) + method public evidenceUnavailable(blue.language.processor.GasMeter$ChildGasLedger):void + method public failedDeterministically(blue.language.processor.GasMeter$ChildGasLedger):void + method public localGasLimitExceeded(blue.bex.gas.BexGasLimitExceededException,java.lang.RuntimeException):java.lang.RuntimeException + method public open(java.lang.String,java.util.Map):blue.language.processor.GasMeter$ChildGasLedger + method public physicalNamespace(java.lang.String):java.lang.String + method public propagateGasExhaustion(blue.language.processor.GasMeter$ChildGasLedger,blue.language.processor.GasLimitExceededException):void + method public runtimeNamespace():java.lang.String + method public separatesRuntimeNamespaces():boolean + method public submit(blue.language.processor.GasMeter$ChildGasLedger):void +class public final blue.bex.compile.BexCompiledProgram + constructor public (blue.bex.compile.BexCompiledProgram$CompiledFunction,java.util.Map,java.util.Map,int,java.lang.String) + constructor public (blue.bex.compile.BexCompiledProgram$CompiledFunction,java.util.Map,java.util.Map,int,java.lang.String,java.util.Set) + method public constant(java.lang.String):blue.bex.value.BexValue + method public constants():java.util.Map + method public entry():blue.bex.compile.BexCompiledProgram$CompiledFunction + method public execute(blue.bex.runtime.BexRuntime):blue.bex.value.BexValue + method public functions():java.util.Map + method public programBlueId():java.lang.String + method public requiredIntrinsicBlueIds():java.util.Set + method public rootFrameSize():int +class public static final blue.bex.compile.BexCompiledProgram$ArgSpec + constructor public (java.lang.String,int,blue.language.snapshot.FrozenNode,java.lang.String) + method public name():java.lang.String + method public pattern():blue.language.snapshot.FrozenNode + method public slot():int + method public sourcePointer():java.lang.String + method public typed():boolean +class public static final blue.bex.compile.BexCompiledProgram$CompiledFunction + constructor public (java.lang.String,java.util.List,java.util.List,blue.bex.runtime.CompiledExpression,int) + method public arg(java.lang.String):blue.bex.compile.BexCompiledProgram$ArgSpec + method public argSlot(java.lang.String):int + method public args():java.util.Collection + method public frameSize():int + method public hasArg(java.lang.String):boolean + method public invokePrepared(blue.bex.runtime.BexRuntime,blue.bex.runtime.CompiledFrame,int[],blue.bex.value.BexValue[]):blue.bex.value.BexValue + method public invokeRoot(blue.bex.runtime.BexRuntime):blue.bex.value.BexValue + method public name():java.lang.String +class public abstract interface blue.bex.compile.BexCompiledProgramCache + method public abstract get(blue.bex.compile.BexCompiledProgramKey):blue.bex.compile.BexCompiledProgram + method public abstract put(blue.bex.compile.BexCompiledProgramKey,blue.bex.compile.BexCompiledProgram):void +class public final blue.bex.compile.BexCompiledProgramKey + constructor public (blue.bex.api.BexProgramSource$Kind,java.lang.String,java.lang.String,java.lang.String) + constructor public (blue.bex.api.BexProgramSource$Kind,java.lang.String,java.lang.String,java.lang.String,java.lang.String) + constructor public (java.lang.String,java.lang.String,java.lang.String) + field public static final BEX_RUNTIME_REGISTRY_IDENTITY:java.lang.String + field public static final COMPILER_IDENTITY:java.lang.String + method public compileEnvironmentIdentity():java.lang.String + method public definitionIdentity():java.lang.String + method public entryName():java.lang.String + method public equals(java.lang.Object):boolean + method public hashCode():int + method public kind():blue.bex.api.BexProgramSource$Kind + method public programIdentity():java.lang.String + method public static from(blue.bex.api.BexProgramSource):blue.bex.compile.BexCompiledProgramKey + method public static from(blue.bex.api.BexProgramSource,java.lang.String):blue.bex.compile.BexCompiledProgramKey +class public final blue.bex.compile.BexCompiler + constructor public (blue.bex.result.BexMetrics) + constructor public (blue.bex.result.BexMetrics,blue.bex.api.BexIntrinsicRegistry) + method public compile(blue.bex.api.BexProgramSource):blue.bex.compile.BexCompiledProgram +class public final blue.bex.compile.BexContainsCache + constructor public () + constructor public (int) + method public synchronized containsBex(blue.language.snapshot.FrozenNode,blue.bex.result.BexMetrics):boolean +class public final blue.bex.compile.BexNodeIdentity + method public static safeBlueId(blue.language.snapshot.FrozenNode):java.lang.String + method public static stable(blue.language.snapshot.FrozenNode):java.lang.String +class public final blue.bex.compile.LruBexCompiledProgramCache implements blue.bex.compile.BexCompiledProgramCache + constructor public () + constructor public (int) + method public synchronized get(blue.bex.compile.BexCompiledProgramKey):blue.bex.compile.BexCompiledProgram + method public synchronized put(blue.bex.compile.BexCompiledProgramKey,blue.bex.compile.BexCompiledProgram):void +class public final blue.bex.gas.BexGasCharge + constructor public (long,blue.bex.gas.BexGasCounter,long,long,java.lang.String,java.lang.String,java.lang.String) + constructor public (long,blue.bex.gas.BexGasCounter,long,long,long,java.lang.String,java.lang.String,java.lang.String) + constructor public (long,java.lang.String,java.lang.String,long,long,java.lang.String,java.lang.String,java.lang.String) + constructor public (long,java.lang.String,java.lang.String,long,long,long,java.lang.String,java.lang.String,java.lang.String) + method public counter():blue.bex.gas.BexGasCounter + method public counterName():java.lang.String + method public equals(java.lang.Object):boolean + method public gas():long + method public hashCode():int + method public namespace():java.lang.String + method public operator():java.lang.String + method public portableCounter():blue.bex.gas.BexGasCounter + method public qualifiedCounterName():java.lang.String + method public quantity():long + method public reason():java.lang.String + method public sequence():long + method public sourcePath():java.lang.String + method public toString():java.lang.String + method public weight():long +class public final blue.bex.gas.BexGasCounter extends java.lang.Enum + field public static final BINDING_READ:blue.bex.gas.BexGasCounter + field public static final BLUE_OUTPUT_BOUNDARY:blue.bex.gas.BexGasCounter + field public static final COLLECTION_ITEM_PRODUCED:blue.bex.gas.BexGasCounter + field public static final COLLECTION_ITEM_VISITED:blue.bex.gas.BexGasCounter + field public static final COMPARISON_NODE_VISITED:blue.bex.gas.BexGasCounter + field public static final CONSTANT_READ:blue.bex.gas.BexGasCounter + field public static final CURRENT_CONTRACT_READ:blue.bex.gas.BexGasCounter + field public static final DOCUMENT_READ:blue.bex.gas.BexGasCounter + field public static final EVENT_APPENDED:blue.bex.gas.BexGasCounter + field public static final EVENT_READ:blue.bex.gas.BexGasCounter + field public static final EXPRESSION_EVALUATED:blue.bex.gas.BexGasCounter + field public static final FUNCTION_CALLED:blue.bex.gas.BexGasCounter + field public static final INTEGER_LIMB_OPERATION:blue.bex.gas.BexGasCounter + field public static final INTRINSIC_CALLED:blue.bex.gas.BexGasCounter + field public static final LIST_ITEM_READ:blue.bex.gas.BexGasCounter + field public static final MANIFEST_IDENTITY:java.lang.String + field public static final NAMESPACE:java.lang.String + field public static final NODE_IDENTITY_REQUESTED:blue.bex.gas.BexGasCounter + field public static final OBJECT_MEMBER_READ:blue.bex.gas.BexGasCounter + field public static final PATCH_APPENDED:blue.bex.gas.BexGasCounter + field public static final POINTER_SEGMENT_READ:blue.bex.gas.BexGasCounter + field public static final POINTER_SEGMENT_WRITTEN:blue.bex.gas.BexGasCounter + field public static final PROCESSING_EVENT_READ:blue.bex.gas.BexGasCounter + field public static final RESULT_VALUE_READ:blue.bex.gas.BexGasCounter + field public static final SCHEDULE_ID:java.lang.String + field public static final SORT_COMPARISON:blue.bex.gas.BexGasCounter + field public static final STATEMENT_EXECUTED:blue.bex.gas.BexGasCounter + field public static final STEPS_READ:blue.bex.gas.BexGasCounter + field public static final TEXT_BLOCK_CONSTRUCTED:blue.bex.gas.BexGasCounter + field public static final TEXT_BLOCK_EXAMINED:blue.bex.gas.BexGasCounter + field public static final TRANSIENT_LIST_ITEM_PRODUCED:blue.bex.gas.BexGasCounter + field public static final TRANSIENT_OBJECT_MEMBER_PRODUCED:blue.bex.gas.BexGasCounter + field public static final VARIABLE_READ:blue.bex.gas.BexGasCounter + method public canonicalName():java.lang.String + method public counterName():java.lang.String + method public defaultWeight():long + method public static defaultWeights():java.util.Map + method public static fromCanonicalName(java.lang.String):blue.bex.gas.BexGasCounter + method public static fromName(java.lang.String):blue.bex.gas.BexGasCounter + method public static valueOf(java.lang.String):blue.bex.gas.BexGasCounter + method public static values():blue.bex.gas.BexGasCounter[] + method public toString():java.lang.String +class public final blue.bex.gas.BexGasLedger + constructor public (java.util.List) + method public equals(java.lang.Object):boolean + method public gasUsed():long + method public hashCode():int + method public manifestIdentity():java.lang.String + method public namedQuantities():java.util.Map + method public quantities():java.util.Map + method public quantity(blue.bex.gas.BexGasCounter):long + method public quantity(java.lang.String,java.lang.String):long + method public scheduleId():java.lang.String + method public static empty():blue.bex.gas.BexGasLedger + method public toString():java.lang.String + method public totalGas():long + method public trace():java.util.List +class public final blue.bex.gas.BexGasLimitExceededException extends blue.bex.BexException + method public admittedGas():long + method public counter():blue.bex.gas.BexGasCounter + method public counterName():java.lang.String + method public effectiveBudget():long + method public hostGasLimitExceeded():blue.language.processor.GasLimitExceededException + method public namespace():java.lang.String + method public quantity():long + method public weight():long +class public final blue.bex.gas.BexGasMeter + constructor public (blue.bex.gas.BexGasSchedule,blue.language.processor.GasMeter$ChildGasLedger) + constructor public (blue.bex.gas.BexGasSchedule,blue.language.processor.GasMeter$ChildGasLedger,long) + constructor public (blue.bex.gas.BexGasSchedule,java.util.Map,long,java.util.Map) + constructor public (blue.bex.gas.BexGasSchedule,long) + constructor public (blue.bex.gas.BexGasSchedule,long,long) + constructor public (blue.bex.gas.BexGasSchedule,long,long,java.util.Map) + field public static final NO_LOCAL_LIMIT:long + method public charge(blue.bex.gas.BexGasCounter):void + method public charge(blue.bex.gas.BexGasCounter,blue.bex.BexSourcePath,java.lang.String,java.lang.String):void + method public charge(blue.bex.gas.BexGasCounter,java.lang.String,java.lang.String,java.lang.String):void + method public charge(blue.bex.gas.BexGasCounter,long):void + method public charge(blue.bex.gas.BexGasCounter,long,blue.bex.BexSourcePath,java.lang.String,java.lang.String):void + method public charge(blue.bex.gas.BexGasCounter,long,java.lang.String):void + method public charge(blue.bex.gas.BexGasCounter,long,java.lang.String,java.lang.String,java.lang.String):void + method public chargeNamed(java.lang.String,java.lang.String,long):void + method public chargeNamed(java.lang.String,java.lang.String,long,blue.bex.BexSourcePath,java.lang.String,java.lang.String):void + method public chargeNamed(java.lang.String,java.lang.String,long,java.lang.String):void + method public chargeNamed(java.lang.String,java.lang.String,long,java.lang.String,java.lang.String,java.lang.String):void + method public chargeNamed(java.lang.String,java.lang.String,long,long,java.lang.String,java.lang.String,java.lang.String):void + method public childLedgerWeights():java.util.Map + method public effectiveBudget():long + method public failHostLedger(java.util.function.Consumer):void + method public hasHostLedger():boolean + method public hostLedgerFinalized():boolean + method public hostLedgerSubmitted():boolean + method public ledger():blue.bex.gas.BexGasLedger + method public localLimit():long + method public parentRemainingGas():long + method public propagateHostGasExhaustion(blue.language.processor.GasLimitExceededException,java.util.function.Consumer,java.util.function.BiConsumer):void + method public registeredNamedWeights():java.util.Map + method public remaining():long + method public remainingGas():long + method public schedule():blue.bex.gas.BexGasSchedule + method public static childLedgerWeights(blue.bex.gas.BexGasSchedule,java.util.Map):java.util.Map + method public static qualifiedCounterName(java.lang.String,java.lang.String):java.lang.String + method public submitHostLedger(java.util.function.Consumer):void + method public totalGas():long + method public trace():java.util.List + method public unavailableHostLedger(java.util.function.Consumer):void + method public used():long +class public final blue.bex.gas.BexGasSchedule + field public final bindingRead:long + field public final blueOutputBoundary:long + field public final collectionItemProduced:long + field public final collectionItemVisited:long + field public final comparisonNodeVisited:long + field public final constantRead:long + field public final currentContractRead:long + field public final documentRead:long + field public final eventAppended:long + field public final eventRead:long + field public final expressionEvaluated:long + field public final functionCalled:long + field public final integerLimbOperation:long + field public final intrinsicCalled:long + field public final listItemRead:long + field public final nodeIdentityRequested:long + field public final objectMemberRead:long + field public final patchAppended:long + field public final pointerSegmentRead:long + field public final pointerSegmentWritten:long + field public final processingEventRead:long + field public final resultValueRead:long + field public final sortComparison:long + field public final statementExecuted:long + field public final stepsRead:long + field public final textBlockConstructed:long + field public final textBlockExamined:long + field public final transientListItemProduced:long + field public final transientObjectMemberProduced:long + field public final variableRead:long + field public static final MANIFEST_IDENTITY:java.lang.String + field public static final SCHEDULE_ID:java.lang.String + method public counterWeights():java.util.Map + method public manifestIdentity():java.lang.String + method public namedWeights():java.util.Map + method public scheduleId():java.lang.String + method public static builder():blue.bex.gas.BexGasSchedule$Builder + method public static defaults():blue.bex.gas.BexGasSchedule + method public toBuilder():blue.bex.gas.BexGasSchedule$Builder + method public weight(blue.bex.gas.BexGasCounter):long + method public weight(java.lang.String):long + method public weights():java.util.Map +class public static final blue.bex.gas.BexGasSchedule$Builder + method public bindingRead(long):blue.bex.gas.BexGasSchedule$Builder + method public blueOutputBoundary(long):blue.bex.gas.BexGasSchedule$Builder + method public build():blue.bex.gas.BexGasSchedule + method public collectionItemProduced(long):blue.bex.gas.BexGasSchedule$Builder + method public collectionItemVisited(long):blue.bex.gas.BexGasSchedule$Builder + method public comparisonNodeVisited(long):blue.bex.gas.BexGasSchedule$Builder + method public constantRead(long):blue.bex.gas.BexGasSchedule$Builder + method public currentContractRead(long):blue.bex.gas.BexGasSchedule$Builder + method public documentRead(long):blue.bex.gas.BexGasSchedule$Builder + method public eventAppended(long):blue.bex.gas.BexGasSchedule$Builder + method public eventRead(long):blue.bex.gas.BexGasSchedule$Builder + method public expressionEvaluated(long):blue.bex.gas.BexGasSchedule$Builder + method public functionCalled(long):blue.bex.gas.BexGasSchedule$Builder + method public integerLimbOperation(long):blue.bex.gas.BexGasSchedule$Builder + method public intrinsicCalled(long):blue.bex.gas.BexGasSchedule$Builder + method public listItemRead(long):blue.bex.gas.BexGasSchedule$Builder + method public nodeIdentityRequested(long):blue.bex.gas.BexGasSchedule$Builder + method public objectMemberRead(long):blue.bex.gas.BexGasSchedule$Builder + method public patchAppended(long):blue.bex.gas.BexGasSchedule$Builder + method public pointerSegmentRead(long):blue.bex.gas.BexGasSchedule$Builder + method public pointerSegmentWritten(long):blue.bex.gas.BexGasSchedule$Builder + method public processingEventRead(long):blue.bex.gas.BexGasSchedule$Builder + method public resultValueRead(long):blue.bex.gas.BexGasSchedule$Builder + method public sortComparison(long):blue.bex.gas.BexGasSchedule$Builder + method public statementExecuted(long):blue.bex.gas.BexGasSchedule$Builder + method public stepsRead(long):blue.bex.gas.BexGasSchedule$Builder + method public textBlockConstructed(long):blue.bex.gas.BexGasSchedule$Builder + method public textBlockExamined(long):blue.bex.gas.BexGasSchedule$Builder + method public transientListItemProduced(long):blue.bex.gas.BexGasSchedule$Builder + method public transientObjectMemberProduced(long):blue.bex.gas.BexGasSchedule$Builder + method public variableRead(long):blue.bex.gas.BexGasSchedule$Builder + method public weight(blue.bex.gas.BexGasCounter,long):blue.bex.gas.BexGasSchedule$Builder + method public weight(java.lang.String,long):blue.bex.gas.BexGasSchedule$Builder +class public final blue.bex.output.BexAdmittedValue + method public node():blue.language.model.Node + method public nodeBlueId():java.lang.String + method public reconstructed():boolean + method public semanticValue():blue.bex.value.BexValue + method public value():blue.bex.value.BexValue +class public final blue.bex.output.BexEstablishedIdentity + constructor public (java.lang.String,blue.language.snapshot.FrozenNode) + method public blueId():java.lang.String + method public frozenValue():blue.language.snapshot.FrozenNode +class public final blue.bex.output.BexOutputAdmission + constructor public (blue.bex.gas.BexGasMeter,blue.bex.output.BexSemanticIdentityBoundary) + method public admit(blue.bex.value.BexValue,blue.bex.output.BexOutputKind):blue.bex.output.BexAdmittedValue + method public semanticIdentityMergeCount():long +class public final blue.bex.output.BexOutputKind extends java.lang.Enum + field public static final EVENT:blue.bex.output.BexOutputKind + field public static final INTRINSIC_INPUT:blue.bex.output.BexOutputKind + field public static final NODE_IDENTITY:blue.bex.output.BexOutputKind + field public static final PATCH_VALUE:blue.bex.output.BexOutputKind + field public static final ROOT_RESULT:blue.bex.output.BexOutputKind + method public reason():java.lang.String + method public static valueOf(java.lang.String):blue.bex.output.BexOutputKind + method public static values():blue.bex.output.BexOutputKind[] +class public abstract interface blue.bex.output.BexSemanticIdentityBoundary + field public static final STANDALONE:blue.bex.output.BexSemanticIdentityBoundary + method public abstract establishIdentity(blue.language.model.Node):blue.bex.output.BexEstablishedIdentity +class public final blue.bex.output.ProcessorExecutionContextBexSemanticIdentityBoundary implements blue.bex.output.BexSemanticIdentityBoundary + constructor public (blue.language.processor.ProcessorExecutionContext) + method public establishIdentity(blue.language.model.Node):blue.bex.output.BexEstablishedIdentity +class public final blue.bex.pointer.BexPointer + method public descendant(java.util.List):blue.bex.pointer.BexPointer + method public equals(java.lang.Object):boolean + method public hashCode():int + method public root():boolean + method public segments():java.util.List + method public static parse(java.lang.String):blue.bex.pointer.BexPointer + method public text():java.lang.String + method public toString():java.lang.String +class public final blue.bex.pointer.BexPointerCache + constructor public () + constructor public (int) + method public capacity():int + method public synchronized get(java.lang.String,blue.bex.result.BexMetrics):blue.bex.pointer.BexPointer +class public final blue.bex.result.BexChangeset + constructor public (java.util.List) + method public asValue():blue.bex.value.BexValue + method public entries():java.util.List + method public static patchEntryValue(blue.bex.result.BexPatchEntry):blue.bex.value.BexValue +class public final blue.bex.result.BexEvents + constructor public (java.util.List) + constructor public (java.util.List,java.util.List) + method public admittedEvents():java.util.List + method public asValue():blue.bex.value.BexValue + method public events():java.util.List +class public final blue.bex.result.BexExecutionResult + constructor public (blue.bex.value.BexValue,blue.bex.result.BexChangeset,blue.bex.result.BexEvents,blue.bex.gas.BexGasLedger,blue.bex.result.BexMetrics) + constructor public (blue.bex.value.BexValue,blue.bex.result.BexChangeset,blue.bex.result.BexEvents,blue.bex.gas.BexGasLedger,blue.bex.result.BexMetrics,blue.bex.output.BexAdmittedValue) + constructor public (blue.bex.value.BexValue,blue.bex.result.BexChangeset,blue.bex.result.BexEvents,java.util.List,blue.bex.result.BexMetrics) + method public changeset():blue.bex.result.BexChangeset + method public events():blue.bex.result.BexEvents + method public gasLedger():blue.bex.gas.BexGasLedger + method public gasTrace():java.util.List + method public gasUsed():long + method public ledger():blue.bex.gas.BexGasLedger + method public metrics():blue.bex.result.BexMetrics + method public output():blue.bex.output.BexAdmittedValue + method public trace():java.util.List + method public value():blue.bex.value.BexValue +class public final blue.bex.result.BexMetrics + constructor public () + method public addCompileNanos(long):void + method public addExecuteNanos(long):void + method public compileCacheHits():long + method public compileCacheMisses():long + method public compileNanos():long + method public compiledExecutions():long + method public containsBexCacheHits():long + method public containsBexCacheMisses():long + method public containsBexScans():long + method public copy():blue.bex.result.BexMetrics + method public currentContractReads():long + method public eventReads():long + method public executeNanos():long + method public expressionEvaluations():long + method public frozenDocumentReads():long + method public frozenOutputConversions():long + method public frozenWriterNodeFallbacks():long + method public functionArgMapAllocations():long + method public functionCalls():long + method public incrementCompileCacheHits():void + method public incrementCompileCacheMisses():void + method public incrementCompiledExecutions():void + method public incrementContainsBexCacheHits():void + method public incrementContainsBexCacheMisses():void + method public incrementContainsBexScans():void + method public incrementCurrentContractReads():void + method public incrementEventReads():void + method public incrementExpressionEvaluations():void + method public incrementFrozenDocumentReads():void + method public incrementFrozenOutputConversions():void + method public incrementFrozenWriterNodeFallbacks():void + method public incrementFunctionArgMapAllocations():void + method public incrementFunctionCalls():void + method public incrementInterpretedFallbacks():void + method public incrementLoopIterations():void + method public incrementNodeMaterializations():void + method public incrementNodeOutputConversions():void + method public incrementPointerCacheHits():void + method public incrementPointerCacheMisses():void + method public incrementPointerParses():void + method public incrementResolvedDocumentReads():void + method public incrementResultOverlayAncestorHits():void + method public incrementResultOverlayDocumentFallbacks():void + method public incrementResultOverlayExactHits():void + method public incrementResultValueReads():void + method public incrementSimpleMaterializations():void + method public incrementStatementExecutions():void + method public incrementStepsReads():void + method public interpretedFallbacks():long + method public loopIterations():long + method public nodeMaterializations():long + method public nodeOutputConversions():long + method public pointerCacheHits():long + method public pointerCacheMisses():long + method public pointerParses():long + method public resolvedDocumentReads():long + method public resultOverlayAncestorHits():long + method public resultOverlayDocumentFallbacks():long + method public resultOverlayExactHits():long + method public resultValueReads():long + method public simpleMaterializations():long + method public statementExecutions():long + method public stepsReads():long +class public final blue.bex.result.BexPatchEntry + constructor public (java.lang.String,java.lang.String,java.lang.String,blue.bex.value.BexValue) + constructor public (java.lang.String,java.lang.String,java.lang.String,blue.bex.value.BexValue,blue.bex.output.BexAdmittedValue) + method public absolutePath():java.lang.String + method public absoluteSegments():java.util.List + method public admittedValue():blue.bex.output.BexAdmittedValue + method public authoredPath():java.lang.String + method public op():java.lang.String + method public val():blue.bex.value.BexValue +class public final blue.bex.result.BexResultOverlay + constructor public (blue.bex.api.BexDocumentView,blue.bex.result.BexMetrics) + constructor public (blue.bex.api.BexDocumentView,blue.bex.result.BexMetrics,blue.language.Blue) + method public append(blue.bex.result.BexPatchEntry):void + method public rootValue():blue.bex.value.BexValue + method public valueAt(java.lang.String,java.util.List):blue.bex.value.BexValue +class public final blue.bex.runtime.BexExecutionAccumulator + constructor public (blue.bex.result.BexResultOverlay) + constructor public (blue.bex.result.BexResultOverlay,blue.bex.output.BexOutputAdmission) + method public appendChange(blue.bex.result.BexPatchEntry):void + method public appendEvent(blue.bex.value.BexValue):void + method public changeset():blue.bex.result.BexChangeset + method public events():blue.bex.result.BexEvents + method public overlay():blue.bex.result.BexResultOverlay +class public final blue.bex.runtime.BexRuntime + constructor public (blue.bex.compile.BexCompiledProgram,blue.bex.api.BexExecutionContext,blue.language.Blue,blue.bex.gas.BexGasSchedule,blue.bex.result.BexMetrics,blue.bex.pointer.BexPointerCache) + constructor public (blue.bex.compile.BexCompiledProgram,blue.bex.api.BexExecutionContext,blue.language.Blue,blue.bex.gas.BexGasSchedule,blue.bex.result.BexMetrics,blue.bex.pointer.BexPointerCache,blue.bex.api.BexIntrinsicRegistry) + method public accumulator():blue.bex.runtime.BexExecutionAccumulator + method public canonicalPointer(java.lang.String):java.lang.String + method public context():blue.bex.api.BexExecutionContext + method public defaultResultValue():blue.bex.value.BexValue + method public execute():blue.bex.result.BexExecutionResult + method public gas():blue.bex.gas.BexGasMeter + method public intrinsics():blue.bex.api.BexIntrinsicRegistry + method public invokeIntrinsic(java.lang.String,blue.bex.value.BexValue,java.util.Map):blue.bex.value.BexValue + method public metrics():blue.bex.result.BexMetrics + method public nodeBlueId(blue.bex.value.BexValue):blue.bex.value.BexValue + method public outputAdmission():blue.bex.output.BexOutputAdmission + method public parseDynamicPointer(java.lang.String):java.util.List + method public pointerCache():blue.bex.pointer.BexPointerCache + method public program():blue.bex.compile.BexCompiledProgram + method public readBinding(java.lang.String,java.util.List):blue.bex.value.BexValue + method public readCurrentContract(java.util.List):blue.bex.value.BexValue + method public readDocument(java.lang.String,java.util.List,boolean):blue.bex.value.BexValue + method public readEvent(java.util.List):blue.bex.value.BexValue + method public readProcessingEvent(java.util.List):blue.bex.value.BexValue + method public readResultValue(java.lang.String,java.util.List):blue.bex.value.BexValue + method public readSteps(java.lang.String,java.util.List):blue.bex.value.BexValue + method public readValuePointer(blue.bex.value.BexValue,java.util.List):blue.bex.value.BexValue + method public resolvePointer(java.lang.String):java.lang.String + method public typeMatcher():blue.bex.type.BexBlueTypeMatcher +class public final blue.bex.runtime.CompileScope + constructor public () + constructor public (blue.bex.runtime.CompileScope) + method public captureVisibility():blue.bex.runtime.CompileScope$Visibility + method public declareOrGetSlot(java.lang.String):int + method public frameSize():int + method public hasSlot(java.lang.String):boolean + method public resolveSlot(java.lang.String):int + method public restoreVisibility(blue.bex.runtime.CompileScope$Visibility):void +class public static final blue.bex.runtime.CompileScope$Visibility +class public abstract interface blue.bex.runtime.CompiledExpression + method public abstract eval(blue.bex.runtime.CompiledFrame):blue.bex.value.BexValue +class public final blue.bex.runtime.CompiledFrame + constructor public (blue.bex.runtime.BexRuntime,int,blue.bex.runtime.CompiledFrame) + method public accumulator():blue.bex.runtime.BexExecutionAccumulator + method public clear(int):void + method public enter(blue.bex.BexSourcePath):blue.bex.BexSourcePath + method public get(int):blue.bex.value.BexValue + method public getRequired(int):blue.bex.value.BexValue + method public isInitialized(int):boolean + method public parent():blue.bex.runtime.CompiledFrame + method public readBinding(java.lang.String,java.util.List):blue.bex.value.BexValue + method public readCurrentContract(java.util.List):blue.bex.value.BexValue + method public readDocument(java.lang.String,java.util.List,boolean):blue.bex.value.BexValue + method public readEvent(java.util.List):blue.bex.value.BexValue + method public readProcessingEvent(java.util.List):blue.bex.value.BexValue + method public restore(blue.bex.BexSourcePath):void + method public returnValue():blue.bex.value.BexValue + method public returnValue(blue.bex.value.BexValue):void + method public runtime():blue.bex.runtime.BexRuntime + method public set(int,blue.bex.value.BexValue):void + method public sourcePath():blue.bex.BexSourcePath +class public abstract interface blue.bex.runtime.CompiledStatement + method public abstract exec(blue.bex.runtime.CompiledFrame):blue.bex.runtime.Control +class public final blue.bex.runtime.Control extends java.lang.Enum + field public static final CONTINUE:blue.bex.runtime.Control + field public static final RETURN:blue.bex.runtime.Control + method public static valueOf(java.lang.String):blue.bex.runtime.Control + method public static values():blue.bex.runtime.Control[] +class public final blue.bex.type.BexBlueTypeMatcher + constructor public (blue.language.Blue) + method public matches(blue.bex.value.BexValue,blue.language.snapshot.FrozenNode,blue.bex.gas.BexGasMeter,blue.bex.BexSourcePath):boolean +class public final blue.bex.value.BexBlueNodeWriter + method public static hasLanguageField(blue.bex.value.BexValue):boolean + method public static isLanguageField(java.lang.String):boolean + method public static toNode(blue.bex.value.BexValue):blue.language.model.Node + method public static toSemanticNode(blue.bex.value.BexValue):blue.language.model.Node +class public final blue.bex.value.BexFrozenWriter + method public static toFrozen(blue.bex.value.BexValue):blue.language.snapshot.FrozenNode + method public static toFrozen(blue.bex.value.BexValue,blue.bex.result.BexMetrics):blue.language.snapshot.FrozenNode + method public toFrozenValue(blue.bex.value.BexValue):blue.language.snapshot.FrozenNode +class public final blue.bex.value.BexNodeWriter + method public static toNode(blue.bex.value.BexValue):blue.language.model.Node +class public final blue.bex.value.BexSimpleWriter + method public static toSimple(blue.bex.value.BexValue):java.lang.Object +class public final blue.bex.value.BexUnicodeOrder + field public static final CODE_POINT_COMPARATOR:java.util.Comparator + method public static compareCodePoints(java.lang.String,java.lang.String):int + method public static sortedCopy(java.util.Collection):java.util.List + method public static sortedCopy(java.util.Collection,blue.bex.value.BexUnicodeOrder$Comparison):java.util.List +class public abstract static interface blue.bex.value.BexUnicodeOrder$Comparison + method public abstract compare(java.lang.String,java.lang.String):int +class public abstract interface blue.bex.value.BexValue + method public abstract asBoolean():boolean + method public abstract asInteger():java.math.BigInteger + method public abstract asNumber():java.math.BigDecimal + method public abstract asText():java.lang.String + method public abstract at(java.lang.String):blue.bex.value.BexValue + method public abstract at(java.util.List):blue.bex.value.BexValue + method public abstract get(java.lang.String):blue.bex.value.BexValue + method public abstract isList():boolean + method public abstract isNull():boolean + method public abstract isObject():boolean + method public abstract isScalar():boolean + method public abstract isUndefined():boolean + method public abstract keys():java.util.List + method public abstract size():int + method public abstract toNode():blue.language.model.Node + method public abstract toSimple():java.lang.Object + method public exactBlueId():java.lang.String + method public isExact():boolean +class public final blue.bex.value.BexValues + field public static final NULL:blue.bex.value.BexValue + field public static final UNDEFINED:blue.bex.value.BexValue + method public static admittedExact(blue.language.snapshot.FrozenNode,java.lang.String,blue.bex.value.BexValue):blue.bex.value.BexValue + method public static empty(blue.bex.value.BexValue):boolean + method public static equal(blue.bex.value.BexValue,blue.bex.value.BexValue):boolean + method public static exact(blue.language.snapshot.FrozenNode,blue.language.snapshot.FrozenNode):blue.bex.value.BexValue + method public static exact(blue.language.snapshot.FrozenNode,blue.language.snapshot.FrozenNode,java.lang.String):blue.bex.value.BexValue + method public static fromSimple(java.lang.Object):blue.bex.value.BexValue + method public static frozen(blue.language.snapshot.FrozenNode):blue.bex.value.BexValue + method public static frozenBlueId(blue.bex.value.BexValue):java.lang.String + method public static kind(blue.bex.value.BexValue):java.lang.String + method public static list(java.util.List):blue.bex.value.BexValue + method public static map(java.util.Map):blue.bex.value.BexValue + method public static nodeCursorTrustedImmutable(blue.language.model.Node):blue.bex.value.BexValue + method public static nodeSnapshot(blue.language.model.Node):blue.bex.value.BexValue + method public static nullValue():blue.bex.value.BexValue + method public static overlay(blue.bex.value.BexValue,java.lang.String,blue.bex.value.BexValue):blue.bex.value.BexValue + method public static pointerSet(blue.bex.value.BexValue,java.util.List,blue.bex.value.BexValue,java.lang.String):blue.bex.value.BexValue + method public static referenceBacked(blue.bex.value.BexValue,blue.language.Blue):blue.bex.value.BexValue + method public static resultOverlayPointerSet(blue.bex.value.BexValue,java.util.List,blue.bex.value.BexValue,java.lang.String):blue.bex.value.BexValue + method public static scalar(java.lang.Object):blue.bex.value.BexValue + method public static transientFrozen(blue.language.snapshot.FrozenNode):blue.bex.value.BexValue + method public static truthy(blue.bex.value.BexValue):boolean + method public static undefined():blue.bex.value.BexValue +class public final blue.bex.value.ChangesetBexValue extends blue.bex.value.AbstractBexValue + constructor public (blue.bex.result.BexChangeset) + method public get(java.lang.String):blue.bex.value.BexValue + method public isList():boolean + method public size():int + method public toNode():blue.language.model.Node + method public toSimple():java.lang.Object + method public volatile asBoolean():boolean synthetic bridge + method public volatile asInteger():java.math.BigInteger synthetic bridge + method public volatile asNumber():java.math.BigDecimal synthetic bridge + method public volatile asText():java.lang.String synthetic bridge + method public volatile at(java.lang.String):blue.bex.value.BexValue synthetic bridge + method public volatile at(java.util.List):blue.bex.value.BexValue synthetic bridge + method public volatile isNull():boolean synthetic bridge + method public volatile isObject():boolean synthetic bridge + method public volatile isScalar():boolean synthetic bridge + method public volatile isUndefined():boolean synthetic bridge + method public volatile keys():java.util.List synthetic bridge +class public final blue.bex.value.EventsBexValue extends blue.bex.value.AbstractBexValue + constructor public (blue.bex.result.BexEvents) + method public get(java.lang.String):blue.bex.value.BexValue + method public isList():boolean + method public size():int + method public toNode():blue.language.model.Node + method public toSimple():java.lang.Object + method public volatile asBoolean():boolean synthetic bridge + method public volatile asInteger():java.math.BigInteger synthetic bridge + method public volatile asNumber():java.math.BigDecimal synthetic bridge + method public volatile asText():java.lang.String synthetic bridge + method public volatile at(java.lang.String):blue.bex.value.BexValue synthetic bridge + method public volatile at(java.util.List):blue.bex.value.BexValue synthetic bridge + method public volatile isNull():boolean synthetic bridge + method public volatile isObject():boolean synthetic bridge + method public volatile isScalar():boolean synthetic bridge + method public volatile isUndefined():boolean synthetic bridge + method public volatile keys():java.util.List synthetic bridge +class public final blue.bex.value.OverlayListBexValue extends blue.bex.value.AbstractBexValue + constructor public (blue.bex.value.BexValue,java.util.Map) + method public get(java.lang.String):blue.bex.value.BexValue + method public isList():boolean + method public size():int + method public toNode():blue.language.model.Node + method public toSimple():java.lang.Object + method public volatile asBoolean():boolean synthetic bridge + method public volatile asInteger():java.math.BigInteger synthetic bridge + method public volatile asNumber():java.math.BigDecimal synthetic bridge + method public volatile asText():java.lang.String synthetic bridge + method public volatile at(java.lang.String):blue.bex.value.BexValue synthetic bridge + method public volatile at(java.util.List):blue.bex.value.BexValue synthetic bridge + method public volatile isNull():boolean synthetic bridge + method public volatile isObject():boolean synthetic bridge + method public volatile isScalar():boolean synthetic bridge + method public volatile isUndefined():boolean synthetic bridge + method public volatile keys():java.util.List synthetic bridge +class public final blue.bex.value.PatchEntryBexValue extends blue.bex.value.AbstractBexValue + constructor public (blue.bex.result.BexPatchEntry) + method public get(java.lang.String):blue.bex.value.BexValue + method public isObject():boolean + method public keys():java.util.List + method public size():int + method public toNode():blue.language.model.Node + method public toSimple():java.lang.Object + method public volatile asBoolean():boolean synthetic bridge + method public volatile asInteger():java.math.BigInteger synthetic bridge + method public volatile asNumber():java.math.BigDecimal synthetic bridge + method public volatile asText():java.lang.String synthetic bridge + method public volatile at(java.lang.String):blue.bex.value.BexValue synthetic bridge + method public volatile at(java.util.List):blue.bex.value.BexValue synthetic bridge + method public volatile isList():boolean synthetic bridge + method public volatile isNull():boolean synthetic bridge + method public volatile isScalar():boolean synthetic bridge + method public volatile isUndefined():boolean synthetic bridge diff --git a/src/test/resources/rich-fixtures/current/14-is-blueid-typed-node-true.yaml b/src/test/resources/rich-fixtures/current/14-is-blueid-typed-node-true.yaml index 89b3e26..4ca517c 100644 --- a/src/test/resources/rich-fixtures/current/14-is-blueid-typed-node-true.yaml +++ b/src/test/resources/rich-fixtures/current/14-is-blueid-typed-node-true.yaml @@ -6,7 +6,7 @@ tags: - $is - blueId blueDefinitions: - HotelOrderType: | + HWdLVeUryPB57JejjKSeNUQ7Un2FmVwZn6pTaGRitRKk: | status: type: Text context: @@ -69,10 +69,10 @@ programSource: | $is: node: type: - blueId: HotelOrderType + blueId: HWdLVeUryPB57JejjKSeNUQ7Un2FmVwZn6pTaGRitRKk status: confirmed pattern: - blueId: HotelOrderType + blueId: HWdLVeUryPB57JejjKSeNUQ7Un2FmVwZn6pTaGRitRKk expectation: outcome: success resultSimple: true diff --git a/src/test/resources/rich-fixtures/current/15-is-blueid-wrong-type-false.yaml b/src/test/resources/rich-fixtures/current/15-is-blueid-wrong-type-false.yaml index 513a26c..3a3ca76 100644 --- a/src/test/resources/rich-fixtures/current/15-is-blueid-wrong-type-false.yaml +++ b/src/test/resources/rich-fixtures/current/15-is-blueid-wrong-type-false.yaml @@ -6,10 +6,10 @@ tags: - $is - blueId blueDefinitions: - HotelOrderType: | + HWdLVeUryPB57JejjKSeNUQ7Un2FmVwZn6pTaGRitRKk: | status: type: Text - RestaurantOrderType: | + H2fzrLKWG5tE4kA1EkGyc9SyaLXZHQTdjpATh3uzCjZn: | restaurantStatus: type: Text context: @@ -72,10 +72,10 @@ programSource: | $is: node: type: - blueId: RestaurantOrderType + blueId: H2fzrLKWG5tE4kA1EkGyc9SyaLXZHQTdjpATh3uzCjZn status: confirmed pattern: - blueId: HotelOrderType + blueId: HWdLVeUryPB57JejjKSeNUQ7Un2FmVwZn6pTaGRitRKk expectation: outcome: success resultSimple: false diff --git a/src/test/resources/rich-fixtures/gas/gas-004-custom-expression-base.yaml b/src/test/resources/rich-fixtures/gas/gas-004-custom-expression-base.yaml index 2a2f969..028d411 100644 --- a/src/test/resources/rich-fixtures/gas/gas-004-custom-expression-base.yaml +++ b/src/test/resources/rich-fixtures/gas/gas-004-custom-expression-base.yaml @@ -1,8 +1,8 @@ fixtureId: GAS-004 -title: Custom expressionBase schedule applies +title: Custom expressionEvaluated schedule applies tags: [gas] gasSchedule: - expressionBase: 10 + expressionEvaluated: 10 programSource: | type: Blue/BEX Program expr: 1 diff --git a/src/test/resources/rich-fixtures/gas/gas-121-custom-function-call.yaml b/src/test/resources/rich-fixtures/gas/gas-121-custom-function-call.yaml index a99fe79..e479229 100644 --- a/src/test/resources/rich-fixtures/gas/gas-121-custom-function-call.yaml +++ b/src/test/resources/rich-fixtures/gas/gas-121-custom-function-call.yaml @@ -1,8 +1,8 @@ fixtureId: GAS-121 -title: Custom functionCall schedule applies +title: Custom functionCalled schedule applies tags: [gas] gasSchedule: - functionCall: 10 + functionCalled: 10 programSource: | type: Blue/BEX Program expr: 1 diff --git a/src/test/resources/rich-fixtures/gas/gas-122-custom-statement-base.yaml b/src/test/resources/rich-fixtures/gas/gas-122-custom-statement-base.yaml index 7ac667e..4d822f7 100644 --- a/src/test/resources/rich-fixtures/gas/gas-122-custom-statement-base.yaml +++ b/src/test/resources/rich-fixtures/gas/gas-122-custom-statement-base.yaml @@ -1,8 +1,8 @@ fixtureId: GAS-122 -title: Custom statementBase schedule applies +title: Custom statementExecuted schedule applies tags: [gas] gasSchedule: - statementBase: 10 + statementExecuted: 10 programSource: | type: Blue/BEX Program do: diff --git a/src/test/resources/rich-fixtures/gas/gas-123-custom-append-event-base.yaml b/src/test/resources/rich-fixtures/gas/gas-123-custom-append-event-base.yaml index 955ad30..6ee5867 100644 --- a/src/test/resources/rich-fixtures/gas/gas-123-custom-append-event-base.yaml +++ b/src/test/resources/rich-fixtures/gas/gas-123-custom-append-event-base.yaml @@ -1,8 +1,8 @@ fixtureId: GAS-123 -title: Custom appendEventBase schedule applies +title: Custom eventAppended schedule applies tags: [gas] gasSchedule: - appendEventBase: 10 + eventAppended: 10 programSource: | type: Blue/BEX Program do: diff --git a/src/test/resources/rich-fixtures/gas/gas-124-custom-append-change-base.yaml b/src/test/resources/rich-fixtures/gas/gas-124-custom-append-change-base.yaml index b675d9b..f918831 100644 --- a/src/test/resources/rich-fixtures/gas/gas-124-custom-append-change-base.yaml +++ b/src/test/resources/rich-fixtures/gas/gas-124-custom-append-change-base.yaml @@ -1,8 +1,8 @@ fixtureId: GAS-124 -title: Custom appendChangeBase schedule applies +title: Custom patchAppended schedule applies tags: [gas] gasSchedule: - appendChangeBase: 10 + patchAppended: 10 programSource: | type: Blue/BEX Program do: diff --git a/src/test/resources/rich-fixtures/gas/gas-126-custom-pointer-set-base.yaml b/src/test/resources/rich-fixtures/gas/gas-126-custom-pointer-set-base.yaml index 35a1e00..598968a 100644 --- a/src/test/resources/rich-fixtures/gas/gas-126-custom-pointer-set-base.yaml +++ b/src/test/resources/rich-fixtures/gas/gas-126-custom-pointer-set-base.yaml @@ -1,8 +1,8 @@ fixtureId: GAS-126 -title: Custom pointerSetBase schedule applies +title: Custom pointerSegmentWritten schedule applies tags: [gas] gasSchedule: - pointerSetBase: 10 + pointerSegmentWritten: 10 programSource: | type: Blue/BEX Program expr: diff --git a/src/test/resources/rich-fixtures/gas/gas-127-custom-foreach-item.yaml b/src/test/resources/rich-fixtures/gas/gas-127-custom-foreach-item.yaml index 44b764d..02fda7e 100644 --- a/src/test/resources/rich-fixtures/gas/gas-127-custom-foreach-item.yaml +++ b/src/test/resources/rich-fixtures/gas/gas-127-custom-foreach-item.yaml @@ -1,8 +1,8 @@ fixtureId: GAS-127 -title: Custom forEachItem schedule applies +title: Custom collectionItemVisited schedule applies tags: [gas] gasSchedule: - forEachItem: 10 + collectionItemVisited: 10 programSource: | type: Blue/BEX Program do: diff --git a/work-status.txt b/work-status.txt new file mode 100644 index 0000000..a2ae71b --- /dev/null +++ b/work-status.txt @@ -0,0 +1 @@ +running From 395c484111f8c4e9e0e98d2db7f1c5b0777bd5a8 Mon Sep 17 00:00:00 2001 From: piotr-blue Date: Wed, 29 Jul 2026 21:09:30 +0200 Subject: [PATCH 02/13] fix: harden final BEX publication boundary --- .../scripts/run-final-publication-gates.sh | 26 +- README.md | 21 +- build.gradle.kts | 381 +++++++++- docs/BEX_CONFORMANCE.md | 21 +- .../java/blue/bex/api/BexGasLedgerHost.java | 47 ++ ...essorExecutionContextBexGasLedgerHost.java | 21 +- src/main/java/blue/bex/gas/BexGasMeter.java | 48 ++ .../java/blue/bex/runtime/BexRuntime.java | 56 +- .../bex/BexExactReferenceDocumentTest.java | 55 +- .../bex/BexExecutionEvidenceLedgerTest.java | 84 ++- .../conformance/BexConformanceReportMain.java | 688 +++++++++++++++--- .../BexConformanceReportTruthfulnessTest.java | 280 ++++++- .../BexHostedRuntimeWorkSessionTest.java | 276 ++++++- .../hosted-release/required-public-api.txt | 5 + 14 files changed, 1797 insertions(+), 212 deletions(-) diff --git a/.github/scripts/run-final-publication-gates.sh b/.github/scripts/run-final-publication-gates.sh index 6840aa6..394381a 100755 --- a/.github/scripts/run-final-publication-gates.sh +++ b/.github/scripts/run-final-publication-gates.sh @@ -17,6 +17,8 @@ readonly BEX_RELEASE_TEMP_ROOT="$( readonly LANGUAGE_CHECKOUT="$BEX_RELEASE_TEMP_ROOT/blue-language-java" readonly FIRST_BEX_CHECKOUT="$BEX_RELEASE_TEMP_ROOT/blue-bex-clean-one" readonly SECOND_BEX_CHECKOUT="$BEX_RELEASE_TEMP_ROOT/blue-bex-clean-two" +readonly LOCAL_FIRST_BEX_CHECKOUT="$BEX_RELEASE_TEMP_ROOT/blue-bex-local-clean-one" +readonly LOCAL_SECOND_BEX_CHECKOUT="$BEX_RELEASE_TEMP_ROOT/blue-bex-local-clean-two" readonly RECEIPT_ROOT="$BEX_RELEASE_TEMP_ROOT/receipts" readonly STANDALONE_FIRST_RECEIPT="$RECEIPT_ROOT/standalone-first.properties" readonly STANDALONE_SECOND_RECEIPT="$RECEIPT_ROOT/standalone-second.properties" @@ -80,12 +82,17 @@ fi cd "$BEX_REPOSITORY" -# Assemble the publication artifacts twice from separate clean checkouts of -# this exact BEX commit, using the standalone published dependency in both. +# Assemble each dependency mode twice from its own pair of clean checkouts of +# this exact BEX commit. Keeping four roots preserves the receipt-owned +# Language JAR and BEX artifacts until every later report has re-hashed them. git clone --no-hardlinks "$BEX_REPOSITORY" "$FIRST_BEX_CHECKOUT" git clone --no-hardlinks "$BEX_REPOSITORY" "$SECOND_BEX_CHECKOUT" +git clone --no-hardlinks "$BEX_REPOSITORY" "$LOCAL_FIRST_BEX_CHECKOUT" +git clone --no-hardlinks "$BEX_REPOSITORY" "$LOCAL_SECOND_BEX_CHECKOUT" git -C "$FIRST_BEX_CHECKOUT" checkout --detach "$BEX_COMMIT" git -C "$SECOND_BEX_CHECKOUT" checkout --detach "$BEX_COMMIT" +git -C "$LOCAL_FIRST_BEX_CHECKOUT" checkout --detach "$BEX_COMMIT" +git -C "$LOCAL_SECOND_BEX_CHECKOUT" checkout --detach "$BEX_COMMIT" mkdir -p "$RECEIPT_ROOT" GRADLE_USER_HOME="$BEX_RELEASE_TEMP_ROOT/gradle-clean-one" \ @@ -116,26 +123,27 @@ GRADLE_USER_HOME="$BEX_RELEASE_TEMP_ROOT/gradle-evidence-verifier" \ # provenance explicit and prevent one mode from borrowing resolution state # from the other. GRADLE_USER_HOME="$BEX_RELEASE_TEMP_ROOT/gradle-standalone-mode" \ - ./gradlew --no-daemon clean test + ./gradlew --no-daemon clean test \ + -PblueLanguageRequireFreshModuleCache=true GRADLE_USER_HOME="$BEX_RELEASE_TEMP_ROOT/gradle-local-clean-one" \ - "$FIRST_BEX_CHECKOUT/gradlew" \ + "$LOCAL_FIRST_BEX_CHECKOUT/gradlew" \ --no-daemon \ - -p "$FIRST_BEX_CHECKOUT" \ + -p "$LOCAL_FIRST_BEX_CHECKOUT" \ clean test writeCleanBuildArtifactHashes \ -PblueLanguageCompositePath="$LANGUAGE_CHECKOUT" GRADLE_USER_HOME="$BEX_RELEASE_TEMP_ROOT/gradle-local-clean-two" \ - "$SECOND_BEX_CHECKOUT/gradlew" \ + "$LOCAL_SECOND_BEX_CHECKOUT/gradlew" \ --no-daemon \ - -p "$SECOND_BEX_CHECKOUT" \ + -p "$LOCAL_SECOND_BEX_CHECKOUT" \ clean test writeCleanBuildArtifactHashes \ -PblueLanguageCompositePath="$LANGUAGE_CHECKOUT" cp \ - "$FIRST_BEX_CHECKOUT/build/reports/bex-release/clean-build-artifacts.properties" \ + "$LOCAL_FIRST_BEX_CHECKOUT/build/reports/bex-release/clean-build-artifacts.properties" \ "$LOCAL_FIRST_RECEIPT" cp \ - "$SECOND_BEX_CHECKOUT/build/reports/bex-release/clean-build-artifacts.properties" \ + "$LOCAL_SECOND_BEX_CHECKOUT/build/reports/bex-release/clean-build-artifacts.properties" \ "$LOCAL_SECOND_RECEIPT" GRADLE_USER_HOME="$BEX_RELEASE_TEMP_ROOT/gradle-local-evidence-verifier" \ diff --git a/README.md b/README.md index 7db95a6..bb47bf9 100644 --- a/README.md +++ b/README.md @@ -889,7 +889,8 @@ export SOURCE_DATE_EPOCH="$(git show -s --format=%ct HEAD)" -PcleanBuildEvidenceTwo=/second/clean/blue-bex-java/build/reports/bex-release/clean-build-artifacts.properties GRADLE_USER_HOME=/tmp/blue-bex-standalone-mode \ - ./gradlew --no-daemon clean test + ./gradlew --no-daemon clean test \ + -PblueLanguageRequireFreshModuleCache=true GRADLE_USER_HOME=/tmp/blue-bex-local-mode \ ./gradlew --no-daemon clean test \ -PblueLanguageCompositePath=/absolute/path/to/clean/blue-language-java @@ -899,20 +900,26 @@ GRADLE_USER_HOME=/tmp/blue-bex-final-standalone \ For an additional local-composite reproducibility pair, add the same explicit `-PblueLanguageCompositePath=/absolute/path/to/clean/blue-language-java` -argument to both clean-checkout builds. Never use one standalone build and one +argument to both builds in a second pair of clean BEX checkouts. Keep all four +checkout roots intact until the final report has re-hashed their BEX outputs +and receipt-owned Language JAR copies. Never use one standalone build and one local-composite build as a two-run pair. The verifier rejects dirty checkouts, different commits, versions or dependency modes, and any mismatch among the -four artifact hashes. Its commit-bound evidence is also compared with the +four artifact hashes. Local-composite mode evidence must also resolve from the +exact published Language source commit at the recorded +`v` tag; a different clean Language checkout is rejected. +Its commit-bound evidence is also compared with the artifacts from the reporting build. The same-working-tree archive gate remains a separate packaging check. The API gate compares the packaged JAR’s complete generated descriptor manifest with the exact source-controlled first-public BEX 2.0 baseline. Removals, descriptor changes, reordering, and unexpected public/protected -additions all fail the gate. A clean dependency-cache run is -separate evidence and remains -`not-executed` unless a controlled isolated run records it. At this source -state, the current +additions all fail the gate. A clean dependency-cache run is separate +commit-bound evidence and is recorded only when a controlled isolated run +uses `-PblueLanguageRequireFreshModuleCache=true`. Later publication +invocations authenticate and reuse that mode evidence instead of incorrectly +requiring the same cache path to be absent again. At this source state, the current hosted runtime session APIs exist only in the sibling working tree and are not present in the published `3.1.0-rc.19` JAR, so standalone release evidence is expected to remain blocked until Blue Language publishes that API surface. diff --git a/build.gradle.kts b/build.gradle.kts index 37cc718..ed3f861 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,6 +7,7 @@ import java.security.MessageDigest import java.time.Instant import java.util.Properties import java.util.zip.ZipFile +import org.apache.commons.compress.archivers.zip.ZipFile as CommonsZipFile import org.gradle.api.tasks.javadoc.Javadoc import org.gradle.external.javadoc.StandardJavadocDocletOptions import org.gradle.api.tasks.bundling.Jar @@ -58,6 +59,10 @@ val blueLanguageModuleVersionCache = ) val blueLanguageModuleVersionCacheInitiallyAbsent = !blueLanguageModuleVersionCache.exists() +val blueLanguageRequireFreshModuleCache = + providers.gradleProperty("blueLanguageRequireFreshModuleCache") + .map(String::toBoolean) + .orElse(false) base { archivesName.set("blue-bex-java") @@ -440,7 +445,10 @@ val sourceReleaseArchive by tasks.registering(Zip::class) { isPreserveFileTimestamps = false isReproducibleFileOrder = true from(sourceReleaseInputs) { - exclude("gradlew") + exclude( + "gradlew", + ".github/scripts/run-final-publication-gates.sh" + ) into(sourceReleaseRoot) } from("gradlew") { @@ -449,6 +457,12 @@ val sourceReleaseArchive by tasks.registering(Zip::class) { unix("rwxr-xr-x") } } + from(".github/scripts/run-final-publication-gates.sh") { + into("$sourceReleaseRoot/.github/scripts") + filePermissions { + unix("rwxr-xr-x") + } + } } val rebuiltSourceReleaseArchive by tasks.registering(Zip::class) { group = "verification" @@ -461,7 +475,10 @@ val rebuiltSourceReleaseArchive by tasks.registering(Zip::class) { isPreserveFileTimestamps = false isReproducibleFileOrder = true from(sourceReleaseInputs) { - exclude("gradlew") + exclude( + "gradlew", + ".github/scripts/run-final-publication-gates.sh" + ) into(sourceReleaseRoot) } from("gradlew") { @@ -470,6 +487,12 @@ val rebuiltSourceReleaseArchive by tasks.registering(Zip::class) { unix("rwxr-xr-x") } } + from(".github/scripts/run-final-publication-gates.sh") { + into("$sourceReleaseRoot/.github/scripts") + filePermissions { + unix("rwxr-xr-x") + } + } } val rebuiltMainJar by tasks.registering(Jar::class) { group = "verification" @@ -577,6 +600,16 @@ val verifyDeterministicArchives by tasks.registering { val rebuiltSourceReleaseHash = sha256(rebuiltSourceRelease) val sourceReleaseByteIdentity = byteIdentical(originalSourceRelease, rebuiltSourceRelease) + val originalExecutableModes = + sourceReleaseExecutableModes( + originalSourceRelease, + sourceReleaseRoot + ) + val rebuiltExecutableModes = + sourceReleaseExecutableModes( + rebuiltSourceRelease, + sourceReleaseRoot + ) check(originalMainHash == rebuiltMainHash) { "Main JAR rebuild differs: $originalMainHash != $rebuiltMainHash" } @@ -594,6 +627,9 @@ val verifyDeterministicArchives by tasks.registering { "Source-release ZIP replica differs: " + "$originalSourceReleaseHash != $rebuiltSourceReleaseHash" } + check(originalExecutableModes == rebuiltExecutableModes) { + "Source-release executable modes differ between assemblies" + } sourceReleaseChecksum.get().writeText( "$originalSourceReleaseHash ${originalSourceRelease.name}\n" ) @@ -624,6 +660,12 @@ val verifyDeterministicArchives by tasks.registering { "included.patterns" to sourceReleaseIncludes.joinToString(","), "independentCleanCheckout" to "false", + "executable.gradlew.mode" to + originalExecutableModes.getValue("gradlew"), + "executable.publicationGate.mode" to + originalExecutableModes.getValue( + ".github/scripts/run-final-publication-gates.sh" + ), "replica.bytes" to rebuiltSourceRelease.length().toString(), "replica.path" to @@ -693,6 +735,11 @@ val cleanBuildArtifactEvidence = layout.buildDirectory.file( "reports/bex-release/clean-build-artifacts.properties" ) +val cleanBuildDependencyArtifact = + layout.buildDirectory.file( + "reports/bex-release/clean-build-inputs/" + + "blue-language-java.jar" + ) val invalidateCleanBuildArtifactEvidence by tasks.registering { group = "verification" description = @@ -700,6 +747,7 @@ val invalidateCleanBuildArtifactEvidence by tasks.registering { outputs.upToDateWhen { false } doLast { cleanBuildArtifactEvidence.get().asFile.delete() + cleanBuildDependencyArtifact.get().asFile.delete() } } listOf( @@ -724,9 +772,11 @@ val writeCleanBuildArtifactHashes by tasks.registering { sourceReleaseArchive ) outputs.file(cleanBuildArtifactEvidence) + outputs.file(cleanBuildDependencyArtifact) outputs.upToDateWhen { false } doFirst { cleanBuildArtifactEvidence.get().asFile.delete() + cleanBuildDependencyArtifact.get().asFile.delete() } doLast { val checkout = @@ -760,6 +810,23 @@ val writeCleanBuildArtifactHashes by tasks.registering { "Expected exactly one Blue Language dependency artifact" } val languageArtifact = languageArtifacts.single() + val languageArtifactCopy = + cleanBuildDependencyArtifact.get().asFile + languageArtifactCopy.parentFile.mkdirs() + languageArtifact.file.copyTo( + languageArtifactCopy, + overwrite = true + ) + check( + languageArtifactCopy.isFile && + languageArtifactCopy.length() == + languageArtifact.file.length() && + sha256(languageArtifactCopy) == + sha256(languageArtifact.file) + ) { + "Failed to preserve the exact Blue Language dependency " + + "artifact with the clean-build receipt" + } val compositeDirectory = blueLanguageCompositePath ?.let { file(it).canonicalFile } @@ -791,7 +858,7 @@ val writeCleanBuildArtifactHashes by tasks.registering { val values = linkedMapOf( "schema" to - "blue-bex-clean-build-artifacts/1.0", + "blue-bex-clean-build-artifacts/1.1", "status" to "passed", "commit" to checkout.commit, "checkout.clean" to "true", @@ -817,9 +884,12 @@ val writeCleanBuildArtifactHashes by tasks.registering { languageArtifact.moduleVersion.id.version ), "dependency.artifact.bytes" to - languageArtifact.file.length().toString(), + languageArtifactCopy.length().toString(), + "dependency.artifact.path" to + languageArtifactCopy.relativeTo(projectDir) + .invariantSeparatorsPath, "dependency.artifact.sha256" to - sha256(languageArtifact.file), + sha256(languageArtifactCopy), "composite.path" to (compositeDirectory?.path ?: ""), "composite.commit" to @@ -922,13 +992,44 @@ val verifyIndependentCleanBuildReproducibility by tasks.registering { "HEAD" ).trim().lowercase() val expectedSchema = - "blue-bex-clean-build-artifacts/1.0" + "blue-bex-clean-build-artifacts/1.1" + val artifactNames = + listOf( + "main", + "sources", + "javadoc", + "sourceRelease" + ) + val artifactPrefix = + "blue-bex-java-${project.version}" + val expectedArtifactPaths = + mapOf( + "main" to + "build/libs/$artifactPrefix.jar", + "sources" to + "build/libs/$artifactPrefix-sources.jar", + "javadoc" to + "build/libs/$artifactPrefix-javadoc.jar", + "sourceRelease" to + "build/distributions/" + + "$artifactPrefix-source-release.zip" + ) + val expectedDependencyArtifactPath = + "build/reports/bex-release/clean-build-inputs/" + + "blue-language-java.jar" val authenticatedRoots = linkedMapOf() val authenticatedGitDirectories = linkedMapOf() val authenticatedFingerprints = linkedMapOf() + val authenticatedArtifacts = + linkedMapOf< + String, + Map> + >() + val authenticatedDependencyArtifacts = + linkedMapOf>() for ((label, evidence) in listOf("first" to first, "second" to second)) { check(evidence["schema"] == expectedSchema) { @@ -1032,11 +1133,134 @@ val verifyIndependentCleanBuildReproducibility by tasks.registering { ) { "$label build has no effective dependency coordinate" } + val buildArtifacts = + linkedMapOf>() + for (artifactName in artifactNames) { + val pathKey = + "artifact.$artifactName.path" + val bytesKey = + "artifact.$artifactName.bytes" + val hashKey = + "artifact.$artifactName.sha256" + val relativePath = + evidence[pathKey] + ?: throw GradleException( + "$label $artifactName path is unavailable" + ) + check( + relativePath == + expectedArtifactPaths.getValue( + artifactName + ) + ) { + "$label $artifactName path is not the expected " + + "release output: $relativePath" + } + check(!File(relativePath).isAbsolute) { + "$label $artifactName path must be relative" + } + val artifact = + File(recordedRoot, relativePath) + .canonicalFile + check( + artifact.toPath().startsWith( + recordedRoot.toPath() + ) && + artifact.isFile + ) { + "$label $artifactName artifact is unavailable " + + "under its authenticated checkout" + } + val recordedBytes = + evidence[bytesKey]?.toLongOrNull() + check( + recordedBytes != null && + recordedBytes == artifact.length() + ) { + "$label $artifactName byte length differs from " + + "its receipt" + } + val recordedHash = evidence[hashKey] + val actualHash = sha256(artifact) + check( + recordedHash?.matches( + Regex("[0-9a-f]{64}") + ) == true && + recordedHash == actualHash + ) { + "$label $artifactName artifact hash differs from " + + "its receipt" + } + buildArtifacts[artifactName] = + mapOf( + "path" to relativePath, + "bytes" to recordedBytes.toString(), + "sha256" to actualHash + ) + } + val dependencyPath = + evidence["dependency.artifact.path"] + ?: throw GradleException( + "$label Blue Language artifact path is unavailable" + ) + check( + dependencyPath == + expectedDependencyArtifactPath && + !File(dependencyPath).isAbsolute + ) { + "$label Blue Language artifact path is not the " + + "expected receipt-owned copy" + } + val dependencyArtifact = + File(recordedRoot, dependencyPath) + .canonicalFile + check( + dependencyArtifact.toPath().startsWith( + recordedRoot.toPath() + ) && + dependencyArtifact.isFile + ) { + "$label Blue Language artifact copy is unavailable " + + "under its authenticated checkout" + } + val dependencyBytes = + evidence["dependency.artifact.bytes"] + ?.toLongOrNull() + check( + dependencyBytes != null && + dependencyBytes == + dependencyArtifact.length() + ) { + "$label Blue Language artifact byte length differs " + + "from its receipt" + } + val dependencyHash = + evidence["dependency.artifact.sha256"] + val actualDependencyHash = + sha256(dependencyArtifact) + check( + dependencyHash?.matches( + Regex("[0-9a-f]{64}") + ) == true && + dependencyHash == + actualDependencyHash + ) { + "$label Blue Language artifact hash differs from " + + "its receipt" + } authenticatedRoots[label] = recordedRoot authenticatedGitDirectories[label] = actualGitDirectory authenticatedFingerprints[label] = fingerprint + authenticatedArtifacts[label] = + buildArtifacts + authenticatedDependencyArtifacts[label] = + mapOf( + "path" to dependencyPath, + "bytes" to dependencyBytes.toString(), + "sha256" to actualDependencyHash + ) } check( authenticatedRoots.getValue("first") != @@ -1066,17 +1290,10 @@ val verifyIndependentCleanBuildReproducibility by tasks.registering { ) { "Independent builds used different source path sets" } - val artifactNames = - listOf( - "main", - "sources", - "javadoc", - "sourceRelease" - ) val values = linkedMapOf( "schema" to - "blue-bex-independent-clean-builds/1.0", + "blue-bex-independent-clean-builds/1.2", "status" to "passed", "commit" to currentCommit, "first.checkout.clean" to "true", @@ -1135,9 +1352,17 @@ val verifyIndependentCleanBuildReproducibility by tasks.registering { "dependency.effectiveCoordinate" ), "dependency.artifact.sha256" to - first.getValue( - "dependency.artifact.sha256" - ), + authenticatedDependencyArtifacts + .getValue("first") + .getValue("sha256"), + "dependency.artifact.path" to + authenticatedDependencyArtifacts + .getValue("first") + .getValue("path"), + "dependency.artifact.bytes" to + authenticatedDependencyArtifacts + .getValue("first") + .getValue("bytes"), "composite.path" to first.getValue("composite.path"), "composite.commit" to @@ -1155,6 +1380,23 @@ val verifyIndependentCleanBuildReproducibility by tasks.registering { "composite.pathCount" to first.getValue("composite.pathCount") ) + for ((label, artifacts) in authenticatedArtifacts) { + for ((artifactName, artifact) in artifacts) { + for ((field, value) in artifact) { + values[ + "$label.artifact.$artifactName.$field" + ] = value + } + } + } + for ((label, artifact) in + authenticatedDependencyArtifacts) { + for ((field, value) in artifact) { + values[ + "$label.dependency.artifact.$field" + ] = value + } + } check( first["dependency.mode"] == second["dependency.mode"] @@ -1173,19 +1415,63 @@ val verifyIndependentCleanBuildReproducibility by tasks.registering { ) { "Clean builds resolved different effective dependency coordinates" } + val firstDependencyHash = + authenticatedDependencyArtifacts + .getValue("first") + .getValue("sha256") + val secondDependencyHash = + authenticatedDependencyArtifacts + .getValue("second") + .getValue("sha256") check( - first["dependency.artifact.sha256"] - ?.matches(Regex("[0-9a-f]{64}")) == - true + firstDependencyHash.matches( + Regex("[0-9a-f]{64}") + ) ) { "First build has no exact Language artifact hash" } check( - first["dependency.artifact.sha256"] == - second["dependency.artifact.sha256"] + firstDependencyHash == + secondDependencyHash ) { "Clean builds resolved different Language artifacts" } + val verifierLanguageArtifacts = + configurations.compileClasspath.get() + .resolvedConfiguration + .resolvedArtifacts + .filter { + it.moduleVersion.id.group == "blue.language" && + it.name == "blue-language-java" && + it.extension == "jar" + } + check(verifierLanguageArtifacts.size == 1) { + "Verifier did not resolve exactly one Blue Language artifact" + } + val verifierLanguageArtifact = + verifierLanguageArtifacts.single() + val verifierLanguageCoordinate = + ( + verifierLanguageArtifact.moduleVersion.id.group + + ":" + + verifierLanguageArtifact.name + + ":" + + verifierLanguageArtifact.moduleVersion.id.version + ) + check( + verifierLanguageCoordinate == + first["dependency.effectiveCoordinate"] + ) { + "Clean builds did not resolve the verifier's exact Blue " + + "Language coordinate" + } + check( + sha256(verifierLanguageArtifact.file) == + firstDependencyHash + ) { + "Clean builds did not use the verifier's exact Blue " + + "Language artifact" + } val compositeKeys = listOf( "composite.path", @@ -1261,12 +1547,18 @@ val verifyIndependentCleanBuildReproducibility by tasks.registering { } } for (artifactName in artifactNames) { - val key = "artifact.$artifactName.sha256" - val firstHash = first[key] - val secondHash = second[key] + val firstHash = + authenticatedArtifacts + .getValue("first") + .getValue(artifactName) + .getValue("sha256") + val secondHash = + authenticatedArtifacts + .getValue("second") + .getValue(artifactName) + .getValue("sha256") check( - firstHash != null && - firstHash.matches(Regex("[0-9a-f]{64}")) + firstHash.matches(Regex("[0-9a-f]{64}")) ) { "First $artifactName hash is unavailable" } @@ -1616,10 +1908,12 @@ val writeDependencyResolutionEvidence by tasks.registering { // resolution above then verifies the resulting JAR against the // source-controlled Maven Central hash. moduleVersionCacheAcceptance = - if (blueLanguageModuleVersionCacheInitiallyAbsent) { + if (!blueLanguageRequireFreshModuleCache.get()) { + "not-required-for-current-run" + } else if (blueLanguageModuleVersionCacheInitiallyAbsent) { "passed" } else { - "not-executed" + "failed" } } else { provenanceStatus = "not-applicable-local-composite" @@ -1658,6 +1952,8 @@ val writeDependencyResolutionEvidence by tasks.registering { blueLanguageModuleVersionCache.canonicalPath, "cache.blueLanguageModuleVersionInitiallyAbsent" to blueLanguageModuleVersionCacheInitiallyAbsent.toString(), + "cache.freshProofRequired" to + blueLanguageRequireFreshModuleCache.get().toString(), "cache.acceptance" to moduleVersionCacheAcceptance, "cache.acceptanceScope" to "standalone-published-blue-language-module-version-cache" @@ -1908,3 +2204,30 @@ fun determineProjectVersion(): String { } return baseVersion + if (System.getenv("CI") == null) "-SNAPSHOT" else "" } + +fun sourceReleaseExecutableModes( + archive: File, + rootDirectory: String +): Map { + val executablePaths = + listOf( + "gradlew", + ".github/scripts/run-final-publication-gates.sh" + ) + CommonsZipFile.builder().setFile(archive).get().use { zip -> + return executablePaths.associateWith { relativePath -> + val entry = + zip.getEntry("$rootDirectory/$relativePath") + ?: throw GradleException( + "Source release is missing executable $relativePath" + ) + val permissionBits = entry.unixMode and 0x1ff + check(permissionBits == 0x1ed) { + "Source-release executable $relativePath has mode " + + permissionBits.toString(8) + + ", expected 755" + } + permissionBits.toString(8).padStart(4, '0') + } + } +} diff --git a/docs/BEX_CONFORMANCE.md b/docs/BEX_CONFORMANCE.md index 129a780..1035e2e 100644 --- a/docs/BEX_CONFORMANCE.md +++ b/docs/BEX_CONFORMANCE.md @@ -105,9 +105,14 @@ export SOURCE_DATE_EPOCH="$(git show -s --format=%ct HEAD)" Both evidence producers must use the same dependency mode. The publication pair uses standalone-published mode. To prove local-composite packaging -separately, run another two-clean-checkout pair with the same explicit +separately, run another two-clean-checkout pair in two additional BEX roots +with the same explicit `-PblueLanguageCompositePath=/absolute/path/to/clean/blue-language-java` -argument on both builds; never compare one build from each mode. +argument on both builds. Keep all four roots until the final report has +re-hashed their outputs and receipt-owned Language JAR copies; never compare +one build from each mode. The local-composite receipt is accepted only when +its live source checkout is the exact published Language commit and carries +the recorded `v` tag. The combined evidence is commit-bound and stale evidence fails closed. The conformance report also requires its own four artifacts to match the hashes @@ -119,7 +124,8 @@ export CI=true export SOURCE_DATE_EPOCH="$(git show -s --format=%ct HEAD)" GRADLE_USER_HOME=/tmp/blue-bex-standalone-mode \ - ./gradlew --no-daemon clean test + ./gradlew --no-daemon clean test \ + -PblueLanguageRequireFreshModuleCache=true GRADLE_USER_HOME=/tmp/blue-bex-local-mode \ ./gradlew --no-daemon clean test \ -PblueLanguageCompositePath=/absolute/path/to/clean/blue-language-java @@ -144,6 +150,9 @@ A module-specific cache acceptance is reported separately for the exact `blue.language:blue-language-java:3.1.0-rc.19` Gradle module-version path. Standalone acceptance passes only when that exact path was absent at project configuration and the subsequently resolved JAR matches the recorded Maven -Central hash. It does not claim that the entire Gradle cache was empty or that -a network fetch was directly observed. Cached local-composite runs remain -`not-executed` for this acceptance. +Central hash in the dedicated run that explicitly requires fresh-cache proof. +That authenticated mode receipt is reused by later publication invocations; +they do not overwrite it or require a populated cache to become absent again. +It does not claim that the entire Gradle cache was empty or that a network +fetch was directly observed. Local-composite runs remain `not-executed` for +this acceptance. diff --git a/src/main/java/blue/bex/api/BexGasLedgerHost.java b/src/main/java/blue/bex/api/BexGasLedgerHost.java index a1c55bf..faa9319 100644 --- a/src/main/java/blue/bex/api/BexGasLedgerHost.java +++ b/src/main/java/blue/bex/api/BexGasLedgerHost.java @@ -3,6 +3,7 @@ import blue.bex.gas.BexGasLimitExceededException; import blue.language.processor.GasMeter; import blue.language.processor.GasLimitExceededException; +import blue.language.processor.RuntimeWorkBudget; import java.util.Map; import java.util.Objects; @@ -18,6 +19,52 @@ public interface BexGasLedgerHost { GasMeter.ChildGasLedger open(String namespace, Map counterWeights); + /** + * Opens an invocation-owned local budget shared by all physical ledgers + * used by one BEX execution. + * + *

Session-backed hosts override this method and return the exact + * capability created by their owning runtime work session. Detached + * compatibility hosts return {@code null}; BEX then retains its + * deterministic wrapper precheck.

+ * + * @param maximumGas non-negative BEX-local maximum + * @return shared host budget, or {@code null} when this host has no + * canonical shared-budget capability + */ + default RuntimeWorkBudget openSharedBudget(long maximumGas) { + if (maximumGas < 0L) { + throw new IllegalArgumentException( + "Shared BEX gas budget must be non-negative"); + } + return null; + } + + /** + * Opens a physical ledger attached to a previously opened shared budget. + * + *

The default preserves detached-host compatibility when no budget was + * supplied and fails closed if a host claims a shared budget without + * implementing attachment.

+ * + * @param namespace logical BEX or intrinsic namespace + * @param counterWeights exact counter catalog + * @param sharedBudget invocation-owned shared local budget + * @return live host ledger + * @throws UnsupportedOperationException if a non-null budget is supplied + * to a compatibility host which cannot attach it + */ + default GasMeter.ChildGasLedger open( + String namespace, + Map counterWeights, + RuntimeWorkBudget sharedBudget) { + if (sharedBudget != null) { + throw new UnsupportedOperationException( + "This gas host cannot attach a shared runtime work budget"); + } + return open(namespace, counterWeights); + } + void submit(GasMeter.ChildGasLedger ledger); /** diff --git a/src/main/java/blue/bex/api/ProcessorExecutionContextBexGasLedgerHost.java b/src/main/java/blue/bex/api/ProcessorExecutionContextBexGasLedgerHost.java index 0383f00..0272eca 100644 --- a/src/main/java/blue/bex/api/ProcessorExecutionContextBexGasLedgerHost.java +++ b/src/main/java/blue/bex/api/ProcessorExecutionContextBexGasLedgerHost.java @@ -7,6 +7,7 @@ import blue.language.processor.ProcessorExecutionContext; import blue.language.processor.ProcessorErrorCategory; import blue.language.processor.ProcessorFailureException; +import blue.language.processor.RuntimeWorkBudget; import blue.language.processor.RuntimeWorkSession; import java.util.Map; @@ -19,7 +20,9 @@ * physical runtime namespace. Callers executing more than one BEX program in * a host invocation must provide distinct physical namespaces; all such * ledgers remain owned by the same {@link RuntimeWorkSession} and therefore - * share its live parent budget.

+ * share its live parent budget. When BEX declares a local cap, the adapter + * also opens one invocation-owned {@link RuntimeWorkBudget} and attaches the + * primary and intrinsic ledgers to that exact shared admission boundary.

*/ public final class ProcessorExecutionContextBexGasLedgerHost implements BexGasLedgerHost { private final RuntimeWorkSession session; @@ -47,11 +50,25 @@ public ProcessorExecutionContextBexGasLedgerHost( @Override public GasMeter.ChildGasLedger open(String namespace, Map counterWeights) { + return open(namespace, counterWeights, null); + } + + @Override + public RuntimeWorkBudget openSharedBudget(long maximumGas) { + return session.openSharedBudget(maximumGas); + } + + @Override + public GasMeter.ChildGasLedger open( + String namespace, + Map counterWeights, + RuntimeWorkBudget sharedBudget) { String logicalNamespace = requireRuntimeNamespace(namespace); return session.openLedger( physicalNamespace(logicalNamespace), - counterWeights); + counterWeights, + sharedBudget); } @Override diff --git a/src/main/java/blue/bex/gas/BexGasMeter.java b/src/main/java/blue/bex/gas/BexGasMeter.java index 1189785..fc102c7 100644 --- a/src/main/java/blue/bex/gas/BexGasMeter.java +++ b/src/main/java/blue/bex/gas/BexGasMeter.java @@ -33,6 +33,7 @@ public final class BexGasMeter { private final long effectiveBudget; private final Map hostLedgers; private final boolean qualifiedHostCounters; + private final boolean hostEnforcesLocalLimit; private final Map registeredNamedWeights; private final List trace = new ArrayList<>(); private long totalGas; @@ -55,6 +56,7 @@ public BexGasMeter(BexGasSchedule schedule, long parentRemainingGas) { NO_LOCAL_LIMIT, Collections.emptyMap(), false, + false, Collections.emptyMap()); } @@ -70,6 +72,7 @@ public BexGasMeter(BexGasSchedule schedule, requireLocalLimit(localLimit), Collections.emptyMap(), false, + false, Collections.emptyMap()); } @@ -85,6 +88,7 @@ public BexGasMeter(BexGasSchedule schedule, requireLocalLimit(localLimit), Collections.emptyMap(), false, + false, registeredNamedWeights); } @@ -108,6 +112,7 @@ public BexGasMeter(BexGasSchedule schedule, requireLocalLimit(localLimit), singletonHostLedger(hostLedger), true, + false, Collections.emptyMap()); } @@ -126,6 +131,38 @@ public BexGasMeter( requireLocalLimit(localLimit), hostLedgers, false, + false, + registeredNamedWeights); + } + + /** + * Creates a hosted meter whose configured local limit is enforced by one + * invocation-owned budget shared by every supplied host ledger. + * + *

The meter retains the configured limit for portable diagnostics but + * does not race the canonical host admission path with a duplicate local + * precheck. The host therefore records the exact rejected charge before + * any corresponding BEX or intrinsic work occurs.

+ * + * @param schedule exact BEX gas schedule + * @param hostLedgers one live physical ledger per logical namespace + * @param localLimit non-negative maximum enforced by the shared host + * budget + * @param registeredNamedWeights exact intrinsic counter registry + * @return live BEX meter using canonical host-side local admission + */ + public static BexGasMeter hostedWithSharedLocalLimit( + BexGasSchedule schedule, + Map hostLedgers, + long localLimit, + Map registeredNamedWeights) { + return new BexGasMeter( + schedule, + parentBudget(hostLedgers), + requireLocalLimit(localLimit), + hostLedgers, + false, + true, registeredNamedWeights); } @@ -134,6 +171,7 @@ private BexGasMeter(BexGasSchedule schedule, long localLimit, Map hostLedgers, boolean qualifiedHostCounters, + boolean hostEnforcesLocalLimit, Map registeredNamedWeights) { this.schedule = Objects.requireNonNull(schedule, "schedule"); this.parentRemainingGas = parentRemainingGas; @@ -143,6 +181,14 @@ private BexGasMeter(BexGasSchedule schedule, : Math.min(parentRemainingGas, localLimit); this.hostLedgers = immutableHostLedgers(hostLedgers); this.qualifiedHostCounters = qualifiedHostCounters; + if (hostEnforcesLocalLimit + && (this.hostLedgers.isEmpty() + || localLimit == NO_LOCAL_LIMIT)) { + throw new IllegalArgumentException( + "Host-enforced local limits require hosted ledgers " + + "and a non-negative local limit"); + } + this.hostEnforcesLocalLimit = hostEnforcesLocalLimit; this.registeredNamedWeights = immutableRegisteredWeights(registeredNamedWeights); } @@ -441,6 +487,8 @@ private void chargeAdmitted(String namespace, */ long localAdmissionBudget = hostLedgers.isEmpty() ? effectiveBudget + : hostEnforcesLocalLimit + ? NO_LOCAL_LIMIT : localLimit; if (localAdmissionBudget != NO_LOCAL_LIMIT && gas > localAdmissionBudget - totalGas) { diff --git a/src/main/java/blue/bex/runtime/BexRuntime.java b/src/main/java/blue/bex/runtime/BexRuntime.java index 811a10d..72c7e64 100644 --- a/src/main/java/blue/bex/runtime/BexRuntime.java +++ b/src/main/java/blue/bex/runtime/BexRuntime.java @@ -26,6 +26,7 @@ import blue.language.processor.InvalidExecutionEvidenceException; import blue.language.processor.PortableLimitExceededException; import blue.language.processor.ProcessorFailureException; +import blue.language.processor.RuntimeWorkBudget; import blue.language.utils.JsonPointer; import java.util.LinkedHashMap; @@ -279,29 +280,53 @@ private static BexGasMeter newGasMeter(BexExecutionContext context, } LinkedHashMap children = new LinkedHashMap<>(); + RuntimeWorkBudget sharedBudget = null; try { + if (context.gasLimit() != BexGasMeter.NO_LOCAL_LIMIT) { + sharedBudget = + host.openSharedBudget(context.gasLimit()); + if (sharedBudget != null + && sharedBudget.maximumGas() + != context.gasLimit()) { + throw new IllegalStateException( + "Gas host returned a shared budget with maximum " + + sharedBudget.maximumGas() + + " instead of " + + context.gasLimit()); + } + } children.put( BexGasCounter.NAMESPACE, requireOpenedLedger( - host.open( + openHostLedger( + host, BexGasCounter.NAMESPACE, - gasSchedule.counterWeights()), + gasSchedule.counterWeights(), + sharedBudget), BexGasCounter.NAMESPACE)); for (Map.Entry> intrinsic : namespaceWeights.entrySet()) { children.put( intrinsic.getKey(), requireOpenedLedger( - host.open( + openHostLedger( + host, intrinsic.getKey(), - intrinsic.getValue()), + intrinsic.getValue(), + sharedBudget), intrinsic.getKey())); } - return new BexGasMeter( - gasSchedule, - children, - context.gasLimit(), - registered); + return sharedBudget == null + ? new BexGasMeter( + gasSchedule, + children, + context.gasLimit(), + registered) + : BexGasMeter.hostedWithSharedLocalLimit( + gasSchedule, + children, + context.gasLimit(), + registered); } catch (RuntimeException | Error openingFailure) { finishOpenedLedgersAfterConstructionFailure( host, children, openingFailure); @@ -309,6 +334,19 @@ private static BexGasMeter newGasMeter(BexExecutionContext context, } } + private static GasMeter.ChildGasLedger openHostLedger( + BexGasLedgerHost host, + String namespace, + Map counterWeights, + RuntimeWorkBudget sharedBudget) { + return sharedBudget == null + ? host.open(namespace, counterWeights) + : host.open( + namespace, + counterWeights, + sharedBudget); + } + private static GasMeter.ChildGasLedger requireOpenedLedger( GasMeter.ChildGasLedger ledger, String namespace) { diff --git a/src/test/java/blue/bex/BexExactReferenceDocumentTest.java b/src/test/java/blue/bex/BexExactReferenceDocumentTest.java index 19480c0..f70f050 100644 --- a/src/test/java/blue/bex/BexExactReferenceDocumentTest.java +++ b/src/test/java/blue/bex/BexExactReferenceDocumentTest.java @@ -13,6 +13,7 @@ import blue.language.processor.InvalidExecutionEvidenceException; import blue.language.provider.CyclicAwareNodeProvider; import blue.language.provider.CyclicSetProof; +import blue.language.provider.CyclicSetProofResult; import blue.language.provider.NodeProviderOutcome; import blue.language.provider.NodeProviderResult; import blue.language.snapshot.FrozenNode; @@ -368,7 +369,40 @@ void nullCyclicProofAfterFoundContentIsInvalidNotUnavailable() { member::isObject); assertTrue(failure.getMessage().contains( - "complete cyclic-set proof")); + "typed proof result")); + assertEquals(1, provider.proofQueries); + } + } + + @Test + void cyclicProofUnavailabilityAfterFoundContentRemainsTransient() { + CyclicFixture fixture = new CyclicFixture(); + CyclicEvidenceProvider provider = + new CyclicEvidenceProvider( + NodeProviderResult.found( + Collections.singletonList( + fixture.resolvedMember)), + CyclicSetProofResult.unavailable( + "cyclic proof store temporarily unavailable")); + + try (Blue blue = new Blue(provider)) { + BexValue member = exactReference( + blue, fixture.memberBlueId); + + assertEquals(fixture.memberBlueId, + member.exactBlueId()); + ExecutionEvidenceUnavailableException failure = + assertThrows( + ExecutionEvidenceUnavailableException.class, + member::isObject); + + assertEquals( + Collections.singletonList( + fixture.memberBlueId), + failure.requiredExactBlueIds()); + assertEquals( + "cyclic proof store temporarily unavailable", + failure.getMessage()); assertEquals(1, provider.proofQueries); } } @@ -388,7 +422,8 @@ void malformedCyclicProofIsDeterministicInvalidEvidence() { NodeProviderResult.found( Collections.singletonList( fixture.resolvedMember)), - wrongProof); + CyclicSetProofResult.found( + wrongProof)); try (Blue blue = new Blue(provider)) { BexValue member = exactReference( @@ -434,12 +469,12 @@ public java.util.List fetchByBlueId( } @Override - public CyclicSetProof cyclicSetProofFor( + public CyclicSetProofResult cyclicSetProofFor( String requestedBlueId) { proofQueries++; return memberBlueId.equals(requestedBlueId) - ? proof - : null; + ? CyclicSetProofResult.found(proof) + : CyclicSetProofResult.notFound(); } } @@ -467,14 +502,14 @@ private CyclicFixture() { private static final class CyclicEvidenceProvider implements NodeProvider, CyclicAwareNodeProvider { private final NodeProviderResult result; - private final CyclicSetProof proof; + private final CyclicSetProofResult proofResult; private int proofQueries; private CyclicEvidenceProvider( NodeProviderResult result, - CyclicSetProof proof) { + CyclicSetProofResult proofResult) { this.result = result; - this.proof = proof; + this.proofResult = proofResult; } @Override @@ -494,10 +529,10 @@ public NodeProviderResult fetchResultByBlueId( } @Override - public CyclicSetProof cyclicSetProofFor( + public CyclicSetProofResult cyclicSetProofFor( String requestedBlueId) { proofQueries++; - return proof; + return proofResult; } } diff --git a/src/test/java/blue/bex/BexExecutionEvidenceLedgerTest.java b/src/test/java/blue/bex/BexExecutionEvidenceLedgerTest.java index cb66a8b..81f21cb 100644 --- a/src/test/java/blue/bex/BexExecutionEvidenceLedgerTest.java +++ b/src/test/java/blue/bex/BexExecutionEvidenceLedgerTest.java @@ -14,7 +14,7 @@ import blue.language.processor.GasSchedule; import blue.language.processor.InvalidExecutionEvidenceException; import blue.language.provider.CyclicAwareNodeProvider; -import blue.language.provider.CyclicSetProof; +import blue.language.provider.CyclicSetProofResult; import blue.language.snapshot.FrozenNode; import blue.language.snapshot.ResolvedSnapshot; import blue.language.utils.CircularBlueIdCalculator; @@ -109,6 +109,49 @@ void hostedCyclicStructuralReadWithMissingProofIsDeterministic() { } } + @Test + void cyclicProofUnavailabilityUsesHostedDiscardLifecycle() { + Node placeholder = obj( + "label", "cyclic-content", + "next", new Node().blueId("this#0")) + .name("hosted-unavailable-cyclic-member"); + List placeholders = + Collections.singletonList(placeholder); + String memberBlueId = + CircularBlueIdCalculator + .calculateCircularSetBlueIds( + placeholders) + .get(0); + Node resolvedMember = placeholder.clone(); + resolvedMember.getProperties().get("next") + .blueId(memberBlueId); + UnavailableProofCyclicProvider provider = + new UnavailableProofCyclicProvider( + memberBlueId, + resolvedMember); + + try (Blue blue = new Blue(provider)) { + RecordingGasHost host = new RecordingGasHost(); + ExecutionEvidenceUnavailableException failure = + assertThrows( + ExecutionEvidenceUnavailableException.class, + () -> executeKindRead( + blue, memberBlueId, host)); + + assertEquals( + Collections.singletonList(memberBlueId), + failure.requiredExactBlueIds()); + assertEquals( + "hosted cyclic proof temporarily unavailable", + failure.getMessage()); + assertEquals(1, provider.proofQueries); + assertEquals(1, host.openCount); + assertEquals(0, host.mergeCount); + assertEquals(1, host.unavailableCount); + assertEquals(0L, host.parent.totalGas()); + } + } + private static void executeKindRead( Blue blue, String blueId, @@ -164,10 +207,43 @@ public List fetchByBlueId( } @Override - public CyclicSetProof cyclicSetProofFor( + public CyclicSetProofResult cyclicSetProofFor( String requestedBlueId) { proofQueries++; - return null; + return CyclicSetProofResult.notFound(); + } + } + + private static final class UnavailableProofCyclicProvider + implements NodeProvider, CyclicAwareNodeProvider { + private final String memberBlueId; + private final Node resolvedMember; + private int proofQueries; + + private UnavailableProofCyclicProvider( + String memberBlueId, + Node resolvedMember) { + this.memberBlueId = memberBlueId; + this.resolvedMember = resolvedMember.clone(); + } + + @Override + public List fetchByBlueId( + String requestedBlueId) { + return memberBlueId.equals(requestedBlueId) + ? Collections.singletonList( + resolvedMember.clone()) + : Collections.emptyList(); + } + + @Override + public CyclicSetProofResult cyclicSetProofFor( + String requestedBlueId) { + proofQueries++; + return memberBlueId.equals(requestedBlueId) + ? CyclicSetProofResult.unavailable( + "hosted cyclic proof temporarily unavailable") + : CyclicSetProofResult.notFound(); } } @@ -178,6 +254,7 @@ private static final class RecordingGasHost private GasMeter.ChildGasLedger child; private int openCount; private int mergeCount; + private int unavailableCount; @Override public GasMeter.ChildGasLedger open( @@ -204,6 +281,7 @@ public void failedDeterministically( @Override public void evidenceUnavailable( GasMeter.ChildGasLedger ledger) { + unavailableCount++; assertEquals(child, ledger); } } diff --git a/src/test/java/blue/bex/conformance/BexConformanceReportMain.java b/src/test/java/blue/bex/conformance/BexConformanceReportMain.java index 0fcafa6..396013c 100644 --- a/src/test/java/blue/bex/conformance/BexConformanceReportMain.java +++ b/src/test/java/blue/bex/conformance/BexConformanceReportMain.java @@ -1,8 +1,9 @@ package blue.bex.conformance; import blue.language.processor.RuntimeWorkSession; +import blue.language.processor.RuntimeWorkBudget; import blue.language.provider.CyclicAwareNodeProvider; -import blue.language.provider.CyclicSetProof; +import blue.language.provider.CyclicSetProofResult; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; @@ -95,7 +96,8 @@ public static void main(String[] args) throws Exception { Map specification = specificationEvidence(projectDir, baseline); Map versionAutomation = - versionAutomationEvidence(projectDir, baseline); + versionAutomationEvidence( + projectDir, projectVersion, baseline); Map namedEvidence = namedReleaseEvidence(tests); Map gasExhaustionTraceExamples = @@ -104,7 +106,7 @@ public static void main(String[] args) throws Exception { "gasExhaustionTraceExamples", gasExhaustionTraceExamples); Map hostedLocalLimitCapability = - hostedLocalLimitCapability(); + hostedLocalLimitCapability(tests); Map cyclicProofUnavailabilityCapability = cyclicProofUnavailabilityCapability(tests); Map hostLongTrace = @@ -148,7 +150,9 @@ public static void main(String[] args) throws Exception { languageReleaseIdentity, representationMatrixResult); - if (currentModeFailures.isEmpty()) { + if (currentModeFailures.isEmpty() + && modeRunCanPersistEvidence( + dependencyMode, dependencyResolution)) { persistModeEvidence( persistentEvidenceRoot, dependencyMode, @@ -157,12 +161,13 @@ public static void main(String[] args) throws Exception { sourceState, compositePath, dependencyResolution, - tests, - normativeVectorCoverage, - artifacts, + tests, + normativeVectorCoverage, + artifacts, projectDir, specification, - namedEvidence); + namedEvidence, + publishedApiInspection); } Map buildModes = buildModeMatrix( persistentEvidenceRoot, @@ -171,13 +176,19 @@ public static void main(String[] args) throws Exception { sourceState, compositePath, publishedApiInspection); + boolean exactFinalArtifactProven = + bindLanguageReleaseIdentityToModes( + languageReleaseIdentity, + buildModes); releaseGates.put( "cleanDependencyCacheAcceptance", cleanDependencyCacheAcceptance(buildModes)); boolean bothModesPassed = Boolean.TRUE.equals(buildModes.get("allRequiredModesPassed")); boolean releaseReady = - currentModeFailures.isEmpty() && bothModesPassed; + currentModeFailures.isEmpty() + && bothModesPassed + && exactFinalArtifactProven; Map report = new LinkedHashMap(); report.put("schema", "blue-bex-hosted-release-report/2.0"); @@ -557,6 +568,9 @@ private static Map dependencyResolutionEvidence( "provenance.networkFetchObservation")), "cleanDependencyCacheAcceptance", map( "status", evidence.get("cache.acceptance"), + "freshProofRequired", + Boolean.parseBoolean(evidence.get( + "cache.freshProofRequired")), "scope", evidence.get("cache.acceptanceScope"), "moduleVersionPath", @@ -569,10 +583,26 @@ private static Map dependencyResolutionEvidence( "passed".equals( evidence.get("cache.acceptance")) ? "exact-blue-language-module-version-cache-was-absent-before-resolution" - : "exact-blue-language-module-version-cache-was-not-proven-absent-before-resolution"), + : Boolean.parseBoolean(evidence.get( + "cache.freshProofRequired")) + ? "exact-blue-language-module-version-cache-was-not-proven-absent-before-resolution" + : "fresh-module-cache-proof-not-required-for-current-run"), "compositePath", evidence.get("composite.path")); } + static boolean modeRunCanPersistEvidence( + String dependencyMode, + Map dependencyResolution) { + if (!"standalone-published".equals(dependencyMode)) { + return true; + } + Map cache = castMap( + dependencyResolution.get( + "cleanDependencyCacheAcceptance")); + return Boolean.TRUE.equals(cache.get("freshProofRequired")) + && "passed".equals(cache.get("status")); + } + private static String joinCoordinate( String group, String name, @@ -601,19 +631,56 @@ private static Map specificationEvidence( actual.equals(expected)); } - private static Map versionAutomationEvidence( + static Map versionAutomationEvidence( Path projectDir, + String projectVersion, Map baseline) throws IOException { Path czToml = projectDir.resolve(".cz.toml"); String actual = Files.isRegularFile(czToml) ? sha256(czToml) : "unavailable"; String expected = baseline.get("czTomlSha256"); + String configuredVersion = readCommitizenVersion(czToml); + String expectedVersion = projectVersion.endsWith("-SNAPSHOT") + ? projectVersion.substring( + 0, + projectVersion.length() + - "-SNAPSHOT".length()) + : projectVersion; return map( "path", ".cz.toml", "sha256", actual, - "baselineSha256", expected, - "unchanged", actual.equals(expected)); + "historicalBaselineSha256", expected, + "matchesHistoricalBaseline", actual.equals(expected), + "configuredVersion", configuredVersion, + "projectVersion", projectVersion, + "matchesProjectVersion", + configuredVersion.equals(expectedVersion)); + } + + private static String readCommitizenVersion(Path czToml) + throws IOException { + if (!Files.isRegularFile(czToml)) { + return "unavailable"; + } + for (String line : Files.readAllLines( + czToml, StandardCharsets.UTF_8)) { + String trimmed = line.trim(); + int equals = trimmed.indexOf('='); + if (equals < 0 + || !"version".equals( + trimmed.substring(0, equals).trim())) { + continue; + } + int firstQuote = trimmed.indexOf('"', equals + 1); + int lastQuote = trimmed.lastIndexOf('"'); + if (equals >= 0 + && firstQuote > equals + && lastQuote > firstQuote) { + return trimmed.substring(firstQuote + 1, lastQuote); + } + } + return "unavailable"; } private static Map namedReleaseEvidence( @@ -982,7 +1049,11 @@ private static List currentModeFailures( "passed".equals(dependencyResolution.get("status")), "dependency-resolution-evidence-not-passing"); if ("standalone-published".equals( - dependencyResolution.get("mode"))) { + dependencyResolution.get("mode")) + && Boolean.TRUE.equals(castMap( + dependencyResolution.get( + "cleanDependencyCacheAcceptance")) + .get("freshProofRequired"))) { require( failures, "passed".equals(castMap( @@ -1019,8 +1090,10 @@ private static List currentModeFailures( "specification-identity-differs-from-baseline"); require( failures, - Boolean.TRUE.equals(versionAutomation.get("unchanged")), - "cz-toml-differs-from-baseline"); + Boolean.TRUE.equals( + versionAutomation.get( + "matchesProjectVersion")), + "cz-toml-version-differs-from-project-version"); require( failures, "1.8".equals(System.getProperty( @@ -1208,13 +1281,22 @@ private static Map languageReleaseIdentity( if (compositePath != null && Files.isDirectory(compositePath)) { SourceState state = sourceState(compositePath); + List tagsAtHead = + gitTagsAtHead(compositePath); localMatchesPublished = - commitIdentified - && !state.worktreeDirty + !state.worktreeDirty && state.completeWorkspace() - && publishedCommit.equalsIgnoreCase( - state.commit); - localSource = state.report(); + && publishedSourceIdentityMatches( + declaredDependency, + publishedApiInspection, + state.commit, + tagsAtHead); + localSource = new LinkedHashMap( + state.report()); + localSource.put("tagsAtHead", tagsAtHead); + localSource.put( + "matchesPublishedIdentity", + localMatchesPublished); } Map resolvedArtifact = castMap(dependencyResolution.get("artifact")); @@ -1224,11 +1306,14 @@ private static Map languageReleaseIdentity( boolean standalone = "standalone-published".equals( dependencyResolution.get("mode")); - boolean resolvedArtifactExact = - !standalone - || publishedHash != null + boolean resolvedArtifactHashMatchesPublished = + standalone + && publishedHash != null && publishedHash.equals( resolvedArtifact.get("sha256")); + boolean resolvedArtifactIdentitySatisfied = + !standalone + || resolvedArtifactHashMatchesPublished; boolean exactFinalArtifactProven = commitIdentified && hashIdentified @@ -1236,12 +1321,14 @@ private static Map languageReleaseIdentity( && coordinateMatches && localMatchesPublished && dependencyResolved - && resolvedArtifactExact; + && resolvedArtifactIdentitySatisfied; return map( "schema", - "blue-bex-language-release-identity/1.0", + "blue-bex-language-release-identity/1.1", "exactFinalArtifactProven", exactFinalArtifactProven, + "currentDependencyExactFinalArtifactProven", + exactFinalArtifactProven, "declaredCoordinate", declaredDependency, "publishedCoordinate", publishedApiInspection.get("coordinate"), @@ -1254,12 +1341,18 @@ private static Map languageReleaseIdentity( publishedApiInspection.get("status"), "dependencyResolutionPassed", dependencyResolved, + "resolvedArtifactHashComparisonApplicable", + standalone, "resolvedArtifactMatchesPublishedHash", - resolvedArtifactExact, + resolvedArtifactHashMatchesPublished, + "resolvedArtifactIdentitySatisfied", + resolvedArtifactIdentitySatisfied, "resolvedArtifact", resolvedArtifact, "localCompositeSource", localSource, "localCompositeMatchesPublishedCommit", localMatchesPublished, + "localCompositeMatchesPublishedIdentity", + localMatchesPublished, "failures", exactFinalArtifactProven ? Collections.emptyList() @@ -1279,12 +1372,12 @@ private static Map languageReleaseIdentity( dependencyResolved ? null : "dependency-resolution-not-passing", - resolvedArtifactExact + resolvedArtifactIdentitySatisfied ? null : "resolved-artifact-hash-mismatch", localMatchesPublished ? null - : "local-composite-not-clean-exact-published-commit") + : "local-composite-not-clean-exact-published-commit-and-version-tag") .stream() .filter(Objects::nonNull) .collect(Collectors.toList())); @@ -1363,7 +1456,9 @@ private static void persistModeEvidence( List artifacts, Path projectDir, Map specification, - Map namedEvidence) throws Exception { + Map namedEvidence, + Map publishedApiInspection) + throws Exception { Path modeRoot = root.resolve("modes").resolve(mode); Path artifactRoot = modeRoot.resolve("artifacts"); Files.createDirectories(artifactRoot); @@ -1371,7 +1466,7 @@ private static void persistModeEvidence( new LinkedHashMap(); values.put( "schema", - "blue-bex-build-mode-evidence/2.0"); + "blue-bex-build-mode-evidence/2.2"); values.put("status", "passed"); values.put("mode", mode); values.put("declared.coordinate", declaredDependency); @@ -1448,6 +1543,12 @@ private static void persistModeEvidence( dependencyResolution.get( "cleanDependencyCacheAcceptance")) .get("status"))); + values.put( + "dependency.cache.freshProofRequired", + String.valueOf(castMap( + dependencyResolution.get( + "cleanDependencyCacheAcceptance")) + .get("freshProofRequired"))); values.put( "dependency.cache.acceptanceScope", String.valueOf(castMap( @@ -1519,6 +1620,8 @@ private static void persistModeEvidence( } SourceState dependencySource = sourceState(compositePath); + List tagsAtHead = + gitTagsAtHead(compositePath); values.put( "composite.path", compositePath.toString()); @@ -1538,6 +1641,24 @@ private static void persistModeEvidence( !dependencySource.worktreeDirty && dependencySource .completeWorkspace())); + values.put( + "composite.publishedSourceCommit", + String.valueOf( + publishedApiInspection.get( + "source.commit"))); + values.put( + "composite.publishedSourceTag", + String.valueOf( + publishedApiInspection.get( + "source.tag"))); + values.put( + "composite.matchesPublishedIdentity", + String.valueOf( + publishedSourceIdentityMatches( + declaredDependency, + publishedApiInspection, + dependencySource.commit, + tagsAtHead))); } writeEvidence(modeRoot.resolve("mode.properties"), values); } @@ -1641,7 +1762,7 @@ private static Map validateModeEvidence( List failures = new ArrayList(); require( failures, - "blue-bex-build-mode-evidence/2.0".equals( + "blue-bex-build-mode-evidence/2.2".equals( evidence.get("schema")), "unknown-evidence-schema"); require( @@ -1736,6 +1857,11 @@ private static Map validateModeEvidence( "passed".equals(evidence.get( "dependency.cache.acceptance")), "standalone-blue-language-module-version-cache-not-accepted"); + require( + failures, + Boolean.parseBoolean(evidence.get( + "dependency.cache.freshProofRequired")), + "standalone-fresh-module-cache-proof-was-not-required"); require( failures, "standalone-published-blue-language-module-version-cache" @@ -1805,6 +1931,8 @@ private static Map validateModeEvidence( if (samePath && Files.isDirectory(composite)) { SourceState dependencySource = sourceState(composite); + List tagsAtHead = + gitTagsAtHead(composite); boolean sourceMatches = dependencySource.fingerprint.sha256.equals( evidence.get( @@ -1818,10 +1946,33 @@ private static Map validateModeEvidence( && Boolean.parseBoolean( evidence.get( "composite.releaseInputsCommitted")); + boolean publishedIdentityMatches = + publishedSourceIdentityMatches( + declaredDependency, + publishedApiInspection, + dependencySource.commit, + tagsAtHead) + && Objects.equals( + publishedApiInspection.get( + "source.commit"), + evidence.get( + "composite.publishedSourceCommit")) + && Objects.equals( + publishedApiInspection.get( + "source.tag"), + evidence.get( + "composite.publishedSourceTag")) + && Boolean.parseBoolean( + evidence.get( + "composite.matchesPublishedIdentity")); require( failures, sourceMatches, "local-composite-source-state-changed"); + require( + failures, + publishedIdentityMatches, + "local-composite-not-bound-to-published-language-identity"); compositeSource = map( "path", composite.toString(), "recordedCommit", @@ -1832,6 +1983,15 @@ private static Map validateModeEvidence( "recordedReleaseSourceSha256", evidence.get( "composite.releaseSourceSha256"), + "recordedPublishedSourceCommit", + evidence.get( + "composite.publishedSourceCommit"), + "recordedPublishedSourceTag", + evidence.get( + "composite.publishedSourceTag"), + "tagsAtHead", tagsAtHead, + "matchesPublishedIdentity", + publishedIdentityMatches, "current", dependencySource.report(), "matchesRecordedEvidence", sourceMatches); } @@ -1875,6 +2035,9 @@ private static Map validateModeEvidence( "status", evidence.get( "dependency.cache.acceptance"), + "freshProofRequired", + Boolean.parseBoolean(evidence.get( + "dependency.cache.freshProofRequired")), "scope", evidence.get( "dependency.cache.acceptanceScope"), @@ -1893,6 +2056,117 @@ private static Map validateModeEvidence( "failures", failures); } + static boolean publishedSourceIdentityMatches( + String declaredDependency, + Map publishedApiInspection, + String sourceCommit, + Collection tagsAtHead) { + String publishedCoordinate = + publishedApiInspection.get("coordinate"); + String publishedCommit = + publishedApiInspection.get("source.commit"); + String publishedTag = + publishedApiInspection.get("source.tag"); + int firstSeparator = declaredDependency.indexOf(':'); + int lastSeparator = declaredDependency.lastIndexOf(':'); + boolean coordinateShapeValid = + firstSeparator > 0 + && lastSeparator > firstSeparator + 1 + && lastSeparator + == declaredDependency.indexOf( + ':', firstSeparator + 1) + && lastSeparator + 1 + < declaredDependency.length(); + String version = coordinateShapeValid + && lastSeparator + 1 < declaredDependency.length() + ? declaredDependency.substring(lastSeparator + 1) + : ""; + return coordinateShapeValid + && declaredDependency.equals(publishedCoordinate) + && publishedCommit != null + && publishedCommit.matches("[0-9a-fA-F]{40}") + && publishedCommit.equalsIgnoreCase(sourceCommit) + && publishedTag != null + && publishedTag.equals("v" + version) + && tagsAtHead != null + && tagsAtHead.contains(publishedTag); + } + + private static boolean localModeMatchesPublishedIdentity( + Map buildModes) { + Map local = + castMap(buildModes.get("localComposite")); + Map compositeSource = + castMap(local.get("compositeSource")); + return "passed".equals(local.get("status")) + && Boolean.TRUE.equals( + compositeSource.get( + "matchesPublishedIdentity")); + } + + static boolean bindLanguageReleaseIdentityToModes( + Map identity, + Map buildModes) { + boolean localMatches = + localModeMatchesPublishedIdentity(buildModes); + boolean currentDependencyExact = + Boolean.TRUE.equals( + identity.get( + "currentDependencyExactFinalArtifactProven")); + boolean exact = currentDependencyExact && localMatches; + identity.put( + "localCompositeMatchesPublishedCommit", + localMatches); + identity.put( + "localCompositeMatchesPublishedIdentity", + localMatches); + identity.put( + "validatedLocalCompositeModeMatchesPublishedIdentity", + localMatches); + identity.put( + "validatedLocalCompositeModeFailure", + localMatches + ? null + : "validated-local-composite-mode-not-bound-to-published-language-identity"); + Set failures = new LinkedHashSet(); + Object existingFailures = identity.get("failures"); + if (existingFailures instanceof Collection) { + for (Object failure + : (Collection) existingFailures) { + if (failure != null) { + failures.add(String.valueOf(failure)); + } + } + } + if (!localMatches) { + failures.add( + "validated-local-composite-mode-not-bound-to-published-language-identity"); + } + identity.put( + "failures", + new ArrayList(failures)); + identity.put("exactFinalArtifactProven", exact); + return exact; + } + + private static List gitTagsAtHead(Path repository) + throws Exception { + String output = git( + repository, + "tag", + "--points-at", + "HEAD"); + List tags = new ArrayList(); + for (String line : output.split("\\R")) { + String tag = line.trim(); + if (!tag.isEmpty()) { + tags.add(tag); + } + } + Collections.sort(tags); + return Collections.unmodifiableList(tags); + } + private static Map cleanDependencyCacheAcceptance( Map buildModes) { Map standalone = castMap( @@ -1993,17 +2267,22 @@ private static String prefix(String value) { : value.substring(0, separator); } - private static Map hostedLocalLimitCapability() { + private static Map hostedLocalLimitCapability( + TestEvidence tests) { List observedOpenLedgerSignatures = new ArrayList(); - boolean openLedgerAcceptsMaximumBudget = false; + List observedOpenSharedBudgetSignatures = + new ArrayList(); + boolean openLedgerAcceptsSharedBudget = false; for (Method method : RuntimeWorkSession.class.getMethods()) { - if (!"openLedger".equals(method.getName())) { + Class[] parameters = method.getParameterTypes(); + if (!"openLedger".equals(method.getName()) + && !"openSharedBudget".equals( + method.getName())) { continue; } - Class[] parameters = method.getParameterTypes(); - StringBuilder signature = - new StringBuilder("openLedger("); + StringBuilder signature = new StringBuilder( + method.getName()).append('('); for (int index = 0; index < parameters.length; index++) { if (index > 0) { signature.append(','); @@ -2011,30 +2290,46 @@ private static Map hostedLocalLimitCapability() { signature.append(parameters[index].getName()); } signature.append(')'); - observedOpenLedgerSignatures.add( - signature.toString()); - if (parameters.length == 3 + if ("openLedger".equals(method.getName())) { + observedOpenLedgerSignatures.add( + signature.toString()); + } else { + observedOpenSharedBudgetSignatures.add( + signature.toString()); + } + if ("openLedger".equals(method.getName()) + && parameters.length == 3 && String.class.equals(parameters[0]) && Map.class.isAssignableFrom(parameters[1]) - && (Long.TYPE.equals(parameters[2]) - || Long.class.equals(parameters[2]))) { - openLedgerAcceptsMaximumBudget = true; + && RuntimeWorkBudget.class.equals( + parameters[2])) { + openLedgerAcceptsSharedBudget = true; } } Collections.sort(observedOpenLedgerSignatures); + Collections.sort(observedOpenSharedBudgetSignatures); - /* - * The current host publishes no invocation-owned capped scope which - * can be shared by independently named physical ledgers. In - * particular, openLedger only accepts a namespace and counter - * catalog, so BEX cannot make its physical ledger and a later - * intrinsic ledger consume one BEX-local cap through the canonical - * session admission path. - */ boolean invocationOwnedSharedCappedBudgetScope = false; + for (Method method : RuntimeWorkSession.class.getMethods()) { + Class[] parameters = method.getParameterTypes(); + if ("openSharedBudget".equals(method.getName()) + && parameters.length == 1 + && Long.TYPE.equals(parameters[0]) + && RuntimeWorkBudget.class.equals( + method.getReturnType())) { + invocationOwnedSharedCappedBudgetScope = true; + } + } + Map sharedBudgetEvidence = + tests.namedEvidence( + "hostedLocalLimitIsSharedAcrossBexAndIntrinsicLedgers"); + boolean sharedBudgetTestPassed = + "passed".equals( + sharedBudgetEvidence.get("status")); boolean capabilityAvailable = - openLedgerAcceptsMaximumBudget - && invocationOwnedSharedCappedBudgetScope; + openLedgerAcceptsSharedBudget + && invocationOwnedSharedCappedBudgetScope + && sharedBudgetTestPassed; return map( "schema", "blue-bex-hosted-local-limit-capability/1.0", @@ -2044,22 +2339,35 @@ private static Map hostedLocalLimitCapability() { "workstream-2-property-1", "observedRuntimeWorkSessionOpenLedgerSignatures", observedOpenLedgerSignatures, + "observedRuntimeWorkSessionOpenSharedBudgetSignatures", + observedOpenSharedBudgetSignatures, "runtimeWorkSessionOpenLedgerAcceptsMaximumBudget", - openLedgerAcceptsMaximumBudget, + false, + "runtimeWorkSessionOpenLedgerAcceptsSharedBudget", + openLedgerAcceptsSharedBudget, "invocationOwnedSharedCappedBudgetScope", invocationOwnedSharedCappedBudgetScope, + "sharedHostBudgetActive", + capabilityAvailable, "bexWrapperPrecheckOccursBeforeWork", - true, - "bexPhysicalLedgerAndIntrinsicLedgerShareLocalCap", false, + "canonicalHostPrecheckOccursBeforeWork", + sharedBudgetTestPassed, + "bexPhysicalLedgerAndIntrinsicLedgerShareLocalCap", + sharedBudgetTestPassed, "canonicalSessionRecordedLocalRejection", - false, + sharedBudgetTestPassed, + "sharedBudgetHostedExecutionEvidence", + sharedBudgetEvidence, "assessment", - "The wrapper precheck is exact and occurs before work, " - + "but the current RuntimeWorkSession cannot enforce " - + "one BEX-local cap across BEX and intrinsic " - + "physical ledgers or record the local rejection " - + "through its canonical rejection path."); + capabilityAvailable + ? "RuntimeWorkSession supplies an invocation-owned " + + "shared budget, BEX attaches both its portable and " + + "intrinsic physical ledgers, and the focused hosted " + + "execution proves that rejection is session-recorded " + + "before intrinsic work." + : "The shared-budget API shape or its focused hosted " + + "execution evidence is incomplete."); } private static Map @@ -2075,7 +2383,7 @@ private static Map hostedLocalLimitCapability() { } returnType = method.getReturnType().getName(); typedOutcome = - !CyclicSetProof.class.equals( + CyclicSetProofResult.class.equals( method.getReturnType()); } @@ -2085,9 +2393,22 @@ private static Map hostedLocalLimitCapability() { Map nullProofEvidence = tests.namedEvidence( "nullCyclicProofAfterFoundContentIsInvalidNotUnavailable"); - boolean proofLayerTransientUnavailableExpressible = false; + Map proofUnavailableEvidence = + tests.namedEvidence( + "cyclicProofUnavailabilityAfterFoundContentRemainsTransient"); + Map hostedUnavailableEvidence = + tests.namedEvidence( + "hostedCyclicProofUnavailabilityUsesSessionDiscardLifecycle"); + boolean proofLayerTransientUnavailableExpressible = + typedOutcome + && "passed".equals( + proofUnavailableEvidence.get("status")); + boolean hostedUnavailableLifecyclePassed = + "passed".equals( + hostedUnavailableEvidence.get("status")); boolean capabilityAvailable = typedOutcome - && proofLayerTransientUnavailableExpressible; + && proofLayerTransientUnavailableExpressible + && hostedUnavailableLifecyclePassed; return map( "schema", @@ -2108,6 +2429,8 @@ private static Map hostedLocalLimitCapability() { contentFetchEvidence, "nullProofAfterFoundEvidence", nullProofEvidence, + "proofLayerUnavailableEvidence", + proofUnavailableEvidence, "contentFetchEvidenceIsProofLayerEvidence", false, "directInvalidProofCoverage", @@ -2115,20 +2438,20 @@ private static Map hostedLocalLimitCapability() { ? "passed" : nullProofEvidence.get("status"), "hostedUnavailableProofLifecyclePathAvailable", - false, + hostedUnavailableLifecyclePassed, "hostedUnavailableProofStructuralReadCoverage", - "absent", + hostedUnavailableEvidence.get("status"), + "hostedUnavailableProofStructuralReadEvidence", + hostedUnavailableEvidence, "assessment", - "CyclicAwareNodeProvider.cyclicSetProofFor returns a proof " - + "or null. After content is FOUND, null is converted " - + "to INVALID_EVIDENCE, so the current host cannot " - + "express transient proof-layer unavailability. " - + "Direct invalid-proof classification is covered, " - + "but no hosted unavailable-proof lifecycle " - + "structural-read path exists to cover. " - + "The content-fetch unavailable test stops before " - + "the proof query and is not proof-unavailability " - + "coverage."); + capabilityAvailable + ? "CyclicSetProofResult distinguishes FOUND, " + + "NOT_FOUND, UNAVAILABLE, and INVALID_EVIDENCE. " + + "Focused structural-read evidence proves that " + + "proof-layer UNAVAILABLE remains transient and " + + "uses the hosted discard lifecycle." + : "The typed cyclic-proof outcome or focused " + + "UNAVAILABLE lifecycle evidence is incomplete."); } private static List knownLimitations( @@ -2540,9 +2863,10 @@ private static String markdownReport( output.append("\nSpecification SHA-256: `") .append(castMap(report.get("specification")) .get("sha256")) - .append("`. `.cz.toml` unchanged: `") + .append("`. `.cz.toml` matches the project version: `") .append(castMap(report.get( - "versionAutomation")).get("unchanged")) + "versionAutomation")).get( + "matchesProjectVersion")) .append("`.\n\n"); Map hostedLocalLimit = @@ -2553,18 +2877,22 @@ private static String markdownReport( .append(hostedLocalLimit.get("status")) .append("`\n"); output.append("- `RuntimeWorkSession.openLedger` accepts a " - + "maximum budget: `") + + "shared budget: `") .append(hostedLocalLimit.get( - "runtimeWorkSessionOpenLedgerAcceptsMaximumBudget")) + "runtimeWorkSessionOpenLedgerAcceptsSharedBudget")) .append("`\n"); output.append("- Invocation-owned shared capped scope: `") .append(hostedLocalLimit.get( "invocationOwnedSharedCappedBudgetScope")) .append("`\n"); - output.append("- Exact wrapper precheck before work: `") + output.append("- Duplicate BEX wrapper precheck active: `") .append(hostedLocalLimit.get( "bexWrapperPrecheckOccursBeforeWork")) .append("`\n"); + output.append("- Canonical host precheck before work: `") + .append(hostedLocalLimit.get( + "canonicalHostPrecheckOccursBeforeWork")) + .append("`\n"); output.append("- BEX and intrinsic physical ledgers share the " + "BEX-local cap: `") .append(hostedLocalLimit.get( @@ -2642,34 +2970,16 @@ private static String markdownReport( } if (localLimitBlocked) { output.append( - "The current `RuntimeWorkSession` exposes no " - + "invocation-owned capped scope and no " - + "`openLedger` maximum-budget parameter. " - + "Consequently, BEX and intrinsic physical " - + "ledgers cannot share or report one BEX-local " - + "cap, and a local rejection cannot be " - + "registered through the canonical session " - + "path. The exact wrapper precheck still occurs " - + "before work, but workstream-2 property 1 is " - + "not fully satisfiable in BEX alone.\n"); + String.valueOf( + hostedLocalLimit.get("assessment"))) + .append("\n"); } if (cyclicProofUnavailableBlocked) { - output.append( - "\nThe current `CyclicAwareNodeProvider` proof contract " - + "returns `CyclicSetProof` or `null`; after " - + "content is found, `VerifyingNodeProvider` " - + "classifies a null proof as invalid evidence. " - + "It cannot preserve transient proof-layer " - + "unavailability. Direct invalid-proof " - + "classification is covered, but no hosted " - + "unavailable-proof lifecycle structural-read " - + "path exists to cover. The passing content-fetch " - + "unavailable test stops before any proof query " - + "and is not claimed as unavailable-proof " - + "coverage. Release readiness remains " - + "fail-closed until the host exposes and " - + "preserves a typed unavailable proof " - + "outcome.\n"); + output.append("\n") + .append(String.valueOf( + cyclicProofUnavailability.get( + "assessment"))) + .append("\n"); } if (publishedApiBlocked) { output.append( @@ -3269,6 +3579,7 @@ && cleanBuildReceiptMatchesAggregate( "second.checkout.gitDirectory")); boolean distinctCheckouts = false; boolean liveCheckoutStateMatches = false; + boolean receiptArtifactsMatch = false; if (firstRoot != null && secondRoot != null && firstGitDirectory != null @@ -3298,9 +3609,27 @@ && liveCleanCheckoutMatches( "second", evidence, sourceCommit); + receiptArtifactsMatch = + cleanBuildReceiptArtifactsMatch( + firstRealRoot, + firstReceiptValues, + artifactPaths.keySet(), + projectVersion) + && cleanBuildReceiptArtifactsMatch( + secondRealRoot, + secondReceiptValues, + artifactPaths.keySet(), + projectVersion) + && cleanBuildReceiptDependencyMatches( + firstRealRoot, + firstReceiptValues) + && cleanBuildReceiptDependencyMatches( + secondRealRoot, + secondReceiptValues); } catch (Exception invalid) { distinctCheckouts = false; liveCheckoutStateMatches = false; + receiptArtifactsMatch = false; } } Map resolvedArtifact = @@ -3387,7 +3716,7 @@ && liveCleanCheckoutMatches( "composite.pathCount")); } boolean passed = - "blue-bex-independent-clean-builds/1.0" + "blue-bex-independent-clean-builds/1.2" .equals(evidence.get("schema")) && "passed".equals( evidence.get("status")) @@ -3401,6 +3730,7 @@ && liveCleanCheckoutMatches( "second.checkout.clean")) && receiptsValid && receiptContentsMatch + && receiptArtifactsMatch && distinctCheckouts && liveCheckoutStateMatches && dependencyInputMatches @@ -3425,6 +3755,8 @@ && liveCleanCheckoutMatches( "receiptsValid", receiptsValid, "receiptContentsMatchAggregate", receiptContentsMatch, + "receiptArtifactsMatch", + receiptArtifactsMatch, "distinctCheckouts", distinctCheckouts, "liveCheckoutStateMatches", liveCheckoutStateMatches, @@ -3448,7 +3780,7 @@ static boolean cleanBuildReceiptMatchesAggregate( String prefix, Map receipt, Collection artifactNames) { - if (!"blue-bex-clean-build-artifacts/1.0".equals( + if (!"blue-bex-clean-build-artifacts/1.1".equals( receipt.get("schema")) || !"passed".equals(receipt.get("status")) || !"true".equals(receipt.get("checkout.clean")) @@ -3478,6 +3810,8 @@ static boolean cleanBuildReceiptMatchesAggregate( "dependency.mode", "dependency.coordinate", "dependency.effectiveCoordinate", + "dependency.artifact.path", + "dependency.artifact.bytes", "dependency.artifact.sha256", "composite.path", "composite.commit", @@ -3493,12 +3827,35 @@ static boolean cleanBuildReceiptMatchesAggregate( return false; } } + for (String field + : new String[] {"path", "bytes", "sha256"}) { + String receiptKey = + "dependency.artifact." + field; + if (!Objects.equals( + receipt.get(receiptKey), + aggregate.get( + prefix + "." + receiptKey))) { + return false; + } + } for (String artifactName : artifactNames) { - String key = - "artifact." + artifactName + ".sha256"; + String artifactPrefix = + "artifact." + artifactName + "."; + for (String field + : new String[] {"path", "bytes", "sha256"}) { + String receiptKey = artifactPrefix + field; + String aggregateKey = + prefix + "." + receiptKey; + if (!Objects.equals( + receipt.get(receiptKey), + aggregate.get(aggregateKey))) { + return false; + } + } if (!Objects.equals( - receipt.get(key), - aggregate.get(key)) + receipt.get(artifactPrefix + "sha256"), + aggregate.get( + artifactPrefix + "sha256")) || !"true".equals(aggregate.get( "artifact." + artifactName + ".byteIdentical"))) { @@ -3508,6 +3865,107 @@ static boolean cleanBuildReceiptMatchesAggregate( return true; } + static boolean cleanBuildReceiptArtifactsMatch( + Path checkoutRoot, + Map receipt, + Collection artifactNames, + String projectVersion) { + try { + Path realRoot = checkoutRoot.toRealPath(); + String artifactPrefix = + "blue-bex-java-" + projectVersion; + Map expectedPaths = + stringMap( + "main", + "build/libs/" + artifactPrefix + ".jar", + "sources", + "build/libs/" + artifactPrefix + + "-sources.jar", + "javadoc", + "build/libs/" + artifactPrefix + + "-javadoc.jar", + "sourceRelease", + "build/distributions/" + artifactPrefix + + "-source-release.zip"); + for (String artifactName : artifactNames) { + String expected = expectedPaths.get(artifactName); + String recorded = + receipt.get( + "artifact." + artifactName + ".path"); + if (expected == null + || recorded == null + || Paths.get(recorded).isAbsolute() + || !expected.equals(recorded)) { + return false; + } + Path artifact = + realRoot.resolve(recorded).normalize(); + if (!artifact.startsWith(realRoot) + || !Files.isRegularFile(artifact)) { + return false; + } + Path realArtifact = artifact.toRealPath(); + if (!realArtifact.startsWith(realRoot)) { + return false; + } + long recordedBytes = parseLong(receipt.get( + "artifact." + artifactName + ".bytes")); + String recordedHash = receipt.get( + "artifact." + artifactName + ".sha256"); + if (recordedBytes < 0 + || Files.size(realArtifact) + != recordedBytes + || recordedHash == null + || !recordedHash.matches("[0-9a-f]{64}") + || !recordedHash.equals( + sha256(realArtifact))) { + return false; + } + } + return true; + } catch (Exception invalid) { + return false; + } + } + + static boolean cleanBuildReceiptDependencyMatches( + Path checkoutRoot, + Map receipt) { + try { + Path realRoot = checkoutRoot.toRealPath(); + String expected = + "build/reports/bex-release/clean-build-inputs/" + + "blue-language-java.jar"; + String recorded = + receipt.get("dependency.artifact.path"); + if (!expected.equals(recorded) + || Paths.get(recorded).isAbsolute()) { + return false; + } + Path artifact = + realRoot.resolve(recorded).normalize(); + if (!artifact.startsWith(realRoot) + || !Files.isRegularFile(artifact)) { + return false; + } + Path realArtifact = artifact.toRealPath(); + if (!realArtifact.startsWith(realRoot)) { + return false; + } + long recordedBytes = parseLong( + receipt.get("dependency.artifact.bytes")); + String recordedHash = + receipt.get("dependency.artifact.sha256"); + return recordedBytes >= 0 + && Files.size(realArtifact) == recordedBytes + && recordedHash != null + && recordedHash.matches("[0-9a-f]{64}") + && recordedHash.equals(sha256(realArtifact)); + } catch (Exception invalid) { + return false; + } + } + static boolean liveCleanCheckoutMatches( Path checkoutRoot, Path recordedGitDirectory, diff --git a/src/test/java/blue/bex/conformance/BexConformanceReportTruthfulnessTest.java b/src/test/java/blue/bex/conformance/BexConformanceReportTruthfulnessTest.java index a5679c4..033fc14 100644 --- a/src/test/java/blue/bex/conformance/BexConformanceReportTruthfulnessTest.java +++ b/src/test/java/blue/bex/conformance/BexConformanceReportTruthfulnessTest.java @@ -3,6 +3,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; @@ -58,7 +59,7 @@ void cleanBuildReceiptMustMatchEveryAggregateInput() { new LinkedHashMap(); receipt.put( "schema", - "blue-bex-clean-build-artifacts/1.0"); + "blue-bex-clean-build-artifacts/1.1"); receipt.put("status", "passed"); receipt.put("checkout.clean", "true"); receipt.put("checkout.root", "/checkout/one"); @@ -73,6 +74,11 @@ void cleanBuildReceiptMustMatchEveryAggregateInput() { receipt.put( "dependency.effectiveCoordinate", "group:name:version"); + receipt.put( + "dependency.artifact.path", + "build/reports/bex-release/clean-build-inputs/" + + "blue-language-java.jar"); + receipt.put("dependency.artifact.bytes", "8"); receipt.put("dependency.artifact.sha256", "language"); receipt.put("composite.path", ""); receipt.put("composite.commit", ""); @@ -80,6 +86,10 @@ void cleanBuildReceiptMustMatchEveryAggregateInput() { receipt.put("composite.gitStatusSha256", ""); receipt.put("composite.workspaceSha256", ""); receipt.put("composite.pathCount", "0"); + receipt.put( + "artifact.main.path", + "build/libs/blue-bex-java-2.0.0.jar"); + receipt.put("artifact.main.bytes", "4"); receipt.put("artifact.main.sha256", "main"); Map aggregate = @@ -100,6 +110,24 @@ void cleanBuildReceiptMustMatchEveryAggregateInput() { aggregate.put( "first.checkout.pathCount", receipt.get("checkout.pathCount")); + aggregate.put( + "first.dependency.artifact.path", + receipt.get("dependency.artifact.path")); + aggregate.put( + "first.dependency.artifact.bytes", + receipt.get("dependency.artifact.bytes")); + aggregate.put( + "first.dependency.artifact.sha256", + receipt.get("dependency.artifact.sha256")); + aggregate.put( + "first.artifact.main.path", + receipt.get("artifact.main.path")); + aggregate.put( + "first.artifact.main.bytes", + receipt.get("artifact.main.bytes")); + aggregate.put( + "first.artifact.main.sha256", + receipt.get("artifact.main.sha256")); aggregate.put( "artifact.main.byteIdentical", "true"); @@ -140,4 +168,254 @@ void emptyDirectoriesCannotMasqueradeAsCleanGitCheckouts( Collections.emptyMap(), "0000000000000000000000000000000000000000")); } + + @Test + void cleanBuildReceiptMustDescribeLiveArtifactBytes( + @TempDir Path temporaryDirectory) throws Exception { + Path artifact = temporaryDirectory.resolve( + "build/libs/blue-bex-java-2.0.0.jar"); + Files.createDirectories(artifact.getParent()); + byte[] content = "real-artifact".getBytes( + StandardCharsets.UTF_8); + Files.write(artifact, content); + + Map receipt = + new LinkedHashMap(); + receipt.put( + "artifact.main.path", + "build/libs/blue-bex-java-2.0.0.jar"); + receipt.put( + "artifact.main.bytes", + String.valueOf(content.length)); + receipt.put( + "artifact.main.sha256", + ConformancePackage.sha256(content)); + + assertTrue( + BexConformanceReportMain + .cleanBuildReceiptArtifactsMatch( + temporaryDirectory, + receipt, + Collections.singleton("main"), + "2.0.0")); + + Files.write( + artifact, + "mutated".getBytes(StandardCharsets.UTF_8)); + assertFalse( + BexConformanceReportMain + .cleanBuildReceiptArtifactsMatch( + temporaryDirectory, + receipt, + Collections.singleton("main"), + "2.0.0")); + } + + @Test + void cleanBuildReceiptMustPreserveExactLanguageArtifact( + @TempDir Path temporaryDirectory) throws Exception { + Path artifact = temporaryDirectory.resolve( + "build/reports/bex-release/clean-build-inputs/" + + "blue-language-java.jar"); + Files.createDirectories(artifact.getParent()); + byte[] content = "language-artifact".getBytes( + StandardCharsets.UTF_8); + Files.write(artifact, content); + + Map receipt = + new LinkedHashMap(); + receipt.put( + "dependency.artifact.path", + "build/reports/bex-release/clean-build-inputs/" + + "blue-language-java.jar"); + receipt.put( + "dependency.artifact.bytes", + String.valueOf(content.length)); + receipt.put( + "dependency.artifact.sha256", + ConformancePackage.sha256(content)); + + assertTrue( + BexConformanceReportMain + .cleanBuildReceiptDependencyMatches( + temporaryDirectory, + receipt)); + + Files.write( + artifact, + "different-language".getBytes( + StandardCharsets.UTF_8)); + assertFalse( + BexConformanceReportMain + .cleanBuildReceiptDependencyMatches( + temporaryDirectory, + receipt)); + } + + @Test + void rotatingRcVersionIsCheckedSemantically( + @TempDir Path temporaryDirectory) throws Exception { + Files.write( + temporaryDirectory.resolve(".cz.toml"), + Arrays.asList( + "[tool.commitizen]", + "version_scheme = \"semver\"", + "version = \"2.0.0-rc.7\""), + StandardCharsets.UTF_8); + + Map release = + BexConformanceReportMain.versionAutomationEvidence( + temporaryDirectory, + "2.0.0-rc.7", + Collections.emptyMap()); + Map local = + BexConformanceReportMain.versionAutomationEvidence( + temporaryDirectory, + "2.0.0-rc.7-SNAPSHOT", + Collections.emptyMap()); + Map mismatched = + BexConformanceReportMain.versionAutomationEvidence( + temporaryDirectory, + "2.0.0-rc.8", + Collections.emptyMap()); + + assertTrue(Boolean.TRUE.equals( + release.get("matchesProjectVersion"))); + assertTrue(Boolean.TRUE.equals( + local.get("matchesProjectVersion"))); + assertFalse(Boolean.TRUE.equals( + mismatched.get("matchesProjectVersion"))); + } + + @Test + void onlyFreshStandaloneRunCanReplaceModeEvidence() { + Map dependency = + new LinkedHashMap(); + Map cache = + new LinkedHashMap(); + dependency.put("cleanDependencyCacheAcceptance", cache); + + cache.put("freshProofRequired", Boolean.TRUE); + cache.put("status", "passed"); + assertTrue( + BexConformanceReportMain.modeRunCanPersistEvidence( + "standalone-published", dependency)); + + cache.put("freshProofRequired", Boolean.FALSE); + assertFalse( + BexConformanceReportMain.modeRunCanPersistEvidence( + "standalone-published", dependency)); + + cache.put("freshProofRequired", Boolean.TRUE); + cache.put("status", "failed"); + assertFalse( + BexConformanceReportMain.modeRunCanPersistEvidence( + "standalone-published", dependency)); + + assertTrue( + BexConformanceReportMain.modeRunCanPersistEvidence( + "local-composite", dependency)); + } + + @Test + void localCompositeIdentityMustMatchPublishedCommitAndVersionTag() { + String coordinate = + "blue.language:blue-language-java:3.1.0-rc.20"; + String commit = + "0123456789abcdef0123456789abcdef01234567"; + Map inspection = + new LinkedHashMap(); + inspection.put("coordinate", coordinate); + inspection.put("source.commit", commit); + inspection.put("source.tag", "v3.1.0-rc.20"); + + assertTrue( + BexConformanceReportMain + .publishedSourceIdentityMatches( + coordinate, + inspection, + commit, + Collections.singleton( + "v3.1.0-rc.20"))); + assertFalse( + BexConformanceReportMain + .publishedSourceIdentityMatches( + coordinate, + inspection, + "1123456789abcdef0123456789abcdef01234567", + Collections.singleton( + "v3.1.0-rc.20"))); + assertFalse( + BexConformanceReportMain + .publishedSourceIdentityMatches( + coordinate, + inspection, + commit, + Collections.singleton( + "v3.1.0-rc.19"))); + + inspection.put("source.tag", "release-3.1.0-rc.20"); + assertFalse( + BexConformanceReportMain + .publishedSourceIdentityMatches( + coordinate, + inspection, + commit, + Collections.singleton( + "release-3.1.0-rc.20"))); + } + + @Test + void finalIdentityCannotClaimAnUnvalidatedLocalMode() { + Map identity = + new LinkedHashMap(); + identity.put( + "currentDependencyExactFinalArtifactProven", + Boolean.TRUE); + identity.put("exactFinalArtifactProven", Boolean.TRUE); + identity.put( + "localCompositeMatchesPublishedCommit", + Boolean.TRUE); + identity.put( + "localCompositeMatchesPublishedIdentity", + Boolean.TRUE); + identity.put( + "failures", + Collections.emptyList()); + + Map local = + new LinkedHashMap(); + local.put("status", "stale-or-failed"); + local.put( + "compositeSource", + Collections.singletonMap( + "matchesPublishedIdentity", + Boolean.FALSE)); + Map modes = + Collections.singletonMap( + "localComposite", + local); + + assertFalse( + BexConformanceReportMain + .bindLanguageReleaseIdentityToModes( + identity, + modes)); + assertEquals( + Boolean.FALSE, + identity.get( + "exactFinalArtifactProven")); + assertEquals( + Boolean.FALSE, + identity.get( + "localCompositeMatchesPublishedCommit")); + assertEquals( + Boolean.FALSE, + identity.get( + "localCompositeMatchesPublishedIdentity")); + assertTrue( + String.valueOf(identity.get("failures")) + .contains( + "validated-local-composite-mode-not-bound-to-published-language-identity")); + } } diff --git a/src/test/java/blue/language/processor/BexHostedRuntimeWorkSessionTest.java b/src/test/java/blue/language/processor/BexHostedRuntimeWorkSessionTest.java index 8d00e8f..eed7645 100644 --- a/src/test/java/blue/language/processor/BexHostedRuntimeWorkSessionTest.java +++ b/src/test/java/blue/language/processor/BexHostedRuntimeWorkSessionTest.java @@ -21,10 +21,13 @@ import blue.language.Blue; import blue.language.NodeProvider; import blue.language.model.Node; +import blue.language.provider.CyclicAwareNodeProvider; +import blue.language.provider.CyclicSetProofResult; import blue.language.provider.NodeProviderOutcome; import blue.language.provider.NodeProviderResult; import blue.language.snapshot.FrozenNode; import blue.language.utils.BlueIdCalculator; +import blue.language.utils.CircularBlueIdCalculator; import org.junit.jupiter.api.Test; import java.nio.charset.StandardCharsets; @@ -38,6 +41,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; import static blue.bex.test.BexTestFixtures.frozen; import static blue.bex.test.BexTestFixtures.list; @@ -795,15 +799,15 @@ void hostedGasExhaustionRetainsExactBexPrefixAndNoOutput() { } @Test - void localLimitMapsToProcessorGasCategoryWithoutInventingHostRejection() { + void localLimitUsesCanonicalSessionRecordedHostRejection() { GasMeter parent = parentMeter(100L); RuntimeWorkSession session = session(parent); RecordingSessionHost host = new RecordingSessionHost(session, "bex:local-limit"); AtomicInteger identityCalls = new AtomicInteger(); - ProcessorFailureException failure = assertThrows( - ProcessorFailureException.class, + GasLimitExceededException failure = assertThrows( + GasLimitExceededException.class, () -> BexEngine.builder() .build() .compileAndExecute( @@ -818,30 +822,124 @@ void localLimitMapsToProcessorGasCategoryWithoutInventingHostRejection() { .establishIdentity(node); }))); + assertSame(host.propagatedExhaustion, failure); + assertEquals("bex:local-limit", failure.namespace()); assertEquals( - ProcessorErrorCategory.GasLimitExceeded, - failure.errorCategory()); - assertTrue( - failure.getCause() - instanceof BexGasLimitExceededException); - BexGasLimitExceededException local = - (BexGasLimitExceededException) failure.getCause(); - assertNull(local.hostGasLimitExceeded()); - assertEquals( - BexGasCounter.EXPRESSION_EVALUATED, - local.counter()); - assertEquals(2L, local.admittedGas()); - assertEquals(2L, local.effectiveBudget()); + BexGasCounter.EXPRESSION_EVALUATED.canonicalName(), + failure.counter()); + assertEquals(2L, failure.admittedGas()); + assertEquals(2L, failure.effectiveBudget()); assertEquals(0, identityCalls.get()); assertEquals(0, host.submitCount); assertEquals(1, host.deterministicFailureCount); - assertEquals(1, session.stagedTrace().size()); - assertTrue(session.isOpen()); - - session.failDeterministically(); + assertEquals(1, host.openSharedBudgetCount); + assertEquals(2L, host.sharedBudget.maximumGas()); + assertFalse(session.isOpen()); assertEquals(2L, parent.totalGas()); } + @Test + void hostedLocalLimitIsSharedAcrossBexAndIntrinsicLedgers() { + final String intrinsicBlueId = + "TestSharedLocalBudgetIntrinsic"; + final String intrinsicCounter = "work"; + final long intrinsicWeight = 3L; + AtomicLong gasBeforeIntrinsicWork = + new AtomicLong(-1L); + AtomicInteger workAfterCharge = + new AtomicInteger(); + BexEngine engine = BexEngine.builder() + .intrinsic( + intrinsicBlueId, + "test-shared-local-budget/1", + Collections.singletonMap( + intrinsicCounter, + intrinsicWeight), + invocation -> { + gasBeforeIntrinsicWork.set( + invocation.gasUsed()); + invocation.charge( + intrinsicCounter, + 1L, + "shared-local-budget-probe"); + workAfterCharge.incrementAndGet(); + return BexValues.scalar(true); + }) + .build(); + BexProgramSource source = + BexProgramSource.expression( + frozen(op( + "$intrinsic", + obj( + "type", + obj( + "blueId", + intrinsicBlueId))))); + + GasMeter baselineParent = parentMeter(1_000L); + RuntimeWorkSession baselineSession = + session(baselineParent); + RecordingSessionHost baselineHost = + new RecordingSessionHost( + baselineSession, + "bex:shared-local-baseline"); + engine.compileAndExecute( + source, + context( + baselineHost, + -1L, + BexSemanticIdentityBoundary.STANDALONE)); + baselineSession.complete(); + long sharedLimit = gasBeforeIntrinsicWork.get(); + assertTrue(sharedLimit > 0L); + assertEquals(1, workAfterCharge.get()); + + workAfterCharge.set(0); + GasMeter parent = parentMeter(1_000L); + RuntimeWorkSession session = session(parent); + RecordingSessionHost host = + new RecordingSessionHost( + session, "bex:shared-local"); + + GasLimitExceededException failure = assertThrows( + GasLimitExceededException.class, + () -> engine.compileAndExecute( + source, + context( + host, + sharedLimit, + BexSemanticIdentityBoundary + .STANDALONE))); + + assertSame(host.propagatedExhaustion, failure); + assertEquals( + "bex:shared-local/intrinsic-" + + intrinsicBlueId, + failure.namespace()); + assertEquals(intrinsicCounter, failure.counter()); + assertEquals(1L, failure.quantity()); + assertEquals(intrinsicWeight, failure.weight()); + assertEquals(sharedLimit, failure.admittedGas()); + assertEquals(sharedLimit, failure.effectiveBudget()); + assertEquals(0, workAfterCharge.get(), + "rejected intrinsic work must not run"); + assertEquals(1, host.openSharedBudgetCount); + assertEquals(sharedLimit, + host.sharedBudget.maximumGas()); + assertEquals(sharedLimit, + host.sharedBudget.admittedGas()); + assertEquals( + java.util.Arrays.asList( + BexGasCounter.NAMESPACE, + "intrinsic-" + intrinsicBlueId), + host.openLogicalNamespaces); + assertEquals(0, host.submitCount); + assertEquals(2, host.deterministicFailureCount); + assertEquals(0, host.unavailableCount); + assertFalse(session.isOpen()); + assertEquals(sharedLimit, parent.totalGas()); + } + @Test void runtimeNamespaceSeparatorIsReservedAndIntrinsicFlatteningFailsClosed() { RuntimeWorkSession session = session(parentMeter(100L)); @@ -1097,6 +1195,84 @@ void hostedOpaqueCyclicMemberSupportsIdentityAndOutputWithoutProofDemand() { entry.counter()))); } + @Test + void hostedCyclicProofUnavailabilityUsesSessionDiscardLifecycle() { + Node placeholder = obj( + "label", "hosted-proof-unavailable", + "next", new Node().blueId("this#0")) + .name("hosted-proof-unavailable-member"); + List placeholders = + Collections.singletonList(placeholder); + String memberBlueId = + CircularBlueIdCalculator + .calculateCircularSetBlueIds( + placeholders) + .get(0); + Node resolvedMember = placeholder.clone(); + resolvedMember.getProperties().get("next") + .blueId(memberBlueId); + CyclicProofUnavailableProvider provider = + new CyclicProofUnavailableProvider( + memberBlueId, + resolvedMember); + GasMeter parent = parentMeter(1_000L); + RuntimeWorkSession session = session(parent); + RecordingSessionHost host = + new RecordingSessionHost( + session, + "bex:cyclic-proof-unavailable"); + + try (Blue blue = new Blue(provider)) { + BexExecutionContext context = + BexExecutionContext.builder() + .document( + new FrozenBexDocumentView( + FrozenNode.fromResolvedNode( + obj( + "member", + new Node().blueId( + memberBlueId))))) + .gasLedgerHost(host) + .semanticIdentityBoundary( + BexSemanticIdentityBoundary + .STANDALONE) + .build(); + + ExecutionEvidenceUnavailableException failure = + assertThrows( + ExecutionEvidenceUnavailableException.class, + () -> BexEngine.builder() + .blue(blue) + .build() + .compileAndExecute( + BexProgramSource.expression( + frozen(op( + "$kind", + op( + "$document", + "/member")))), + context)); + + assertEquals( + Collections.singletonList(memberBlueId), + failure.requiredExactBlueIds()); + assertEquals( + "hosted cyclic proof store temporarily unavailable", + failure.getMessage()); + assertEquals(1, provider.proofQueries); + assertEquals(0, host.submitCount); + assertEquals(0, + host.deterministicFailureCount); + assertEquals(1, host.unavailableCount); + assertTrue(session.isOpen()); + assertFalse(session.stagedTrace().isEmpty()); + } + + session.suspend(); + assertEquals(0L, parent.totalGas()); + assertEquals(1_000L, parent.remainingGas()); + } + @Test void intrinsicUnavailableUsesHostedDiscardLifecycle() { ExecutionEvidenceUnavailableException expected = @@ -1380,6 +1556,39 @@ private static BexExecutionContext context( return builder.build(); } + private static final class CyclicProofUnavailableProvider + implements NodeProvider, CyclicAwareNodeProvider { + private final String memberBlueId; + private final Node resolvedMember; + private int proofQueries; + + private CyclicProofUnavailableProvider( + String memberBlueId, + Node resolvedMember) { + this.memberBlueId = memberBlueId; + this.resolvedMember = resolvedMember.clone(); + } + + @Override + public List fetchByBlueId( + String requestedBlueId) { + return memberBlueId.equals(requestedBlueId) + ? Collections.singletonList( + resolvedMember.clone()) + : Collections.emptyList(); + } + + @Override + public CyclicSetProofResult cyclicSetProofFor( + String requestedBlueId) { + proofQueries++; + return memberBlueId.equals(requestedBlueId) + ? CyclicSetProofResult.unavailable( + "hosted cyclic proof store temporarily unavailable") + : CyclicSetProofResult.notFound(); + } + } + private static final class RecordingSessionHost implements BexGasLedgerHost { private final ProcessorExecutionContextBexGasLedgerHost delegate; @@ -1390,6 +1599,8 @@ private static final class RecordingSessionHost private int submitCount; private int deterministicFailureCount; private int unavailableCount; + private int openSharedBudgetCount; + private RuntimeWorkBudget sharedBudget; private GasLimitExceededException propagatedExhaustion; private RecordingSessionHost( @@ -1400,13 +1611,36 @@ private RecordingSessionHost( session, runtimeNamespace); } + @Override + public RuntimeWorkBudget openSharedBudget( + long maximumGas) { + openSharedBudgetCount++; + sharedBudget = + delegate.openSharedBudget(maximumGas); + return sharedBudget; + } + @Override public GasMeter.ChildGasLedger open( String namespace, Map counterWeights) { + return open( + namespace, + counterWeights, + null); + } + + @Override + public GasMeter.ChildGasLedger open( + String namespace, + Map counterWeights, + RuntimeWorkBudget sharedBudget) { openLogicalNamespaces.add(namespace); GasMeter.ChildGasLedger ledger = - delegate.open(namespace, counterWeights); + delegate.open( + namespace, + counterWeights, + sharedBudget); openedLedgers.add(ledger); return ledger; } diff --git a/src/test/resources/hosted-release/required-public-api.txt b/src/test/resources/hosted-release/required-public-api.txt index cfa4a6b..6ce537e 100644 --- a/src/test/resources/hosted-release/required-public-api.txt +++ b/src/test/resources/hosted-release/required-public-api.txt @@ -72,6 +72,8 @@ class public abstract interface blue.bex.api.BexGasLedgerHost method public abstract open(java.lang.String,java.util.Map):blue.language.processor.GasMeter$ChildGasLedger method public abstract submit(blue.language.processor.GasMeter$ChildGasLedger):void method public localGasLimitExceeded(blue.bex.gas.BexGasLimitExceededException,java.lang.RuntimeException):java.lang.RuntimeException + method public open(java.lang.String,java.util.Map,blue.language.processor.RuntimeWorkBudget):blue.language.processor.GasMeter$ChildGasLedger + method public openSharedBudget(long):blue.language.processor.RuntimeWorkBudget method public propagateGasExhaustion(blue.language.processor.GasMeter$ChildGasLedger,blue.language.processor.GasLimitExceededException):void method public separatesRuntimeNamespaces():boolean class public final blue.bex.api.BexIntrinsicInvocation @@ -154,6 +156,8 @@ class public final blue.bex.api.ProcessorExecutionContextBexGasLedgerHost implem method public failedDeterministically(blue.language.processor.GasMeter$ChildGasLedger):void method public localGasLimitExceeded(blue.bex.gas.BexGasLimitExceededException,java.lang.RuntimeException):java.lang.RuntimeException method public open(java.lang.String,java.util.Map):blue.language.processor.GasMeter$ChildGasLedger + method public open(java.lang.String,java.util.Map,blue.language.processor.RuntimeWorkBudget):blue.language.processor.GasMeter$ChildGasLedger + method public openSharedBudget(long):blue.language.processor.RuntimeWorkBudget method public physicalNamespace(java.lang.String):java.lang.String method public propagateGasExhaustion(blue.language.processor.GasMeter$ChildGasLedger,blue.language.processor.GasLimitExceededException):void method public runtimeNamespace():java.lang.String @@ -343,6 +347,7 @@ class public final blue.bex.gas.BexGasMeter method public remainingGas():long method public schedule():blue.bex.gas.BexGasSchedule method public static childLedgerWeights(blue.bex.gas.BexGasSchedule,java.util.Map):java.util.Map + method public static hostedWithSharedLocalLimit(blue.bex.gas.BexGasSchedule,java.util.Map,long,java.util.Map):blue.bex.gas.BexGasMeter method public static qualifiedCounterName(java.lang.String,java.lang.String):java.lang.String method public submitHostLedger(java.util.function.Consumer):void method public totalGas():long From 55ec18867eb8ef07e279973643a514318e7c97a1 Mon Sep 17 00:00:00 2001 From: piotr-blue Date: Sun, 2 Aug 2026 20:45:15 +0100 Subject: [PATCH 03/13] fix(build): bind BEX to modular Blue Language projects --- build.gradle.kts | 1789 ++++++++++++++++- .../latest-language-baseline.json | 77 + settings.gradle.kts | 36 +- 3 files changed, 1882 insertions(+), 20 deletions(-) create mode 100644 gradle/verification/latest-language-baseline.json diff --git a/build.gradle.kts b/build.gradle.kts index ed3f861..adcc45d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,7 +7,12 @@ import java.security.MessageDigest import java.time.Instant import java.util.Properties import java.util.zip.ZipFile +import groovy.json.JsonOutput +import groovy.json.JsonSlurper import org.apache.commons.compress.archivers.zip.ZipFile as CommonsZipFile +import org.gradle.api.artifacts.Configuration +import org.gradle.api.artifacts.component.ProjectComponentIdentifier +import org.gradle.api.artifacts.result.ResolvedDependencyResult import org.gradle.api.tasks.javadoc.Javadoc import org.gradle.external.javadoc.StandardJavadocDocletOptions import org.gradle.api.tasks.bundling.Jar @@ -25,8 +30,36 @@ group = "blue.bex" version = determineProjectVersion() val blueLanguagePublishedVersion = "3.1.0-rc.19" +val blueLanguageModelDeclaredCoordinate = + "blue.language:blue-language-model:$blueLanguagePublishedVersion" +val blueLanguageCoreDeclaredCoordinate = + "blue.language:blue-language-core:$blueLanguagePublishedVersion" +val blueLanguageMappingDeclaredCoordinate = + "blue.language:blue-language-mapping:$blueLanguagePublishedVersion" +val blueContractsCoreDeclaredCoordinate = + "blue.language:blue-contracts-core:$blueLanguagePublishedVersion" val blueLanguageDeclaredCoordinate = "blue.language:blue-language-java:$blueLanguagePublishedVersion" +val blueLanguageFocusedCoordinates = + listOf( + blueLanguageModelDeclaredCoordinate, + blueLanguageCoreDeclaredCoordinate, + blueLanguageMappingDeclaredCoordinate, + blueContractsCoreDeclaredCoordinate + ) +val blueLanguageFocusedModuleNames = + linkedSetOf( + "blue-language-model", + "blue-language-core", + "blue-language-mapping", + "blue-contracts-core" + ) +val blueLanguageFocusedProjectPaths = + blueLanguageFocusedModuleNames.associateWith { ":$it" } +val latestLanguageMigrationLock = + layout.projectDirectory.file( + "gradle/verification/latest-language-baseline.json" + ) val blueLanguageCompositePath = providers.gradleProperty("blueLanguageCompositePath") .orNull @@ -59,6 +92,18 @@ val blueLanguageModuleVersionCache = ) val blueLanguageModuleVersionCacheInitiallyAbsent = !blueLanguageModuleVersionCache.exists() +val blueLanguageFocusedModuleVersionCaches = + blueLanguageFocusedModuleNames.associateWith { moduleName -> + File( + gradle.gradleUserHomeDir, + "caches/modules-2/files-2.1/blue.language/" + + "$moduleName/$blueLanguagePublishedVersion" + ) + } +val blueLanguageFocusedModuleVersionCachesInitiallyAbsent = + blueLanguageFocusedModuleVersionCaches.mapValues { (_, cache) -> + !cache.exists() + } val blueLanguageRequireFreshModuleCache = providers.gradleProperty("blueLanguageRequireFreshModuleCache") .map(String::toBoolean) @@ -353,8 +398,362 @@ fun gitWorkspaceFingerprint( ) } +data class FocusedLanguageArtifactEvidence( + val coordinate: String, + val component: String, + val projectPath: String?, + val includedBuildProject: Boolean, + val file: File, + val bytes: Long, + val sha256: String +) + +data class FocusedLanguageResolutionEvidence( + val components: List>, + val edges: List>, + val artifacts: List, + val graphSha256: String +) + +fun resolveFocusedLanguageEvidence( + configuration: Configuration, + requiredProjectPaths: Map, + requireIncludedBuildProjects: Boolean +): FocusedLanguageResolutionEvidence { + val resolution = configuration.incoming.resolutionResult + val components = + resolution.allComponents.map { component -> + val identifier = component.id + val moduleVersion = component.moduleVersion + linkedMapOf( + "id" to identifier.displayName, + "group" to moduleVersion?.group, + "name" to moduleVersion?.name, + "version" to moduleVersion?.version, + "origin" to + if (identifier is ProjectComponentIdentifier) { + if (identifier.build.buildPath == ":") { + "current-build-project" + } else { + "included-build-project" + } + } else { + "external-module" + }, + "projectPath" to + (identifier as? ProjectComponentIdentifier) + ?.projectPath + ) + }.sortedBy { it["id"].toString() } + val edges = + resolution.allDependencies + .filterIsInstance() + .map { dependency -> + linkedMapOf( + "from" to dependency.from.id.displayName, + "requested" to dependency.requested.displayName, + "selected" to dependency.selected.id.displayName + ) + } + .sortedWith( + compareBy>( + { it.getValue("from") }, + { it.getValue("requested") }, + { it.getValue("selected") } + ) + ) + val artifacts = + configuration.resolvedConfiguration.resolvedArtifacts + .filter { it.extension == "jar" } + .map { artifact -> + val identifier = artifact.id.componentIdentifier + val coordinate = + artifact.moduleVersion.id.group + ":" + + artifact.name + ":" + + artifact.moduleVersion.id.version + FocusedLanguageArtifactEvidence( + coordinate = coordinate, + component = identifier.displayName, + projectPath = + (identifier as? ProjectComponentIdentifier) + ?.projectPath, + includedBuildProject = + identifier is ProjectComponentIdentifier && + identifier.build.buildPath != ":", + file = artifact.file.canonicalFile, + bytes = artifact.file.length(), + sha256 = sha256(artifact.file) + ) + } + .sortedWith( + compareBy( + { it.coordinate }, + { it.file.name } + ) + ) + check(artifacts.isNotEmpty()) { + "Focused Blue Language resolution produced no JAR artifacts" + } + for ((moduleName, projectPath) in requiredProjectPaths) { + val matches = + artifacts.filter { + it.coordinate.substringBefore(':') == "blue.language" && + it.coordinate.substringAfter(':') + .substringBefore(':') == moduleName + } + check(matches.size == 1) { + "Expected exactly one focused $moduleName JAR, found " + + matches.joinToString { it.file.path } + } + if (requireIncludedBuildProjects) { + val match = matches.single() + check( + match.includedBuildProject && + match.projectPath == projectPath + ) { + "Focused module $moduleName must resolve directly from " + + "included-build project $projectPath, but resolved " + + "${match.component} (projectPath=${match.projectPath})" + } + } + } + val canonicalGraph = + buildList { + components.forEach { + add( + "component|${it["id"]}|${it["group"]}|" + + "${it["name"]}|${it["version"]}|" + + "${it["origin"]}|${it["projectPath"]}" + ) + } + edges.forEach { + add( + "edge|${it.getValue("from")}|" + + "${it.getValue("requested")}|" + + it.getValue("selected") + ) + } + artifacts.forEach { + add( + "artifact|${it.coordinate}|${it.component}|" + + "${it.projectPath}|${it.bytes}|${it.sha256}" + ) + } + }.joinToString("\n", postfix = "\n") + return FocusedLanguageResolutionEvidence( + components = components, + edges = edges, + artifacts = artifacts, + graphSha256 = sha256( + canonicalGraph.toByteArray(StandardCharsets.UTF_8) + ) + ) +} + +fun Configuration.containsBlueLanguageAggregate(): Boolean = + incoming.resolutionResult.allComponents.any { component -> + component.moduleVersion?.let { + it.group == "blue.language" && + it.name == "blue-language-java" + } == true + } + +fun focusedEvidenceJson( + evidence: FocusedLanguageResolutionEvidence, + mode: String, + declaredCoordinates: List, + compileAggregatePresent: Boolean, + runtimeAggregatePresent: Boolean +): Map = + linkedMapOf( + "schema" to "blue-bex-focused-language-resolution/1.0", + "status" to "passed", + "mode" to mode, + "declaredCoordinates" to declaredCoordinates, + "graphSha256" to evidence.graphSha256, + "componentCount" to evidence.components.size, + "edgeCount" to evidence.edges.size, + "artifactCount" to evidence.artifacts.size, + "components" to evidence.components, + "edges" to evidence.edges, + "artifacts" to evidence.artifacts.map { + linkedMapOf( + "coordinate" to it.coordinate, + "component" to it.component, + "origin" to + if (it.includedBuildProject) { + "included-build-project" + } else { + "external-module" + }, + "projectPath" to it.projectPath, + "path" to it.file.path, + "bytes" to it.bytes, + "sha256" to it.sha256 + ) + }, + "productionClasspaths" to + linkedMapOf( + "compile" to + linkedMapOf( + "aggregatePresent" to compileAggregatePresent + ), + "runtime" to + linkedMapOf( + "aggregatePresent" to runtimeAggregatePresent + ) + ) + ) + +data class JavaImportInventory( + val lineCount: Int, + val fileCount: Int, + val files: Map, + val imports: Map +) + +fun JavaImportInventory.toJson(): Map = + linkedMapOf( + "lineCount" to lineCount, + "fileCount" to fileCount, + "files" to files.toSortedMap(), + "imports" to imports.toSortedMap() + ) + +val allBlueLanguageImportPattern = + Regex("^import\\s+(?:static\\s+)?blue\\.language\\.") +val legacyUtilsImportPattern = + Regex("^import\\s+(?:static\\s+)?blue\\.language\\.utils\\.") +val forbiddenLegacyImportPatterns = + listOf( + legacyUtilsImportPattern, + Regex( + "^import\\s+blue\\.language\\.snapshot\\." + + "ResolvedSnapshot;" + ), + Regex("^import\\s+blue\\.language\\.NodeProvider;"), + Regex( + "^import\\s+blue\\.language\\.BlueOperation" + + "(?:Limits|Outcome|Result);" + ) + ) +val allBlueLanguageImportGitPattern = + "^import (static )?blue\\.language\\." +val legacyUtilsImportGitPattern = + "^import (static )?blue\\.language\\.utils\\." +val forbiddenLegacyImportGitPattern = + "^import (static )?blue\\.language\\." + + "(utils\\.|snapshot\\.ResolvedSnapshot;|NodeProvider;|" + + "BlueOperation(Limits|Outcome|Result);)" + +fun sourceImportInventory( + checkout: File, + sourceRoot: String, + matches: (String) -> Boolean +): JavaImportInventory { + val root = File(checkout, sourceRoot) + val lines = mutableListOf>() + if (root.isDirectory) { + root.walkTopDown() + .filter { it.isFile && it.extension == "java" } + .forEach { source -> + source.useLines { sourceLines -> + sourceLines.forEach { rawLine -> + val line = rawLine.trim() + if (matches(line)) { + lines.add( + source.relativeTo(checkout) + .invariantSeparatorsPath to line + ) + } + } + } + } + } + return JavaImportInventory( + lineCount = lines.size, + fileCount = lines.map { it.first }.toSet().size, + files = lines.groupingBy { it.first }.eachCount(), + imports = lines.groupingBy { it.second }.eachCount() + ) +} + +fun gitImportInventory( + checkout: File, + revision: String, + sourceRoot: String, + pattern: String +): JavaImportInventory { + val output = + commandOutput( + checkout, + "git", + "grep", + "-n", + "-E", + pattern, + revision, + "--", + sourceRoot + ).trim() + val lines = + if (output.isEmpty()) { + emptyList() + } else { + output.lineSequence().map { rawLine -> + val withoutRevision = + rawLine.removePrefix("$revision:") + val pathSeparator = withoutRevision.indexOf(':') + val lineSeparator = + withoutRevision.indexOf(':', pathSeparator + 1) + check(pathSeparator > 0 && lineSeparator > pathSeparator) { + "Unexpected git grep evidence line: $rawLine" + } + val path = withoutRevision.substring(0, pathSeparator) + val imported = + withoutRevision.substring(lineSeparator + 1).trim() + path to imported + }.toList() + } + return JavaImportInventory( + lineCount = lines.size, + fileCount = lines.map { it.first }.toSet().size, + files = lines.groupingBy { it.first }.eachCount(), + imports = lines.groupingBy { it.second }.eachCount() + ) +} + +val blueLanguageFocusedResolution by configurations.creating { + isCanBeConsumed = false + isCanBeResolved = true + isTransitive = true + description = + "Resolves the complete focused Blue Language component graph for " + + "provenance, version, and JAR hash evidence." +} + +val blueLanguageAggregateCompatibility by configurations.creating { + isCanBeConsumed = false + isCanBeResolved = true + description = + "Resolves the aggregate Blue Language facade only for compatibility " + + "and provenance checks; it is not on a BEX production classpath." +} + dependencies { - api(blueLanguageDeclaredCoordinate) + api(blueLanguageModelDeclaredCoordinate) + api(blueLanguageCoreDeclaredCoordinate) + implementation(blueLanguageMappingDeclaredCoordinate) + api(blueContractsCoreDeclaredCoordinate) + + blueLanguageFocusedCoordinates.forEach { coordinate -> + add(blueLanguageFocusedResolution.name, coordinate) + } + + add( + blueLanguageAggregateCompatibility.name, + blueLanguageDeclaredCoordinate + ) testImplementation(platform("org.junit:junit-bom:5.10.2")) testImplementation("org.junit.jupiter:junit-jupiter") @@ -362,6 +761,599 @@ dependencies { testRuntimeOnly("org.junit.platform:junit-platform-launcher") } +val blueLanguageAggregateCompatibilityEvidence = + layout.buildDirectory.file( + "reports/latest-language-migration/" + + "aggregate-compatibility.properties" + ) +val blueLanguageFocusedResolutionEvidence = + layout.buildDirectory.file( + "reports/latest-language-migration/" + + "focused-language-resolution.json" + ) +val writeFocusedLanguageResolutionEvidence by tasks.registering { + group = "verification" + description = + "Records the complete focused Language graph, exact versions, " + + "project origins, and JAR SHA-256 values and rejects the " + + "aggregate facade on BEX production classpaths." + inputs.property("dependency.mode", blueLanguageDependencyMode) + inputs.property( + "dependency.coordinates", + blueLanguageFocusedCoordinates.joinToString(",") + ) + inputs.file(latestLanguageMigrationLock) + inputs.files(blueLanguageFocusedResolution) + outputs.file(blueLanguageFocusedResolutionEvidence) + outputs.upToDateWhen { false } + doFirst { + blueLanguageFocusedResolutionEvidence.get().asFile.delete() + } + doLast { + val compileClasspath = configurations.compileClasspath.get() + val runtimeClasspath = configurations.runtimeClasspath.get() + val compileAggregatePresent = + compileClasspath.containsBlueLanguageAggregate() + val runtimeAggregatePresent = + runtimeClasspath.containsBlueLanguageAggregate() + check(!compileAggregatePresent && !runtimeAggregatePresent) { + "blue-language-java is forbidden on BEX production " + + "compile/runtime classpaths" + } + val evidence = + resolveFocusedLanguageEvidence( + blueLanguageFocusedResolution, + blueLanguageFocusedProjectPaths, + blueLanguageDependencyMode == "local-composite" + ) + @Suppress("UNCHECKED_CAST") + val lock = + JsonSlurper().parse(latestLanguageMigrationLock.asFile) + as Map + val languageLock = lock["language"] as? Map<*, *> + ?: throw GradleException("Language baseline lock is malformed") + val lockedModules = + (languageLock["focusedModules"] as? List<*>) + ?.map { it as Map<*, *> } + ?: throw GradleException( + "Language baseline lock has no focused modules" + ) + check( + lockedModules.map { it["coordinate"] }.toSet() == + blueLanguageFocusedCoordinates.toSet() + ) { + "Focused dependency declarations differ from the BEX-owned lock" + } + if (blueLanguageDependencyMode == "local-composite") { + for (lockedModule in lockedModules) { + val coordinate = lockedModule["coordinate"].toString() + val moduleName = coordinate.split(':')[1] + val artifact = + evidence.artifacts.single { + it.coordinate.substringAfter(':') + .substringBefore(':') == moduleName + } + check( + artifact.projectPath == lockedModule["projectPath"] + ) { + "$moduleName resolved from ${artifact.projectPath}, " + + "not the locked project path " + + lockedModule["projectPath"] + } + check( + artifact.sha256 == + lockedModule["verifiedArtifactSha256"] + ) { + "$moduleName JAR differs from the verified Language " + + "implementation artifact: ${artifact.sha256}" + } + } + } + val output = LinkedHashMap( + focusedEvidenceJson( + evidence, + blueLanguageDependencyMode, + blueLanguageFocusedCoordinates, + compileAggregatePresent, + runtimeAggregatePresent + ) + ) + output["lock"] = + linkedMapOf( + "path" to + latestLanguageMigrationLock.asFile + .relativeTo(projectDir) + .invariantSeparatorsPath, + "sha256" to sha256(latestLanguageMigrationLock.asFile), + "focusedModulesMatch" to true, + "localArtifactsMatchVerifiedImplementation" to + (blueLanguageDependencyMode == "local-composite") + ) + val outputFile = + blueLanguageFocusedResolutionEvidence.get().asFile + outputFile.parentFile.mkdirs() + outputFile.writeText( + JsonOutput.prettyPrint(JsonOutput.toJson(output)) + "\n" + ) + } +} +val latestLanguageMigrationBaselineEvidence = + layout.buildDirectory.file( + "reports/latest-language-migration/baseline.json" + ) +val writeLatestLanguageMigrationBaseline by tasks.registering { + group = "verification" + description = + "Regenerates the migration baseline from the source-controlled " + + "Language/BEX lock and live source inventories." + inputs.file(latestLanguageMigrationLock) + inputs.files( + fileTree("src/main/java") { include("**/*.java") }, + fileTree("src/test/java") { include("**/*.java") }, + layout.projectDirectory.file(".cz.toml") + ) + blueLanguageCompositePath?.let { path -> + inputs.file(file(path).resolve(".cz.toml")) + } + outputs.file(latestLanguageMigrationBaselineEvidence) + outputs.upToDateWhen { false } + doFirst { + latestLanguageMigrationBaselineEvidence.get().asFile.delete() + } + doLast { + @Suppress("UNCHECKED_CAST") + val lock = + JsonSlurper().parse(latestLanguageMigrationLock.asFile) + as Map + val bexLock = lock["bex"] as? Map<*, *> + ?: throw GradleException("BEX migration lock is malformed") + val languageLock = lock["language"] as? Map<*, *> + ?: throw GradleException("Language migration lock is malformed") + val migrationLock = lock["migration"] as? Map<*, *> + ?: throw GradleException("Migration inventory lock is malformed") + val baselineCommit = + bexLock["migrationBaselineCommit"].toString() + val expectedLanguageHead = + languageLock["exactHead"].toString() + val verifiedImplementationCommit = + languageLock["verifiedImplementationCommit"].toString() + val allowedLanguageDiffPaths = + (languageLock["documentationOnlyDiffPaths"] as? List<*>) + ?.map(Any?::toString) + ?.sorted() + ?: emptyList() + val failures = mutableListOf() + fun requireBaseline(value: Boolean, failure: String) { + if (!value) failures.add(failure) + } + fun expectedInventory( + section: String, + scope: String + ): Pair { + val sectionMap = migrationLock[section] as? Map<*, *> + ?: throw GradleException("Missing migration lock: $section") + val scopeMap = sectionMap[scope] as? Map<*, *> + ?: throw GradleException( + "Missing migration lock: $section.$scope" + ) + return ( + (scopeMap["lines"] as Number).toInt() to + (scopeMap["files"] as Number).toInt() + ) + } + fun matchesExpected( + inventory: JavaImportInventory, + expected: Pair + ): Boolean = + inventory.lineCount == expected.first && + inventory.fileCount == expected.second + + commandOutput( + projectDir, + "git", + "cat-file", + "-e", + "$baselineCommit^{commit}" + ) + val bexFingerprint = gitWorkspaceFingerprint(projectDir) + val bexCzToml = file(".cz.toml") + val actualBexCzTomlSha256 = sha256(bexCzToml) + requireBaseline( + actualBexCzTomlSha256 == bexLock["czTomlSha256"], + "bex-cz-toml-differs-from-lock" + ) + + val languageDirectory = + blueLanguageCompositePath + ?.let { file(it).canonicalFile } + val languageFingerprint = + languageDirectory + ?.takeIf(File::isDirectory) + ?.let(::gitWorkspaceFingerprint) + requireBaseline( + languageFingerprint != null, + "language-composite-checkout-unavailable" + ) + val actualLanguageCzTomlSha256 = + languageDirectory + ?.resolve(".cz.toml") + ?.takeIf(File::isFile) + ?.let(::sha256) + requireBaseline( + actualLanguageCzTomlSha256 == + languageLock["czTomlSha256"], + "language-cz-toml-differs-from-lock" + ) + requireBaseline( + languageFingerprint?.commit == expectedLanguageHead, + "language-head-differs-from-lock" + ) + requireBaseline( + languageFingerprint != null && !languageFingerprint.dirty, + "language-git-worktree-dirty" + ) + val changedLanguagePaths = + languageDirectory?.let { + commandOutput( + it, + "git", + "diff", + "--name-only", + "$verifiedImplementationCommit..$expectedLanguageHead" + ).lineSequence() + .map(String::trim) + .filter(String::isNotEmpty) + .sorted() + .toList() + } ?: emptyList() + val codeEquivalent = + languageFingerprint?.commit == expectedLanguageHead && + changedLanguagePaths == allowedLanguageDiffPaths && + actualLanguageCzTomlSha256 == + languageLock["czTomlSha256"] + requireBaseline( + codeEquivalent, + "language-head-not-code-equivalent-to-verified-implementation" + ) + + val baselineProductionImports = + gitImportInventory( + projectDir, + baselineCommit, + "src/main/java", + allBlueLanguageImportGitPattern + ) + val baselineTestImports = + gitImportInventory( + projectDir, + baselineCommit, + "src/test/java", + allBlueLanguageImportGitPattern + ) + val baselineProductionUtils = + gitImportInventory( + projectDir, + baselineCommit, + "src/main/java", + legacyUtilsImportGitPattern + ) + val baselineTestUtils = + gitImportInventory( + projectDir, + baselineCommit, + "src/test/java", + legacyUtilsImportGitPattern + ) + val baselineProductionForbidden = + gitImportInventory( + projectDir, + baselineCommit, + "src/main/java", + forbiddenLegacyImportGitPattern + ) + val baselineTestForbidden = + gitImportInventory( + projectDir, + baselineCommit, + "src/test/java", + forbiddenLegacyImportGitPattern + ) + requireBaseline( + matchesExpected( + baselineProductionImports, + expectedInventory( + "languageImportInventory", + "production" + ) + ) && matchesExpected( + baselineTestImports, + expectedInventory("languageImportInventory", "test") + ), + "baseline-language-import-inventory-differs-from-lock" + ) + requireBaseline( + matchesExpected( + baselineProductionUtils, + expectedInventory("legacyUtilsImports", "production") + ) && matchesExpected( + baselineTestUtils, + expectedInventory("legacyUtilsImports", "test") + ), + "baseline-utils-import-ledger-differs-from-lock" + ) + requireBaseline( + matchesExpected( + baselineProductionForbidden, + expectedInventory( + "allForbiddenLegacyImports", + "production" + ) + ) && matchesExpected( + baselineTestForbidden, + expectedInventory( + "allForbiddenLegacyImports", + "test" + ) + ), + "baseline-forbidden-import-ledger-differs-from-lock" + ) + + val currentProductionImports = + sourceImportInventory( + projectDir, + "src/main/java" + ) { allBlueLanguageImportPattern.containsMatchIn(it) } + val currentTestImports = + sourceImportInventory( + projectDir, + "src/test/java" + ) { allBlueLanguageImportPattern.containsMatchIn(it) } + val currentProductionForbidden = + sourceImportInventory( + projectDir, + "src/main/java" + ) { line -> + forbiddenLegacyImportPatterns.any { + it.containsMatchIn(line) + } + } + val currentTestForbidden = + sourceImportInventory( + projectDir, + "src/test/java" + ) { line -> + forbiddenLegacyImportPatterns.any { + it.containsMatchIn(line) + } + } + + val output = + linkedMapOf( + "schema" to + "blue-bex-latest-language-migration-baseline/1.0", + "status" to + if (failures.isEmpty()) "passed" else "failed", + "failures" to failures, + "lock" to + linkedMapOf( + "path" to + latestLanguageMigrationLock.asFile + .relativeTo(projectDir) + .invariantSeparatorsPath, + "sha256" to + sha256(latestLanguageMigrationLock.asFile) + ), + "toolchain" to + linkedMapOf( + "gradle" to gradle.gradleVersion, + "java" to System.getProperty("java.version"), + "os" to System.getProperty("os.name"), + "architecture" to System.getProperty("os.arch") + ), + "bex" to + linkedMapOf( + "baselineCommit" to baselineCommit, + "currentCommit" to bexFingerprint.commit, + "gitWorktreeDirty" to bexFingerprint.dirty, + "workspaceSha256" to + bexFingerprint.workspaceSha256, + "czToml" to + linkedMapOf( + "expectedSha256" to + bexLock["czTomlSha256"], + "actualSha256" to + actualBexCzTomlSha256, + "matches" to + (actualBexCzTomlSha256 == + bexLock["czTomlSha256"]) + ) + ), + "language" to + linkedMapOf( + "path" to languageDirectory?.path, + "exactCommit" to languageFingerprint?.commit, + "gitWorktreeDirty" to languageFingerprint?.dirty, + "includedBuildCleanClaim" to "not-made", + "verifiedImplementationCommit" to + verifiedImplementationCommit, + "changedPathsSinceVerifiedImplementation" to + changedLanguagePaths, + "allowedDocumentationOnlyPaths" to + allowedLanguageDiffPaths, + "codeEquivalent" to codeEquivalent, + "czToml" to + linkedMapOf( + "expectedSha256" to + languageLock["czTomlSha256"], + "actualSha256" to + actualLanguageCzTomlSha256, + "matches" to + (actualLanguageCzTomlSha256 == + languageLock["czTomlSha256"]) + ), + "focusedModules" to + languageLock["focusedModules"], + "hostingPackageIdentities" to + languageLock["hostingPackageIdentities"] + ), + "sourceApiInventory" to + linkedMapOf( + "baseline" to + linkedMapOf( + "revision" to baselineCommit, + "production" to + baselineProductionImports.toJson(), + "test" to baselineTestImports.toJson() + ), + "current" to + linkedMapOf( + "production" to + currentProductionImports.toJson(), + "test" to currentTestImports.toJson() + ) + ), + "migrationLedger" to + linkedMapOf( + "legacyUtils" to + linkedMapOf( + "before" to + linkedMapOf( + "production" to + baselineProductionUtils.toJson(), + "test" to + baselineTestUtils.toJson() + ), + "after" to + linkedMapOf( + "production" to + sourceImportInventory( + projectDir, + "src/main/java" + ) { + legacyUtilsImportPattern + .containsMatchIn(it) + }.toJson(), + "test" to + sourceImportInventory( + projectDir, + "src/test/java" + ) { + legacyUtilsImportPattern + .containsMatchIn(it) + }.toJson() + ) + ), + "allForbiddenLegacyImports" to + linkedMapOf( + "before" to + linkedMapOf( + "production" to + baselineProductionForbidden + .toJson(), + "test" to + baselineTestForbidden.toJson() + ), + "after" to + linkedMapOf( + "production" to + currentProductionForbidden + .toJson(), + "test" to + currentTestForbidden.toJson() + ) + ) + ) + ) + val outputFile = + latestLanguageMigrationBaselineEvidence.get().asFile + outputFile.parentFile.mkdirs() + outputFile.writeText( + JsonOutput.prettyPrint(JsonOutput.toJson(output)) + "\n" + ) + } +} +val verifyBlueLanguageAggregateCompatibility by tasks.registering { + group = "verification" + description = + "Resolves and inspects the aggregate Blue Language facade without " + + "adding it to a BEX production classpath." + inputs.property("dependency.mode", blueLanguageDependencyMode) + inputs.property( + "dependency.coordinate", + blueLanguageDeclaredCoordinate + ) + inputs.files(blueLanguageAggregateCompatibility) + outputs.file(blueLanguageAggregateCompatibilityEvidence) + outputs.upToDateWhen { false } + doFirst { + blueLanguageAggregateCompatibilityEvidence.get().asFile.delete() + } + doLast { + val matches = + blueLanguageAggregateCompatibility + .resolvedConfiguration + .resolvedArtifacts + .filter { + it.moduleVersion.id.group == "blue.language" && + it.name == "blue-language-java" && + it.extension == "jar" + } + check(matches.size == 1) { + "Expected exactly one aggregate blue-language-java artifact, " + + "found " + + matches.joinToString { it.file.absolutePath } + } + val artifact = matches.single() + check(artifact.file.isFile && artifact.file.length() > 0L) { + "Aggregate Blue Language artifact is missing or empty: " + + artifact.file + } + ZipFile(artifact.file).use { archive -> + check(archive.getEntry("blue/language/Blue.class") != null) { + "Aggregate Blue Language artifact does not expose the " + + "compatibility facade blue.language.Blue: " + + artifact.file + } + } + val component = artifact.id.componentIdentifier + if (blueLanguageDependencyMode == "local-composite") { + val projectComponent = + component as? ProjectComponentIdentifier + check( + projectComponent != null && + projectComponent.build.buildPath != ":" && + projectComponent.projectPath == + ":blue-language-java" + ) { + "Local aggregate compatibility artifact did not resolve " + + "from :blue-language-java: " + + component.displayName + } + } + writeEvidence( + blueLanguageAggregateCompatibilityEvidence.get().asFile, + mapOf( + "schema" to + "blue-bex-language-aggregate-compatibility/1.0", + "status" to "passed", + "mode" to blueLanguageDependencyMode, + "declared.coordinate" to + blueLanguageDeclaredCoordinate, + "effective.component" to component.displayName, + "effective.coordinate" to + ( + artifact.moduleVersion.id.group + + ":" + artifact.name + ":" + + artifact.moduleVersion.id.version + ), + "artifact.path" to artifact.file.canonicalPath, + "artifact.bytes" to artifact.file.length().toString(), + "artifact.sha256" to sha256(artifact.file) + ) + ) + } +} + tasks.test { javaLauncher.set( javaToolchains.launcherFor { @@ -740,6 +1732,10 @@ val cleanBuildDependencyArtifact = "reports/bex-release/clean-build-inputs/" + "blue-language-java.jar" ) +val cleanBuildFocusedDependencyDirectory = + layout.buildDirectory.dir( + "reports/bex-release/clean-build-inputs/focused-language" + ) val invalidateCleanBuildArtifactEvidence by tasks.registering { group = "verification" description = @@ -748,6 +1744,7 @@ val invalidateCleanBuildArtifactEvidence by tasks.registering { doLast { cleanBuildArtifactEvidence.get().asFile.delete() cleanBuildDependencyArtifact.get().asFile.delete() + project.delete(cleanBuildFocusedDependencyDirectory) } } listOf( @@ -760,12 +1757,20 @@ listOf( mustRunAfter(invalidateCleanBuildArtifactEvidence) } } +verifyBlueLanguageAggregateCompatibility { + mustRunAfter(invalidateCleanBuildArtifactEvidence) +} +writeFocusedLanguageResolutionEvidence { + mustRunAfter(invalidateCleanBuildArtifactEvidence) +} val writeCleanBuildArtifactHashes by tasks.registering { group = "verification" description = "Records all four release hashes from one clean committed checkout." dependsOn( invalidateCleanBuildArtifactEvidence, + verifyBlueLanguageAggregateCompatibility, + writeFocusedLanguageResolutionEvidence, mainJar, sourcesJarTask, javadocJarTask, @@ -773,10 +1778,12 @@ val writeCleanBuildArtifactHashes by tasks.registering { ) outputs.file(cleanBuildArtifactEvidence) outputs.file(cleanBuildDependencyArtifact) + outputs.dir(cleanBuildFocusedDependencyDirectory) outputs.upToDateWhen { false } doFirst { cleanBuildArtifactEvidence.get().asFile.delete() cleanBuildDependencyArtifact.get().asFile.delete() + project.delete(cleanBuildFocusedDependencyDirectory) } doLast { val checkout = @@ -798,7 +1805,7 @@ val writeCleanBuildArtifactHashes by tasks.registering { "--absolute-git-dir" ).trim() val languageArtifacts = - configurations.compileClasspath.get() + blueLanguageAggregateCompatibility .resolvedConfiguration .resolvedArtifacts .filter { @@ -827,6 +1834,37 @@ val writeCleanBuildArtifactHashes by tasks.registering { "Failed to preserve the exact Blue Language dependency " + "artifact with the clean-build receipt" } + val focusedResolution = + resolveFocusedLanguageEvidence( + blueLanguageFocusedResolution, + blueLanguageFocusedProjectPaths, + blueLanguageDependencyMode == "local-composite" + ) + val focusedCopyDirectory = + cleanBuildFocusedDependencyDirectory.get().asFile + focusedCopyDirectory.mkdirs() + val focusedArtifactCopies = + focusedResolution.artifacts.mapIndexed { index, artifact -> + val safeCoordinate = + artifact.coordinate.replace( + Regex("[^A-Za-z0-9._-]"), + "_" + ) + val copy = + File( + focusedCopyDirectory, + "%03d-%s.jar".format(index, safeCoordinate) + ) + artifact.file.copyTo(copy, overwrite = true) + check( + copy.length() == artifact.bytes && + sha256(copy) == artifact.sha256 + ) { + "Failed to preserve focused dependency artifact " + + artifact.coordinate + } + artifact to copy + } val compositeDirectory = blueLanguageCompositePath ?.let { file(it).canonicalFile } @@ -890,6 +1928,17 @@ val writeCleanBuildArtifactHashes by tasks.registering { .invariantSeparatorsPath, "dependency.artifact.sha256" to sha256(languageArtifactCopy), + "dependency.aggregateCompatibilityOnly" to "true", + "dependency.focused.declaredCoordinates" to + blueLanguageFocusedCoordinates.joinToString(","), + "dependency.focused.graphSha256" to + focusedResolution.graphSha256, + "dependency.focused.componentCount" to + focusedResolution.components.size.toString(), + "dependency.focused.edgeCount" to + focusedResolution.edges.size.toString(), + "dependency.focused.artifactCount" to + focusedArtifactCopies.size.toString(), "composite.path" to (compositeDirectory?.path ?: ""), "composite.commit" to @@ -918,6 +1967,25 @@ val writeCleanBuildArtifactHashes by tasks.registering { ?: "0" ) ) + focusedArtifactCopies.forEachIndexed { index, pair -> + val (artifact, copy) = pair + val prefix = + "dependency.focused.artifact.%03d".format(index) + values["$prefix.coordinate"] = artifact.coordinate + values["$prefix.component"] = artifact.component + values["$prefix.projectPath"] = + artifact.projectPath.orEmpty() + values["$prefix.origin"] = + if (artifact.includedBuildProject) { + "included-build-project" + } else { + "external-module" + } + values["$prefix.path"] = + copy.relativeTo(projectDir).invariantSeparatorsPath + values["$prefix.bytes"] = copy.length().toString() + values["$prefix.sha256"] = sha256(copy) + } for ((name, artifact) in artifacts) { values["artifact.$name.path"] = artifact.relativeTo(projectDir) @@ -944,6 +2012,10 @@ val verifyIndependentCleanBuildReproducibility by tasks.registering { group = "verification" description = "Compares main, sources, Javadoc, and source-release hashes from two clean checkouts of the same commit." + dependsOn( + verifyBlueLanguageAggregateCompatibility, + writeFocusedLanguageResolutionEvidence + ) val firstEvidencePath = providers.gradleProperty("cleanBuildEvidenceOne") val secondEvidencePath = @@ -1030,6 +2102,10 @@ val verifyIndependentCleanBuildReproducibility by tasks.registering { >() val authenticatedDependencyArtifacts = linkedMapOf>() + val authenticatedFocusedArtifacts = + linkedMapOf>>() + val authenticatedFocusedGraphHashes = + linkedMapOf() for ((label, evidence) in listOf("first" to first, "second" to second)) { check(evidence["schema"] == expectedSchema) { @@ -1248,6 +2324,120 @@ val verifyIndependentCleanBuildReproducibility by tasks.registering { "$label Blue Language artifact hash differs from " + "its receipt" } + check( + evidence["dependency.aggregateCompatibilityOnly"] == + "true" + ) { + "$label aggregate artifact is not labelled smoke-only" + } + check( + evidence["dependency.focused.declaredCoordinates"] == + blueLanguageFocusedCoordinates.joinToString(",") + ) { + "$label focused dependency coordinates differ" + } + val focusedGraphHash = + evidence["dependency.focused.graphSha256"] + check( + focusedGraphHash?.matches(Regex("[0-9a-f]{64}")) == + true + ) { + "$label focused dependency graph hash is unavailable" + } + val focusedArtifactCount = + evidence["dependency.focused.artifactCount"] + ?.toIntOrNull() + check( + focusedArtifactCount != null && + focusedArtifactCount > 0 + ) { + "$label focused dependency artifact count is invalid" + } + val focusedArtifacts = + (0 until focusedArtifactCount).map { index -> + val prefix = + "dependency.focused.artifact.%03d".format(index) + val coordinate = evidence["$prefix.coordinate"] + val component = evidence["$prefix.component"] + val projectPath = + evidence["$prefix.projectPath"].orEmpty() + val origin = evidence["$prefix.origin"] + val relativePath = evidence["$prefix.path"] + check( + coordinate?.split(':')?.size == 3 && + component?.isNotEmpty() == true && + origin in setOf( + "included-build-project", + "external-module" + ) && + relativePath?.startsWith( + "build/reports/bex-release/" + + "clean-build-inputs/focused-language/" + ) == true && + !File(relativePath).isAbsolute + ) { + "$label focused artifact $index metadata is invalid" + } + val copiedArtifact = + File(recordedRoot, relativePath).canonicalFile + check( + copiedArtifact.toPath().startsWith( + recordedRoot.toPath() + ) && copiedArtifact.isFile + ) { + "$label focused artifact $coordinate is unavailable" + } + val recordedBytes = + evidence["$prefix.bytes"]?.toLongOrNull() + val recordedHash = evidence["$prefix.sha256"] + check( + recordedBytes == copiedArtifact.length() && + recordedHash?.matches( + Regex("[0-9a-f]{64}") + ) == true && + recordedHash == sha256(copiedArtifact) + ) { + "$label focused artifact $coordinate differs " + + "from its receipt" + } + val authenticatedCoordinate = + requireNotNull(coordinate) + val authenticatedComponent = + requireNotNull(component) + val authenticatedOrigin = requireNotNull(origin) + val authenticatedHash = requireNotNull(recordedHash) + linkedMapOf( + "coordinate" to authenticatedCoordinate, + "component" to authenticatedComponent, + "projectPath" to projectPath, + "origin" to authenticatedOrigin, + "bytes" to recordedBytes.toString(), + "sha256" to authenticatedHash + ) + } + for ((moduleName, expectedPath) in + blueLanguageFocusedProjectPaths) { + val matches = focusedArtifacts.filter { + it.getValue("coordinate") + .substringAfter(':') + .substringBefore(':') == moduleName + } + check(matches.size == 1) { + "$label receipt does not contain exactly one " + + "$moduleName artifact" + } + if (blueLanguageDependencyMode == "local-composite") { + check( + matches.single().getValue("origin") == + "included-build-project" && + matches.single().getValue("projectPath") == + expectedPath + ) { + "$label $moduleName did not originate from " + + "$expectedPath" + } + } + } authenticatedRoots[label] = recordedRoot authenticatedGitDirectories[label] = actualGitDirectory @@ -1261,6 +2451,8 @@ val verifyIndependentCleanBuildReproducibility by tasks.registering { "bytes" to dependencyBytes.toString(), "sha256" to actualDependencyHash ) + authenticatedFocusedArtifacts[label] = focusedArtifacts + authenticatedFocusedGraphHashes[label] = focusedGraphHash } check( authenticatedRoots.getValue("first") != @@ -1436,8 +2628,49 @@ val verifyIndependentCleanBuildReproducibility by tasks.registering { ) { "Clean builds resolved different Language artifacts" } + val firstFocusedGraphHash = + authenticatedFocusedGraphHashes.getValue("first") + val secondFocusedGraphHash = + authenticatedFocusedGraphHashes.getValue("second") + check(firstFocusedGraphHash == secondFocusedGraphHash) { + "Clean builds resolved different focused Language graphs" + } + check( + authenticatedFocusedArtifacts.getValue("first") == + authenticatedFocusedArtifacts.getValue("second") + ) { + "Clean builds resolved different focused Language JAR sets" + } + val verifierFocusedResolution = + resolveFocusedLanguageEvidence( + blueLanguageFocusedResolution, + blueLanguageFocusedProjectPaths, + blueLanguageDependencyMode == "local-composite" + ) + check( + verifierFocusedResolution.graphSha256 == + firstFocusedGraphHash + ) { + "Clean builds did not use the verifier's exact focused " + + "Language component graph" + } + values["dependency.focused.declaredCoordinates"] = + blueLanguageFocusedCoordinates.joinToString(",") + values["dependency.focused.graphSha256"] = + firstFocusedGraphHash + values["dependency.focused.artifactCount"] = + authenticatedFocusedArtifacts.getValue("first") + .size.toString() + authenticatedFocusedArtifacts.getValue("first") + .forEachIndexed { index, artifact -> + val prefix = + "dependency.focused.artifact.%03d".format(index) + artifact.forEach { (field, value) -> + values["$prefix.$field"] = value + } + } val verifierLanguageArtifacts = - configurations.compileClasspath.get() + blueLanguageAggregateCompatibility .resolvedConfiguration .resolvedArtifacts .filter { @@ -1852,7 +3085,12 @@ val dependencyResolutionEvidence = val writeDependencyResolutionEvidence by tasks.registering { group = "verification" description = - "Resolves blue-language-java and verifies standalone artifacts against recorded Maven Central provenance." + "Verifies focused Language resolution and the smoke-only aggregate " + + "facade against strict standalone provenance evidence." + dependsOn( + verifyBlueLanguageAggregateCompatibility, + writeFocusedLanguageResolutionEvidence + ) outputs.file(dependencyResolutionEvidence) outputs.upToDateWhen { false } doFirst { @@ -1860,7 +3098,7 @@ val writeDependencyResolutionEvidence by tasks.registering { } doLast { val matches = - configurations.compileClasspath.get() + blueLanguageAggregateCompatibility .resolvedConfiguration .resolvedArtifacts .filter { @@ -1869,7 +3107,7 @@ val writeDependencyResolutionEvidence by tasks.registering { it.extension == "jar" } check(matches.size == 1) { - "Expected exactly one blue-language-java compile artifact, found " + + "Expected exactly one smoke-only blue-language-java artifact, found " + matches.joinToString { it.file.absolutePath } } val artifact = matches.single() @@ -1878,6 +3116,12 @@ val writeDependencyResolutionEvidence by tasks.registering { blueLanguageCompositePath ?.let { file(it).canonicalFile } val artifactHash = sha256(artifact.file) + val focusedResolution = + resolveFocusedLanguageEvidence( + blueLanguageFocusedResolution, + blueLanguageFocusedProjectPaths, + blueLanguageDependencyMode == "local-composite" + ) val provenanceStatus: String val moduleVersionCacheAcceptance: String if (blueLanguageDependencyMode == "standalone-published") { @@ -1907,10 +3151,14 @@ val writeDependencyResolutionEvidence by tasks.registering { // this exact Blue Language module/version directory was absent; // resolution above then verifies the resulting JAR against the // source-controlled Maven Central hash. + val allRequiredCachesInitiallyAbsent = + blueLanguageModuleVersionCacheInitiallyAbsent && + blueLanguageFocusedModuleVersionCachesInitiallyAbsent + .values.all { it } moduleVersionCacheAcceptance = if (!blueLanguageRequireFreshModuleCache.get()) { "not-required-for-current-run" - } else if (blueLanguageModuleVersionCacheInitiallyAbsent) { + } else if (allRequiredCachesInitiallyAbsent) { "passed" } else { "failed" @@ -1919,9 +3167,8 @@ val writeDependencyResolutionEvidence by tasks.registering { provenanceStatus = "not-applicable-local-composite" moduleVersionCacheAcceptance = "not-executed" } - writeEvidence( - dependencyResolutionEvidence.get().asFile, - mapOf( + val values = + linkedMapOf( "schema" to "blue-bex-dependency-resolution-evidence/1.0", "status" to "resolved", @@ -1936,6 +3183,17 @@ val writeDependencyResolutionEvidence by tasks.registering { "artifact.path" to artifact.file.canonicalPath, "artifact.bytes" to artifact.file.length().toString(), "artifact.sha256" to artifactHash, + "aggregate.compatibilityOnly" to "true", + "focused.declaredCoordinates" to + blueLanguageFocusedCoordinates.joinToString(","), + "focused.graphSha256" to + focusedResolution.graphSha256, + "focused.componentCount" to + focusedResolution.components.size.toString(), + "focused.edgeCount" to + focusedResolution.edges.size.toString(), + "focused.artifactCount" to + focusedResolution.artifacts.size.toString(), "composite.path" to (compositeDirectory?.path ?: ""), "repository.policy" to "maven-central-only", @@ -1956,8 +3214,30 @@ val writeDependencyResolutionEvidence by tasks.registering { blueLanguageRequireFreshModuleCache.get().toString(), "cache.acceptance" to moduleVersionCacheAcceptance, "cache.acceptanceScope" to - "standalone-published-blue-language-module-version-cache" + "standalone-published-focused-and-aggregate-language-module-version-caches" ) + blueLanguageFocusedModuleVersionCaches + .toSortedMap() + .forEach { (moduleName, cache) -> + values["cache.focused.$moduleName.path"] = + cache.canonicalPath + values["cache.focused.$moduleName.initiallyAbsent"] = + blueLanguageFocusedModuleVersionCachesInitiallyAbsent + .getValue(moduleName) + .toString() + } + focusedResolution.artifacts.forEachIndexed { index, focused -> + val prefix = "focused.artifact.%03d".format(index) + values["$prefix.coordinate"] = focused.coordinate + values["$prefix.component"] = focused.component + values["$prefix.projectPath"] = + focused.projectPath.orEmpty() + values["$prefix.bytes"] = focused.bytes.toString() + values["$prefix.sha256"] = focused.sha256 + } + writeEvidence( + dependencyResolutionEvidence.get().asFile, + values ) } } @@ -2020,14 +3300,10 @@ writeBexConformanceReport { mustRunAfter(tasks.test) } -tasks.test { - finalizedBy(writeBexConformanceReport) -} - tasks.register("bexConformanceReport") { group = "verification" description = "Runs all tests and produces the machine-readable BEX 2.0 conformance report." - dependsOn(tasks.test) + dependsOn(tasks.test, writeBexConformanceReport) } val bexReleaseEvidence by tasks.registering { @@ -2059,6 +3335,481 @@ val bexReleaseEvidence by tasks.registering { } } +val bexWorkingVerificationReport = + layout.buildDirectory.file( + "reports/latest-language-migration/final.json" + ) +val publicApiClassificationLedger = + layout.projectDirectory.file("docs/public-api-classification.json") +val writeProvisionalBexWorkingReceipt = { + blueLanguageFocusedResolutionEvidence.get().asFile.delete() + blueLanguageAggregateCompatibilityEvidence.get().asFile.delete() + latestLanguageMigrationBaselineEvidence.get().asFile.delete() + val outputFile = bexWorkingVerificationReport.get().asFile + outputFile.parentFile.mkdirs() + outputFile.writeText( + JsonOutput.prettyPrint( + JsonOutput.toJson( + linkedMapOf( + "schema" to + "blue-bex-working-verification/2.0", + "status" to "in-progress-or-failed", + "workingReady" to false, + "workingFailures" to + listOf("verification-did-not-complete"), + "recommendedCommand" to + "./gradlew bexWorkingVerification " + + "-PblueLanguageCompositePath=" + + (blueLanguageCompositePath ?: ""), + "recommendedCommandExecuted" to false + ) + ) + ) + "\n" + ) +} +gradle.taskGraph.whenReady { + val workingReportRequested = + allTasks.any { + it.path == ":bexWorkingVerification" || + it.path == ":writeBexWorkingVerificationReport" + } + if (workingReportRequested && !gradle.startParameter.isDryRun) { + writeProvisionalBexWorkingReceipt() + } +} +val initializeBexWorkingVerificationReceipt by tasks.registering { + group = "verification" + description = + "Invalidates migration evidence and writes a provisional red " + + "receipt before compilation or dependency resolution starts." + outputs.upToDateWhen { false } + doLast { + writeProvisionalBexWorkingReceipt() + } +} +val writeBexWorkingVerificationReport by tasks.registering { + group = "verification" + description = + "Writes the publication-independent BEX working-verification " + + "report for the exact local modular Language checkout." + dependsOn( + initializeBexWorkingVerificationReceipt, + tasks.test, + writeBexConformanceReport, + sourceReleaseArchive, + verifyBlueLanguageAggregateCompatibility, + writeFocusedLanguageResolutionEvidence, + writeLatestLanguageMigrationBaseline + ) + val conformanceReport = + layout.buildDirectory.file( + "reports/bex-conformance/report.json" + ) + inputs.file(conformanceReport) + inputs.file(blueLanguageAggregateCompatibilityEvidence) + inputs.file(blueLanguageFocusedResolutionEvidence) + inputs.file(latestLanguageMigrationBaselineEvidence) + inputs.file(latestLanguageMigrationLock) + inputs.file(publicApiClassificationLedger) + inputs.property("dependency.mode", blueLanguageDependencyMode) + inputs.property( + "focused.coordinates", + blueLanguageFocusedCoordinates.joinToString(",") + ) + outputs.file(bexWorkingVerificationReport) + outputs.upToDateWhen { false } + doLast { + val failures = mutableListOf() + fun requireWorking(value: Boolean, failure: String) { + if (!value) failures.add(failure) + } + fun mapValue(value: Any?): Map<*, *> = + value as? Map<*, *> ?: emptyMap() + fun child(parent: Map<*, *>, name: String): Map<*, *> = + mapValue(parent[name]) + fun intValue(parent: Map<*, *>, name: String): Int = + (parent[name] as? Number)?.toInt() ?: -1 + fun passed(parent: Map<*, *>, name: String): Boolean = + child(parent, name)["status"] == "passed" + + val conformanceFile = conformanceReport.get().asFile + check(conformanceFile.isFile) { + "Conformance report is missing: $conformanceFile" + } + @Suppress("UNCHECKED_CAST") + val report = + JsonSlurper().parse(conformanceFile) + as Map + @Suppress("UNCHECKED_CAST") + val focusedResolution = + JsonSlurper().parse( + blueLanguageFocusedResolutionEvidence.get().asFile + ) as Map + @Suppress("UNCHECKED_CAST") + val migrationBaseline = + JsonSlurper().parse( + latestLanguageMigrationBaselineEvidence.get().asFile + ) as Map + @Suppress("UNCHECKED_CAST") + val publicApiClassification = + JsonSlurper().parse(publicApiClassificationLedger.asFile) + as Map + val baselineLanguage = child(migrationBaseline, "language") + val baselineBex = child(migrationBaseline, "bex") + val languageCzToml = child(baselineLanguage, "czToml") + val bexCzToml = child(baselineBex, "czToml") + requireWorking( + migrationBaseline["status"] == "passed", + "migration-baseline-lock-validation-not-passing" + ) + requireWorking( + baselineLanguage["codeEquivalent"] == true, + "language-not-code-equivalent-to-verified-implementation" + ) + requireWorking( + languageCzToml["matches"] == true, + "language-cz-toml-differs-from-lock" + ) + requireWorking( + bexCzToml["matches"] == true, + "bex-cz-toml-differs-from-lock" + ) + val apiInventory = + child(publicApiClassification, "inventory") + val apiClassifications = + child(publicApiClassification, "classifications") + val classifiedApiTypes = + apiClassifications.values.flatMap { value -> + (value as? List<*>)?.map(Any?::toString) + ?: emptyList() + } + val requiredApiInventory = + file(apiInventory["path"].toString()) + requireWorking( + publicApiClassification["schema"] == + "blue-bex-public-api-classification/1.0" && + requiredApiInventory.isFile && + sha256(requiredApiInventory) == + apiInventory["sha256"] && + classifiedApiTypes.size == + (apiInventory["publicTypeCount"] as? Number) + ?.toInt() && + classifiedApiTypes.toSet().size == + classifiedApiTypes.size, + "public-api-classification-ledger-not-current" + ) + val productionClasspaths = + child(focusedResolution, "productionClasspaths") + requireWorking( + focusedResolution["status"] == "passed" && + focusedResolution["mode"] == "local-composite" && + focusedResolution["graphSha256"] + ?.toString() + ?.matches(Regex("[0-9a-f]{64}")) == true && + child(productionClasspaths, "compile") + ["aggregatePresent"] == false && + child(productionClasspaths, "runtime") + ["aggregatePresent"] == false, + "focused-language-resolution-or-production-classpath-gate-not-passing" + ) + val totals = child(report, "finalTotals") + val tests = child(totals, "tests") + val behavior = child(totals, "behaviorFixtures") + val gas = child(totals, "gasMicrofixtures") + val vectors = child(totals, "normativeVectors") + val operators = child(totals, "operators") + + val testsExecuted = intValue(tests, "executed") + val testsPassed = intValue(tests, "passed") + val testsFailed = intValue(tests, "failed") + val testsSkipped = intValue(tests, "skipped") + val testsUnclassified = + if ( + testsExecuted >= 0 && testsPassed >= 0 && + testsFailed >= 0 && testsSkipped >= 0 + ) { + testsExecuted - testsPassed - + testsFailed - testsSkipped + } else { + -1 + } + requireWorking( + testsExecuted > 0 && testsFailed == 0 && + testsSkipped == 0 && testsUnclassified == 0 && + tests["zeroFailures"] == true && + tests["zeroSkips"] == true, + "ordinary-tests-not-passing-with-zero-skips-and-zero-unclassified" + ) + requireWorking( + intValue(behavior, "required") == 105 && + intValue(behavior, "executedAndPassing") == 105, + "behavior-fixtures-not-105-of-105" + ) + requireWorking( + intValue(gas, "required") == 30 && + intValue(gas, "executedAndPassing") == 30, + "gas-microfixtures-not-30-of-30" + ) + requireWorking( + intValue(vectors, "required") == 60 && + intValue(vectors, "executedAndPassing") == 60 && + vectors["allPassing"] == true, + "normative-vectors-not-60-of-60" + ) + requireWorking( + intValue(operators, "required") == 86 && + intValue(operators, "executedAndPassing") == 86, + "operator-coverage-not-86-of-86" + ) + + val releaseGates = child(report, "releaseGates") + requireWorking( + passed(releaseGates, "deterministicArchives"), + "bex-owned-reproducibility-check-not-passing" + ) + requireWorking( + passed(releaseGates, "binaryApi"), + "binary-source-api-report-not-passing" + ) + requireWorking( + passed(releaseGates, "java8Bytecode"), + "java8-bytecode-check-not-passing" + ) + requireWorking( + child(report, "semanticBoundaryInvocationEvidence") + ["status"] == "passed" && + child(report, "ledgerLifecycleEvidence") + ["status"] == "passed", + "hosted-contracts-boundary-evidence-not-passing" + ) + val semanticParityPassed = + child(report, "representationMatrixResult")["status"] == + "passed" && + child(report, "intrinsicEvidence")["status"] == + "passed" && + vectors["allPassing"] == true && + intValue(operators, "executedAndPassing") == 86 + requireWorking( + semanticParityPassed, + "semantic-parity-evidence-not-passing" + ) + val counterCoverage = child(report, "counterCoverage") + val gasParityPassed = + counterCoverage["allMicrofixturesPassing"] == true && + counterCoverage["vocabularyComplete"] == true && + intValue(counterCoverage, "declaredCounterCount") == 30 && + intValue(counterCoverage, "executedMicrofixtureCount") == 30 && + intValue(counterCoverage, "passingMicrofixtureCount") == 30 && + child(report, "gasExhaustionEvidence")["status"] == + "passed" && + child(report, "finiteLoopEvidence")["status"] == + "passed" && + intValue(gas, "executedAndPassing") == 30 + requireWorking( + gasParityPassed, + "gas-parity-evidence-not-passing" + ) + requireWorking( + child(child(report, "dependency"), "resolution") + ["status"] == "passed", + "aggregate-compatibility-resolution-report-not-passing" + ) + requireWorking( + (report["artifacts"] as? List<*>)?.size == 4, + "working-artifacts-not-all-present" + ) + + fun legacyImportCount(sourceRoot: File): Int = + if (!sourceRoot.isDirectory) { + 0 + } else { + sourceRoot.walkTopDown() + .filter { it.isFile && it.extension == "java" } + .sumOf { source -> + source.useLines { lines -> + lines.count { line -> + forbiddenLegacyImportPatterns.any { + pattern -> pattern.containsMatchIn(line) + } + } + } + } + } + val productionLegacyImports = + legacyImportCount(file("src/main/java")) + val testLegacyImports = + legacyImportCount(file("src/test/java")) + requireWorking( + productionLegacyImports == 0, + "production-legacy-language-imports-present" + ) + requireWorking( + testLegacyImports == 0, + "test-legacy-language-imports-present" + ) + + val compositeDirectory = + blueLanguageCompositePath + ?.let { file(it).canonicalFile } + requireWorking( + blueLanguageDependencyMode == "local-composite" && + compositeDirectory?.isDirectory == true, + "bex-working-verification-requires-blueLanguageCompositePath" + ) + val languageFingerprint = + compositeDirectory + ?.takeIf { it.isDirectory } + ?.let(::gitWorkspaceFingerprint) + requireWorking( + languageFingerprint != null && + !languageFingerprint.dirty, + "local-language-checkout-is-dirty-or-unavailable" + ) + + val aggregateEvidence = readEvidence( + blueLanguageAggregateCompatibilityEvidence + .get().asFile + ) + requireWorking( + aggregateEvidence["status"] == "passed" && + aggregateEvidence["mode"] == "local-composite", + "aggregate-language-compatibility-smoke-not-passing" + ) + + val workingReady = failures.isEmpty() + val output = LinkedHashMap(report) + output["schema"] = + "blue-bex-working-verification/2.0" + output["status"] = + if (workingReady) "passed" else "failed" + output["workingReady"] = workingReady + output["workingFailures"] = failures + output["strictRelease"] = + linkedMapOf( + "releaseReady" to report["releaseReady"], + "failures" to report["currentModeFailures"], + "evidence" to report["hostedStandaloneMatrix"] + ) + val standalonePublished = + child( + child(report, "hostedStandaloneMatrix"), + "standalonePublished" + ) + output["publishedModeStatus"] = + standalonePublished["status"] ?: "not-executed" + output["recommendedCommand"] = + "./gradlew bexWorkingVerification " + + "-PblueLanguageCompositePath=" + + (compositeDirectory?.path ?: "") + output["recommendedCommandExecuted"] = false + output["reportProducerTask"] = + ":writeBexWorkingVerificationReport" + output["migrationBaseline"] = migrationBaseline + output["languageCodeEquivalence"] = baselineLanguage + output["sourceApiInventory"] = + migrationBaseline["sourceApiInventory"] + output["publicApiClassification"] = + publicApiClassification + output["migrationLedger"] = + migrationBaseline["migrationLedger"] + output["focusedLanguageResolution"] = focusedResolution + output["semanticParity"] = + linkedMapOf( + "status" to + if (semanticParityPassed) "passed" else "failed", + "normativeVectors" to vectors, + "behaviorFixtures" to behavior, + "operators" to operators, + "identities" to report["identities"], + "representationMatrixResult" to + report["representationMatrixResult"], + "intrinsicEvidence" to report["intrinsicEvidence"] + ) + output["gasParity"] = + linkedMapOf( + "status" to + if (gasParityPassed) "passed" else "failed", + "scope" to + "same-run-local-composite-semantic-and-exact-gas-evidence", + "gasMicrofixtures" to gas, + "counterCoverage" to report["counterCoverage"], + "gasExhaustionEvidence" to + report["gasExhaustionEvidence"], + "finiteLoopEvidence" to report["finiteLoopEvidence"], + "ledgerLifecycleEvidence" to + report["ledgerLifecycleEvidence"] + ) + output["hostedBoundaryResults"] = + linkedMapOf( + "semanticBoundaryInvocationEvidence" to + report["semanticBoundaryInvocationEvidence"], + "ledgerLifecycleEvidence" to + report["ledgerLifecycleEvidence"], + "hostedLocalLimitCapability" to + report["hostedLocalLimitCapability"], + "cyclicProofUnavailabilityCapability" to + report["cyclicProofUnavailabilityCapability"], + "hostedOutcomes" to report["hostedOutcomes"] + ) + output["workingDependency"] = + linkedMapOf( + "focusedCoordinates" to + blueLanguageFocusedCoordinates, + "languageCommit" to + languageFingerprint?.commit, + "languageWorkspaceSha256" to + languageFingerprint?.workspaceSha256, + "focusedResolution" to focusedResolution, + "aggregateCompatibility" to + aggregateEvidence.toSortedMap(), + "aggregateCompatibilityOnly" to true, + "productionLegacyImports" to + productionLegacyImports, + "testLegacyImports" to testLegacyImports + ) + val outputFile = + bexWorkingVerificationReport.get().asFile + outputFile.parentFile.mkdirs() + outputFile.writeText( + JsonOutput.prettyPrint(JsonOutput.toJson(output)) + "\n" + ) + check(workingReady) { + "BEX working verification is not ready: " + + failures.joinToString("; ") + + ". See " + outputFile + } + } +} + +listOf( + tasks.test, + writeBexConformanceReport, + sourceReleaseArchive, + verifyBlueLanguageAggregateCompatibility, + writeFocusedLanguageResolutionEvidence, + writeLatestLanguageMigrationBaseline +).forEach { verificationTask -> + verificationTask.configure { + mustRunAfter(initializeBexWorkingVerificationReceipt) + } +} + +val bexWorkingVerification by tasks.registering { + group = "verification" + description = + "Runs the complete local-composite BEX working gate without " + + "requiring a published Language release." + dependsOn(writeBexWorkingVerificationReport) +} + +val bexReleaseVerify by tasks.registering { + group = "verification" + description = + "Runs the strict published/local release matrix and fails closed " + + "when compatible published Language evidence is unavailable." + dependsOn(bexReleaseEvidence) +} + tasks.check { dependsOn( verifyDeterministicArchives, @@ -2145,12 +3896,12 @@ publishing { tasks.withType< org.gradle.api.publish.maven.tasks.PublishToMavenRepository >().configureEach { - dependsOn(bexReleaseEvidence) + dependsOn(bexReleaseVerify) } tasks.withType< org.gradle.api.publish.maven.tasks.PublishToMavenLocal >().configureEach { - dependsOn(bexReleaseEvidence) + dependsOn(bexReleaseVerify) } tasks.matching { it.name in setOf( @@ -2162,7 +3913,7 @@ tasks.matching { "jreleaserUpload" ) }.configureEach { - dependsOn(bexReleaseEvidence) + dependsOn(bexReleaseVerify) } if (System.getenv("CI") != null) { diff --git a/gradle/verification/latest-language-baseline.json b/gradle/verification/latest-language-baseline.json new file mode 100644 index 0000000..6f381bb --- /dev/null +++ b/gradle/verification/latest-language-baseline.json @@ -0,0 +1,77 @@ +{ + "schema": "blue-bex-latest-language-baseline/1.0", + "bex": { + "migrationBaselineCommit": "395c484111f8c4e9e0e98d2db7f1c5b0777bd5a8", + "czTomlSha256": "2dd317fbe362561f0e9827c705ff6260fd6df26c16518e0acce99ecba595a4b1" + }, + "language": { + "exactHead": "9a607e584ff5dd973684d35d71eb4022d946b760", + "verifiedImplementationCommit": "63a9ed6a1a66d47119a80d16ed2ab0beda0d2453", + "verifiedReleaseVersion": "3.1.0-rc.18", + "documentationOnlyDiffPaths": [ + "LICENSE", + "docs/collection-paths-and-cohesion-migration-report.md", + "reports/modernization/phase-collection-paths-final.json" + ], + "czTomlSha256": "f6717f9a9e38df0dea4b5eeec5a0264c0490856ef4c261c053a06883ba3e69f2", + "focusedModules": [ + { + "coordinate": "blue.language:blue-language-model:3.1.0-rc.19", + "projectPath": ":blue-language-model", + "verifiedArtifactSha256": "ef55be8331147442b858474add4782489d993568effe30202a9c4a8b014d5bd8" + }, + { + "coordinate": "blue.language:blue-language-core:3.1.0-rc.19", + "projectPath": ":blue-language-core", + "verifiedArtifactSha256": "a7d3c72640ab8ac5832feaad576cd1a56457cb87eaf07323fe04a88ae5730740" + }, + { + "coordinate": "blue.language:blue-language-mapping:3.1.0-rc.19", + "projectPath": ":blue-language-mapping", + "verifiedArtifactSha256": "d9141d5c611bde7eb6a21bce3dc4bc0df7d8167f013eeaef2a365dd0a6af329b" + }, + { + "coordinate": "blue.language:blue-contracts-core:3.1.0-rc.19", + "projectPath": ":blue-contracts-core", + "verifiedArtifactSha256": "ec45224ffee3e0c47246869d89c002657c9d1f348af8c553be3b6c0874bf7bae" + } + ], + "hostingPackageIdentities": { + "languageRegistry": "sha256:b705171a6ca62c990792bcb78db9d921caf5b0ed06370648b9a81769d69dd71e", + "contractsRegistry": "sha256:46a7744c1cbfa4b00e1d8a99f6ca3f0089ef697de968fee08547894ab02b0ca1", + "contractsGas": "sha256:88c7bbe77d531c9e973cae13002c3464a2c14568833adf5d804d13b7b3d26af5" + } + }, + "migration": { + "legacyUtilsImports": { + "production": { + "lines": 37, + "files": 23 + }, + "test": { + "lines": 14, + "files": 13 + } + }, + "allForbiddenLegacyImports": { + "production": { + "lines": 42, + "files": 24 + }, + "test": { + "lines": 26, + "files": 15 + } + }, + "languageImportInventory": { + "production": { + "lines": 139, + "files": 43 + }, + "test": { + "lines": 150, + "files": 34 + } + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 2ad4e2f..dcc4fc2 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -23,10 +23,44 @@ if (blueLanguageCompositePath != null) { "blueLanguageCompositePath is not a Gradle build: " + compositeDirectory.absolutePath } + val requiredLanguageProjects = + listOf( + "blue-language-model", + "blue-language-core", + "blue-language-mapping", + "blue-language-ipfs", + "blue-contracts-core", + "blue-conformance", + "blue-language-java" + ) + val missingLanguageProjects = + requiredLanguageProjects.filter { projectName -> + val projectDirectory = + file("${compositeDirectory.path}/$projectName") + !projectDirectory.isDirectory || + (!file("${projectDirectory.path}/build.gradle.kts").isFile && + !file("${projectDirectory.path}/build.gradle").isFile) + } + require(missingLanguageProjects.isEmpty()) { + "blueLanguageCompositePath does not contain the required Gradle " + + "subprojects: " + missingLanguageProjects.joinToString(", ") + } includeBuild(compositeDirectory) { dependencySubstitution { + substitute(module("blue.language:blue-language-model")) + .using(project(":blue-language-model")) + substitute(module("blue.language:blue-language-core")) + .using(project(":blue-language-core")) + substitute(module("blue.language:blue-language-mapping")) + .using(project(":blue-language-mapping")) + substitute(module("blue.language:blue-language-ipfs")) + .using(project(":blue-language-ipfs")) + substitute(module("blue.language:blue-contracts-core")) + .using(project(":blue-contracts-core")) + substitute(module("blue.language:blue-conformance")) + .using(project(":blue-conformance")) substitute(module("blue.language:blue-language-java")) - .using(project(":")) + .using(project(":blue-language-java")) } } } From 85805450d055b386718618995f5f4cd79f6bdf23 Mon Sep 17 00:00:00 2001 From: piotr-blue Date: Sun, 2 Aug 2026 20:45:38 +0100 Subject: [PATCH 04/13] refactor(api): migrate removed Language APIs and restore local compilation --- docs/LATEST_LANGUAGE_API_MIGRATION.md | 65 ++++++ docs/latest-language-api-migration.json | 202 ++++++++++++++++++ docs/public-api-classification.json | 104 +++++++++ src/main/java/blue/bex/api/BexEngine.java | 25 ++- .../blue/bex/api/BexIntrinsicRegistry.java | 50 ++++- .../blue/bex/api/BexTypeBlueIdResolver.java | 21 ++ .../blue/bex/api/FrozenBexDocumentView.java | 2 +- ...cessorExecutionContextBexDocumentView.java | 2 +- ...essorExecutionContextBexGasLedgerHost.java | 43 +++- .../java/blue/bex/compile/BexCompiler.java | 14 +- .../bex/output/BexEstablishedIdentity.java | 2 +- .../blue/bex/output/BexOutputAdmission.java | 2 +- .../output/BexSemanticIdentityBoundary.java | 4 +- .../java/blue/bex/pointer/BexPointer.java | 2 +- .../java/blue/bex/result/BexPatchEntry.java | 2 +- .../blue/bex/result/BexResultOverlay.java | 8 +- .../java/blue/bex/runtime/BexRuntime.java | 10 +- .../blue/bex/type/BexBlueTypeMatcher.java | 27 ++- .../java/blue/bex/value/AbstractBexValue.java | 2 +- .../blue/bex/value/AdmittedExactBexValue.java | 2 +- .../blue/bex/value/BexBlueNodeWriter.java | 4 +- .../blue/bex/value/BexBlueValueImporter.java | 80 +++++++ src/main/java/blue/bex/value/BexValues.java | 79 ++----- .../blue/bex/value/FrozenNodeBexValue.java | 2 +- .../java/blue/bex/value/ListBexValue.java | 2 +- src/main/java/blue/bex/value/MapBexValue.java | 2 +- .../java/blue/bex/value/NodeBexValue.java | 2 +- .../java/blue/bex/value/NullBexValue.java | 2 +- .../blue/bex/value/OverlayMapBexValue.java | 2 +- .../java/blue/bex/value/ScalarBexValue.java | 2 +- .../blue/bex/value/UndefinedBexValue.java | 2 +- 31 files changed, 643 insertions(+), 125 deletions(-) create mode 100644 docs/LATEST_LANGUAGE_API_MIGRATION.md create mode 100644 docs/latest-language-api-migration.json create mode 100644 docs/public-api-classification.json create mode 100644 src/main/java/blue/bex/api/BexTypeBlueIdResolver.java create mode 100644 src/main/java/blue/bex/value/BexBlueValueImporter.java diff --git a/docs/LATEST_LANGUAGE_API_MIGRATION.md b/docs/LATEST_LANGUAGE_API_MIGRATION.md new file mode 100644 index 0000000..1131041 --- /dev/null +++ b/docs/LATEST_LANGUAGE_API_MIGRATION.md @@ -0,0 +1,65 @@ +# Latest Blue Language API migration ledger + +This ledger describes the complete production binary-API delta caused by the +move from the removed monolithic Language facade to the modular Language API. +It is the human-readable companion to +[`latest-language-api-migration.json`](latest-language-api-migration.json). + +## Audited source state + +| Input | Exact state | +|---|---| +| BEX baseline | `395c484111f8c4e9e0e98d2db7f1c5b0777bd5a8` | +| BEX migration | Uncommitted working-tree delta rooted at the exact baseline above | +| Language target | `9a607e584ff5dd973684d35d71eb4022d946b760` | +| Language verified implementation | `63a9ed6a1a66d47119a80d16ed2ab0beda0d2453` | +| Language target delta | `LICENSE`, one migration report, and one modernization report only | +| Previous API manifest SHA-256 | `830caa187023079ba53fa76d2932e6e12cb8c93be3f90ac887ad374d6642b315` | +| Migrated API manifest SHA-256 | `43aea6ae9de6f39729f93c146ff5453887da9a2303f6f4f33573ba47f37d5be0` | + +The migration target cannot truthfully name its eventual BEX commit while that +commit is being assembled. The Git commit containing this ledger is the target +revision; the baseline commit above is the exact revision against which every +entry was audited. + +## Exact descriptor changes + +There are six removals and eleven additions. No other generated production API +descriptor changed. + +| Change | Classification | Exact signature | Replacement or purpose | Compatibility impact | +|---|---|---|---|---| +| Removed | stable API | `method public blue(blue.language.Blue):blue.bex.api.BexEngine$Builder` | Replaced by `language(BlueLanguage)` because the monolithic facade was removed. | Binary and source breaking for callers of `blue`. | +| Added | stable API | `method public language(blue.language.runtime.BlueLanguage):blue.bex.api.BexEngine$Builder` | Supported modular runtime entry point. | Additive alone; migration target for the removed method. | +| Added | intrinsic SPI | `method public intrinsic(java.lang.Class,blue.bex.api.BexTypeBlueIdResolver,java.lang.String,java.util.Map,blue.bex.api.BexIntrinsicProcessor):blue.bex.api.BexEngine$Builder` | Explicit application-owned class-to-BlueId policy. | Binary and source compatible addition. | +| Added | intrinsic SPI | `method public with(java.lang.Class,blue.bex.api.BexTypeBlueIdResolver,java.lang.String,java.util.Map,blue.bex.api.BexIntrinsicProcessor):blue.bex.api.BexIntrinsicRegistry` | Explicit resolver variant of immutable registration. | Binary and source compatible addition. | +| Added | intrinsic SPI | `method public register(java.lang.Class,blue.bex.api.BexTypeBlueIdResolver,java.lang.String,java.util.Map,blue.bex.api.BexIntrinsicProcessor):blue.bex.api.BexIntrinsicRegistry$Builder` | Explicit resolver variant of builder registration. | Binary and source compatible addition. | +| Added | intrinsic SPI | `class public abstract interface blue.bex.api.BexTypeBlueIdResolver` | BEX-owned replacement boundary for the removed Language resolver utility. | Binary and source compatible addition. | +| Added | intrinsic SPI | `method public abstract resolve(java.lang.Class):java.lang.String` | Exact mapping operation; class names are never treated as identities. | Compatible member of a new functional SPI. | +| Removed | internal implementation | `blue.bex.result.BexResultOverlay::(blue.bex.api.BexDocumentView,blue.bex.result.BexMetrics,blue.language.Blue)` | Replaced by the same constructor with `BlueLanguage`. | Binary and source breaking for direct users of the public-but-internal type. | +| Added | internal implementation | `blue.bex.result.BexResultOverlay::(blue.bex.api.BexDocumentView,blue.bex.result.BexMetrics,blue.language.runtime.BlueLanguage)` | Modular runtime replacement. | Additive alone; migration target for the removed constructor. | +| Removed | internal implementation | `blue.bex.runtime.BexRuntime::(blue.bex.compile.BexCompiledProgram,blue.bex.api.BexExecutionContext,blue.language.Blue,blue.bex.gas.BexGasSchedule,blue.bex.result.BexMetrics,blue.bex.pointer.BexPointerCache)` | Replaced by the same constructor with `BlueLanguage`. | Binary and source breaking for direct users of the public-but-internal type. | +| Added | internal implementation | `blue.bex.runtime.BexRuntime::(blue.bex.compile.BexCompiledProgram,blue.bex.api.BexExecutionContext,blue.language.runtime.BlueLanguage,blue.bex.gas.BexGasSchedule,blue.bex.result.BexMetrics,blue.bex.pointer.BexPointerCache)` | Modular runtime replacement. | Additive alone; migration target for the removed constructor. | +| Removed | internal implementation | `blue.bex.runtime.BexRuntime::(blue.bex.compile.BexCompiledProgram,blue.bex.api.BexExecutionContext,blue.language.Blue,blue.bex.gas.BexGasSchedule,blue.bex.result.BexMetrics,blue.bex.pointer.BexPointerCache,blue.bex.api.BexIntrinsicRegistry)` | Replaced by the same constructor with `BlueLanguage`. | Binary and source breaking for direct users of the public-but-internal type. | +| Added | internal implementation | `blue.bex.runtime.BexRuntime::(blue.bex.compile.BexCompiledProgram,blue.bex.api.BexExecutionContext,blue.language.runtime.BlueLanguage,blue.bex.gas.BexGasSchedule,blue.bex.result.BexMetrics,blue.bex.pointer.BexPointerCache,blue.bex.api.BexIntrinsicRegistry)` | Modular runtime replacement. | Additive alone; migration target for the removed constructor. | +| Removed | internal implementation | `blue.bex.type.BexBlueTypeMatcher::(blue.language.Blue)` | Replaced by the constructor accepting `BlueLanguage`. | Binary and source breaking for direct users of the public-but-internal type. | +| Added | internal implementation | `blue.bex.type.BexBlueTypeMatcher::(blue.language.runtime.BlueLanguage)` | Supported modular matcher/runtime boundary. | Additive alone; migration target for the removed constructor. | +| Removed | host SPI | `method public static referenceBacked(blue.bex.value.BexValue,blue.language.Blue):blue.bex.value.BexValue` | Replaced by the overload using the modular graph-capable runtime. | Binary and source breaking for direct host callers. | +| Added | host SPI | `method public static referenceBacked(blue.bex.value.BexValue,blue.language.runtime.BlueLanguage):blue.bex.value.BexValue` | Verified, demand-driven reference materialization through `BlueLanguage`. | Additive alone; migration target for host integrations. | + +The exact string-BlueId intrinsic methods remain authoritative. The retained +class convenience does not infer identity from a class name: it uses either an +explicit `BexTypeBlueIdResolver` or the focused annotated-type mapping boundary. + +## Public API classification and deterministic inventory + +[`public-api-classification.json`](public-api-classification.json) classifies +all 72 public production types as stable API, host SPI, intrinsic SPI, internal +implementation, or conformance-only. The exact 798 class/member descriptors are +source-controlled in +`src/test/resources/hosted-release/required-public-api.txt`; that file is the +machine-comparable inventory, while the JSON file supplies intent metadata. + +At this audited state the required inventory is byte-for-byte identical to +`build/reports/bex-release/public-api.txt`. Build wiring should continue to +generate the latter from compiled classes and fail on any diff from the former. diff --git a/docs/latest-language-api-migration.json b/docs/latest-language-api-migration.json new file mode 100644 index 0000000..2b86949 --- /dev/null +++ b/docs/latest-language-api-migration.json @@ -0,0 +1,202 @@ +{ + "schema": "blue-bex-api-migration-ledger/1.0", + "title": "Blue Language modular API migration", + "sourceState": { + "bexBaselineCommit": "395c484111f8c4e9e0e98d2db7f1c5b0777bd5a8", + "bexMigrationState": "working-tree delta rooted at bexBaselineCommit", + "languageExactCommit": "9a607e584ff5dd973684d35d71eb4022d946b760", + "languageVerifiedImplementationCommit": "63a9ed6a1a66d47119a80d16ed2ab0beda0d2453", + "languageDeltaClassification": "documentation-and-license-only", + "languageDeltaPaths": [ + "LICENSE", + "docs/collection-paths-and-cohesion-migration-report.md", + "reports/modernization/phase-collection-paths-final.json" + ] + }, + "manifests": { + "beforeSha256": "830caa187023079ba53fa76d2932e6e12cb8c93be3f90ac887ad374d6642b315", + "afterSha256": "43aea6ae9de6f39729f93c146ff5453887da9a2303f6f4f33573ba47f37d5be0", + "requiredPath": "src/test/resources/hosted-release/required-public-api.txt", + "generatedPath": "build/reports/bex-release/public-api.txt" + }, + "changes": [ + { + "id": "builder-blue-remove", + "kind": "removed", + "signature": "method public blue(blue.language.Blue):blue.bex.api.BexEngine$Builder", + "classification": "stable API", + "rationale": "The monolithic blue.language.Blue facade was removed by the modular Language API.", + "replacement": "method public language(blue.language.runtime.BlueLanguage):blue.bex.api.BexEngine$Builder", + "binaryCompatibility": "breaking: callers linked to Builder.blue(Blue) must relink", + "sourceCompatibility": "breaking: callers must construct BlueLanguage and call language" + }, + { + "id": "builder-language-add", + "kind": "added", + "signature": "method public language(blue.language.runtime.BlueLanguage):blue.bex.api.BexEngine$Builder", + "classification": "stable API", + "rationale": "BEX now accepts the supported modular Language runtime facade.", + "replacement": "replaces method public blue(blue.language.Blue):blue.bex.api.BexEngine$Builder", + "binaryCompatibility": "additive by itself; paired removal is binary-breaking", + "sourceCompatibility": "migration target for Builder.blue callers" + }, + { + "id": "engine-explicit-resolver-add", + "kind": "added", + "signature": "method public intrinsic(java.lang.Class,blue.bex.api.BexTypeBlueIdResolver,java.lang.String,java.util.Map,blue.bex.api.BexIntrinsicProcessor):blue.bex.api.BexEngine$Builder", + "classification": "intrinsic SPI", + "rationale": "Class convenience must use an explicit application-owned class-to-BlueId boundary when annotation resolution is not applicable.", + "replacement": "additive overload; exact string-BlueId registration remains authoritative", + "binaryCompatibility": "compatible additive overload", + "sourceCompatibility": "compatible additive overload" + }, + { + "id": "registry-explicit-resolver-add", + "kind": "added", + "signature": "method public with(java.lang.Class,blue.bex.api.BexTypeBlueIdResolver,java.lang.String,java.util.Map,blue.bex.api.BexIntrinsicProcessor):blue.bex.api.BexIntrinsicRegistry", + "classification": "intrinsic SPI", + "rationale": "The removed Language BlueIdResolver cannot be used; mapping policy is explicit and BEX-owned.", + "replacement": "additive overload; with(String,...) remains the exact identity API", + "binaryCompatibility": "compatible additive overload", + "sourceCompatibility": "compatible additive overload" + }, + { + "id": "registry-builder-explicit-resolver-add", + "kind": "added", + "signature": "method public register(java.lang.Class,blue.bex.api.BexTypeBlueIdResolver,java.lang.String,java.util.Map,blue.bex.api.BexIntrinsicProcessor):blue.bex.api.BexIntrinsicRegistry$Builder", + "classification": "intrinsic SPI", + "rationale": "The builder exposes the same explicit mapping boundary as the immutable registry.", + "replacement": "additive overload; register(String,...) remains the exact identity API", + "binaryCompatibility": "compatible additive overload", + "sourceCompatibility": "compatible additive overload" + }, + { + "id": "resolver-type-add", + "kind": "added", + "signature": "class public abstract interface blue.bex.api.BexTypeBlueIdResolver", + "classification": "intrinsic SPI", + "rationale": "BEX owns the optional class-mapping policy instead of depending on removed Language utilities.", + "replacement": "new functional SPI used only by class-based intrinsic convenience overloads", + "binaryCompatibility": "compatible additive type", + "sourceCompatibility": "compatible additive type" + }, + { + "id": "resolver-method-add", + "kind": "added", + "signature": "method public abstract resolve(java.lang.Class):java.lang.String", + "classification": "intrinsic SPI", + "rationale": "Returns an exact BlueId or null without class-name inference.", + "replacement": "new single abstract method of BexTypeBlueIdResolver", + "binaryCompatibility": "compatible as part of a new type", + "sourceCompatibility": "compatible lambda/function target" + }, + { + "id": "overlay-blue-remove", + "kind": "removed", + "signature": "constructor public (blue.bex.api.BexDocumentView,blue.bex.result.BexMetrics,blue.language.Blue)", + "owner": "blue.bex.result.BexResultOverlay", + "classification": "internal implementation", + "rationale": "The old monolithic Language facade no longer exists.", + "replacement": "constructor public (blue.bex.api.BexDocumentView,blue.bex.result.BexMetrics,blue.language.runtime.BlueLanguage)", + "binaryCompatibility": "breaking for consumers of this public-but-internal constructor", + "sourceCompatibility": "breaking: replace Blue with BlueLanguage" + }, + { + "id": "overlay-language-add", + "kind": "added", + "signature": "constructor public (blue.bex.api.BexDocumentView,blue.bex.result.BexMetrics,blue.language.runtime.BlueLanguage)", + "owner": "blue.bex.result.BexResultOverlay", + "classification": "internal implementation", + "rationale": "Reference-backed overlay reads require the modular Language runtime.", + "replacement": "replaces the constructor accepting blue.language.Blue", + "binaryCompatibility": "additive by itself; paired removal is binary-breaking", + "sourceCompatibility": "migration target for internal integrations" + }, + { + "id": "runtime-blue-basic-remove", + "kind": "removed", + "signature": "constructor public (blue.bex.compile.BexCompiledProgram,blue.bex.api.BexExecutionContext,blue.language.Blue,blue.bex.gas.BexGasSchedule,blue.bex.result.BexMetrics,blue.bex.pointer.BexPointerCache)", + "owner": "blue.bex.runtime.BexRuntime", + "classification": "internal implementation", + "rationale": "The old monolithic Language facade no longer exists.", + "replacement": "same constructor descriptor with blue.language.runtime.BlueLanguage", + "binaryCompatibility": "breaking for consumers of this public-but-internal constructor", + "sourceCompatibility": "breaking: replace Blue with BlueLanguage" + }, + { + "id": "runtime-language-basic-add", + "kind": "added", + "signature": "constructor public (blue.bex.compile.BexCompiledProgram,blue.bex.api.BexExecutionContext,blue.language.runtime.BlueLanguage,blue.bex.gas.BexGasSchedule,blue.bex.result.BexMetrics,blue.bex.pointer.BexPointerCache)", + "owner": "blue.bex.runtime.BexRuntime", + "classification": "internal implementation", + "rationale": "Runtime reference and type operations use the modular Language runtime.", + "replacement": "replaces the corresponding constructor accepting blue.language.Blue", + "binaryCompatibility": "additive by itself; paired removal is binary-breaking", + "sourceCompatibility": "migration target for internal integrations" + }, + { + "id": "runtime-blue-intrinsic-remove", + "kind": "removed", + "signature": "constructor public (blue.bex.compile.BexCompiledProgram,blue.bex.api.BexExecutionContext,blue.language.Blue,blue.bex.gas.BexGasSchedule,blue.bex.result.BexMetrics,blue.bex.pointer.BexPointerCache,blue.bex.api.BexIntrinsicRegistry)", + "owner": "blue.bex.runtime.BexRuntime", + "classification": "internal implementation", + "rationale": "The old monolithic Language facade no longer exists.", + "replacement": "same constructor descriptor with blue.language.runtime.BlueLanguage", + "binaryCompatibility": "breaking for consumers of this public-but-internal constructor", + "sourceCompatibility": "breaking: replace Blue with BlueLanguage" + }, + { + "id": "runtime-language-intrinsic-add", + "kind": "added", + "signature": "constructor public (blue.bex.compile.BexCompiledProgram,blue.bex.api.BexExecutionContext,blue.language.runtime.BlueLanguage,blue.bex.gas.BexGasSchedule,blue.bex.result.BexMetrics,blue.bex.pointer.BexPointerCache,blue.bex.api.BexIntrinsicRegistry)", + "owner": "blue.bex.runtime.BexRuntime", + "classification": "internal implementation", + "rationale": "Runtime reference and type operations use the modular Language runtime.", + "replacement": "replaces the corresponding constructor accepting blue.language.Blue", + "binaryCompatibility": "additive by itself; paired removal is binary-breaking", + "sourceCompatibility": "migration target for internal integrations" + }, + { + "id": "matcher-blue-remove", + "kind": "removed", + "signature": "constructor public (blue.language.Blue)", + "owner": "blue.bex.type.BexBlueTypeMatcher", + "classification": "internal implementation", + "rationale": "Type matching moved from the old facade to focused modular matching/runtime APIs.", + "replacement": "constructor public (blue.language.runtime.BlueLanguage)", + "binaryCompatibility": "breaking for consumers of this public-but-internal constructor", + "sourceCompatibility": "breaking: replace Blue with BlueLanguage" + }, + { + "id": "matcher-language-add", + "kind": "added", + "signature": "constructor public (blue.language.runtime.BlueLanguage)", + "owner": "blue.bex.type.BexBlueTypeMatcher", + "classification": "internal implementation", + "rationale": "The matcher uses the supported modular runtime and focused FrozenTypeMatcher API.", + "replacement": "replaces the constructor accepting blue.language.Blue", + "binaryCompatibility": "additive by itself; paired removal is binary-breaking", + "sourceCompatibility": "migration target for internal integrations" + }, + { + "id": "reference-backed-blue-remove", + "kind": "removed", + "signature": "method public static referenceBacked(blue.bex.value.BexValue,blue.language.Blue):blue.bex.value.BexValue", + "classification": "host SPI", + "rationale": "Demand-driven reference materialization must use the modular Language graph boundary.", + "replacement": "method public static referenceBacked(blue.bex.value.BexValue,blue.language.runtime.BlueLanguage):blue.bex.value.BexValue", + "binaryCompatibility": "breaking for direct host callers", + "sourceCompatibility": "breaking: replace Blue with BlueLanguage" + }, + { + "id": "reference-backed-language-add", + "kind": "added", + "signature": "method public static referenceBacked(blue.bex.value.BexValue,blue.language.runtime.BlueLanguage):blue.bex.value.BexValue", + "classification": "host SPI", + "rationale": "BlueLanguage supplies the verified graph expansion boundary needed by lazy exact values.", + "replacement": "replaces referenceBacked(BexValue, blue.language.Blue)", + "binaryCompatibility": "additive by itself; paired removal is binary-breaking", + "sourceCompatibility": "migration target for host integrations" + } + ] +} diff --git a/docs/public-api-classification.json b/docs/public-api-classification.json new file mode 100644 index 0000000..b793ab2 --- /dev/null +++ b/docs/public-api-classification.json @@ -0,0 +1,104 @@ +{ + "schema": "blue-bex-public-api-classification/1.0", + "inventory": { + "path": "src/test/resources/hosted-release/required-public-api.txt", + "sha256": "43aea6ae9de6f39729f93c146ff5453887da9a2303f6f4f33573ba47f37d5be0", + "manifestSchema": "blue-bex-binary-api-manifest/1.0", + "publicTypeCount": 72, + "publicDescriptorCount": 798 + }, + "sourceState": { + "bexBaselineCommit": "395c484111f8c4e9e0e98d2db7f1c5b0777bd5a8", + "bexMigrationState": "working-tree delta rooted at bexBaselineCommit", + "languageExactCommit": "9a607e584ff5dd973684d35d71eb4022d946b760", + "languageVerifiedImplementationCommit": "63a9ed6a1a66d47119a80d16ed2ab0beda0d2453" + }, + "classifications": { + "stable API": [ + "blue.bex.BexException", + "blue.bex.BexSourcePath", + "blue.bex.api.BexEngine", + "blue.bex.api.BexEngine$Builder", + "blue.bex.api.BexExecutionContext", + "blue.bex.api.BexExecutionContext$Builder", + "blue.bex.api.BexMetricsSink", + "blue.bex.api.BexProgramSource", + "blue.bex.api.BexProgramSource$Kind", + "blue.bex.api.BexStepResults", + "blue.bex.api.BexStepResults$Builder", + "blue.bex.compile.BexCompiledProgram", + "blue.bex.compile.BexCompiledProgramCache", + "blue.bex.compile.BexCompiledProgramKey", + "blue.bex.compile.BexCompiler", + "blue.bex.compile.LruBexCompiledProgramCache", + "blue.bex.gas.BexGasCharge", + "blue.bex.gas.BexGasCounter", + "blue.bex.gas.BexGasLedger", + "blue.bex.gas.BexGasLimitExceededException", + "blue.bex.gas.BexGasMeter", + "blue.bex.gas.BexGasSchedule", + "blue.bex.gas.BexGasSchedule$Builder", + "blue.bex.result.BexChangeset", + "blue.bex.result.BexEvents", + "blue.bex.result.BexExecutionResult", + "blue.bex.result.BexMetrics", + "blue.bex.result.BexPatchEntry", + "blue.bex.value.BexUnicodeOrder", + "blue.bex.value.BexUnicodeOrder$Comparison", + "blue.bex.value.BexValue", + "blue.bex.value.BexValues" + ], + "host SPI": [ + "blue.bex.api.BexDocumentView", + "blue.bex.api.BexGasLedgerHost", + "blue.bex.api.FrozenBexDocumentView", + "blue.bex.api.ProcessorExecutionContextBexDocumentView", + "blue.bex.api.ProcessorExecutionContextBexGasLedgerHost", + "blue.bex.output.BexAdmittedValue", + "blue.bex.output.BexEstablishedIdentity", + "blue.bex.output.BexOutputAdmission", + "blue.bex.output.BexOutputKind", + "blue.bex.output.BexSemanticIdentityBoundary", + "blue.bex.output.ProcessorExecutionContextBexSemanticIdentityBoundary" + ], + "intrinsic SPI": [ + "blue.bex.api.BexIntrinsicInvocation", + "blue.bex.api.BexIntrinsicProcessor", + "blue.bex.api.BexIntrinsicRegistry", + "blue.bex.api.BexIntrinsicRegistry$Builder", + "blue.bex.api.BexTypeBlueIdResolver" + ], + "internal implementation": [ + "blue.bex.compile.BexCompiledProgram$ArgSpec", + "blue.bex.compile.BexCompiledProgram$CompiledFunction", + "blue.bex.compile.BexContainsCache", + "blue.bex.compile.BexNodeIdentity", + "blue.bex.pointer.BexPointer", + "blue.bex.pointer.BexPointerCache", + "blue.bex.result.BexResultOverlay", + "blue.bex.runtime.BexExecutionAccumulator", + "blue.bex.runtime.BexRuntime", + "blue.bex.runtime.CompileScope", + "blue.bex.runtime.CompileScope$Visibility", + "blue.bex.runtime.CompiledExpression", + "blue.bex.runtime.CompiledFrame", + "blue.bex.runtime.CompiledStatement", + "blue.bex.runtime.Control", + "blue.bex.type.BexBlueTypeMatcher", + "blue.bex.value.BexBlueNodeWriter", + "blue.bex.value.BexFrozenWriter", + "blue.bex.value.BexNodeWriter", + "blue.bex.value.BexSimpleWriter", + "blue.bex.value.ChangesetBexValue", + "blue.bex.value.EventsBexValue", + "blue.bex.value.OverlayListBexValue", + "blue.bex.value.PatchEntryBexValue" + ], + "conformance-only": [] + }, + "notes": [ + "Classification is intent metadata; required-public-api.txt remains the exact descriptor inventory.", + "Public visibility alone does not make an internal implementation type stable.", + "Conformance-only types are test-source artifacts and therefore absent from the production binary manifest." + ] +} diff --git a/src/main/java/blue/bex/api/BexEngine.java b/src/main/java/blue/bex/api/BexEngine.java index 631c51c..b6d62d0 100644 --- a/src/main/java/blue/bex/api/BexEngine.java +++ b/src/main/java/blue/bex/api/BexEngine.java @@ -11,8 +11,8 @@ import blue.bex.result.BexExecutionResult; import blue.bex.result.BexMetrics; import blue.bex.runtime.BexRuntime; -import blue.language.Blue; import blue.language.registry.BlueCoreTypeRegistry; +import blue.language.runtime.BlueLanguage; import java.util.Map; @@ -25,7 +25,7 @@ * emit events, or perform host actions.

*/ public final class BexEngine { - private final Blue blue; + private final BlueLanguage blue; private final BexGasSchedule gasSchedule; private final BexCompiledProgramCache cache; private final BexMetricsSink metricsSink; @@ -124,14 +124,15 @@ private void validateIntrinsicSupport(BexCompiledProgram program) { } public static final class Builder { - private Blue blue = new Blue(); + private BlueLanguage blue = BlueLanguage.builder().build(); private BexGasSchedule gasSchedule = BexGasSchedule.defaults(); private BexCompiledProgramCache cache = new LruBexCompiledProgramCache(); private BexMetricsSink metricsSink = BexMetricsSink.NOOP; private BexIntrinsicRegistry intrinsics = BexIntrinsicRegistry.empty(); - public Builder blue(Blue blue) { - this.blue = blue != null ? blue : new Blue(); + public Builder language(BlueLanguage blue) { + this.blue = blue != null + ? blue : BlueLanguage.builder().build(); return this; } @@ -176,6 +177,20 @@ public Builder intrinsic(Class typeClass, return this; } + public Builder intrinsic(Class typeClass, + BexTypeBlueIdResolver typeBlueIdResolver, + String registryIdentity, + Map counterWeights, + BexIntrinsicProcessor processor) { + this.intrinsics = this.intrinsics.with( + typeClass, + typeBlueIdResolver, + registryIdentity, + counterWeights, + processor); + return this; + } + public BexEngine build() { return new BexEngine(this); } diff --git a/src/main/java/blue/bex/api/BexIntrinsicRegistry.java b/src/main/java/blue/bex/api/BexIntrinsicRegistry.java index 433e990..9692449 100644 --- a/src/main/java/blue/bex/api/BexIntrinsicRegistry.java +++ b/src/main/java/blue/bex/api/BexIntrinsicRegistry.java @@ -7,7 +7,7 @@ import blue.bex.value.BexUnicodeOrder; import blue.bex.value.BexValue; import blue.bex.value.BexValues; -import blue.language.utils.BlueIdResolver; +import blue.language.mapping.TypeClassResolver; import java.util.Collections; import java.util.LinkedHashMap; @@ -108,9 +108,23 @@ public BexIntrinsicRegistry with(Class typeClass, String registryIdentity, Map counterWeights, BexIntrinsicProcessor processor) { + return with( + typeClass, + BexIntrinsicRegistry::resolveAnnotatedTypeBlueId, + registryIdentity, + counterWeights, + processor); + } + + public BexIntrinsicRegistry with(Class typeClass, + BexTypeBlueIdResolver typeBlueIdResolver, + String registryIdentity, + Map counterWeights, + BexIntrinsicProcessor processor) { return toBuilder() .register( typeClass, + typeBlueIdResolver, registryIdentity, counterWeights, processor) @@ -249,6 +263,22 @@ private static String namespaceFor(String blueId) { return "intrinsic-" + blueId; } + private static String resolveAnnotatedTypeBlueId(Class typeClass) { + TypeClassResolver resolver = new TypeClassResolver(); + try { + resolver.registerAnnotatedClass(typeClass); + } catch (IllegalArgumentException unannotated) { + return null; + } + for (Map.Entry> entry + : resolver.getBlueIdMap().entrySet()) { + if (typeClass.equals(entry.getValue())) { + return entry.getKey(); + } + } + return null; + } + private static void appendIdentityToken( StringBuilder destination, String value) { @@ -347,11 +377,27 @@ public Builder register(Class typeClass, String registryIdentity, Map counterWeights, BexIntrinsicProcessor processor) { + return register( + typeClass, + BexIntrinsicRegistry::resolveAnnotatedTypeBlueId, + registryIdentity, + counterWeights, + processor); + } + + public Builder register(Class typeClass, + BexTypeBlueIdResolver typeBlueIdResolver, + String registryIdentity, + Map counterWeights, + BexIntrinsicProcessor processor) { if (typeClass == null) { throw new IllegalArgumentException( "intrinsic type class is required"); } - String blueId = BlueIdResolver.resolveBlueId(typeClass); + Objects.requireNonNull( + typeBlueIdResolver, + "intrinsic type BlueId resolver"); + String blueId = typeBlueIdResolver.resolve(typeClass); if (blueId == null || blueId.trim().isEmpty()) { throw new IllegalArgumentException( "intrinsic type class must have a resolvable @TypeBlueId: " diff --git a/src/main/java/blue/bex/api/BexTypeBlueIdResolver.java b/src/main/java/blue/bex/api/BexTypeBlueIdResolver.java new file mode 100644 index 0000000..1661d1a --- /dev/null +++ b/src/main/java/blue/bex/api/BexTypeBlueIdResolver.java @@ -0,0 +1,21 @@ +package blue.bex.api; + +/** + * Resolves the exact Blue type identity used by the class-based intrinsic + * registration convenience. + * + *

The string-based intrinsic API remains authoritative. This SPI keeps + * Java-object mapping policy outside the BEX registry and lets applications + * provide their own explicit class-to-identity catalog.

+ */ +@FunctionalInterface +public interface BexTypeBlueIdResolver { + + /** + * Resolves one Java type to an exact BlueId. + * + * @param type Java type to resolve + * @return exact BlueId, or {@code null} when the type is not registered + */ + String resolve(Class type); +} diff --git a/src/main/java/blue/bex/api/FrozenBexDocumentView.java b/src/main/java/blue/bex/api/FrozenBexDocumentView.java index 4d60fff..651f5f1 100644 --- a/src/main/java/blue/bex/api/FrozenBexDocumentView.java +++ b/src/main/java/blue/bex/api/FrozenBexDocumentView.java @@ -3,7 +3,7 @@ import blue.bex.value.BexValue; import blue.bex.value.BexValues; import blue.language.snapshot.FrozenNode; -import blue.language.utils.JsonPointer; +import blue.language.model.wire.JsonPointer; import java.util.ArrayList; import java.util.List; diff --git a/src/main/java/blue/bex/api/ProcessorExecutionContextBexDocumentView.java b/src/main/java/blue/bex/api/ProcessorExecutionContextBexDocumentView.java index 348d1ec..34e9bf4 100644 --- a/src/main/java/blue/bex/api/ProcessorExecutionContextBexDocumentView.java +++ b/src/main/java/blue/bex/api/ProcessorExecutionContextBexDocumentView.java @@ -4,7 +4,7 @@ import blue.bex.value.BexValues; import blue.language.snapshot.FrozenNode; import blue.language.processor.ProcessorExecutionContext; -import blue.language.utils.JsonPointer; +import blue.language.model.wire.JsonPointer; import java.util.Objects; diff --git a/src/main/java/blue/bex/api/ProcessorExecutionContextBexGasLedgerHost.java b/src/main/java/blue/bex/api/ProcessorExecutionContextBexGasLedgerHost.java index 0272eca..408a5e9 100644 --- a/src/main/java/blue/bex/api/ProcessorExecutionContextBexGasLedgerHost.java +++ b/src/main/java/blue/bex/api/ProcessorExecutionContextBexGasLedgerHost.java @@ -25,6 +25,7 @@ * primary and intrinsic ledgers to that exact shared admission boundary.

*/ public final class ProcessorExecutionContextBexGasLedgerHost implements BexGasLedgerHost { + private final ProcessorExecutionContext context; private final RuntimeWorkSession session; private final String runtimeNamespace; @@ -35,13 +36,16 @@ public ProcessorExecutionContextBexGasLedgerHost(ProcessorExecutionContext conte public ProcessorExecutionContextBexGasLedgerHost( ProcessorExecutionContext context, String runtimeNamespace) { - this(Objects.requireNonNull(context, "context").runtimeWorkSession(), - runtimeNamespace); + this.context = Objects.requireNonNull(context, "context"); + this.session = null; + this.runtimeNamespace = + requireRuntimeNamespace(runtimeNamespace); } public ProcessorExecutionContextBexGasLedgerHost( RuntimeWorkSession session, String runtimeNamespace) { + this.context = null; this.session = Objects.requireNonNull(session, "session"); this.runtimeNamespace = requireRuntimeNamespace(runtimeNamespace); @@ -55,7 +59,9 @@ public GasMeter.ChildGasLedger open(String namespace, @Override public RuntimeWorkBudget openSharedBudget(long maximumGas) { - return session.openSharedBudget(maximumGas); + return session != null + ? session.openSharedBudget(maximumGas) + : BexGasLedgerHost.super.openSharedBudget(maximumGas); } @Override @@ -65,15 +71,28 @@ public GasMeter.ChildGasLedger open( RuntimeWorkBudget sharedBudget) { String logicalNamespace = requireRuntimeNamespace(namespace); - return session.openLedger( - physicalNamespace(logicalNamespace), - counterWeights, - sharedBudget); + String physicalNamespace = physicalNamespace(logicalNamespace); + if (session != null) { + return session.openLedger( + physicalNamespace, + counterWeights, + sharedBudget); + } + if (sharedBudget != null) { + throw new IllegalArgumentException( + "ProcessorExecutionContext does not expose shared runtime budgets"); + } + return context.newRuntimeGasLedger( + physicalNamespace, counterWeights); } @Override public void submit(GasMeter.ChildGasLedger ledger) { - session.submit(ledger); + if (session != null) { + session.submit(ledger); + } else { + context.submitRuntimeGasLedger(ledger); + } } @Override @@ -118,8 +137,12 @@ public void propagateGasExhaustion( GasMeter.ChildGasLedger ledger, GasLimitExceededException exhaustion) { Objects.requireNonNull(ledger, "ledger"); - session.propagateGasExhaustion( - Objects.requireNonNull(exhaustion, "exhaustion")); + GasLimitExceededException exact = + Objects.requireNonNull(exhaustion, "exhaustion"); + if (session != null) { + session.propagateGasExhaustion(exact); + } + throw exact; } public String runtimeNamespace() { diff --git a/src/main/java/blue/bex/compile/BexCompiler.java b/src/main/java/blue/bex/compile/BexCompiler.java index 6ce569f..6c7ce9c 100644 --- a/src/main/java/blue/bex/compile/BexCompiler.java +++ b/src/main/java/blue/bex/compile/BexCompiler.java @@ -12,6 +12,7 @@ import blue.bex.result.BexMetrics; import blue.language.model.Node; import blue.language.model.Schema; +import blue.language.registry.BlueCoreTypeRegistry; import blue.language.snapshot.FrozenNode; import java.util.ArrayDeque; @@ -23,15 +24,18 @@ import java.util.Map; import java.util.Set; -import static blue.language.utils.Properties.BOOLEAN_TYPE_BLUE_ID; -import static blue.language.utils.Properties.DOUBLE_TYPE_BLUE_ID; -import static blue.language.utils.Properties.INTEGER_TYPE_BLUE_ID; -import static blue.language.utils.Properties.TEXT_TYPE_BLUE_ID; - /** * Compiler from frozen BEX Blue data to specialized runtime objects. */ public final class BexCompiler { + private static final String TEXT_TYPE_BLUE_ID = + BlueCoreTypeRegistry.INSTANCE.blueId("Text"); + private static final String INTEGER_TYPE_BLUE_ID = + BlueCoreTypeRegistry.INSTANCE.blueId("Integer"); + private static final String DOUBLE_TYPE_BLUE_ID = + BlueCoreTypeRegistry.INSTANCE.blueId("Double"); + private static final String BOOLEAN_TYPE_BLUE_ID = + BlueCoreTypeRegistry.INSTANCE.blueId("Boolean"); private static final Set RESERVED_BLUE_KEYS = reservedBlueKeys(); private final BexContainsCache containsCache = new BexContainsCache(); diff --git a/src/main/java/blue/bex/output/BexEstablishedIdentity.java b/src/main/java/blue/bex/output/BexEstablishedIdentity.java index 234800d..e26a5f9 100644 --- a/src/main/java/blue/bex/output/BexEstablishedIdentity.java +++ b/src/main/java/blue/bex/output/BexEstablishedIdentity.java @@ -1,7 +1,7 @@ package blue.bex.output; import blue.language.snapshot.FrozenNode; -import blue.language.utils.BlueIds; +import blue.language.identity.BlueIds; import java.util.Objects; diff --git a/src/main/java/blue/bex/output/BexOutputAdmission.java b/src/main/java/blue/bex/output/BexOutputAdmission.java index bed8687..a2c6f84 100644 --- a/src/main/java/blue/bex/output/BexOutputAdmission.java +++ b/src/main/java/blue/bex/output/BexOutputAdmission.java @@ -13,7 +13,7 @@ import blue.language.processor.PortableLimitExceededException; import blue.language.processor.ProcessorFailureException; import blue.language.snapshot.FrozenNode; -import blue.language.utils.BlueIds; +import blue.language.identity.BlueIds; import java.util.IdentityHashMap; import java.util.Map; diff --git a/src/main/java/blue/bex/output/BexSemanticIdentityBoundary.java b/src/main/java/blue/bex/output/BexSemanticIdentityBoundary.java index 614be10..d84ed2c 100644 --- a/src/main/java/blue/bex/output/BexSemanticIdentityBoundary.java +++ b/src/main/java/blue/bex/output/BexSemanticIdentityBoundary.java @@ -2,7 +2,7 @@ import blue.language.model.Node; import blue.language.snapshot.FrozenNode; -import blue.language.utils.BlueIdCalculator; +import blue.language.identity.DirectBlueIdCalculator; /** * Host-owned Blue semantic identity establishment. @@ -17,7 +17,7 @@ public interface BexSemanticIdentityBoundary { FrozenNode frozen = FrozenNode.fromResolvedNode( node.clone()); return new BexEstablishedIdentity( - BlueIdCalculator.calculateBlueId( + DirectBlueIdCalculator.calculateBlueId( frozen.toNode()), frozen); }; diff --git a/src/main/java/blue/bex/pointer/BexPointer.java b/src/main/java/blue/bex/pointer/BexPointer.java index a6c34e2..d4ecc60 100644 --- a/src/main/java/blue/bex/pointer/BexPointer.java +++ b/src/main/java/blue/bex/pointer/BexPointer.java @@ -1,7 +1,7 @@ package blue.bex.pointer; import blue.bex.BexException; -import blue.language.utils.JsonPointer; +import blue.language.model.wire.JsonPointer; import java.util.Collections; import java.util.List; diff --git a/src/main/java/blue/bex/result/BexPatchEntry.java b/src/main/java/blue/bex/result/BexPatchEntry.java index afafa13..6f28576 100644 --- a/src/main/java/blue/bex/result/BexPatchEntry.java +++ b/src/main/java/blue/bex/result/BexPatchEntry.java @@ -4,7 +4,7 @@ import blue.bex.output.BexAdmittedValue; import blue.bex.value.BexValue; import blue.bex.value.BexValues; -import blue.language.utils.JsonPointer; +import blue.language.model.wire.JsonPointer; import java.util.Collections; import java.util.List; diff --git a/src/main/java/blue/bex/result/BexResultOverlay.java b/src/main/java/blue/bex/result/BexResultOverlay.java index 5ed4e8f..3add772 100644 --- a/src/main/java/blue/bex/result/BexResultOverlay.java +++ b/src/main/java/blue/bex/result/BexResultOverlay.java @@ -3,8 +3,8 @@ import blue.bex.api.BexDocumentView; import blue.bex.value.BexValue; import blue.bex.value.BexValues; -import blue.language.Blue; -import blue.language.utils.JsonPointer; +import blue.language.model.wire.JsonPointer; +import blue.language.runtime.BlueLanguage; import java.util.ArrayList; import java.util.List; @@ -16,7 +16,7 @@ public final class BexResultOverlay { private final BexDocumentView document; private final List entries = new ArrayList<>(); private final BexMetrics metrics; - private final Blue blue; + private final BlueLanguage blue; public BexResultOverlay(BexDocumentView document, BexMetrics metrics) { this(document, metrics, null); @@ -25,7 +25,7 @@ public BexResultOverlay(BexDocumentView document, BexMetrics metrics) { public BexResultOverlay( BexDocumentView document, BexMetrics metrics, - Blue blue) { + BlueLanguage blue) { this.document = document; this.metrics = metrics; this.blue = blue; diff --git a/src/main/java/blue/bex/runtime/BexRuntime.java b/src/main/java/blue/bex/runtime/BexRuntime.java index 72c7e64..29a0457 100644 --- a/src/main/java/blue/bex/runtime/BexRuntime.java +++ b/src/main/java/blue/bex/runtime/BexRuntime.java @@ -19,7 +19,7 @@ import blue.bex.type.BexBlueTypeMatcher; import blue.bex.value.BexValue; import blue.bex.value.BexValues; -import blue.language.Blue; +import blue.language.runtime.BlueLanguage; import blue.language.processor.GasMeter; import blue.language.processor.GasLimitExceededException; import blue.language.processor.ExecutionEvidenceUnavailableException; @@ -27,7 +27,7 @@ import blue.language.processor.PortableLimitExceededException; import blue.language.processor.ProcessorFailureException; import blue.language.processor.RuntimeWorkBudget; -import blue.language.utils.JsonPointer; +import blue.language.model.wire.JsonPointer; import java.util.LinkedHashMap; import java.util.List; @@ -46,7 +46,7 @@ public final class BexRuntime { private final BexPointerCache pointerCache; private final BexExecutionAccumulator accumulator; private final BexBlueTypeMatcher typeMatcher; - private final Blue blue; + private final BlueLanguage blue; private final BexIntrinsicRegistry intrinsics; private final BexOutputAdmission outputAdmission; private final BexGasLedgerHost gasLedgerHost; @@ -54,7 +54,7 @@ public final class BexRuntime { public BexRuntime(BexCompiledProgram program, BexExecutionContext context, - Blue blue, + BlueLanguage blue, BexGasSchedule gasSchedule, BexMetrics metrics, BexPointerCache pointerCache) { @@ -63,7 +63,7 @@ public BexRuntime(BexCompiledProgram program, public BexRuntime(BexCompiledProgram program, BexExecutionContext context, - Blue blue, + BlueLanguage blue, BexGasSchedule gasSchedule, BexMetrics metrics, BexPointerCache pointerCache, diff --git a/src/main/java/blue/bex/type/BexBlueTypeMatcher.java b/src/main/java/blue/bex/type/BexBlueTypeMatcher.java index 4beda68..4bec15b 100644 --- a/src/main/java/blue/bex/type/BexBlueTypeMatcher.java +++ b/src/main/java/blue/bex/type/BexBlueTypeMatcher.java @@ -7,16 +7,17 @@ import blue.bex.value.BexBlueNodeWriter; import blue.bex.value.BexValue; import blue.bex.value.BexValues; -import blue.language.Blue; import blue.language.model.Node; import blue.language.model.Schema; +import blue.language.registry.BlueCoreTypeRegistry; +import blue.language.runtime.BlueLanguage; import blue.language.processor.ExecutionEvidenceUnavailableException; import blue.language.processor.GasLimitExceededException; import blue.language.processor.InvalidExecutionEvidenceException; import blue.language.processor.PortableLimitExceededException; import blue.language.processor.ProcessorFailureException; import blue.language.snapshot.FrozenNode; -import blue.language.utils.FrozenTypeMatcher; +import blue.language.matching.FrozenTypeMatcher; import java.math.BigDecimal; import java.math.BigInteger; @@ -25,22 +26,26 @@ import java.util.List; import java.util.Map; -import static blue.language.utils.Properties.BOOLEAN_TYPE_BLUE_ID; -import static blue.language.utils.Properties.DOUBLE_TYPE_BLUE_ID; -import static blue.language.utils.Properties.INTEGER_TYPE_BLUE_ID; -import static blue.language.utils.Properties.TEXT_TYPE_BLUE_ID; - /** * BEX boundary adapter for Blue's node/type matcher. */ public final class BexBlueTypeMatcher { private static final int TEXT_BLOCK_CODE_POINTS = 64; - - private final Blue blue; + private static final String TEXT_TYPE_BLUE_ID = + BlueCoreTypeRegistry.INSTANCE.blueId("Text"); + private static final String INTEGER_TYPE_BLUE_ID = + BlueCoreTypeRegistry.INSTANCE.blueId("Integer"); + private static final String DOUBLE_TYPE_BLUE_ID = + BlueCoreTypeRegistry.INSTANCE.blueId("Double"); + private static final String BOOLEAN_TYPE_BLUE_ID = + BlueCoreTypeRegistry.INSTANCE.blueId("Boolean"); + + private final BlueLanguage blue; private final FrozenTypeMatcher matcher; - public BexBlueTypeMatcher(Blue blue) { - this.blue = blue != null ? blue : new Blue(); + public BexBlueTypeMatcher(BlueLanguage blue) { + this.blue = blue != null + ? blue : BlueLanguage.builder().build(); this.matcher = FrozenTypeMatcher.withVerifiedReferenceMaterializer( reference -> FrozenNode.fromResolvedNode( BexValues.referenceBacked( diff --git a/src/main/java/blue/bex/value/AbstractBexValue.java b/src/main/java/blue/bex/value/AbstractBexValue.java index 367fb4a..7383d2c 100644 --- a/src/main/java/blue/bex/value/AbstractBexValue.java +++ b/src/main/java/blue/bex/value/AbstractBexValue.java @@ -3,7 +3,7 @@ import blue.bex.BexException; import blue.language.model.Node; import blue.language.snapshot.FrozenNode; -import blue.language.utils.JsonPointer; +import blue.language.model.wire.JsonPointer; import java.math.BigDecimal; import java.math.BigInteger; diff --git a/src/main/java/blue/bex/value/AdmittedExactBexValue.java b/src/main/java/blue/bex/value/AdmittedExactBexValue.java index 7f52e17..e0d4bc0 100644 --- a/src/main/java/blue/bex/value/AdmittedExactBexValue.java +++ b/src/main/java/blue/bex/value/AdmittedExactBexValue.java @@ -2,7 +2,7 @@ import blue.language.model.Node; import blue.language.snapshot.FrozenNode; -import blue.language.utils.JsonPointer; +import blue.language.model.wire.JsonPointer; import java.math.BigDecimal; import java.math.BigInteger; diff --git a/src/main/java/blue/bex/value/BexBlueNodeWriter.java b/src/main/java/blue/bex/value/BexBlueNodeWriter.java index b4b2c5a..4ec9e08 100644 --- a/src/main/java/blue/bex/value/BexBlueNodeWriter.java +++ b/src/main/java/blue/bex/value/BexBlueNodeWriter.java @@ -3,8 +3,8 @@ import blue.bex.BexException; import blue.language.model.Node; import blue.language.model.Schema; -import blue.language.utils.BlueIds; -import blue.language.utils.Nodes; +import blue.language.identity.BlueIds; +import blue.language.model.Nodes; import java.math.BigDecimal; import java.math.BigInteger; diff --git a/src/main/java/blue/bex/value/BexBlueValueImporter.java b/src/main/java/blue/bex/value/BexBlueValueImporter.java new file mode 100644 index 0000000..383c273 --- /dev/null +++ b/src/main/java/blue/bex/value/BexBlueValueImporter.java @@ -0,0 +1,80 @@ +package blue.bex.value; + +import blue.language.model.Node; +import blue.language.model.NodeWireForm; +import blue.language.model.Schema; +import blue.language.model.SchemaWireForm; +import blue.language.registry.BlueCoreTypeRegistry; + +import java.util.Map; + +/** + * BEX-owned boundary from final Language model objects to transient BEX wire + * values. + * + *

Language remains authoritative for reserved-field projection, list + * controls, scalar canonicalization, and malformed-shape rejection. The + * schema callback preserves the legacy BEX rule that explicitly core-typed + * scalar constraints are imported as their scalar payload.

+ */ +final class BexBlueValueImporter { + private static final String TEXT_TYPE_BLUE_ID = + BlueCoreTypeRegistry.INSTANCE.blueId("Text"); + private static final String INTEGER_TYPE_BLUE_ID = + BlueCoreTypeRegistry.INSTANCE.blueId("Integer"); + private static final String DOUBLE_TYPE_BLUE_ID = + BlueCoreTypeRegistry.INSTANCE.blueId("Double"); + private static final String BOOLEAN_TYPE_BLUE_ID = + BlueCoreTypeRegistry.INSTANCE.blueId("Boolean"); + + private BexBlueValueImporter() { + } + + static Object node(Node node) { + return NodeWireForm.get(node); + } + + static Map schema(Schema schema) { + return SchemaWireForm.get(schema, BexBlueValueImporter::schemaNode); + } + + private static Object schemaNode(Node node) { + if (isCoreTypedScalar(node)) { + return node.getValue(); + } + return NodeWireForm.get(node); + } + + private static boolean isCoreTypedScalar(Node node) { + if (node == null + || node.getValue() == null + || node.getName() != null + || node.getDescription() != null + || node.getItems() != null + || node.getProperties() != null + || node.getContracts() != null + || node.getBlueId() != null + || node.getSchema() != null + || node.getMergePolicy() != null + || node.getPreviousBlueId() != null + || node.getPosition() != null + || node.getBlue() != null + || node.getItemType() != null + || node.getKeyType() != null + || node.getValueType() != null) { + return false; + } + if (node.getType() == null) { + return true; + } + String typeBlueId = node.getType().getBlueId(); + return TEXT_TYPE_BLUE_ID.equals(typeBlueId) + || INTEGER_TYPE_BLUE_ID.equals(typeBlueId) + || DOUBLE_TYPE_BLUE_ID.equals(typeBlueId) + || BOOLEAN_TYPE_BLUE_ID.equals(typeBlueId) + || "Text".equals(typeBlueId) + || "Integer".equals(typeBlueId) + || "Double".equals(typeBlueId) + || "Boolean".equals(typeBlueId); + } +} diff --git a/src/main/java/blue/bex/value/BexValues.java b/src/main/java/blue/bex/value/BexValues.java index cc616f6..43739a9 100644 --- a/src/main/java/blue/bex/value/BexValues.java +++ b/src/main/java/blue/bex/value/BexValues.java @@ -1,19 +1,17 @@ package blue.bex.value; import blue.bex.BexException; -import blue.language.Blue; -import blue.language.BlueOperationLimits; -import blue.language.BlueOperationOutcome; -import blue.language.BlueOperationResult; +import blue.language.api.BlueOperationLimits; +import blue.language.api.BlueOperationOutcome; +import blue.language.api.BlueOperationResult; import blue.language.model.Node; import blue.language.model.Schema; import blue.language.processor.ExecutionEvidenceUnavailableException; import blue.language.processor.InvalidExecutionEvidenceException; import blue.language.snapshot.FrozenNode; -import blue.language.snapshot.ResolvedSnapshot; -import blue.language.utils.JsonPointer; -import blue.language.utils.NodeToMapListOrValue; -import blue.language.utils.SchemaToMapListOrValue; +import blue.language.merge.ResolvedSnapshot; +import blue.language.model.wire.JsonPointer; +import blue.language.runtime.BlueLanguage; import java.math.BigDecimal; import java.math.BigInteger; @@ -24,11 +22,6 @@ import java.util.Map; import java.util.TreeSet; -import static blue.language.utils.Properties.BOOLEAN_TYPE_BLUE_ID; -import static blue.language.utils.Properties.DOUBLE_TYPE_BLUE_ID; -import static blue.language.utils.Properties.INTEGER_TYPE_BLUE_ID; -import static blue.language.utils.Properties.TEXT_TYPE_BLUE_ID; - /** * Value factories and shared value helpers. */ @@ -155,12 +148,13 @@ public static BexValue admittedExact(FrozenNode frozenValue, * Adds demand-driven, verified reference materialization to an exact * frozen value. * - *

{@link Blue#resolveToSnapshot(Object)} intentionally leaves untyped + *

{@link blue.language.merge.BlueSnapshots#resolve(Node)} intentionally leaves untyped * pure references collapsed. BEX may carry those values by identity * without loading them, but semantic operations such as member access, * kind inspection, or key enumeration must establish their content. - * {@link Blue#expandLimited(Node, BlueOperationLimits)} is the structured - * Language boundary that both obtains and verifies that evidence. + * {@link blue.language.graph.BlueGraph#expandLimited(Node, BlueOperationLimits)} + * is the focused Language boundary + * that both obtains and verifies that evidence. * Provider absence and temporary unavailability remain incomplete * execution evidence, while invalid evidence remains a deterministic * failure; neither is converted to BEX {@code undefined}.

@@ -170,7 +164,8 @@ public static BexValue admittedExact(FrozenNode frozenValue, * @return a lazy reference-backed exact value, or {@code value} when it is * not backed by a {@link FrozenNode} */ - public static BexValue referenceBacked(BexValue value, Blue blue) { + public static BexValue referenceBacked( + BexValue value, BlueLanguage blue) { if (value instanceof FrozenNodeBexValue && blue != null) { return ((FrozenNodeBexValue) value) .withReferenceMaterializer( @@ -180,8 +175,8 @@ public static BexValue referenceBacked(BexValue value, Blue blue) { } private static ResolvedSnapshot loadReference( - Blue blue, String blueId) { - BlueOperationResult result = blue.expandLimited( + BlueLanguage blue, String blueId) { + BlueOperationResult result = blue.graph().expandLimited( new Node().blueId(blueId), BlueOperationLimits.demandedPath("")); if (result.outcome() == BlueOperationOutcome.INVALID) { @@ -218,7 +213,7 @@ public static BexValue transientFrozen(FrozenNode node) { if (node == null) { return UNDEFINED; } - return fromSimple(NodeToMapListOrValue.get(node.toNode())); + return fromSimple(BexBlueValueImporter.node(node.toNode())); } static BexValue schemaSnapshot(Schema schema) { @@ -226,49 +221,7 @@ static BexValue schemaSnapshot(Schema schema) { return UNDEFINED; } // Schema is the value of a node's "schema" key, not another schema-bearing node. - return fromSimple(SchemaToMapListOrValue.get( - schema.clone(), - BexValues::schemaNodeToSimple)); - } - - private static Object schemaNodeToSimple(Node node) { - if (isCoreTypedScalar(node)) { - return node.getValue(); - } - return NodeToMapListOrValue.get(node); - } - - private static boolean isCoreTypedScalar(Node node) { - if (node == null - || node.getValue() == null - || node.getName() != null - || node.getDescription() != null - || node.getItems() != null - || node.getProperties() != null - || node.getContracts() != null - || node.getBlueId() != null - || node.getSchema() != null - || node.getMergePolicy() != null - || node.getPreviousBlueId() != null - || node.getPosition() != null - || node.getBlue() != null - || node.getItemType() != null - || node.getKeyType() != null - || node.getValueType() != null) { - return false; - } - if (node.getType() == null) { - return true; - } - String typeBlueId = node.getType().getBlueId(); - return TEXT_TYPE_BLUE_ID.equals(typeBlueId) - || INTEGER_TYPE_BLUE_ID.equals(typeBlueId) - || DOUBLE_TYPE_BLUE_ID.equals(typeBlueId) - || BOOLEAN_TYPE_BLUE_ID.equals(typeBlueId) - || "Text".equals(typeBlueId) - || "Integer".equals(typeBlueId) - || "Double".equals(typeBlueId) - || "Boolean".equals(typeBlueId); + return fromSimple(BexBlueValueImporter.schema(schema.clone())); } public static String frozenBlueId(BexValue value) { diff --git a/src/main/java/blue/bex/value/FrozenNodeBexValue.java b/src/main/java/blue/bex/value/FrozenNodeBexValue.java index 42375f4..d1d3b0c 100644 --- a/src/main/java/blue/bex/value/FrozenNodeBexValue.java +++ b/src/main/java/blue/bex/value/FrozenNodeBexValue.java @@ -3,7 +3,7 @@ import blue.bex.BexException; import blue.language.model.Node; import blue.language.snapshot.FrozenNode; -import blue.language.snapshot.ResolvedSnapshot; +import blue.language.merge.ResolvedSnapshot; import java.math.BigDecimal; import java.math.BigInteger; diff --git a/src/main/java/blue/bex/value/ListBexValue.java b/src/main/java/blue/bex/value/ListBexValue.java index 13f6e1c..78a0e5c 100644 --- a/src/main/java/blue/bex/value/ListBexValue.java +++ b/src/main/java/blue/bex/value/ListBexValue.java @@ -3,7 +3,7 @@ import blue.bex.BexException; import blue.language.model.Node; import blue.language.snapshot.FrozenNode; -import blue.language.utils.JsonPointer; +import blue.language.model.wire.JsonPointer; import java.math.BigDecimal; import java.math.BigInteger; diff --git a/src/main/java/blue/bex/value/MapBexValue.java b/src/main/java/blue/bex/value/MapBexValue.java index 4157ed0..0e43a29 100644 --- a/src/main/java/blue/bex/value/MapBexValue.java +++ b/src/main/java/blue/bex/value/MapBexValue.java @@ -3,7 +3,7 @@ import blue.bex.BexException; import blue.language.model.Node; import blue.language.snapshot.FrozenNode; -import blue.language.utils.JsonPointer; +import blue.language.model.wire.JsonPointer; import java.math.BigDecimal; import java.math.BigInteger; diff --git a/src/main/java/blue/bex/value/NodeBexValue.java b/src/main/java/blue/bex/value/NodeBexValue.java index f0e24b3..12520cc 100644 --- a/src/main/java/blue/bex/value/NodeBexValue.java +++ b/src/main/java/blue/bex/value/NodeBexValue.java @@ -3,7 +3,7 @@ import blue.bex.BexException; import blue.language.model.Node; import blue.language.snapshot.FrozenNode; -import blue.language.utils.JsonPointer; +import blue.language.model.wire.JsonPointer; import java.math.BigDecimal; import java.math.BigInteger; diff --git a/src/main/java/blue/bex/value/NullBexValue.java b/src/main/java/blue/bex/value/NullBexValue.java index e0d9cfe..6ab54e8 100644 --- a/src/main/java/blue/bex/value/NullBexValue.java +++ b/src/main/java/blue/bex/value/NullBexValue.java @@ -3,7 +3,7 @@ import blue.bex.BexException; import blue.language.model.Node; import blue.language.snapshot.FrozenNode; -import blue.language.utils.JsonPointer; +import blue.language.model.wire.JsonPointer; import java.math.BigDecimal; import java.math.BigInteger; diff --git a/src/main/java/blue/bex/value/OverlayMapBexValue.java b/src/main/java/blue/bex/value/OverlayMapBexValue.java index 3da9490..8bb006d 100644 --- a/src/main/java/blue/bex/value/OverlayMapBexValue.java +++ b/src/main/java/blue/bex/value/OverlayMapBexValue.java @@ -3,7 +3,7 @@ import blue.bex.BexException; import blue.language.model.Node; import blue.language.snapshot.FrozenNode; -import blue.language.utils.JsonPointer; +import blue.language.model.wire.JsonPointer; import java.math.BigDecimal; import java.math.BigInteger; diff --git a/src/main/java/blue/bex/value/ScalarBexValue.java b/src/main/java/blue/bex/value/ScalarBexValue.java index 19acdc7..da56aa0 100644 --- a/src/main/java/blue/bex/value/ScalarBexValue.java +++ b/src/main/java/blue/bex/value/ScalarBexValue.java @@ -3,7 +3,7 @@ import blue.bex.BexException; import blue.language.model.Node; import blue.language.snapshot.FrozenNode; -import blue.language.utils.JsonPointer; +import blue.language.model.wire.JsonPointer; import java.math.BigDecimal; import java.math.BigInteger; diff --git a/src/main/java/blue/bex/value/UndefinedBexValue.java b/src/main/java/blue/bex/value/UndefinedBexValue.java index d169280..d83edd5 100644 --- a/src/main/java/blue/bex/value/UndefinedBexValue.java +++ b/src/main/java/blue/bex/value/UndefinedBexValue.java @@ -3,7 +3,7 @@ import blue.bex.BexException; import blue.language.model.Node; import blue.language.snapshot.FrozenNode; -import blue.language.utils.JsonPointer; +import blue.language.model.wire.JsonPointer; import java.math.BigDecimal; import java.math.BigInteger; From b9ab06341d8610097e734275f76a2668c1bb68b4 Mon Sep 17 00:00:00 2001 From: piotr-blue Date: Sun, 2 Aug 2026 20:46:00 +0100 Subject: [PATCH 05/13] test: close latest-Language BEX conformance and hosted boundary --- docs/FIXTURES.md | 15 +- specifications/blue-bex-specification-2.0.md | 12 +- .../blue/bex/BexBlueTypeMatchingGasTest.java | 68 ++--- .../java/blue/bex/BexBlueTypeSupportTest.java | 12 +- .../blue/bex/BexCompiledProgramCacheTest.java | 6 +- .../BexCompilerScalarNormalizationTest.java | 10 +- .../BexCompositeExhaustionEvidenceTest.java | 26 +- .../BexDiagnosticAdmissionOrderingTest.java | 14 +- .../bex/BexExactReferenceDocumentTest.java | 54 ++-- .../bex/BexExecutionEvidenceLedgerTest.java | 26 +- src/test/java/blue/bex/BexIntrinsicTest.java | 8 +- .../java/blue/bex/BexLazyBindingTest.java | 4 +- .../bex/BexProgramParsingBoundaryTest.java | 46 +++ .../java/blue/bex/BexRichFixtureTest.java | 36 +-- .../java/blue/bex/BexSchemaValueTest.java | 7 +- .../BexStructuredReferenceEvidenceTest.java | 24 +- .../blue/bex/BexUseCaseConformanceTest.java | 6 +- ...xternalCustomerPayNoteBexFunctionTest.java | 10 +- ...nformanceDocumentationConsistencyTest.java | 128 +++++++++ .../BexConformancePackageIntegrityTest.java | 4 +- .../BexConformancePropertyTest.java | 2 +- .../conformance/BexEngineFixtureAdapter.java | 26 +- .../BexRepresentationInvarianceTest.java | 18 +- .../bex/conformance/ConformancePackage.java | 6 +- .../BexSemanticIdentityIntegrationTest.java | 29 +- src/test/java/blue/bex/test/TestBlue.java | 267 ++++++++++++++++++ .../BexHostedRuntimeWorkSessionTest.java | 42 +-- .../hosted-release/baseline.properties | 2 +- .../hosted-release/required-public-api.txt | 17 +- 29 files changed, 694 insertions(+), 231 deletions(-) create mode 100644 src/test/java/blue/bex/BexProgramParsingBoundaryTest.java create mode 100644 src/test/java/blue/bex/conformance/BexConformanceDocumentationConsistencyTest.java create mode 100644 src/test/java/blue/bex/test/TestBlue.java diff --git a/docs/FIXTURES.md b/docs/FIXTURES.md index 95378e5..6b38029 100644 --- a/docs/FIXTURES.md +++ b/docs/FIXTURES.md @@ -173,15 +173,16 @@ including numeric-kind preservation and rejection of undefined list slots. byte length and SHA-256 digest. It binds the exact BEX runtime registry, gas manifest, vector map, and operator map. -The implementation-baseline identities are: +The implementation-baseline inventory and identities are: ```text -runtime registry: - sha256:23d282ec1c0bb016263922b1b49c369fdd537efdcf23e005eceeb888d7763fe1 -gas manifest: - sha256:41247c820d91a12fdfc17fd9e787a5d8d668d8acc5954fdcb131715bf9e6147d -fixture package: - sha256:f5f64a38152ef0e50ebb1b03caaa1b07fd556552eb071b940937079fc0234dfe +normative vectors: 60 +behavior fixtures: 105 +gas microfixtures: 30 +normative operators: 86 +runtime registry: sha256:23d282ec1c0bb016263922b1b49c369fdd537efdcf23e005eceeb888d7763fe1 +gas manifest: sha256:41247c820d91a12fdfc17fd9e787a5d8d668d8acc5954fdcb131715bf9e6147d +fixture package: sha256:a1b7bb2b3687389409bc9d0aa450c734f7856d2bcb818c95f4d7ecb19095d20e ``` Run the complete package and generate machine-readable evidence with: diff --git a/specifications/blue-bex-specification-2.0.md b/specifications/blue-bex-specification-2.0.md index 62ee856..1f41f6d 100644 --- a/specifications/blue-bex-specification-2.0.md +++ b/specifications/blue-bex-specification-2.0.md @@ -1746,10 +1746,18 @@ fixture package SHA-256 A conforming BEX 2.0 implementation MUST report the exact runtime-registry, gas-manifest, and fixture-package identities it implements and passes. -The implementation-baseline fixture package is bound to the exact BEX runtime registry and `blue-bex/gas/2.0` manifest. It covers 57 normative vectors with 102 behavior fixtures, including direct executable coverage of every normative operator, and 30 gas-counter microfixtures. Its identity is: +The implementation-baseline fixture package is bound to the exact BEX runtime +registry and `blue-bex/gas/2.0` manifest. Its authoritative inventory and +identities are: ```text -sha256:f5f64a38152ef0e50ebb1b03caaa1b07fd556552eb071b940937079fc0234dfe +normative vectors: 60 +behavior fixtures: 105 +gas microfixtures: 30 +normative operators: 86 +runtime registry: sha256:23d282ec1c0bb016263922b1b49c369fdd537efdcf23e005eceeb888d7763fe1 +gas manifest: sha256:41247c820d91a12fdfc17fd9e787a5d8d668d8acc5954fdcb131715bf9e6147d +fixture package: sha256:a1b7bb2b3687389409bc9d0aa450c734f7856d2bcb818c95f4d7ecb19095d20e ``` ## 17. Conformance Vectors diff --git a/src/test/java/blue/bex/BexBlueTypeMatchingGasTest.java b/src/test/java/blue/bex/BexBlueTypeMatchingGasTest.java index 724fce9..49c1c3f 100644 --- a/src/test/java/blue/bex/BexBlueTypeMatchingGasTest.java +++ b/src/test/java/blue/bex/BexBlueTypeMatchingGasTest.java @@ -12,8 +12,8 @@ import blue.bex.type.BexBlueTypeMatcher; import blue.bex.value.BexValue; import blue.bex.value.BexValues; -import blue.language.Blue; -import blue.language.NodeProvider; +import blue.bex.test.TestBlue; +import blue.language.provider.NodeProvider; import blue.language.model.Node; import blue.language.processor.ExecutionEvidenceUnavailableException; import blue.language.processor.InvalidExecutionEvidenceException; @@ -21,7 +21,7 @@ import blue.language.processor.ProcessorFailureException; import blue.language.provider.NodeProviderResult; import blue.language.snapshot.FrozenNode; -import blue.language.utils.BlueIdCalculator; +import blue.language.identity.DirectBlueIdCalculator; import org.junit.jupiter.api.Test; import java.util.Collections; @@ -38,9 +38,9 @@ import static org.junit.jupiter.api.Assertions.assertTrue; class BexBlueTypeMatchingGasTest { - private final Blue blue = new Blue(); + private final TestBlue blue = new TestBlue(); private final BexEngine engine = - BexEngine.builder().blue(blue).build(); + BexEngine.builder().language(blue.runtime()).build(); @Test void structuralIsChargesEveryComparedSemanticOccurrence() { @@ -180,7 +180,7 @@ void recursiveMatchIsAdmittedBeforeTheChildComparison() { BexGasSchedule.defaults(), 1L); assertThrows(BexGasLimitExceededException.class, - () -> new BexBlueTypeMatcher(blue).matches( + () -> new BexBlueTypeMatcher(blue.runtime()).matches( BexValues.fromSimple(candidate), pattern, meter, @@ -199,12 +199,12 @@ void unavailableNestedExactEvidenceIsNotConvertedToATypeMismatch() { "value", new Node().value("known"))); String blueId = - BlueIdCalculator.calculateBlueId(content); + DirectBlueIdCalculator.calculateBlueId(content); NodeProvider provider = providerReturning( NodeProviderResult.unavailable( "type evidence is offline")); - try (Blue unavailableBlue = new Blue(provider)) { + try (TestBlue unavailableBlue = new TestBlue(provider)) { BexGasMeter meter = new BexGasMeter( BexGasSchedule.defaults(), 1_000_000L); @@ -212,7 +212,7 @@ void unavailableNestedExactEvidenceIsNotConvertedToATypeMismatch() { assertThrows( ExecutionEvidenceUnavailableException.class, () -> new BexBlueTypeMatcher( - unavailableBlue) + unavailableBlue.runtime()) .matches( transientWithReference( unavailableBlue, @@ -243,12 +243,12 @@ void invalidNestedExactEvidenceIsNotConvertedToATypeMismatch() { "value", new Node().value("known"))); String blueId = - BlueIdCalculator.calculateBlueId(content); + DirectBlueIdCalculator.calculateBlueId(content); NodeProvider provider = providerReturning( NodeProviderResult.invalidEvidence( "type evidence is invalid")); - try (Blue invalidBlue = new Blue(provider)) { + try (TestBlue invalidBlue = new TestBlue(provider)) { BexGasMeter meter = new BexGasMeter( BexGasSchedule.defaults(), 1_000_000L); @@ -256,7 +256,7 @@ void invalidNestedExactEvidenceIsNotConvertedToATypeMismatch() { assertThrows( InvalidExecutionEvidenceException.class, () -> new BexBlueTypeMatcher( - invalidBlue) + invalidBlue.runtime()) .matches( transientWithReference( invalidBlue, @@ -280,7 +280,7 @@ void invalidNestedExactEvidenceIsNotConvertedToATypeMismatch() { @Test void malformedLocalBlueShapeRemainsATypeMismatch() { String blueId = - BlueIdCalculator.calculateBlueId( + DirectBlueIdCalculator.calculateBlueId( new Node().value("referenced")); Map malformed = new LinkedHashMap<>(); @@ -290,7 +290,7 @@ void malformedLocalBlueShapeRemainsATypeMismatch() { BexGasSchedule.defaults(), 1_000_000L); - assertFalse(new BexBlueTypeMatcher(blue) + assertFalse(new BexBlueTypeMatcher(blue.runtime()) .matches( BexValues.fromSimple(malformed), nonEmptyPattern(), @@ -311,7 +311,7 @@ void isPropagatesAnArbitraryReferenceProviderFailure() { "value", new Node().value("known"))); String blueId = - BlueIdCalculator.calculateBlueId(content); + DirectBlueIdCalculator.calculateBlueId(content); IllegalStateException expected = new IllegalStateException( "reference provider implementation defect"); @@ -319,7 +319,7 @@ void isPropagatesAnArbitraryReferenceProviderFailure() { throw expected; }; - try (Blue providerBlue = new Blue(provider)) { + try (TestBlue providerBlue = new TestBlue(provider)) { IllegalStateException observed = assertThrows( IllegalStateException.class, () -> executeProviderBackedIs( @@ -336,7 +336,7 @@ void deterministicProcessorFailureWinsOverNestedUnavailabilityInIs() { "value", new Node().value("known"))); String blueId = - BlueIdCalculator.calculateBlueId(content); + DirectBlueIdCalculator.calculateBlueId(content); ExecutionEvidenceUnavailableException nested = new ExecutionEvidenceUnavailableException( "nested evidence detail"); @@ -350,7 +350,7 @@ void deterministicProcessorFailureWinsOverNestedUnavailabilityInIs() { throw expected; }; - try (Blue providerBlue = new Blue(provider)) { + try (TestBlue providerBlue = new TestBlue(provider)) { ProcessorFailureException observed = assertThrows( ProcessorFailureException.class, () -> executeProviderBackedIs( @@ -366,13 +366,13 @@ void wideTransientCandidateDoesNotDemandARejectedLaterChild() { new AtomicInteger(); Node content = new Node().value("known"); String blueId = - BlueIdCalculator.calculateBlueId(content); + DirectBlueIdCalculator.calculateBlueId(content); NodeProvider provider = countingProvider( providerDemands, NodeProviderResult.unavailable( "later child is offline")); - try (Blue unavailableBlue = new Blue(provider)) { + try (TestBlue unavailableBlue = new TestBlue(provider)) { Map wide = new LinkedHashMap<>(); for (int index = 0; index < 256; index++) { @@ -397,7 +397,7 @@ void wideTransientCandidateDoesNotDemandARejectedLaterChild() { assertThrows( BexGasLimitExceededException.class, () -> new BexBlueTypeMatcher( - unavailableBlue).matches( + unavailableBlue.runtime()).matches( BexValues.map(wide), pattern, meter, @@ -415,13 +415,13 @@ void deepTransientCandidateStopsBeforeRejectedExactLeaf() { new AtomicInteger(); Node content = new Node().value("known"); String blueId = - BlueIdCalculator.calculateBlueId(content); + DirectBlueIdCalculator.calculateBlueId(content); NodeProvider provider = countingProvider( providerDemands, NodeProviderResult.unavailable( "deep leaf is offline")); - try (Blue unavailableBlue = new Blue(provider)) { + try (TestBlue unavailableBlue = new TestBlue(provider)) { int depth = 32; BexValue candidate = exactReference( unavailableBlue, blueId); @@ -450,7 +450,7 @@ void deepTransientCandidateStopsBeforeRejectedExactLeaf() { assertThrows( BexGasLimitExceededException.class, () -> new BexBlueTypeMatcher( - unavailableBlue).matches( + unavailableBlue.runtime()).matches( deepCandidate, pattern, meter, @@ -470,13 +470,13 @@ void exactStructuralCursorDoesNotDemandRejectedNestedReference() { new AtomicInteger(); Node content = new Node().value("known"); String blueId = - BlueIdCalculator.calculateBlueId(content); + DirectBlueIdCalculator.calculateBlueId(content); NodeProvider provider = countingProvider( providerDemands, NodeProviderResult.unavailable( "nested exact child is offline")); - try (Blue unavailableBlue = new Blue(provider)) { + try (TestBlue unavailableBlue = new TestBlue(provider)) { FrozenNode exactRoot = FrozenNode.fromResolvedNode( new Node().properties( @@ -487,7 +487,7 @@ void exactStructuralCursorDoesNotDemandRejectedNestedReference() { BexValue candidate = BexValues.referenceBacked( BexValues.frozen(exactRoot), - unavailableBlue); + unavailableBlue.runtime()); FrozenNode pattern = FrozenNode.fromResolvedNode( new Node().properties( @@ -501,7 +501,7 @@ void exactStructuralCursorDoesNotDemandRejectedNestedReference() { assertThrows( BexGasLimitExceededException.class, () -> new BexBlueTypeMatcher( - unavailableBlue).matches( + unavailableBlue.runtime()).matches( candidate, pattern, meter, @@ -522,7 +522,7 @@ private BexExecutionResult run(String... lines) { } private static void executeProviderBackedIs( - Blue blue, + TestBlue blue, String blueId) { FrozenNode document = FrozenNode.fromResolvedNode( new Node().properties( @@ -544,7 +544,7 @@ private static void executeProviderBackedIs( " value: expected")); BexEngine.builder() - .blue(blue) + .language(blue.runtime()) .build() .compileAndExecute( BexProgramSource.inline( @@ -573,7 +573,7 @@ private static String repeat(char value, int count) { } private static BexValue transientWithReference( - Blue blue, + TestBlue blue, String blueId) { Map candidate = new LinkedHashMap<>(); @@ -584,19 +584,19 @@ private static BexValue transientWithReference( FrozenNode.fromNode( new Node().blueId( blueId))), - blue)); + blue.runtime())); return BexValues.map(candidate); } private static BexValue exactReference( - Blue blue, + TestBlue blue, String blueId) { return BexValues.referenceBacked( BexValues.frozen( FrozenNode.fromNode( new Node().blueId( blueId))), - blue); + blue.runtime()); } private static FrozenNode nonEmptyPattern() { diff --git a/src/test/java/blue/bex/BexBlueTypeSupportTest.java b/src/test/java/blue/bex/BexBlueTypeSupportTest.java index 7f520dc..314ce0f 100644 --- a/src/test/java/blue/bex/BexBlueTypeSupportTest.java +++ b/src/test/java/blue/bex/BexBlueTypeSupportTest.java @@ -8,8 +8,9 @@ import blue.bex.value.BexNodeWriter; import blue.bex.value.BexValue; import blue.bex.value.BexValues; -import blue.language.Blue; +import blue.bex.test.TestBlue; import blue.language.model.Node; +import blue.language.registry.BlueCoreTypeRegistry; import blue.language.snapshot.FrozenNode; import org.junit.jupiter.api.Test; @@ -24,7 +25,6 @@ import static blue.bex.test.BexTestFixtures.op; import static blue.bex.test.BexTestFixtures.simple; import static blue.bex.test.BexTestFixtures.stepExpr; -import static blue.language.utils.Properties.INTEGER_TYPE_BLUE_ID; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; @@ -32,7 +32,9 @@ import static org.junit.jupiter.api.Assertions.assertTrue; class BexBlueTypeSupportTest { - private static final Blue YAML_BLUE = new Blue(); + private static final String INTEGER_TYPE_BLUE_ID = + BlueCoreTypeRegistry.INSTANCE.blueId("Integer"); + private static final TestBlue YAML_BLUE = new TestBlue(); private static final Node HOTEL_ORDER_TYPE = YAML_BLUE.yamlToNode(yaml( "status:", " type: Text")); @@ -43,7 +45,7 @@ class BexBlueTypeSupportTest { FrozenNode.fromNode(HOTEL_ORDER_TYPE).blueId(); private static final String RESTAURANT_ORDER_TYPE_ID = FrozenNode.fromNode(RESTAURANT_ORDER_TYPE).blueId(); - private final Blue blue = new Blue(blueId -> { + private final TestBlue blue = new TestBlue(blueId -> { if (HOTEL_ORDER_TYPE_ID.equals(blueId)) { return Collections.singletonList(HOTEL_ORDER_TYPE.clone()); } @@ -53,7 +55,7 @@ class BexBlueTypeSupportTest { } return Collections.emptyList(); }); - private final BexEngine engine = BexEngine.builder().blue(blue).build(); + private final BexEngine engine = BexEngine.builder().language(blue.runtime()).build(); @Test void functionArgAcceptsMatchingPrimitiveType() { diff --git a/src/test/java/blue/bex/BexCompiledProgramCacheTest.java b/src/test/java/blue/bex/BexCompiledProgramCacheTest.java index f07ee00..7c7c1e9 100644 --- a/src/test/java/blue/bex/BexCompiledProgramCacheTest.java +++ b/src/test/java/blue/bex/BexCompiledProgramCacheTest.java @@ -6,7 +6,7 @@ import blue.bex.compile.BexCompiledProgramKey; import blue.bex.compile.BexNodeIdentity; import blue.bex.compile.LruBexCompiledProgramCache; -import blue.language.Blue; +import blue.bex.test.TestBlue; import blue.language.snapshot.FrozenNode; import org.junit.jupiter.api.Test; @@ -14,7 +14,7 @@ import static org.junit.jupiter.api.Assertions.*; class BexCompiledProgramCacheTest { - private final Blue blue = new Blue(); + private final TestBlue blue = new TestBlue(); @Test void differentNodesWithoutBlueIdDoNotCollide() { @@ -99,7 +99,7 @@ void schemaDifferencesParticipateInCacheKeyAndCompiledBehavior() { assertNotEquals(BexCompiledProgramKey.from(minLengthOne), BexCompiledProgramKey.from(minLengthFive)); BexEngine engine = BexEngine.builder() - .blue(blue) + .language(blue.runtime()) .cache(new LruBexCompiledProgramCache()) .build(); diff --git a/src/test/java/blue/bex/BexCompilerScalarNormalizationTest.java b/src/test/java/blue/bex/BexCompilerScalarNormalizationTest.java index 7e3c925..0a8b6b0 100644 --- a/src/test/java/blue/bex/BexCompilerScalarNormalizationTest.java +++ b/src/test/java/blue/bex/BexCompilerScalarNormalizationTest.java @@ -6,8 +6,9 @@ import blue.bex.api.BexStepResults; import blue.bex.result.BexExecutionResult; import blue.bex.value.BexValues; -import blue.language.Blue; +import blue.bex.test.TestBlue; import blue.language.model.Node; +import blue.language.registry.BlueCoreTypeRegistry; import blue.language.snapshot.FrozenNode; import org.junit.jupiter.api.Test; @@ -20,12 +21,13 @@ import static blue.bex.test.BexTestFixtures.obj; import static blue.bex.test.BexTestFixtures.op; import static blue.bex.test.BexTestFixtures.simple; -import static blue.language.utils.Properties.TEXT_TYPE_BLUE_ID; import static org.junit.jupiter.api.Assertions.assertEquals; class BexCompilerScalarNormalizationTest { - private final Blue blue = new Blue(); - private final BexEngine engine = BexEngine.builder().blue(blue).build(); + private static final String TEXT_TYPE_BLUE_ID = + BlueCoreTypeRegistry.INSTANCE.blueId("Text"); + private final TestBlue blue = new TestBlue(); + private final BexEngine engine = BexEngine.builder().language(blue.runtime()).build(); @Test void blueAuthoredCoreTypedScalarsRemainBexScalars() { diff --git a/src/test/java/blue/bex/BexCompositeExhaustionEvidenceTest.java b/src/test/java/blue/bex/BexCompositeExhaustionEvidenceTest.java index e2bd7a6..cf201e8 100644 --- a/src/test/java/blue/bex/BexCompositeExhaustionEvidenceTest.java +++ b/src/test/java/blue/bex/BexCompositeExhaustionEvidenceTest.java @@ -19,15 +19,15 @@ import blue.bex.runtime.BexRuntime; import blue.bex.value.BexValue; import blue.bex.value.BexValues; -import blue.language.Blue; -import blue.language.NodeProvider; +import blue.bex.test.TestBlue; +import blue.language.provider.NodeProvider; import blue.language.model.Node; import blue.language.processor.GasLimitExceededException; import blue.language.processor.GasMeter; import blue.language.processor.GasSchedule; import blue.language.processor.GasTraceEntry; import blue.language.snapshot.FrozenNode; -import blue.language.utils.BlueIdCalculator; +import blue.language.identity.DirectBlueIdCalculator; import org.junit.jupiter.api.Test; import java.math.BigInteger; @@ -121,7 +121,7 @@ void largeFiniteForEachStopsBeforeRejectedIterationAndIsColdWarmStable() { void exhaustionIsStableAcrossInlineColdReferenceAndWarmReferenceDocuments() { Node document = obj("items", integerList(48)); String documentBlueId = - BlueIdCalculator.calculateBlueId(document); + DirectBlueIdCalculator.calculateBlueId(document); FrozenNode inlineDocument = FrozenNode.fromResolvedNode(document); FrozenNode referenceDocument = FrozenNode.fromNode( @@ -148,11 +148,11 @@ void exhaustionIsStableAcrossInlineColdReferenceAndWarmReferenceDocuments() { ExactDocumentProvider warmProvider = new ExactDocumentProvider( documentBlueId, document); - try (Blue inlineBlue = new Blue(); - Blue coldBlue = new Blue(coldProvider); - Blue warmBlue = new Blue(warmProvider)) { + try (TestBlue inlineBlue = new TestBlue(); + TestBlue coldBlue = new TestBlue(coldProvider); + TestBlue warmBlue = new TestBlue(warmProvider)) { BexEngine inlineEngine = BexEngine.builder() - .blue(inlineBlue) + .language(inlineBlue.runtime()) .build(); BexCompiledProgram inlineProgram = inlineEngine.compile(source); @@ -186,7 +186,7 @@ void exhaustionIsStableAcrossInlineColdReferenceAndWarmReferenceDocuments() { inlineView); BexEngine coldEngine = BexEngine.builder() - .blue(coldBlue) + .language(coldBlue.runtime()) .build(); LimitedEvidence coldReference = assertRejectedAtPrefix( @@ -201,7 +201,7 @@ void exhaustionIsStableAcrossInlineColdReferenceAndWarmReferenceDocuments() { new Node().blueId(documentBlueId)); int warmupDemands = warmProvider.demands; BexEngine warmEngine = BexEngine.builder() - .blue(warmBlue) + .language(warmBlue.runtime()) .build(); LimitedEvidence warmReference = assertRejectedAtPrefix( @@ -461,9 +461,9 @@ void rejectedPatchAppendDoesNotMutateChangesetOrOverlay() { sentinelEvent(), op("$return", true))); - try (Blue blue = new Blue()) { + try (TestBlue blue = new TestBlue()) { BexEngine engine = BexEngine.builder() - .blue(blue) + .language(blue.runtime()) .build(); BexCompiledProgram program = engine.compile(source(programNode)); @@ -484,7 +484,7 @@ void rejectedPatchAppendDoesNotMutateChangesetOrOverlay() { host, boundary, target.prefixGas), - blue, + blue.runtime(), BexGasSchedule.defaults(), new BexMetrics(), new BexPointerCache(), diff --git a/src/test/java/blue/bex/BexDiagnosticAdmissionOrderingTest.java b/src/test/java/blue/bex/BexDiagnosticAdmissionOrderingTest.java index 64e7671..123648e 100644 --- a/src/test/java/blue/bex/BexDiagnosticAdmissionOrderingTest.java +++ b/src/test/java/blue/bex/BexDiagnosticAdmissionOrderingTest.java @@ -13,7 +13,7 @@ import blue.bex.result.BexExecutionResult; import blue.bex.result.BexMetrics; import blue.bex.runtime.BexRuntime; -import blue.language.Blue; +import blue.bex.test.TestBlue; import blue.language.model.Node; import org.junit.jupiter.api.Test; @@ -118,9 +118,9 @@ void rejectedIterationReadDoesNotAddAnUnfinishedLoopMetric() { "do", list())), op("$return", true))); - try (Blue blue = new Blue()) { + try (TestBlue blue = new TestBlue()) { BexEngine engine = BexEngine.builder() - .blue(blue) + .language(blue.runtime()) .build(); BexCompiledProgram program = engine.compile( BexProgramSource.inline(frozen(programNode))); @@ -142,7 +142,7 @@ void rejectedIterationReadDoesNotAddAnUnfinishedLoopMetric() { BexRuntime runtime = new BexRuntime( program, context(prefixGas), - blue, + blue.runtime(), BexGasSchedule.defaults(), metrics, new BexPointerCache(), @@ -177,9 +177,9 @@ private static void assertRejectedReadMetric( private static RejectedExecution reject( Node programNode, long gasLimit) { - try (Blue blue = new Blue()) { + try (TestBlue blue = new TestBlue()) { BexEngine engine = BexEngine.builder() - .blue(blue) + .language(blue.runtime()) .build(); BexCompiledProgram program = engine.compile( BexProgramSource.inline(frozen(programNode))); @@ -187,7 +187,7 @@ private static RejectedExecution reject( BexRuntime runtime = new BexRuntime( program, context(gasLimit), - blue, + blue.runtime(), BexGasSchedule.defaults(), metrics, new BexPointerCache(), diff --git a/src/test/java/blue/bex/BexExactReferenceDocumentTest.java b/src/test/java/blue/bex/BexExactReferenceDocumentTest.java index f70f050..035ff87 100644 --- a/src/test/java/blue/bex/BexExactReferenceDocumentTest.java +++ b/src/test/java/blue/bex/BexExactReferenceDocumentTest.java @@ -6,19 +6,19 @@ import blue.bex.api.FrozenBexDocumentView; import blue.bex.value.BexValue; import blue.bex.value.BexValues; -import blue.language.Blue; -import blue.language.NodeProvider; +import blue.bex.test.TestBlue; +import blue.language.provider.NodeProvider; import blue.language.model.Node; import blue.language.processor.ExecutionEvidenceUnavailableException; import blue.language.processor.InvalidExecutionEvidenceException; import blue.language.provider.CyclicAwareNodeProvider; import blue.language.provider.CyclicSetProof; import blue.language.provider.CyclicSetProofResult; -import blue.language.provider.NodeProviderOutcome; +import blue.language.api.NodeProviderOutcome; import blue.language.provider.NodeProviderResult; import blue.language.snapshot.FrozenNode; -import blue.language.snapshot.ResolvedSnapshot; -import blue.language.utils.CircularBlueIdCalculator; +import blue.language.merge.ResolvedSnapshot; +import blue.language.identity.CircularSetIdentityCalculator; import org.junit.jupiter.api.Test; import java.math.BigInteger; @@ -46,7 +46,7 @@ void semanticAccessMaterializesVerifiedReferenceButExactIdentityDoesNot() { : Collections.emptyList(); }; - try (Blue blue = new Blue(provider)) { + try (TestBlue blue = new TestBlue(provider)) { ResolvedSnapshot snapshot = blue.resolveToSnapshot( obj("x", new Node().blueId(blueId))); assertTrue(snapshot.frozenResolvedRoot() @@ -57,7 +57,7 @@ void semanticAccessMaterializesVerifiedReferenceButExactIdentityDoesNot() { BexValues.exact( snapshot.frozenCanonicalRoot(), snapshot.frozenResolvedRoot()), - blue); + blue.runtime()); BexValue referenced = root.get("x"); assertEquals(blueId, referenced.exactBlueId()); @@ -84,14 +84,14 @@ void unavailableReferenceEvidencePropagatesInsteadOfBecomingUndefined() { return Collections.emptyList(); }; - try (Blue blue = new Blue(provider)) { + try (TestBlue blue = new TestBlue(provider)) { ResolvedSnapshot snapshot = blue.resolveToSnapshot( obj("x", new Node().blueId(unavailableBlueId))); BexValue root = BexValues.referenceBacked( BexValues.exact( snapshot.frozenCanonicalRoot(), snapshot.frozenResolvedRoot()), - blue); + blue.runtime()); RuntimeException failure = assertThrows( RuntimeException.class, @@ -113,7 +113,7 @@ void resultOverlayCanPatchThroughAProviderBackedExactReference() { : Collections.emptyList(); }; - try (Blue blue = new Blue(provider)) { + try (TestBlue blue = new TestBlue(provider)) { ResolvedSnapshot snapshot = blue.resolveToSnapshot( obj("x", new Node().blueId(blueId))); BexExecutionContext context = BexExecutionContext.builder() @@ -131,7 +131,7 @@ void resultOverlayCanPatchThroughAProviderBackedExactReference() { op("$return", op("$resultValue", "/x")))); BexValue result = BexEngine.builder() - .blue(blue) + .language(blue.runtime()) .build() .compileAndExecute( BexProgramSource.inline( @@ -157,7 +157,7 @@ void collapsedReferenceWithoutAResolverIsIncompleteNotAbsent() { Node content = obj("a", 1); String blueId = calculateBlueId(content); ResolvedSnapshot snapshot; - try (Blue blue = new Blue()) { + try (TestBlue blue = new TestBlue()) { snapshot = blue.resolveToSnapshot( obj("x", new Node().blueId(blueId))); } @@ -184,7 +184,7 @@ void cyclicMemberStructuralReadRequiresAndAcceptsCompleteSetProof() { java.util.List placeholders = Collections.singletonList(member); String memberBlueId = - CircularBlueIdCalculator + CircularSetIdentityCalculator .calculateCircularSetBlueIds(placeholders) .get(0); Node resolvedMember = member.clone(); @@ -197,14 +197,14 @@ void cyclicMemberStructuralReadRequiresAndAcceptsCompleteSetProof() { placeholders); assertTrue(memberBlueId.contains("#")); - try (Blue blue = new Blue(provider)) { + try (TestBlue blue = new TestBlue(provider)) { ResolvedSnapshot snapshot = blue.resolveToSnapshot( obj("x", new Node().blueId(memberBlueId))); BexValue root = BexValues.referenceBacked( BexValues.exact( snapshot.frozenCanonicalRoot(), snapshot.frozenResolvedRoot()), - blue); + blue.runtime()); assertEquals(memberBlueId, root.get("x").exactBlueId()); @@ -217,11 +217,11 @@ void cyclicMemberStructuralReadRequiresAndAcceptsCompleteSetProof() { NodeProvider proofless = requested -> memberBlueId.equals(requested) ? Collections.singletonList(resolvedMember.clone()) : Collections.emptyList(); - try (Blue blue = new Blue(proofless)) { + try (TestBlue blue = new TestBlue(proofless)) { BexValue prooflessMember = BexValues.referenceBacked( BexValues.frozen(FrozenNode.fromNode( new Node().blueId(memberBlueId))), - blue); + blue.runtime()); assertEquals(memberBlueId, prooflessMember.exactBlueId()); assertThrows( @@ -295,13 +295,13 @@ void nestedCyclicResolvedBodyRemainsOpaqueUntilCompleteProof() { fixture.resolvedMember, Collections.singletonList( fixture.placeholder)); - try (Blue blue = new Blue(provider)) { + try (TestBlue blue = new TestBlue(provider)) { BexValue verifiedParent = BexValues.referenceBacked( BexValues.exact( canonicalParent, resolvedParent), - blue); + blue.runtime()); BexValue verifiedCyclic = verifiedParent.get("cyclic"); @@ -325,7 +325,7 @@ void cyclicMemberContentFetchUnavailabilityStopsBeforeProofQuery() { "cyclic member content temporarily unavailable"), null); - try (Blue blue = new Blue(provider)) { + try (TestBlue blue = new TestBlue(provider)) { BexValue member = exactReference( blue, fixture.memberBlueId); @@ -357,7 +357,7 @@ void nullCyclicProofAfterFoundContentIsInvalidNotUnavailable() { fixture.resolvedMember)), null); - try (Blue blue = new Blue(provider)) { + try (TestBlue blue = new TestBlue(provider)) { BexValue member = exactReference( blue, fixture.memberBlueId); @@ -385,7 +385,7 @@ void cyclicProofUnavailabilityAfterFoundContentRemainsTransient() { CyclicSetProofResult.unavailable( "cyclic proof store temporarily unavailable")); - try (Blue blue = new Blue(provider)) { + try (TestBlue blue = new TestBlue(provider)) { BexValue member = exactReference( blue, fixture.memberBlueId); @@ -425,7 +425,7 @@ void malformedCyclicProofIsDeterministicInvalidEvidence() { CyclicSetProofResult.found( wrongProof)); - try (Blue blue = new Blue(provider)) { + try (TestBlue blue = new TestBlue(provider)) { BexValue member = exactReference( blue, fixture.memberBlueId); @@ -490,7 +490,7 @@ private CyclicFixture() { .name("cyclic-member"); java.util.List placeholders = Collections.singletonList(placeholder); - memberBlueId = CircularBlueIdCalculator + memberBlueId = CircularSetIdentityCalculator .calculateCircularSetBlueIds(placeholders) .get(0); resolvedMember = placeholder.clone(); @@ -537,17 +537,17 @@ public CyclicSetProofResult cyclicSetProofFor( } private static String calculateBlueId(Node node) { - try (Blue blue = new Blue()) { + try (TestBlue blue = new TestBlue()) { return blue.calculateBlueId(node); } } private static BexValue exactReference( - Blue blue, String blueId) { + TestBlue blue, String blueId) { return BexValues.referenceBacked( BexValues.frozen(FrozenNode.fromNode( new Node().blueId(blueId))), - blue); + blue.runtime()); } private static String messageChain(Throwable failure) { diff --git a/src/test/java/blue/bex/BexExecutionEvidenceLedgerTest.java b/src/test/java/blue/bex/BexExecutionEvidenceLedgerTest.java index 81f21cb..c1961ab 100644 --- a/src/test/java/blue/bex/BexExecutionEvidenceLedgerTest.java +++ b/src/test/java/blue/bex/BexExecutionEvidenceLedgerTest.java @@ -6,8 +6,8 @@ import blue.bex.api.BexProgramSource; import blue.bex.api.FrozenBexDocumentView; import blue.bex.output.BexSemanticIdentityBoundary; -import blue.language.Blue; -import blue.language.NodeProvider; +import blue.bex.test.TestBlue; +import blue.language.provider.NodeProvider; import blue.language.model.Node; import blue.language.processor.ExecutionEvidenceUnavailableException; import blue.language.processor.GasMeter; @@ -16,8 +16,8 @@ import blue.language.provider.CyclicAwareNodeProvider; import blue.language.provider.CyclicSetProofResult; import blue.language.snapshot.FrozenNode; -import blue.language.snapshot.ResolvedSnapshot; -import blue.language.utils.CircularBlueIdCalculator; +import blue.language.merge.ResolvedSnapshot; +import blue.language.identity.CircularSetIdentityCalculator; import org.junit.jupiter.api.Test; import java.util.Collections; @@ -39,7 +39,7 @@ void transientProviderUnavailabilityCommitsNoChildLedger() { NodeProvider unavailable = ignored -> Collections.emptyList(); - try (Blue blue = new Blue(unavailable)) { + try (TestBlue blue = new TestBlue(unavailable)) { RecordingGasHost host = new RecordingGasHost(); assertThrows( ExecutionEvidenceUnavailableException.class, @@ -60,7 +60,7 @@ void deterministicInvalidEvidenceCommitsAdmittedTraceOnce() { "deterministic invalid provider evidence"); }; - try (Blue blue = new Blue(invalid)) { + try (TestBlue blue = new TestBlue(invalid)) { RecordingGasHost host = new RecordingGasHost(); assertThrows( InvalidExecutionEvidenceException.class, @@ -81,7 +81,7 @@ void hostedCyclicStructuralReadWithMissingProofIsDeterministic() { List placeholders = Collections.singletonList(placeholder); String memberBlueId = - CircularBlueIdCalculator + CircularSetIdentityCalculator .calculateCircularSetBlueIds( placeholders) .get(0); @@ -92,7 +92,7 @@ void hostedCyclicStructuralReadWithMissingProofIsDeterministic() { new ProoflessCyclicProvider( memberBlueId, resolvedMember); - try (Blue blue = new Blue(provider)) { + try (TestBlue blue = new TestBlue(provider)) { RecordingGasHost host = new RecordingGasHost(); InvalidExecutionEvidenceException failure = assertThrows( @@ -118,7 +118,7 @@ void cyclicProofUnavailabilityUsesHostedDiscardLifecycle() { List placeholders = Collections.singletonList(placeholder); String memberBlueId = - CircularBlueIdCalculator + CircularSetIdentityCalculator .calculateCircularSetBlueIds( placeholders) .get(0); @@ -130,7 +130,7 @@ void cyclicProofUnavailabilityUsesHostedDiscardLifecycle() { memberBlueId, resolvedMember); - try (Blue blue = new Blue(provider)) { + try (TestBlue blue = new TestBlue(provider)) { RecordingGasHost host = new RecordingGasHost(); ExecutionEvidenceUnavailableException failure = assertThrows( @@ -153,7 +153,7 @@ void cyclicProofUnavailabilityUsesHostedDiscardLifecycle() { } private static void executeKindRead( - Blue blue, + TestBlue blue, String blueId, RecordingGasHost host) { ResolvedSnapshot document = blue.resolveToSnapshot( @@ -168,7 +168,7 @@ private static void executeKindRead( BexSemanticIdentityBoundary.STANDALONE) .build(); BexEngine.builder() - .blue(blue) + .language(blue.runtime()) .build() .compileAndExecute( BexProgramSource.inline(FrozenNode.fromResolvedNode( @@ -179,7 +179,7 @@ private static void executeKindRead( } private static String calculateBlueId(Node node) { - try (Blue blue = new Blue()) { + try (TestBlue blue = new TestBlue()) { return blue.calculateBlueId(node); } } diff --git a/src/test/java/blue/bex/BexIntrinsicTest.java b/src/test/java/blue/bex/BexIntrinsicTest.java index 9fa4ae0..9bf99e1 100644 --- a/src/test/java/blue/bex/BexIntrinsicTest.java +++ b/src/test/java/blue/bex/BexIntrinsicTest.java @@ -16,11 +16,11 @@ import blue.bex.result.BexExecutionResult; import blue.bex.value.BexValue; import blue.bex.value.BexValues; -import blue.language.Blue; +import blue.bex.test.TestBlue; import blue.language.model.Node; import blue.language.model.TypeBlueId; import blue.language.snapshot.FrozenNode; -import blue.language.utils.BlueIdCalculator; +import blue.language.identity.DirectBlueIdCalculator; import org.junit.jupiter.api.Test; import java.util.LinkedHashMap; @@ -45,7 +45,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; class BexIntrinsicTest { - private static final Blue BLUE = new Blue(); + private static final TestBlue BLUE = new TestBlue(); private static final String ECHO_BLUE_ID = "TestIntrinsicEcho"; private static final String GAS_BLUE_ID = "TestIntrinsicGas"; private static final String TEST_REGISTRY_IDENTITY = "test-intrinsics/1"; @@ -317,7 +317,7 @@ void intrinsicExactFieldUsesSharedSemanticAdmissionAndMemoization() { boundaryCalls.incrementAndGet(); Node exact = node.clone(); return new BexEstablishedIdentity( - BlueIdCalculator.calculateBlueId(exact), + DirectBlueIdCalculator.calculateBlueId(exact), FrozenNode.fromResolvedNode(exact)); }; BexValue[] exactFromIntrinsic = new BexValue[1]; diff --git a/src/test/java/blue/bex/BexLazyBindingTest.java b/src/test/java/blue/bex/BexLazyBindingTest.java index 8a5f008..e9f385b 100644 --- a/src/test/java/blue/bex/BexLazyBindingTest.java +++ b/src/test/java/blue/bex/BexLazyBindingTest.java @@ -13,7 +13,7 @@ import blue.bex.value.BexNodeWriter; import blue.bex.value.BexValue; import blue.bex.value.BexValues; -import blue.language.Blue; +import blue.bex.test.TestBlue; import blue.language.model.Node; import blue.language.snapshot.FrozenNode; import org.junit.jupiter.api.Test; @@ -765,7 +765,7 @@ void supplierFailureAddsNoGasBeyondTheExistingBindingRead() { .build(); BexCompiledProgram program = BexEngine.builder().build() .compile(BexProgramSource.inline(frozen(stepExpr(op("$binding", "broken"))))); - BexRuntime runtime = new BexRuntime(program, context, new Blue(), schedule, + BexRuntime runtime = new BexRuntime(program, context, new TestBlue().runtime(), schedule, new BexMetrics(), new BexPointerCache()); assertThrows(IllegalStateException.class, () -> runtime.readBinding("broken", Collections.emptyList())); diff --git a/src/test/java/blue/bex/BexProgramParsingBoundaryTest.java b/src/test/java/blue/bex/BexProgramParsingBoundaryTest.java new file mode 100644 index 0000000..16094dc --- /dev/null +++ b/src/test/java/blue/bex/BexProgramParsingBoundaryTest.java @@ -0,0 +1,46 @@ +package blue.bex; + +import blue.bex.test.TestBlue; +import blue.language.model.Node; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Arrays; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class BexProgramParsingBoundaryTest { + @Test + void bexEmptyOperatorSurvivesPreprocessingInExactInsertionOrder() { + try (TestBlue blue = new TestBlue()) { + Node program = blue.yamlToBexSource(String.join("\n", + "type: Blue/BEX Program", + "expr:", + " before: first", + " $empty: ''", + " after: last")); + Node expression = program.getProperties().get("expr"); + + assertEquals(Arrays.asList("before", "$empty", "after"), + new ArrayList<>(expression.getProperties().keySet())); + assertEquals("", + expression.getProperties().get("$empty").getValue()); + } + } + + @Test + void ordinaryBlueParsingStillRejectsMalformedEmptyPlaceholder() { + try (TestBlue blue = new TestBlue()) { + IllegalArgumentException failure = assertThrows( + IllegalArgumentException.class, + () -> blue.yamlToNode(String.join("\n", + "items:", + " - $empty: false"))); + + assertTrue(failure.getMessage().contains( + "$empty\" list placeholder must have exact shape")); + } + } +} diff --git a/src/test/java/blue/bex/BexRichFixtureTest.java b/src/test/java/blue/bex/BexRichFixtureTest.java index a009596..f6a4240 100644 --- a/src/test/java/blue/bex/BexRichFixtureTest.java +++ b/src/test/java/blue/bex/BexRichFixtureTest.java @@ -13,10 +13,10 @@ import blue.bex.value.BexNodeWriter; import blue.bex.value.BexValue; import blue.bex.value.BexValues; -import blue.language.Blue; +import blue.bex.test.TestBlue; import blue.language.model.Node; import blue.language.snapshot.FrozenNode; -import blue.language.snapshot.ResolvedSnapshot; +import blue.language.merge.ResolvedSnapshot; import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.TestFactory; import org.yaml.snakeyaml.Yaml; @@ -49,7 +49,7 @@ class BexRichFixtureTest { private static final String FIXTURE_ROOT = "rich-fixtures"; - private static final Blue YAML_BLUE = new Blue(); + private static final TestBlue YAML_BLUE = new TestBlue(); private static final String TINY_EVENT_PROGRAM = String.join("\n", "type: Blue/BEX Program", "do:", @@ -72,7 +72,7 @@ Collection richFixtures() throws Exception { private void runFixture(Path path) throws Exception { Map fixture = readFixture(path); validateFixtureShape(fixture, path); - Blue blue = blueForFixture(fixture); + TestBlue blue = blueForFixture(fixture); Map expectation = map(fixture.get("expectation")); String outcome = string(expectation.get("outcome")); assertNotNull(outcome, "Fixture outcome is required: " + path); @@ -122,7 +122,7 @@ private void runFixture(Path path) throws Exception { result, expectation, isLegacyGasFixture(path)); } - private void assertParseOrOutputConversionError(Map fixture, Map expectation, Blue blue) { + private void assertParseOrOutputConversionError(Map fixture, Map expectation, TestBlue blue) { Node program; try { program = parseProgram(fixture, blue); @@ -152,7 +152,7 @@ private void assertOutputConversionError(BexValue value, Map exp assertErrorContains(thrown, expectation); } - private void assertGasProperty(BexEngine engine, BexCompiledProgram compiled, BexExecutionContext context, Map expectation, Blue blue) { + private void assertGasProperty(BexEngine engine, BexCompiledProgram compiled, BexExecutionContext context, Map expectation, TestBlue blue) { String property = string(expectation.get("property")); if (!"gasUsedGreaterThanEquivalentTinyEvent".equals(property)) { fail("Unsupported gas property: " + property); @@ -209,7 +209,7 @@ private boolean isLegacyGasFixture(Path path) { && "gas".equals(parent.getFileName().toString()); } - private BexExecutionContext context(Map fixture, Blue blue) { + private BexExecutionContext context(Map fixture, TestBlue blue) { Map context = map(fixture.get("context")); String scope = string(context.get("documentScope")); if (scope == null) { @@ -243,9 +243,9 @@ private BexExecutionContext context(Map fixture, Blue blue) { return builder.build(); } - private BexEngine engineForFixture(Map fixture, Blue blue) { + private BexEngine engineForFixture(Map fixture, TestBlue blue) { return BexEngine.builder() - .blue(blue) + .language(blue.runtime()) .gasSchedule(gasSchedule(fixture)) .build(); } @@ -320,35 +320,37 @@ private BexStepResults steps(Object stepsObject) { return builder.build(); } - private Blue blueForFixture(Map fixture) { + private TestBlue blueForFixture(Map fixture) { Map definitions = map(fixture.get("blueDefinitions")); if (definitions.isEmpty()) { - return new Blue(); + return new TestBlue(); } Map> parsed = new LinkedHashMap<>(); for (Map.Entry entry : definitions.entrySet()) { parsed.put(entry.getKey(), Collections.singletonList(YAML_BLUE.yamlToNode(requiredString(entry.getValue(), "blueDefinitions." + entry.getKey())))); } - return new Blue(blueId -> { + return new TestBlue(blueId -> { List nodes = parsed.get(blueId); return nodes != null ? nodes : Collections.emptyList(); }); } - private Node parseProgram(Map fixture, Blue blue) { - return blue.yamlToNode(requiredString(fixture.get("programSource"), "programSource")); + private Node parseProgram(Map fixture, TestBlue blue) { + return blue.yamlToBexSource(requiredString( + fixture.get("programSource"), "programSource")); } - private Node parseNodeSource(String source, Blue blue) { + private Node parseNodeSource(String source, TestBlue blue) { if (source == null || source.trim().isEmpty()) { return blue.yamlToNode("{}"); } return blue.yamlToNode(source); } - private BexProgramSource source(String source, Blue blue) { - return BexProgramSource.inline(FrozenNode.fromResolvedNode(blue.yamlToNode(source))); + private BexProgramSource source(String source, TestBlue blue) { + return BexProgramSource.inline(FrozenNode.fromResolvedNode( + blue.yamlToBexSource(source))); } @SuppressWarnings("unchecked") diff --git a/src/test/java/blue/bex/BexSchemaValueTest.java b/src/test/java/blue/bex/BexSchemaValueTest.java index f828d1e..53644c5 100644 --- a/src/test/java/blue/bex/BexSchemaValueTest.java +++ b/src/test/java/blue/bex/BexSchemaValueTest.java @@ -51,7 +51,8 @@ void allSchemaKeywordsRoundTripThroughBexObjectForm() { assertTrue(schema.get("required").asBoolean()); assertEquals(BigInteger.valueOf(2), schema.get("multipleOf").asInteger()); assertEquals("draft", schema.get("enum").get("0").asText()); - assertEquals("Active option", schema.get("enum").get("1").get("name").asText()); + assertEquals("Active option", + schema.get("enum").get("1").get("type").get("name").asText()); assertEquals("active", schema.get("enum").get("1").get("value").asText()); Node roundTripped = BexNodeWriter.toNode(BexValues.fromSimple(sourceValue.toSimple())); @@ -92,7 +93,9 @@ private static Schema allKeywordsSchema() { .maxFields(integer(5)) .enumValues(Arrays.asList( new Node().value("draft"), - new Node().name("Active option").value("active"))); + new Node() + .type(new Node().name("Active option")) + .value("active"))); } private static Node integer(long value) { diff --git a/src/test/java/blue/bex/BexStructuredReferenceEvidenceTest.java b/src/test/java/blue/bex/BexStructuredReferenceEvidenceTest.java index 1d764de..4aa8132 100644 --- a/src/test/java/blue/bex/BexStructuredReferenceEvidenceTest.java +++ b/src/test/java/blue/bex/BexStructuredReferenceEvidenceTest.java @@ -2,12 +2,12 @@ import blue.bex.value.BexValue; import blue.bex.value.BexValues; -import blue.language.Blue; -import blue.language.NodeProvider; +import blue.bex.test.TestBlue; +import blue.language.provider.NodeProvider; import blue.language.model.Node; import blue.language.processor.ExecutionEvidenceUnavailableException; import blue.language.processor.InvalidExecutionEvidenceException; -import blue.language.provider.NodeProviderOutcome; +import blue.language.api.NodeProviderOutcome; import blue.language.provider.NodeProviderResult; import blue.language.snapshot.FrozenNode; import org.junit.jupiter.api.Test; @@ -31,7 +31,7 @@ void providerNotFoundIsIncompleteExecutionEvidenceNotSemanticAbsence() { MutableProvider provider = new MutableProvider( NodeProviderResult.notFound()); - try (Blue blue = new Blue(provider)) { + try (TestBlue blue = new TestBlue(provider)) { ExecutionEvidenceUnavailableException failure = assertThrows( ExecutionEvidenceUnavailableException.class, () -> exactReference(blue, blueId).isObject()); @@ -53,7 +53,7 @@ void providerUnavailableRetainsItsDiagnosticAndRequiredIdentity() { MutableProvider provider = new MutableProvider( NodeProviderResult.unavailable("feeder is offline")); - try (Blue blue = new Blue(provider)) { + try (TestBlue blue = new TestBlue(provider)) { ExecutionEvidenceUnavailableException failure = assertThrows( ExecutionEvidenceUnavailableException.class, () -> exactReference(blue, blueId).keys()); @@ -73,7 +73,7 @@ void invalidProviderEvidenceIsASeparateDeterministicFailure() { NodeProviderResult.invalidEvidence( "signature does not match")); - try (Blue blue = new Blue(provider)) { + try (TestBlue blue = new TestBlue(provider)) { InvalidExecutionEvidenceException failure = assertThrows( InvalidExecutionEvidenceException.class, () -> exactReference(blue, blueId).get("value")); @@ -93,7 +93,7 @@ void foundContentWithMismatchedIdentityIsInvalidEvidence() { Collections.singletonList( obj("value", "different")))); - try (Blue blue = new Blue(provider)) { + try (TestBlue blue = new TestBlue(provider)) { InvalidExecutionEvidenceException failure = assertThrows( InvalidExecutionEvidenceException.class, () -> exactReference( @@ -126,7 +126,7 @@ public NodeProviderResult fetchResultByBlueId( } }; - try (Blue blue = new Blue(provider)) { + try (TestBlue blue = new TestBlue(provider)) { IllegalStateException failure = assertThrows( IllegalStateException.class, () -> exactReference(blue, blueId).isObject()); @@ -143,7 +143,7 @@ void priorValidMaterializationDoesNotHideAChangedProviderOutcome() { NodeProviderResult.found( Collections.singletonList(content))); - try (Blue blue = new Blue(provider)) { + try (TestBlue blue = new TestBlue(provider)) { assertTrue(exactReference(blue, blueId).isObject()); provider.set(NodeProviderResult.unavailable( "second attempt cannot acquire evidence")); @@ -160,15 +160,15 @@ void priorValidMaterializationDoesNotHideAChangedProviderOutcome() { } private static BexValue exactReference( - Blue blue, String blueId) { + TestBlue blue, String blueId) { return BexValues.referenceBacked( BexValues.frozen(FrozenNode.fromNode( new Node().blueId(blueId))), - blue); + blue.runtime()); } private static String calculateBlueId(Node node) { - try (Blue blue = new Blue()) { + try (TestBlue blue = new TestBlue()) { return blue.calculateBlueId(node); } } diff --git a/src/test/java/blue/bex/BexUseCaseConformanceTest.java b/src/test/java/blue/bex/BexUseCaseConformanceTest.java index cf77ed9..63f5c37 100644 --- a/src/test/java/blue/bex/BexUseCaseConformanceTest.java +++ b/src/test/java/blue/bex/BexUseCaseConformanceTest.java @@ -6,7 +6,7 @@ import blue.bex.api.FrozenBexDocumentView; import blue.bex.result.BexExecutionResult; import blue.bex.value.BexValues; -import blue.language.Blue; +import blue.bex.test.TestBlue; import blue.language.model.Node; import blue.language.snapshot.FrozenNode; import org.junit.jupiter.api.Test; @@ -26,8 +26,8 @@ import static org.junit.jupiter.api.Assertions.assertThrows; class BexUseCaseConformanceTest { - private final Blue blue = new Blue(); - private final BexEngine engine = BexEngine.builder().blue(blue).build(); + private final TestBlue blue = new TestBlue(); + private final BexEngine engine = BexEngine.builder().language(blue.runtime()).build(); @Test void existsDistinguishesMissingFromPresentFalsyValues() { diff --git a/src/test/java/blue/bex/ExternalCustomerPayNoteBexFunctionTest.java b/src/test/java/blue/bex/ExternalCustomerPayNoteBexFunctionTest.java index 9fbb6bb..d36b639 100644 --- a/src/test/java/blue/bex/ExternalCustomerPayNoteBexFunctionTest.java +++ b/src/test/java/blue/bex/ExternalCustomerPayNoteBexFunctionTest.java @@ -9,10 +9,10 @@ import blue.bex.result.BexMetrics; import blue.bex.value.BexValue; import blue.bex.value.BexValues; -import blue.language.Blue; +import blue.bex.test.TestBlue; import blue.language.model.Node; import blue.language.snapshot.FrozenNode; -import blue.language.utils.JsonPointer; +import blue.language.model.wire.JsonPointer; import org.junit.jupiter.api.Test; import java.io.ByteArrayOutputStream; @@ -32,9 +32,9 @@ class ExternalCustomerPayNoteBexFunctionTest { @Test void executesSnapshotResolvedFunctionFromFixtureWithAttachedEvent() throws IOException { - Blue blue = new Blue(); - Node programDocument = blue.yamlToNode(readResource(BEX_FIXTURE)); - Node eventEnvelope = blue.yamlToNode(readResource(EVENT_FIXTURE)); + TestBlue blue = new TestBlue(); + Node programDocument = blue.yamlToBexSource(readResource(BEX_FIXTURE)); + Node eventEnvelope = blue.parseSourceYaml(readResource(EVENT_FIXTURE)); Node programNode = requiredNode(programDocument, "/contracts/processPackageCustomerPayNoteSnapshotResolved/steps/0"); Node definitionNode = requiredNode(programDocument, "/contracts/packageFulfillmentBexDefinition"); diff --git a/src/test/java/blue/bex/conformance/BexConformanceDocumentationConsistencyTest.java b/src/test/java/blue/bex/conformance/BexConformanceDocumentationConsistencyTest.java new file mode 100644 index 0000000..ba1913a --- /dev/null +++ b/src/test/java/blue/bex/conformance/BexConformanceDocumentationConsistencyTest.java @@ -0,0 +1,128 @@ +package blue.bex.conformance; + +import org.junit.jupiter.api.Test; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Keeps human-readable conformance claims bound to the machine-readable + * package rather than to copied historical counts or identities. + */ +class BexConformanceDocumentationConsistencyTest { + private static final String SPECIFICATION = + "specifications/blue-bex-specification-2.0.md"; + private static final String FIXTURE_GUIDE = "docs/FIXTURES.md"; + + @Test + void specificationAndFixtureGuideMatchMachineReadablePackage() + throws Exception { + Map fixtureManifest = + ConformancePackage.fixtureManifest(); + Map gasManifest = + ConformancePackage.gasManifest(); + Map registryManifest = + ConformancePackage.registryManifest(); + Map operatorCoverage = ConformancePackage.loadMap( + ConformancePackage.FIXTURE_ROOT + "operator-coverage.yaml"); + + int vectors = integer(fixtureManifest, "vectorCount"); + int behaviorFixtures = + integer(fixtureManifest, "behaviorFixtureCount"); + int gasFixtures = integer(fixtureManifest, "gasFixtureCount"); + int operators = integer(operatorCoverage, "operatorCount"); + String fixtureIdentity = text(fixtureManifest, "packageIdentity"); + String gasIdentity = text(gasManifest, "packageIdentity"); + String registryIdentity = text(registryManifest, "packageIdentity"); + + assertEquals(registryIdentity, + fixtureManifest.get("registryPackageIdentity")); + assertEquals(gasIdentity, + fixtureManifest.get("gasManifestPackageIdentity")); + assertEquals(fixtureIdentity, + registryManifest.get("fixturePackageIdentity")); + + assertDocument( + SPECIFICATION, + vectors, + behaviorFixtures, + gasFixtures, + operators, + registryIdentity, + gasIdentity, + fixtureIdentity); + assertDocument( + FIXTURE_GUIDE, + vectors, + behaviorFixtures, + gasFixtures, + operators, + registryIdentity, + gasIdentity, + fixtureIdentity); + } + + private static void assertDocument( + String relativePath, + int vectors, + int behaviorFixtures, + int gasFixtures, + int operators, + String registryIdentity, + String gasIdentity, + String fixtureIdentity) throws Exception { + Path path = Paths.get("").toAbsolutePath().resolve(relativePath); + assertTrue(Files.isRegularFile(path), + "Missing conformance document: " + relativePath); + String document = new String( + Files.readAllBytes(path), StandardCharsets.UTF_8); + + assertEquals(String.valueOf(vectors), + field(document, "normative vectors", relativePath)); + assertEquals(String.valueOf(behaviorFixtures), + field(document, "behavior fixtures", relativePath)); + assertEquals(String.valueOf(gasFixtures), + field(document, "gas microfixtures", relativePath)); + assertEquals(String.valueOf(operators), + field(document, "normative operators", relativePath)); + assertEquals(registryIdentity, + field(document, "runtime registry", relativePath)); + assertEquals(gasIdentity, + field(document, "gas manifest", relativePath)); + assertEquals(fixtureIdentity, + field(document, "fixture package", relativePath)); + } + + private static String field( + String document, + String name, + String relativePath) { + Pattern pattern = Pattern.compile( + "(?m)^" + Pattern.quote(name) + ":[ \\t]*(\\S+)[ \\t]*$"); + Matcher matcher = pattern.matcher(document); + assertTrue(matcher.find(), + relativePath + " must state " + name); + String value = matcher.group(1); + assertFalse(matcher.find(), + relativePath + " must state " + name + " exactly once"); + return value; + } + + private static int integer(Map source, String key) { + return ConformancePackage.integer( + source.get(key), key).intValueExact(); + } + + private static String text(Map source, String key) { + return ConformancePackage.text(source.get(key), key); + } +} diff --git a/src/test/java/blue/bex/conformance/BexConformancePackageIntegrityTest.java b/src/test/java/blue/bex/conformance/BexConformancePackageIntegrityTest.java index 4fb53f6..6eedd7f 100644 --- a/src/test/java/blue/bex/conformance/BexConformancePackageIntegrityTest.java +++ b/src/test/java/blue/bex/conformance/BexConformancePackageIntegrityTest.java @@ -1,6 +1,6 @@ package blue.bex.conformance; -import blue.language.Blue; +import blue.bex.test.TestBlue; import blue.language.model.Node; import blue.language.snapshot.FrozenNode; import org.junit.jupiter.api.Test; @@ -303,7 +303,7 @@ void registryFilesBlueIdsAndFixtureIntrinsicBindingsAreExact() { Set declaredBlueIds = new LinkedHashSet(); Set fixtureOnlyBlueIds = new LinkedHashSet(); - try (Blue blue = new Blue()) { + try (TestBlue blue = new TestBlue()) { for (Object value : ConformancePackage.list( registry.get("entries"), "registry.entries")) { Map entry = diff --git a/src/test/java/blue/bex/conformance/BexConformancePropertyTest.java b/src/test/java/blue/bex/conformance/BexConformancePropertyTest.java index bbdd874..c0c82bf 100644 --- a/src/test/java/blue/bex/conformance/BexConformancePropertyTest.java +++ b/src/test/java/blue/bex/conformance/BexConformancePropertyTest.java @@ -9,7 +9,7 @@ import blue.bex.result.BexMetrics; import blue.language.model.Node; import blue.language.snapshot.FrozenNode; -import blue.language.utils.JsonPointer; +import blue.language.model.wire.JsonPointer; import org.junit.jupiter.api.Test; import java.math.BigDecimal; diff --git a/src/test/java/blue/bex/conformance/BexEngineFixtureAdapter.java b/src/test/java/blue/bex/conformance/BexEngineFixtureAdapter.java index 1f61cc6..df21d06 100644 --- a/src/test/java/blue/bex/conformance/BexEngineFixtureAdapter.java +++ b/src/test/java/blue/bex/conformance/BexEngineFixtureAdapter.java @@ -18,15 +18,15 @@ import blue.bex.result.BexExecutionResult; import blue.bex.value.BexValue; import blue.bex.value.BexValues; -import blue.language.Blue; -import blue.language.NodeProvider; +import blue.bex.test.TestBlue; +import blue.language.provider.NodeProvider; import blue.language.model.Node; import blue.language.processor.GasMeter; import blue.language.processor.GasSchedule; import blue.language.processor.GasTraceEntry; import blue.language.snapshot.FrozenNode; -import blue.language.snapshot.ResolvedSnapshot; -import blue.language.utils.BlueIdCalculator; +import blue.language.merge.ResolvedSnapshot; +import blue.language.identity.DirectBlueIdCalculator; import java.lang.reflect.InvocationTargetException; import java.math.BigDecimal; @@ -74,7 +74,7 @@ BexFixtureRun execute(ConformancePackage.Fixture fixture, parseProviderNodes(providerData), batching(variant)); - try (Blue blue = new Blue(provider)) { + try (TestBlue blue = new TestBlue(provider)) { Map effectiveRoot = effectiveRoot(context, variant, providerData); Node root = rootNode(blue, effectiveRoot, variant); @@ -101,7 +101,7 @@ BexFixtureRun execute(ConformancePackage.Fixture fixture, parentBudget, localLimit); BexEngine engine = BexEngine.builder() - .blue(blue) + .language(blue.runtime()) .intrinsics(fixtureIntrinsics()) .build(); @@ -187,7 +187,7 @@ BexFixtureRun execute(ConformancePackage.Fixture fixture, } private static BexExecutionContext executionContext( - Blue blue, + TestBlue blue, ResolvedSnapshot root, Map context, RecordingGasHost gasHost, @@ -225,7 +225,7 @@ blue, valueOrEmpty(context.get("currentContract")))) } private static BexStepResults stepResults( - Blue blue, + TestBlue blue, Map steps) { BexStepResults.Builder builder = BexStepResults.builder(); for (Map.Entry entry : steps.entrySet()) { @@ -234,7 +234,7 @@ private static BexStepResults stepResults( return builder.build(); } - private static BexValue exactValue(Blue blue, Object value) { + private static BexValue exactValue(TestBlue blue, Object value) { ResolvedSnapshot snapshot = blue.resolveToSnapshot( ConformancePackage.semanticNode(blue, value)); @@ -245,7 +245,7 @@ private static BexValue exactValue(Blue blue, Object value) { private static ResolvedSnapshot resolveDocumentSnapshot( ConformancePackage.Fixture fixture, - Blue blue, + TestBlue blue, Node root, Map variant, RecordingNodeProvider provider) { @@ -275,7 +275,7 @@ private static ResolvedSnapshot resolveDocumentSnapshot( } private static Node rootNode( - Blue blue, + TestBlue blue, Map root, Map variant) { Object rawJson = variant.get("rawRootDocumentJson"); @@ -355,7 +355,7 @@ private static Object deepCopy(Object value) { private static Map parseProviderNodes( Map provider) { Map result = new LinkedHashMap(); - try (Blue parser = new Blue()) { + try (TestBlue parser = new TestBlue()) { for (Map.Entry entry : provider.entrySet()) { result.put(entry.getKey(), ConformancePackage.semanticNode( @@ -726,7 +726,7 @@ public BexEstablishedIdentity establishIdentity(Node node) { complexIdentityCalls++; } return new BexEstablishedIdentity( - BlueIdCalculator.calculateBlueId(node), + DirectBlueIdCalculator.calculateBlueId(node), FrozenNode.fromResolvedNode(node.clone())); } } diff --git a/src/test/java/blue/bex/conformance/BexRepresentationInvarianceTest.java b/src/test/java/blue/bex/conformance/BexRepresentationInvarianceTest.java index 9714c5b..864d9c3 100644 --- a/src/test/java/blue/bex/conformance/BexRepresentationInvarianceTest.java +++ b/src/test/java/blue/bex/conformance/BexRepresentationInvarianceTest.java @@ -14,12 +14,12 @@ import blue.bex.result.BexPatchEntry; import blue.bex.value.BexValue; import blue.bex.value.BexValues; -import blue.language.Blue; -import blue.language.NodeProvider; +import blue.bex.test.TestBlue; +import blue.language.provider.NodeProvider; import blue.language.model.Node; import blue.language.provider.ExactNodeGraphFragments; import blue.language.snapshot.FrozenNode; -import blue.language.utils.BlueIdCalculator; +import blue.language.identity.DirectBlueIdCalculator; import org.junit.jupiter.api.Test; import java.util.ArrayList; @@ -198,7 +198,7 @@ private static Observation execute( Node eventRoot = present( eventGraph.roots().get(0), variant.rootForm); - try (Blue blue = new Blue(provider)) { + try (TestBlue blue = new TestBlue(provider)) { if (variant.cacheForm == CacheForm.WARM) { materialize(blue, programRoot, variant.exactForm); materialize(blue, documentRoot, variant.exactForm); @@ -214,7 +214,7 @@ private static Observation execute( CountingIdentityBoundary boundary = new CountingIdentityBoundary(); BexEngine engine = BexEngine.builder() - .blue(blue) + .language(blue.runtime()) .intrinsics(representationIntrinsics()) .build(); BexCompiledProgram compiled = engine.compile( @@ -284,7 +284,7 @@ private static Observation execute( } private static ExactPair runtimePair( - Blue blue, + TestBlue blue, Node presented, ExactForm exactForm) { if (exactForm == ExactForm.MATERIALIZED) { @@ -301,7 +301,7 @@ private static ExactPair runtimePair( } private static ExactPair materialize( - Blue blue, + TestBlue blue, Node presented, ExactForm exactForm) { Node expanded = blue.expand(presented); @@ -536,7 +536,7 @@ private static LogicalInputs create() { * so inline children and materialized children carry the same * exact scalar identities. */ - try (Blue blue = new Blue()) { + try (TestBlue blue = new TestBlue()) { Node textPattern = blue.yamlToNode("type: Text"); Node program = stepDo(list( op("$let", obj( @@ -703,7 +703,7 @@ public BexEstablishedIdentity establishIdentity(Node node) { FrozenNode frozen = FrozenNode.fromResolvedNode(node.clone()); return new BexEstablishedIdentity( - BlueIdCalculator.calculateBlueId( + DirectBlueIdCalculator.calculateBlueId( frozen.toNode()), frozen); } diff --git a/src/test/java/blue/bex/conformance/ConformancePackage.java b/src/test/java/blue/bex/conformance/ConformancePackage.java index 7db4b2d..7e44134 100644 --- a/src/test/java/blue/bex/conformance/ConformancePackage.java +++ b/src/test/java/blue/bex/conformance/ConformancePackage.java @@ -1,6 +1,6 @@ package blue.bex.conformance; -import blue.language.Blue; +import blue.bex.test.TestBlue; import blue.language.model.Node; import org.yaml.snakeyaml.Yaml; @@ -187,7 +187,7 @@ static String json(Object value, boolean sortKeys) { return result.toString(); } - static Node node(Blue blue, Object value) { + static Node node(TestBlue blue, Object value) { return blue.jsonToNode(json(value, false)); } @@ -240,7 +240,7 @@ static Node syntaxNode(Object value) { * Exact pure references and explicitly typed nodes retain their Blue * meaning. */ - static Node semanticNode(Blue blue, Object value) { + static Node semanticNode(TestBlue blue, Object value) { if (value == null) { return new Node(); } diff --git a/src/test/java/blue/bex/output/BexSemanticIdentityIntegrationTest.java b/src/test/java/blue/bex/output/BexSemanticIdentityIntegrationTest.java index 826f7b3..b3f391e 100644 --- a/src/test/java/blue/bex/output/BexSemanticIdentityIntegrationTest.java +++ b/src/test/java/blue/bex/output/BexSemanticIdentityIntegrationTest.java @@ -14,6 +14,7 @@ import blue.bex.value.BexValue; import blue.bex.value.BexValues; import blue.language.model.Node; +import blue.language.registry.BlueCoreTypeRegistry; import blue.language.processor.ExecutionEvidenceUnavailableException; import blue.language.processor.GasLimitExceededException; import blue.language.processor.GasMeter; @@ -22,7 +23,7 @@ import blue.language.processor.ProcessorErrorCategory; import blue.language.processor.ProcessorFailureException; import blue.language.snapshot.FrozenNode; -import blue.language.utils.BlueIdCalculator; +import blue.language.identity.DirectBlueIdCalculator; import org.junit.jupiter.api.Test; import java.math.BigDecimal; @@ -61,7 +62,7 @@ void standaloneAndCustomBoundariesReturnTheirFrozenExactResult() { assertTrue(standaloneValue.value().isExact()); assertEquals( standaloneValue.nodeBlueId(), - BlueIdCalculator.calculateBlueId( + DirectBlueIdCalculator.calculateBlueId( standaloneValue.node())); Node hostNormalized = @@ -69,7 +70,7 @@ void standaloneAndCustomBoundariesReturnTheirFrozenExactResult() { FrozenNode hostFrozen = FrozenNode.fromResolvedNode(hostNormalized); String hostBlueId = - BlueIdCalculator.calculateBlueId(hostNormalized); + DirectBlueIdCalculator.calculateBlueId(hostNormalized); BexSemanticIdentityBoundary hosted = ignored -> new BexEstablishedIdentity(hostBlueId, hostFrozen); BexAdmittedValue hostedValue = admission(hosted).admit( @@ -93,7 +94,7 @@ void ordinaryNonCyclicExactRootBypassesHostSemanticBoundary() { FrozenNode frozen = FrozenNode.fromResolvedNode(content); String blueId = - BlueIdCalculator.calculateBlueId(content); + DirectBlueIdCalculator.calculateBlueId(content); BexValue exact = BexValues.exact(frozen, frozen, blueId); RecordingBoundary boundary = new RecordingBoundary(); @@ -153,12 +154,10 @@ void integralDecimalCrossesTheHostBoundaryAsBlueDouble() { new BigDecimal("1.0"), supplied.getRawValue()); assertEquals( - blue.language.utils.Properties - .DOUBLE_TYPE_BLUE_ID, + BlueCoreTypeRegistry.INSTANCE.blueId("Double"), supplied.getType().getBlueId()); assertEquals( - blue.language.utils.Properties - .DOUBLE_TYPE_BLUE_ID, + BlueCoreTypeRegistry.INSTANCE.blueId("Double"), admitted.node().getType().getBlueId()); assertEquals( "double", @@ -192,7 +191,7 @@ void hostNormalizationWinsWhileMatchingExactDescendantsStayLocal() { Node exactContent = obj( "deep", "already-resolved"); String exactChildBlueId = - BlueIdCalculator.calculateBlueId( + DirectBlueIdCalculator.calculateBlueId( exactContent); FrozenNode exactChildFrozen = FrozenNode.fromResolvedNode( @@ -207,7 +206,7 @@ void hostNormalizationWinsWhileMatchingExactDescendantsStayLocal() { FrozenNode.fromResolvedNode( mismatchedSourceContent); String mismatchedSourceBlueId = - BlueIdCalculator.calculateBlueId( + DirectBlueIdCalculator.calculateBlueId( mismatchedSourceContent); BexValue mismatchedSourceExact = BexValues.exact( @@ -215,11 +214,11 @@ void hostNormalizationWinsWhileMatchingExactDescendantsStayLocal() { mismatchedSourceFrozen, mismatchedSourceBlueId); String hostMismatchBlueId = - BlueIdCalculator.calculateBlueId( + DirectBlueIdCalculator.calculateBlueId( obj("host", "authoritative")); String authoredReferenceBlueId = - BlueIdCalculator.calculateBlueId( + DirectBlueIdCalculator.calculateBlueId( obj("remote", "content")); BexValue supplied = BexValues.fromSimple( map( @@ -249,7 +248,7 @@ void hostNormalizationWinsWhileMatchingExactDescendantsStayLocal() { FrozenNode.fromResolvedNode( hostNormalized); String hostBlueId = - BlueIdCalculator.calculateBlueId( + DirectBlueIdCalculator.calculateBlueId( hostNormalized); BexValue admitted = admission(ignored -> @@ -553,7 +552,7 @@ void failedAdmissionMutatesNeitherPatchNorEventBuffers() { @Test void exactCyclicMemberStaysOpaqueAndTransientCounterfeitsFailClosed() { String cyclicMember = - BlueIdCalculator.calculateBlueId( + DirectBlueIdCalculator.calculateBlueId( new Node().value("cyclic-set")) + "#0"; FrozenNode reference = FrozenNode.fromResolvedNode( @@ -755,7 +754,7 @@ public BexEstablishedIdentity establishIdentity( Node exact = node.clone(); inputs.add(exact); return new BexEstablishedIdentity( - BlueIdCalculator.calculateBlueId(exact), + DirectBlueIdCalculator.calculateBlueId(exact), FrozenNode.fromResolvedNode(exact)); } } diff --git a/src/test/java/blue/bex/test/TestBlue.java b/src/test/java/blue/bex/test/TestBlue.java new file mode 100644 index 0000000..2c4892b --- /dev/null +++ b/src/test/java/blue/bex/test/TestBlue.java @@ -0,0 +1,267 @@ +package blue.bex.test; + +import blue.language.codec.BlueFormat; +import blue.language.merge.ResolvedSnapshot; +import blue.language.model.Node; +import blue.language.model.Schema; +import blue.language.identity.DirectBlueIdCalculator; +import blue.language.processor.ContractProcessorRegistry; +import blue.language.processor.ContractProcessorRegistryBuilder; +import blue.language.processor.ContractMatchingService; +import blue.language.processor.DocumentProcessor; +import blue.language.processor.registry.BlueRuntimeTypeRegistry; +import blue.language.processor.registry.RuntimeTypeAliases; +import blue.language.provider.NodeProvider; +import blue.language.provider.SequentialNodeProvider; +import blue.language.runtime.BlueLanguage; + +import java.util.Arrays; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Test-only convenience facade over the focused modular Language services. + * + *

This preserves the terse fixture setup formerly supplied by the + * aggregate {@code Blue} facade without making BEX production or test code + * depend on the aggregate Language artifact.

+ */ +public final class TestBlue implements AutoCloseable { + private static final Node BEX_PROGRAM_TYPE = + new Node().name("Blue/BEX Program"); + private static final String BEX_PROGRAM_TYPE_BLUE_ID = + DirectBlueIdCalculator.calculateBlueId(BEX_PROGRAM_TYPE); + private static final Node BEX_DEFINITION_TYPE = + new Node().name("Blue/BEX Definition"); + private static final String BEX_DEFINITION_TYPE_BLUE_ID = + DirectBlueIdCalculator.calculateBlueId(BEX_DEFINITION_TYPE); + private static final String BEX_EMPTY_OPERATOR = "$empty"; + private static final String SHIELDED_BEX_EMPTY_OPERATOR = + "__blue_bex_preprocess_shielded_empty_operator__"; + private final BlueLanguage language; + private final NodeProvider provider; + private DocumentProcessor documentProcessor; + + public TestBlue() { + this(blueId -> null); + } + + public TestBlue(NodeProvider provider) { + ContractProcessorRegistry contracts = + ContractProcessorRegistryBuilder.create() + .registerDefaults() + .build() + .snapshot(); + this.provider = new SequentialNodeProvider(Arrays.asList( + BlueRuntimeTypeRegistry.getDefault() + .asProcessorSnapshotProvider(), + contracts.exactTypeProvider(), + TestBlue::bexTypeByBlueId, + provider)); + Map bexAliases = new LinkedHashMap<>(); + bexAliases.put(BEX_PROGRAM_TYPE.getName(), + BEX_PROGRAM_TYPE_BLUE_ID); + bexAliases.put(BEX_DEFINITION_TYPE.getName(), + BEX_DEFINITION_TYPE_BLUE_ID); + Map environmentImports = + new LinkedHashMap<>(RuntimeTypeAliases.NAME_TO_BLUE_ID); + environmentImports.putAll(bexAliases); + this.language = BlueLanguage.builder() + .nodeProvider(this.provider) + .preprocessingAliases(bexAliases) + .environmentImports(environmentImports) + .build(); + } + + public BlueLanguage runtime() { + return language; + } + + public Node yamlToNode(String yaml) { + return language.preprocessing().preprocess( + language.codec().parseSource(yaml, BlueFormat.YAML)); + } + + /** + * Parses a BEX-bearing Source Document without letting BEX's + * {@code $empty} expression operator collide with Blue Language's + * identically spelled list-placeholder control. + * + *

The operator key is replaced only across this explicit BEX source + * boundary, the ordinary mandatory preprocessing pipeline is then run, + * and the key is restored by rebuilding every property map in its + * original insertion order. {@link #yamlToNode(String)} deliberately does + * not use this boundary, so ordinary Blue documents retain strict + * malformed-placeholder validation.

+ */ + public Node yamlToBexSource(String yaml) { + Node source = language.codec().parseSource(yaml, BlueFormat.YAML); + rewritePropertyKey( + source, + BEX_EMPTY_OPERATOR, + SHIELDED_BEX_EMPTY_OPERATOR, + Collections.newSetFromMap( + new IdentityHashMap())); + Node preprocessed = language.preprocessing().preprocess(source); + rewritePropertyKey( + preprocessed, + SHIELDED_BEX_EMPTY_OPERATOR, + BEX_EMPTY_OPERATOR, + Collections.newSetFromMap( + new IdentityHashMap())); + return preprocessed; + } + + public Node jsonToNode(String json) { + return language.preprocessing().preprocess( + parseSourceJson(json)); + } + + public Node parseSourceJson(String json) { + return language.codec().parseSource(json, BlueFormat.JSON); + } + + /** + * Parses authored YAML without resolving environment-owned type aliases. + * This is used when a fixture is an external event payload whose host + * registry is intentionally outside BEX's test composition. + */ + public Node parseSourceYaml(String yaml) { + return language.codec().parseSource(yaml, BlueFormat.YAML); + } + + public Node expand(Node node) { + return language.graph().expand(node); + } + + public ResolvedSnapshot resolveToSnapshot(Node node) { + return language.snapshots().resolve(node); + } + + public String calculateBlueId(Node node) { + return language.identity().directBlueId(node); + } + + public boolean nodeMatchesType(Node candidate, Node type) { + return language.matching().matches(candidate, type); + } + + /** + * Legacy processor-construction support for the hosted adapter test. + * New integration code should use the focused Contracts composition. + */ + public DocumentProcessor getDocumentProcessor() { + if (documentProcessor == null) { + documentProcessor = DocumentProcessor.builder() + .nodeProvider(provider) + .matchingService(new ContractMatchingService( + language.processing().runtimeAccess())) + .build(); + } + return documentProcessor; + } + + private static List bexTypeByBlueId(String blueId) { + if (BEX_PROGRAM_TYPE_BLUE_ID.equals(blueId)) { + return Collections.singletonList(BEX_PROGRAM_TYPE.clone()); + } + if (BEX_DEFINITION_TYPE_BLUE_ID.equals(blueId)) { + return Collections.singletonList(BEX_DEFINITION_TYPE.clone()); + } + return Collections.emptyList(); + } + + private static void rewritePropertyKey( + Node node, + String from, + String to, + Set visited) { + if (node == null || !visited.add(node)) { + return; + } + rewritePropertyKey(node.getType(), from, to, visited); + rewritePropertyKey(node.getItemType(), from, to, visited); + rewritePropertyKey(node.getKeyType(), from, to, visited); + rewritePropertyKey(node.getValueType(), from, to, visited); + rewritePropertyKey(node.getContracts(), from, to, visited); + rewritePropertyKey(node.getBlue(), from, to, visited); + rewriteSchemaPropertyKeys(node.getSchema(), from, to, visited); + if (node.getItems() != null) { + for (Node item : node.getItems()) { + rewritePropertyKey(item, from, to, visited); + } + } + Map properties = node.getProperties(); + if (properties == null) { + return; + } + if (properties.containsKey(from) && properties.containsKey(to)) { + throw new IllegalArgumentException( + "BEX source contains the reserved preprocessing shield key: " + + to); + } + Map rewritten = new LinkedHashMap<>(); + for (Map.Entry entry : properties.entrySet()) { + String key = from.equals(entry.getKey()) ? to : entry.getKey(); + rewritten.put(key, entry.getValue()); + rewritePropertyKey(entry.getValue(), from, to, visited); + } + node.properties(rewritten); + } + + private static void rewriteSchemaPropertyKeys( + Schema schema, + String from, + String to, + Set visited) { + if (schema == null) { + return; + } + rewritePropertyKey(schema.getRequired(), from, to, visited); + rewritePropertyKey(schema.getMinLength(), from, to, visited); + rewritePropertyKey(schema.getMaxLength(), from, to, visited); + rewritePropertyKey(schema.getMinimum(), from, to, visited); + rewritePropertyKey(schema.getMaximum(), from, to, visited); + rewritePropertyKey(schema.getExclusiveMinimum(), from, to, visited); + rewritePropertyKey(schema.getExclusiveMaximum(), from, to, visited); + rewritePropertyKey(schema.getMultipleOf(), from, to, visited); + rewritePropertyKey(schema.getMinItems(), from, to, visited); + rewritePropertyKey(schema.getMaxItems(), from, to, visited); + rewritePropertyKey(schema.getUniqueItems(), from, to, visited); + rewritePropertyKey(schema.getMinFields(), from, to, visited); + rewritePropertyKey(schema.getMaxFields(), from, to, visited); + if (schema.getEnum() != null) { + for (Node value : schema.getEnum()) { + rewritePropertyKey(value, from, to, visited); + } + } + } + + @Override + public void close() { + RuntimeException failure = null; + if (documentProcessor != null) { + try { + documentProcessor.close(); + } catch (RuntimeException closeFailure) { + failure = closeFailure; + } + } + try { + language.close(); + } catch (RuntimeException closeFailure) { + if (failure == null) { + failure = closeFailure; + } else if (failure != closeFailure) { + failure.addSuppressed(closeFailure); + } + } + if (failure != null) { + throw failure; + } + } +} diff --git a/src/test/java/blue/language/processor/BexHostedRuntimeWorkSessionTest.java b/src/test/java/blue/language/processor/BexHostedRuntimeWorkSessionTest.java index eed7645..5de952c 100644 --- a/src/test/java/blue/language/processor/BexHostedRuntimeWorkSessionTest.java +++ b/src/test/java/blue/language/processor/BexHostedRuntimeWorkSessionTest.java @@ -18,16 +18,16 @@ import blue.bex.result.BexMetrics; import blue.bex.runtime.BexRuntime; import blue.bex.value.BexValues; -import blue.language.Blue; -import blue.language.NodeProvider; +import blue.bex.test.TestBlue; +import blue.language.provider.NodeProvider; import blue.language.model.Node; import blue.language.provider.CyclicAwareNodeProvider; import blue.language.provider.CyclicSetProofResult; -import blue.language.provider.NodeProviderOutcome; +import blue.language.api.NodeProviderOutcome; import blue.language.provider.NodeProviderResult; import blue.language.snapshot.FrozenNode; -import blue.language.utils.BlueIdCalculator; -import blue.language.utils.CircularBlueIdCalculator; +import blue.language.identity.DirectBlueIdCalculator; +import blue.language.identity.CircularSetIdentityCalculator; import org.junit.jupiter.api.Test; import java.nio.charset.StandardCharsets; @@ -430,11 +430,11 @@ void deterministicFailureDiscardsBufferedOutputsAndLeavesPrefixForOwner() { BexCompiledProgram compiled = BexEngine.builder().build().compile(source); BexMetrics metrics = new BexMetrics(); - try (Blue blue = new Blue()) { + try (TestBlue blue = new TestBlue()) { BexRuntime runtime = new BexRuntime( compiled, context, - blue, + blue.runtime(), BexGasSchedule.defaults(), metrics, new BexPointerCache()); @@ -536,7 +536,7 @@ void providerUnavailableUsesHostedSuspensionAndRestoresParentBudget() { Node exactContent = obj( "value", "temporarily-offline"); String exactBlueId = - BlueIdCalculator.calculateBlueId(exactContent); + DirectBlueIdCalculator.calculateBlueId(exactContent); AtomicInteger providerDemands = new AtomicInteger(); NodeProvider provider = new NodeProvider() { @@ -583,12 +583,12 @@ public NodeProviderResult fetchResultByBlueId( .STANDALONE) .build(); - try (Blue blue = new Blue(provider)) { + try (TestBlue blue = new TestBlue(provider)) { ExecutionEvidenceUnavailableException failure = assertThrows( ExecutionEvidenceUnavailableException.class, () -> BexEngine.builder() - .blue(blue) + .language(blue.runtime()) .build() .compileAndExecute( BexProgramSource.expression( @@ -634,9 +634,9 @@ void hostRejectionPropagatesTheExactRecordedException() { new RecordingSessionHost(session, "bex:exhaustion"); AtomicInteger identityCalls = new AtomicInteger(); - try (Blue blue = new Blue()) { + try (TestBlue blue = new TestBlue()) { BexEngine engine = BexEngine.builder() - .blue(blue) + .language(blue.runtime()) .build(); BexCompiledProgram compiled = engine.compile(literalExpression()); @@ -650,7 +650,7 @@ void hostRejectionPropagatesTheExactRecordedException() { return BexSemanticIdentityBoundary.STANDALONE .establishIdentity(node); }), - blue, + blue.runtime(), BexGasSchedule.defaults(), new BexMetrics(), new BexPointerCache(), @@ -1070,9 +1070,9 @@ void constructionProcessorFailureWinsOverNestedUnavailability() { @Test void processorExecutionContextUsesItsInvocationSemanticOutputBoundary() { - try (Blue blue = new Blue()) { - ProcessorEngine.Execution execution = - new ProcessorEngine.Execution( + try (TestBlue blue = new TestBlue()) { + ProcessorInvocationState execution = + new ProcessorInvocationState( blue.getDocumentProcessor(), new Node().properties( Collections.emptyMap())); @@ -1108,7 +1108,7 @@ void processorExecutionContextUsesItsInvocationSemanticOutputBoundary() { assertTrue(result.output().reconstructed()); assertEquals( result.output().nodeBlueId(), - BlueIdCalculator.calculateBlueId( + DirectBlueIdCalculator.calculateBlueId( result.output().node())); processorContext.applyBufferedEffects(); } @@ -1133,7 +1133,7 @@ void processorExecutionContextUsesItsInvocationSemanticOutputBoundary() { @Test void hostedOpaqueCyclicMemberSupportsIdentityAndOutputWithoutProofDemand() { String memberBlueId = - BlueIdCalculator.calculateBlueId( + DirectBlueIdCalculator.calculateBlueId( new Node().value("hosted-cyclic-set")) + "#0"; FrozenNode document = FrozenNode.fromResolvedNode( @@ -1204,7 +1204,7 @@ void hostedCyclicProofUnavailabilityUsesSessionDiscardLifecycle() { List placeholders = Collections.singletonList(placeholder); String memberBlueId = - CircularBlueIdCalculator + CircularSetIdentityCalculator .calculateCircularSetBlueIds( placeholders) .get(0); @@ -1222,7 +1222,7 @@ void hostedCyclicProofUnavailabilityUsesSessionDiscardLifecycle() { session, "bex:cyclic-proof-unavailable"); - try (Blue blue = new Blue(provider)) { + try (TestBlue blue = new TestBlue(provider)) { BexExecutionContext context = BexExecutionContext.builder() .document( @@ -1242,7 +1242,7 @@ void hostedCyclicProofUnavailabilityUsesSessionDiscardLifecycle() { assertThrows( ExecutionEvidenceUnavailableException.class, () -> BexEngine.builder() - .blue(blue) + .language(blue.runtime()) .build() .compileAndExecute( BexProgramSource.expression( diff --git a/src/test/resources/hosted-release/baseline.properties b/src/test/resources/hosted-release/baseline.properties index 7920ab0..b4bdd36 100644 --- a/src/test/resources/hosted-release/baseline.properties +++ b/src/test/resources/hosted-release/baseline.properties @@ -19,4 +19,4 @@ javadocJarSha256=7811d8ac0259b62e54ccc1cae182e5c95559d9577349ce0ac7df001e73dc1ab blueLanguageCommit=f1f33ce30ab578bd6aedcdd81164fec85ad9fb87 blueLanguageWorkspaceSha256=c496d34d0539b19ef3d343864348adab9dde62ac2dad0a5bb320cdf35319ba5e czTomlSha256=2dd317fbe362561f0e9827c705ff6260fd6df26c16518e0acce99ecba595a4b1 -specificationSha256=b25d6d255f84c584ed7a484411430fab50c18142a1bb6c08cfb104acf09d6f69 +specificationSha256=1725878bcb59f2d2a60bae2ada582a18dc964f4dbc61377aaae195a773765f92 diff --git a/src/test/resources/hosted-release/required-public-api.txt b/src/test/resources/hosted-release/required-public-api.txt index 6ce537e..f05fe3c 100644 --- a/src/test/resources/hosted-release/required-public-api.txt +++ b/src/test/resources/hosted-release/required-public-api.txt @@ -27,13 +27,14 @@ class public final blue.bex.api.BexEngine method public static builder():blue.bex.api.BexEngine$Builder class public static final blue.bex.api.BexEngine$Builder constructor public () - method public blue(blue.language.Blue):blue.bex.api.BexEngine$Builder method public build():blue.bex.api.BexEngine method public cache(blue.bex.compile.BexCompiledProgramCache):blue.bex.api.BexEngine$Builder method public gasSchedule(blue.bex.gas.BexGasSchedule):blue.bex.api.BexEngine$Builder + method public intrinsic(java.lang.Class,blue.bex.api.BexTypeBlueIdResolver,java.lang.String,java.util.Map,blue.bex.api.BexIntrinsicProcessor):blue.bex.api.BexEngine$Builder method public intrinsic(java.lang.Class,java.lang.String,java.util.Map,blue.bex.api.BexIntrinsicProcessor):blue.bex.api.BexEngine$Builder method public intrinsic(java.lang.String,java.lang.String,java.util.Map,blue.bex.api.BexIntrinsicProcessor):blue.bex.api.BexEngine$Builder method public intrinsics(blue.bex.api.BexIntrinsicRegistry):blue.bex.api.BexEngine$Builder + method public language(blue.language.runtime.BlueLanguage):blue.bex.api.BexEngine$Builder method public metrics(blue.bex.api.BexMetricsSink):blue.bex.api.BexEngine$Builder class public final blue.bex.api.BexExecutionContext method public binding(java.lang.String):blue.bex.value.BexValue @@ -100,11 +101,13 @@ class public final blue.bex.api.BexIntrinsicRegistry method public static empty():blue.bex.api.BexIntrinsicRegistry method public supportedBlueIds():java.util.Set method public supports(java.lang.String):boolean + method public with(java.lang.Class,blue.bex.api.BexTypeBlueIdResolver,java.lang.String,java.util.Map,blue.bex.api.BexIntrinsicProcessor):blue.bex.api.BexIntrinsicRegistry method public with(java.lang.Class,java.lang.String,java.util.Map,blue.bex.api.BexIntrinsicProcessor):blue.bex.api.BexIntrinsicRegistry method public with(java.lang.String,java.lang.String,java.util.Map,blue.bex.api.BexIntrinsicProcessor):blue.bex.api.BexIntrinsicRegistry class public static final blue.bex.api.BexIntrinsicRegistry$Builder constructor public () method public build():blue.bex.api.BexIntrinsicRegistry + method public register(java.lang.Class,blue.bex.api.BexTypeBlueIdResolver,java.lang.String,java.util.Map,blue.bex.api.BexIntrinsicProcessor):blue.bex.api.BexIntrinsicRegistry$Builder method public register(java.lang.Class,java.lang.String,java.util.Map,blue.bex.api.BexIntrinsicProcessor):blue.bex.api.BexIntrinsicRegistry$Builder method public register(java.lang.String,java.lang.String,java.lang.String,java.util.Map,blue.bex.api.BexIntrinsicProcessor):blue.bex.api.BexIntrinsicRegistry$Builder method public register(java.lang.String,java.lang.String,java.util.Map,blue.bex.api.BexIntrinsicProcessor):blue.bex.api.BexIntrinsicRegistry$Builder @@ -135,6 +138,8 @@ class public static final blue.bex.api.BexStepResults$Builder method public build():blue.bex.api.BexStepResults method public put(java.lang.String,blue.bex.result.BexExecutionResult):blue.bex.api.BexStepResults$Builder method public put(java.lang.String,blue.bex.value.BexValue):blue.bex.api.BexStepResults$Builder +class public abstract interface blue.bex.api.BexTypeBlueIdResolver + method public abstract resolve(java.lang.Class):java.lang.String class public final blue.bex.api.FrozenBexDocumentView implements blue.bex.api.BexDocumentView constructor public (blue.language.snapshot.FrozenNode) constructor public (blue.language.snapshot.FrozenNode,blue.language.snapshot.FrozenNode,java.lang.String) @@ -575,7 +580,7 @@ class public final blue.bex.result.BexPatchEntry method public val():blue.bex.value.BexValue class public final blue.bex.result.BexResultOverlay constructor public (blue.bex.api.BexDocumentView,blue.bex.result.BexMetrics) - constructor public (blue.bex.api.BexDocumentView,blue.bex.result.BexMetrics,blue.language.Blue) + constructor public (blue.bex.api.BexDocumentView,blue.bex.result.BexMetrics,blue.language.runtime.BlueLanguage) method public append(blue.bex.result.BexPatchEntry):void method public rootValue():blue.bex.value.BexValue method public valueAt(java.lang.String,java.util.List):blue.bex.value.BexValue @@ -588,8 +593,8 @@ class public final blue.bex.runtime.BexExecutionAccumulator method public events():blue.bex.result.BexEvents method public overlay():blue.bex.result.BexResultOverlay class public final blue.bex.runtime.BexRuntime - constructor public (blue.bex.compile.BexCompiledProgram,blue.bex.api.BexExecutionContext,blue.language.Blue,blue.bex.gas.BexGasSchedule,blue.bex.result.BexMetrics,blue.bex.pointer.BexPointerCache) - constructor public (blue.bex.compile.BexCompiledProgram,blue.bex.api.BexExecutionContext,blue.language.Blue,blue.bex.gas.BexGasSchedule,blue.bex.result.BexMetrics,blue.bex.pointer.BexPointerCache,blue.bex.api.BexIntrinsicRegistry) + constructor public (blue.bex.compile.BexCompiledProgram,blue.bex.api.BexExecutionContext,blue.language.runtime.BlueLanguage,blue.bex.gas.BexGasSchedule,blue.bex.result.BexMetrics,blue.bex.pointer.BexPointerCache) + constructor public (blue.bex.compile.BexCompiledProgram,blue.bex.api.BexExecutionContext,blue.language.runtime.BlueLanguage,blue.bex.gas.BexGasSchedule,blue.bex.result.BexMetrics,blue.bex.pointer.BexPointerCache,blue.bex.api.BexIntrinsicRegistry) method public accumulator():blue.bex.runtime.BexExecutionAccumulator method public canonicalPointer(java.lang.String):java.lang.String method public context():blue.bex.api.BexExecutionContext @@ -654,7 +659,7 @@ class public final blue.bex.runtime.Control extends java.lang.Enum method public static valueOf(java.lang.String):blue.bex.runtime.Control method public static values():blue.bex.runtime.Control[] class public final blue.bex.type.BexBlueTypeMatcher - constructor public (blue.language.Blue) + constructor public (blue.language.runtime.BlueLanguage) method public matches(blue.bex.value.BexValue,blue.language.snapshot.FrozenNode,blue.bex.gas.BexGasMeter,blue.bex.BexSourcePath):boolean class public final blue.bex.value.BexBlueNodeWriter method public static hasLanguageField(blue.bex.value.BexValue):boolean @@ -714,7 +719,7 @@ class public final blue.bex.value.BexValues method public static nullValue():blue.bex.value.BexValue method public static overlay(blue.bex.value.BexValue,java.lang.String,blue.bex.value.BexValue):blue.bex.value.BexValue method public static pointerSet(blue.bex.value.BexValue,java.util.List,blue.bex.value.BexValue,java.lang.String):blue.bex.value.BexValue - method public static referenceBacked(blue.bex.value.BexValue,blue.language.Blue):blue.bex.value.BexValue + method public static referenceBacked(blue.bex.value.BexValue,blue.language.runtime.BlueLanguage):blue.bex.value.BexValue method public static resultOverlayPointerSet(blue.bex.value.BexValue,java.util.List,blue.bex.value.BexValue,java.lang.String):blue.bex.value.BexValue method public static scalar(java.lang.Object):blue.bex.value.BexValue method public static transientFrozen(blue.language.snapshot.FrozenNode):blue.bex.value.BexValue From 169e589266d79baf77e61d485cddf2960424f16b Mon Sep 17 00:00:00 2001 From: piotr-blue Date: Sun, 2 Aug 2026 20:51:21 +0100 Subject: [PATCH 06/13] build: record working local BEX artifact checkpoint --- build.gradle.kts | 98 ++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 86 insertions(+), 12 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index adcc45d..53f9603 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -56,6 +56,12 @@ val blueLanguageFocusedModuleNames = ) val blueLanguageFocusedProjectPaths = blueLanguageFocusedModuleNames.associateWith { ":$it" } +val requiredBexRegistryIdentity = + "sha256:23d282ec1c0bb016263922b1b49c369fdd537efdcf23e005eceeb888d7763fe1" +val requiredBexGasManifestIdentity = + "sha256:41247c820d91a12fdfc17fd9e787a5d8d668d8acc5954fdcb131715bf9e6147d" +val requiredBexFixturePackageIdentity = + "sha256:a1b7bb2b3687389409bc9d0aa450c734f7856d2bcb818c95f4d7ecb19095d20e" val latestLanguageMigrationLock = layout.projectDirectory.file( "gradle/verification/latest-language-baseline.json" @@ -3485,6 +3491,22 @@ val writeBexWorkingVerificationReport by tasks.registering { } val requiredApiInventory = file(apiInventory["path"].toString()) + val requiredApiTypes = + if (requiredApiInventory.isFile) { + requiredApiInventory.readLines().mapNotNull { line -> + if (line.startsWith("class public ")) { + line.substringBefore(" extends ") + .substringBefore(" implements ") + .removePrefix("class public ") + .trim() + .substringAfterLast(' ') + } else { + null + } + }.toSet() + } else { + emptySet() + } requireWorking( publicApiClassification["schema"] == "blue-bex-public-api-classification/1.0" && @@ -3495,7 +3517,8 @@ val writeBexWorkingVerificationReport by tasks.registering { (apiInventory["publicTypeCount"] as? Number) ?.toInt() && classifiedApiTypes.toSet().size == - classifiedApiTypes.size, + classifiedApiTypes.size && + classifiedApiTypes.toSet() == requiredApiTypes, "public-api-classification-ledger-not-current" ) val productionClasspaths = @@ -3562,6 +3585,22 @@ val writeBexWorkingVerificationReport by tasks.registering { "operator-coverage-not-86-of-86" ) + val identities = child(report, "identities") + val exactIdentitiesPassed = + identities["bexRegistry"] == requiredBexRegistryIdentity && + identities["fixtureBindsRegistry"] == + requiredBexRegistryIdentity && + identities["gasManifest"] == + requiredBexGasManifestIdentity && + identities["fixtureBindsGas"] == + requiredBexGasManifestIdentity && + identities["fixturePackage"] == + requiredBexFixturePackageIdentity + requireWorking( + exactIdentitiesPassed, + "normative-registry-gas-or-fixture-identity-mismatch" + ) + val releaseGates = child(report, "releaseGates") requireWorking( passed(releaseGates, "deterministicArchives"), @@ -3587,8 +3626,11 @@ val writeBexWorkingVerificationReport by tasks.registering { "passed" && child(report, "intrinsicEvidence")["status"] == "passed" && + intValue(behavior, "required") == 105 && + intValue(behavior, "executedAndPassing") == 105 && vectors["allPassing"] == true && - intValue(operators, "executedAndPassing") == 86 + intValue(operators, "executedAndPassing") == 86 && + exactIdentitiesPassed requireWorking( semanticParityPassed, "semantic-parity-evidence-not-passing" @@ -3604,7 +3646,8 @@ val writeBexWorkingVerificationReport by tasks.registering { "passed" && child(report, "finiteLoopEvidence")["status"] == "passed" && - intValue(gas, "executedAndPassing") == 30 + intValue(gas, "executedAndPassing") == 30 && + exactIdentitiesPassed requireWorking( gasParityPassed, "gas-parity-evidence-not-passing" @@ -3684,24 +3727,55 @@ val writeBexWorkingVerificationReport by tasks.registering { if (workingReady) "passed" else "failed" output["workingReady"] = workingReady output["workingFailures"] = failures - output["strictRelease"] = - linkedMapOf( - "releaseReady" to report["releaseReady"], - "failures" to report["currentModeFailures"], - "evidence" to report["hostedStandaloneMatrix"] - ) + val workingTests = linkedMapOf() + tests.forEach { (key, value) -> + workingTests[key.toString()] = value + } + workingTests["unclassified"] = testsUnclassified + workingTests["zeroUnclassified"] = testsUnclassified == 0 + val workingFinalTotals = linkedMapOf() + totals.forEach { (key, value) -> + workingFinalTotals[key.toString()] = value + } + workingFinalTotals["tests"] = workingTests + output["tests"] = workingTests + output["finalTotals"] = workingFinalTotals + val hostedStandaloneMatrix = + child(report, "hostedStandaloneMatrix") val standalonePublished = child( - child(report, "hostedStandaloneMatrix"), + hostedStandaloneMatrix, "standalonePublished" ) + val strictReleaseFailures = + ((report["currentModeFailures"] as? List<*>) + ?.map(Any?::toString) + ?: emptyList()).toMutableList() + if (hostedStandaloneMatrix["allRequiredModesPassed"] != true) { + strictReleaseFailures += + "published-local-mode-matrix-not-passing" + } + output["strictRelease"] = + linkedMapOf( + "releaseReady" to report["releaseReady"], + "failures" to strictReleaseFailures.distinct(), + "allRequiredModesPassed" to + hostedStandaloneMatrix["allRequiredModesPassed"], + "publishedModeStatus" to + (standalonePublished["status"] ?: "not-executed"), + "evidence" to hostedStandaloneMatrix + ) output["publishedModeStatus"] = standalonePublished["status"] ?: "not-executed" output["recommendedCommand"] = "./gradlew bexWorkingVerification " + "-PblueLanguageCompositePath=" + (compositeDirectory?.path ?: "") - output["recommendedCommandExecuted"] = false + output["recommendedCommandExecuted"] = + gradle.startParameter.taskNames.any { requestedTask -> + requestedTask.substringAfterLast(':') == + "bexWorkingVerification" + } output["reportProducerTask"] = ":writeBexWorkingVerificationReport" output["migrationBaseline"] = migrationBaseline @@ -3730,7 +3804,7 @@ val writeBexWorkingVerificationReport by tasks.registering { "status" to if (gasParityPassed) "passed" else "failed", "scope" to - "same-run-local-composite-semantic-and-exact-gas-evidence", + "same-run-local-composite-exact-gas-evidence", "gasMicrofixtures" to gas, "counterCoverage" to report["counterCoverage"], "gasExhaustionEvidence" to From 2c089952e596c0bbdd49448693c464c2a107551b Mon Sep 17 00:00:00 2001 From: piotr-blue Date: Sun, 2 Aug 2026 22:36:45 +0100 Subject: [PATCH 07/13] refactor: finish modular BEX 2.0 modernization --- .../scripts/compare-independent-builds.mjs | 55 + .../compare-local-published-evidence.mjs | 99 + .../scripts/run-final-publication-gates.sh | 359 +- .github/workflows/build.yml | 59 +- .github/workflows/release-rc.yml | 28 +- .github/workflows/release.yml | 20 +- README.md | 1026 +---- blue-bex-conformance/build.gradle.kts | 248 + .../bex/benchmark/BexBenchmarkSupport.java | 172 + .../blue/bex/benchmark/BexCoreBenchmark.java | 483 ++ .../java/blue/bex/benchmark/package-info.java | 9 + .../processor/BexHostedGasBenchmark.java | 140 + blue-bex-conformance/src/main/java/.gitkeep | 1 + blue-bex-contracts/build.gradle.kts | 21 + .../BexContractsExecutionContext.java | 69 + .../BexContractsFailureBoundary.java | 80 + ...cessorExecutionContextBexDocumentView.java | 17 +- ...essorExecutionContextBexGasLedgerHost.java | 255 ++ ...ionContextBexSemanticIdentityBoundary.java | 9 +- .../java/blue/bex/contracts/package-info.java | 14 + blue-bex-core/build.gradle.kts | 21 + .../src}/main/java/blue/bex/BexException.java | 0 ...ExecutionEvidenceUnavailableException.java | 43 + .../BexInvalidExecutionEvidenceException.java | 10 + .../main/java/blue/bex/BexSourcePath.java | 0 .../java/blue/bex/api/BexDocumentView.java | 3 +- .../main/java/blue/bex/api/BexEngine.java | 72 +- .../blue/bex/api/BexExecutionContext.java | 63 +- .../java/blue/bex/api/BexFailureBoundary.java | 83 + .../java/blue/bex/api/BexGasLedgerHost.java | 36 +- .../blue/bex/api/BexIntrinsicInvocation.java | 0 .../blue/bex/api/BexIntrinsicProcessor.java | 0 .../blue/bex/api/BexIntrinsicRegistry.java | 25 +- .../java/blue/bex/api/BexMetricsSink.java | 6 +- .../java/blue/bex/api/BexProgramSource.java | 7 +- .../java/blue/bex/api/BexStepResults.java | 3 +- .../blue/bex/api/BexTypeBlueIdResolver.java | 0 .../blue/bex/api/FrozenBexDocumentView.java | 0 .../main/java/blue/bex/api/package-info.java | 14 + .../blue/bex/compile/BexCompilationInput.java | 28 + .../blue/bex/compile/BexCompiledProgram.java | 114 +- .../bex/compile/BexCompiledProgramCache.java | 0 .../bex/compile/BexCompiledProgramKey.java | 21 +- .../BexCompiledProgramRuntimeAccess.java | 20 + .../java/blue/bex/compile/BexCompiler.java | 22 + .../bex/compile/BexCompilerRuntimeAccess.java | 18 + .../blue/bex/compile/BexCompilerSupport.java | 412 ++ .../blue/bex/compile/BexContainsCache.java | 4 +- .../blue/bex/compile/BexExecutionMachine.java | 73 + .../bex/compile/BexExpressionCompiler.java | 693 +++ .../java/blue/bex/compile/BexGasWork.java | 779 ++++ .../blue/bex/compile/BexGasWorkSupport.java | 592 +++ .../blue/bex/compile/BexIntrinsicCatalog.java | 13 + .../blue/bex/compile/BexNodeFingerprint.java | 0 .../blue/bex/compile/BexNodeIdentity.java | 0 .../java/blue/bex/compile/BexOperands.java | 19 +- .../blue/bex/compile/BexOperatorCatalog.java | 784 ++++ .../blue/bex/compile/BexPatchEntryParser.java | 3 +- .../blue/bex/compile/BexProgramCompiler.java | 363 ++ .../bex/compile/BexStatementCompiler.java | 243 + .../java/blue/bex/compile/BexStatements.java | 36 +- .../main/java/blue/bex/compile/CallExpr.java | 12 +- .../bex/compile/CollectionExpressions.java | 24 +- .../java/blue/bex/compile}/CompileScope.java | 18 +- .../blue/bex/compile}/CompiledExpression.java | 3 +- .../java/blue/bex/compile}/CompiledFrame.java | 67 +- .../blue/bex/compile/CompiledStatement.java | 6 + .../bex/compile/ConstructedExpressions.java | 116 + .../main/java/blue/bex/compile/Control.java | 7 + .../java/blue/bex/compile/ExpressionBase.java | 81 + .../bex/compile/LogicNumericExpressions.java | 22 +- .../compile/LruBexCompiledProgramCache.java | 0 .../bex/compile/ObjectResultExpressions.java | 9 +- .../blue/bex/compile/ReadExpressions.java | 150 + .../bex/compile/TypeStringExpressions.java | 11 +- .../java/blue/bex/compile/package-info.java | 12 + .../java/blue/bex/gas/BexGasAdmission.java | 145 + .../main/java/blue/bex/gas/BexGasBudget.java | 42 + .../main/java/blue/bex/gas/BexGasCharge.java | 0 .../blue/bex/gas/BexGasChargeContext.java | 41 + .../main/java/blue/bex/gas/BexGasCounter.java | 0 .../blue/bex/gas/BexGasCounterCatalog.java | 127 + .../java/blue/bex/gas/BexGasHostSession.java | 235 + .../main/java/blue/bex/gas/BexGasLedger.java | 31 +- .../blue/bex/gas/BexGasLedgerCapability.java | 20 + .../blue/bex/gas/BexGasLedgerLifecycle.java | 55 + .../bex/gas/BexGasLimitExceededException.java | 25 +- .../java/blue/bex/gas/BexGasManifest.java | 0 .../main/java/blue/bex/gas/BexGasMeter.java | 500 ++ .../java/blue/bex/gas/BexGasSchedule.java | 0 .../blue/bex/gas/BexGasTraceRecorder.java | 53 + .../blue/bex/gas/BexHostGasExhaustion.java | 46 + .../java/blue/bex/gas/BexSharedGasBudget.java | 8 + .../main/java/blue/bex/gas/package-info.java | 13 + .../blue/bex/output/BexAdmittedValue.java | 0 .../bex/output/BexEstablishedIdentity.java | 0 .../blue/bex/output/BexFailurePolicy.java | 47 + .../blue/bex/output/BexOutputAdmission.java | 27 +- .../java/blue/bex/output/BexOutputKind.java | 0 .../output/BexSemanticIdentityBoundary.java | 0 .../java/blue/bex/output/package-info.java | 13 + .../src/main/java/blue/bex/package-info.java | 12 + .../java/blue/bex/pointer/BexPointer.java | 0 .../blue/bex/pointer/BexPointerCache.java | 4 +- .../java/blue/bex/pointer/package-info.java | 12 + .../java/blue/bex/result/BexChangeset.java | 3 +- .../main/java/blue/bex/result/BexEvents.java | 3 +- .../blue/bex/result/BexExecutionResult.java | 19 +- .../main/java/blue/bex/result/BexMetrics.java | 60 + .../blue/bex/result/BexMetricsRecorder.java | 138 + .../blue/bex/result/BexMetricsSnapshot.java | 50 + .../java/blue/bex/result/BexPatchEntry.java | 3 +- .../blue/bex/result/BexResultOverlay.java | 12 +- .../java/blue/bex/result/package-info.java | 13 + .../bex/runtime/BexExecutionAccumulator.java | 0 .../java/blue/bex/runtime/BexRuntime.java | 281 ++ .../blue/bex/runtime/BexRuntimeContext.java | 23 + .../bex/runtime/BexRuntimeGasSession.java | 272 ++ .../bex/runtime/BexRuntimeIntrinsics.java | 49 + .../blue/bex/runtime/BexStepResultView.java | 9 + .../java/blue/bex/runtime/package-info.java | 15 + .../java/blue/bex/spi/BexDocumentAccess.java | 11 + .../main/java/blue/bex/spi/package-info.java | 10 + .../blue/bex/type/BexBlueTypeMatcher.java | 26 + .../blue/bex/type/BexFrozenTypeMatcher.java | 159 + .../blue/bex/type/BexPatternValidator.java | 272 ++ .../bex/type/BexTypeMatchWorkRecorder.java | 122 + .../java/blue/bex/type/BexTypeMatcher.java | 390 ++ .../main/java/blue/bex/type/package-info.java | 11 + .../java/blue/bex/value/AbstractBexValue.java | 0 .../blue/bex/value/AdmittedExactBexValue.java | 0 .../blue/bex/value/BexBlueNodeWriter.java | 0 .../blue/bex/value/BexBlueValueImporter.java | 0 .../blue/bex/value/BexChangesetValueView.java | 8 + .../main/java/blue/bex/value/BexEquality.java | 0 .../blue/bex/value/BexEventsValueView.java | 8 + .../java/blue/bex/value/BexFrozenWriter.java | 9 +- .../java/blue/bex/value/BexNodeWriter.java | 0 .../blue/bex/value/BexPatchValueView.java | 8 + .../java/blue/bex/value/BexSimpleWriter.java | 0 .../java/blue/bex/value/BexTruthiness.java | 0 .../java/blue/bex/value/BexUnicodeOrder.java | 0 .../main/java/blue/bex/value/BexValue.java | 5 + .../java/blue/bex/value/BexValueKind.java | 24 + .../java/blue/bex/value/BexValueMetrics.java | 7 + .../main/java/blue/bex/value/BexValues.java | 31 +- .../blue/bex/value/ChangesetBexValue.java | 6 +- .../java/blue/bex/value/EventsBexValue.java | 6 +- .../blue/bex/value/FrozenNodeBexValue.java | 0 .../java/blue/bex/value/ListBexValue.java | 0 .../main/java/blue/bex/value/MapBexValue.java | 0 .../java/blue/bex/value/NodeBexValue.java | 0 .../java/blue/bex/value/NullBexValue.java | 0 .../blue/bex/value/OverlayListBexValue.java | 0 .../blue/bex/value/OverlayMapBexValue.java | 0 .../blue/bex/value/PatchEntryBexValue.java | 6 +- .../blue/bex/value/PointerSetBexValue.java | 0 .../java/blue/bex/value/ScalarBexValue.java | 0 .../blue/bex/value/UndefinedBexValue.java | 0 .../java/blue/bex/value/package-info.java | 13 + .../blue/bex/gas/blue-bex-gas-2.0.yaml | 0 blue-bex-java/build.gradle.kts | 22 + blue-bex-java/src/main/java/.gitkeep | 1 + build-logic/build.gradle.kts | 81 + build-logic/settings.gradle.kts | 1 + .../bex/buildlogic/ApiEvidencePlugin.java | 49 + .../ArchitectureVerificationPlugin.java | 55 + .../ConformanceConventionsPlugin.java | 25 + .../Java8LibraryConventionsPlugin.java | 91 + .../bex/buildlogic/JmhConventionsPlugin.java | 116 + .../LanguageDependencyModeExtension.java | 20 + .../LanguageDependencyModePlugin.java | 109 + .../PublicationConventionsPlugin.java | 71 + .../bex/buildlogic/ReleaseEvidencePlugin.java | 90 + .../ReproducibleArchivesPlugin.java | 108 + .../buildlogic/RootOrchestrationPlugin.java | 437 ++ .../blue/bex/buildlogic/package-info.java | 12 + .../tasks/GenerateApiClassificationTask.java | 189 + .../GenerateBenchmarkEnvironmentTask.java | 60 + .../tasks/GenerateDependencyEvidenceTask.java | 179 + .../GenerateModernizationReportTask.java | 522 +++ .../tasks/GenerateReleaseReportTask.java | 283 ++ .../tasks/GenerateSourceFingerprintTask.java | 171 + .../tasks/GenerateWorkingReportTask.java | 779 ++++ .../tasks/VerifyBexArchitectureTask.java | 351 ++ .../tasks/VerifyJava8BytecodeTask.java | 69 + .../VerifyLegacyLanguageImportsTask.java | 69 + .../tasks/VerifyPublishedLanguageTask.java | 316 ++ .../bex/buildlogic/tasks/package-info.java | 11 + build.gradle.kts | 4027 +---------------- docs/BEX_CONFORMANCE.md | 218 +- docs/LATEST_LANGUAGE_API_MIGRATION.md | 92 +- docs/adding-an-operator.md | 62 + docs/architecture.md | 91 + docs/blue-output-boundary.md | 73 + docs/compiler-and-ir.md | 83 + docs/conformance.md | 82 + docs/contracts-hosting.md | 100 + docs/gas-and-exhaustion.md | 90 + docs/intrinsics.md | 81 + docs/latest-language-api-migration.json | 373 +- docs/migrating-to-modular-blue-language.md | 119 + docs/program-model.md | 107 + docs/public-api-classification.json | 75 +- docs/release.md | 86 + docs/runtime-and-context.md | 83 + docs/start-here.md | 98 + docs/values-and-identity.md | 105 + examples/README.md | 14 + examples/build.gradle.kts | 27 + .../blue/bex/examples/HostedBexExample.java | 58 + .../bex/examples/HostedConsumerSmoke.java | 306 ++ .../bex/examples/StandaloneBexExample.java | 47 + .../java/blue/bex/examples/package-info.java | 10 + .../bex/examples/HostedConsumerSmokeTest.java | 22 + .../api/modernization-added-descriptors.txt | 483 ++ .../api/modernization-removed-descriptors.txt | 254 ++ ...-checkpoint-public-api-classification.json | 104 + .../api/working-checkpoint-public-api.txt | 799 ++++ .../latest-language-baseline.json | 98 +- settings.gradle.kts | 86 +- ...essorExecutionContextBexGasLedgerHost.java | 176 - .../java/blue/bex/compile/BexCompiler.java | 1632 ------- .../java/blue/bex/compile/BexExpressions.java | 1676 ------- src/main/java/blue/bex/gas/BexGasMeter.java | 905 ---- src/main/java/blue/bex/result/BexMetrics.java | 142 - .../java/blue/bex/runtime/BexRuntime.java | 511 --- .../blue/bex/runtime/CompiledStatement.java | 5 - src/main/java/blue/bex/runtime/Control.java | 6 - .../blue/bex/type/BexBlueTypeMatcher.java | 972 ---- .../blue/bex/BexBlueTypeMatchingGasTest.java | 9 +- .../BexCompositeExhaustionEvidenceTest.java | 40 +- .../bex/BexConcurrentEngineIsolationTest.java | 179 + .../blue/bex/BexDependencyBoundaryTest.java | 30 +- .../BexDiagnosticAdmissionOrderingTest.java | 24 +- .../java/blue/bex/BexExactGasRuleTest.java | 19 +- .../bex/BexExactReferenceDocumentTest.java | 20 +- .../bex/BexExecutionEvidenceLedgerTest.java | 19 +- src/test/java/blue/bex/BexIntrinsicTest.java | 54 + .../java/blue/bex/BexLazyBindingTest.java | 4 +- .../java/blue/bex/BexPointerSet20Test.java | 4 +- .../BexStructuredReferenceEvidenceTest.java | 22 +- .../blue/bex/api/Bex20ApiSurfaceTest.java | 16 + .../bex/compile/BexCompileBoundaryTest.java | 86 + .../BexCompiledIrImmutabilityTest.java | 217 + .../bex/compile/BexOperatorCatalogTest.java | 256 ++ .../BexConformancePropertyTest.java | 6 +- .../conformance/BexConformanceReportMain.java | 252 +- .../conformance/BexEngineFixtureAdapter.java | 18 +- .../BexModernizationPropertyTest.java | 375 ++ .../blue/bex/gas/BexGasPrimitivesTest.java | 61 +- .../BexSemanticIdentityIntegrationTest.java | 8 +- .../bex/test/TestGasLedgerCapability.java | 66 + .../BexHostedRuntimeWorkSessionTest.java | 80 +- .../hosted-release/required-public-api.txt | 505 ++- 255 files changed, 20827 insertions(+), 12071 deletions(-) create mode 100644 .github/scripts/compare-independent-builds.mjs create mode 100644 .github/scripts/compare-local-published-evidence.mjs mode change 100755 => 100644 .github/scripts/run-final-publication-gates.sh create mode 100644 blue-bex-conformance/build.gradle.kts create mode 100644 blue-bex-conformance/src/jmh/java/blue/bex/benchmark/BexBenchmarkSupport.java create mode 100644 blue-bex-conformance/src/jmh/java/blue/bex/benchmark/BexCoreBenchmark.java create mode 100644 blue-bex-conformance/src/jmh/java/blue/bex/benchmark/package-info.java create mode 100644 blue-bex-conformance/src/jmh/java/blue/language/processor/BexHostedGasBenchmark.java create mode 100644 blue-bex-conformance/src/main/java/.gitkeep create mode 100644 blue-bex-contracts/build.gradle.kts create mode 100644 blue-bex-contracts/src/main/java/blue/bex/contracts/BexContractsExecutionContext.java create mode 100644 blue-bex-contracts/src/main/java/blue/bex/contracts/BexContractsFailureBoundary.java rename {src/main/java/blue/bex/api => blue-bex-contracts/src/main/java/blue/bex/contracts}/ProcessorExecutionContextBexDocumentView.java (79%) create mode 100644 blue-bex-contracts/src/main/java/blue/bex/contracts/ProcessorExecutionContextBexGasLedgerHost.java rename {src/main/java/blue/bex/output => blue-bex-contracts/src/main/java/blue/bex/contracts}/ProcessorExecutionContextBexSemanticIdentityBoundary.java (80%) create mode 100644 blue-bex-contracts/src/main/java/blue/bex/contracts/package-info.java create mode 100644 blue-bex-core/build.gradle.kts rename {src => blue-bex-core/src}/main/java/blue/bex/BexException.java (100%) create mode 100644 blue-bex-core/src/main/java/blue/bex/BexExecutionEvidenceUnavailableException.java create mode 100644 blue-bex-core/src/main/java/blue/bex/BexInvalidExecutionEvidenceException.java rename {src => blue-bex-core/src}/main/java/blue/bex/BexSourcePath.java (100%) rename {src => blue-bex-core/src}/main/java/blue/bex/api/BexDocumentView.java (83%) rename {src => blue-bex-core/src}/main/java/blue/bex/api/BexEngine.java (74%) rename {src => blue-bex-core/src}/main/java/blue/bex/api/BexExecutionContext.java (91%) create mode 100644 blue-bex-core/src/main/java/blue/bex/api/BexFailureBoundary.java rename {src => blue-bex-core/src}/main/java/blue/bex/api/BexGasLedgerHost.java (81%) rename {src => blue-bex-core/src}/main/java/blue/bex/api/BexIntrinsicInvocation.java (100%) rename {src => blue-bex-core/src}/main/java/blue/bex/api/BexIntrinsicProcessor.java (100%) rename {src => blue-bex-core/src}/main/java/blue/bex/api/BexIntrinsicRegistry.java (96%) rename {src => blue-bex-core/src}/main/java/blue/bex/api/BexMetricsSink.java (58%) rename {src => blue-bex-core/src}/main/java/blue/bex/api/BexProgramSource.java (91%) rename {src => blue-bex-core/src}/main/java/blue/bex/api/BexStepResults.java (92%) rename {src => blue-bex-core/src}/main/java/blue/bex/api/BexTypeBlueIdResolver.java (100%) rename {src => blue-bex-core/src}/main/java/blue/bex/api/FrozenBexDocumentView.java (100%) create mode 100644 blue-bex-core/src/main/java/blue/bex/api/package-info.java create mode 100644 blue-bex-core/src/main/java/blue/bex/compile/BexCompilationInput.java rename {src => blue-bex-core/src}/main/java/blue/bex/compile/BexCompiledProgram.java (58%) rename {src => blue-bex-core/src}/main/java/blue/bex/compile/BexCompiledProgramCache.java (100%) rename {src => blue-bex-core/src}/main/java/blue/bex/compile/BexCompiledProgramKey.java (79%) create mode 100644 blue-bex-core/src/main/java/blue/bex/compile/BexCompiledProgramRuntimeAccess.java create mode 100644 blue-bex-core/src/main/java/blue/bex/compile/BexCompiler.java create mode 100644 blue-bex-core/src/main/java/blue/bex/compile/BexCompilerRuntimeAccess.java create mode 100644 blue-bex-core/src/main/java/blue/bex/compile/BexCompilerSupport.java rename {src => blue-bex-core/src}/main/java/blue/bex/compile/BexContainsCache.java (98%) create mode 100644 blue-bex-core/src/main/java/blue/bex/compile/BexExecutionMachine.java create mode 100644 blue-bex-core/src/main/java/blue/bex/compile/BexExpressionCompiler.java create mode 100644 blue-bex-core/src/main/java/blue/bex/compile/BexGasWork.java create mode 100644 blue-bex-core/src/main/java/blue/bex/compile/BexGasWorkSupport.java create mode 100644 blue-bex-core/src/main/java/blue/bex/compile/BexIntrinsicCatalog.java rename {src => blue-bex-core/src}/main/java/blue/bex/compile/BexNodeFingerprint.java (100%) rename {src => blue-bex-core/src}/main/java/blue/bex/compile/BexNodeIdentity.java (100%) rename {src => blue-bex-core/src}/main/java/blue/bex/compile/BexOperands.java (89%) create mode 100644 blue-bex-core/src/main/java/blue/bex/compile/BexOperatorCatalog.java rename {src => blue-bex-core/src}/main/java/blue/bex/compile/BexPatchEntryParser.java (95%) create mode 100644 blue-bex-core/src/main/java/blue/bex/compile/BexProgramCompiler.java create mode 100644 blue-bex-core/src/main/java/blue/bex/compile/BexStatementCompiler.java rename {src => blue-bex-core/src}/main/java/blue/bex/compile/BexStatements.java (92%) rename {src => blue-bex-core/src}/main/java/blue/bex/compile/CallExpr.java (67%) rename {src => blue-bex-core/src}/main/java/blue/bex/compile/CollectionExpressions.java (96%) rename {src/main/java/blue/bex/runtime => blue-bex-core/src/main/java/blue/bex/compile}/CompileScope.java (82%) rename {src/main/java/blue/bex/runtime => blue-bex-core/src/main/java/blue/bex/compile}/CompiledExpression.java (57%) rename {src/main/java/blue/bex/runtime => blue-bex-core/src/main/java/blue/bex/compile}/CompiledFrame.java (53%) create mode 100644 blue-bex-core/src/main/java/blue/bex/compile/CompiledStatement.java create mode 100644 blue-bex-core/src/main/java/blue/bex/compile/ConstructedExpressions.java create mode 100644 blue-bex-core/src/main/java/blue/bex/compile/Control.java create mode 100644 blue-bex-core/src/main/java/blue/bex/compile/ExpressionBase.java rename {src => blue-bex-core/src}/main/java/blue/bex/compile/LogicNumericExpressions.java (92%) rename {src => blue-bex-core/src}/main/java/blue/bex/compile/LruBexCompiledProgramCache.java (100%) rename {src => blue-bex-core/src}/main/java/blue/bex/compile/ObjectResultExpressions.java (96%) create mode 100644 blue-bex-core/src/main/java/blue/bex/compile/ReadExpressions.java rename {src => blue-bex-core/src}/main/java/blue/bex/compile/TypeStringExpressions.java (98%) create mode 100644 blue-bex-core/src/main/java/blue/bex/compile/package-info.java create mode 100644 blue-bex-core/src/main/java/blue/bex/gas/BexGasAdmission.java create mode 100644 blue-bex-core/src/main/java/blue/bex/gas/BexGasBudget.java rename {src => blue-bex-core/src}/main/java/blue/bex/gas/BexGasCharge.java (100%) create mode 100644 blue-bex-core/src/main/java/blue/bex/gas/BexGasChargeContext.java rename {src => blue-bex-core/src}/main/java/blue/bex/gas/BexGasCounter.java (100%) create mode 100644 blue-bex-core/src/main/java/blue/bex/gas/BexGasCounterCatalog.java create mode 100644 blue-bex-core/src/main/java/blue/bex/gas/BexGasHostSession.java rename {src => blue-bex-core/src}/main/java/blue/bex/gas/BexGasLedger.java (80%) create mode 100644 blue-bex-core/src/main/java/blue/bex/gas/BexGasLedgerCapability.java create mode 100644 blue-bex-core/src/main/java/blue/bex/gas/BexGasLedgerLifecycle.java rename {src => blue-bex-core/src}/main/java/blue/bex/gas/BexGasLimitExceededException.java (83%) rename {src => blue-bex-core/src}/main/java/blue/bex/gas/BexGasManifest.java (100%) create mode 100644 blue-bex-core/src/main/java/blue/bex/gas/BexGasMeter.java rename {src => blue-bex-core/src}/main/java/blue/bex/gas/BexGasSchedule.java (100%) create mode 100644 blue-bex-core/src/main/java/blue/bex/gas/BexGasTraceRecorder.java create mode 100644 blue-bex-core/src/main/java/blue/bex/gas/BexHostGasExhaustion.java create mode 100644 blue-bex-core/src/main/java/blue/bex/gas/BexSharedGasBudget.java create mode 100644 blue-bex-core/src/main/java/blue/bex/gas/package-info.java rename {src => blue-bex-core/src}/main/java/blue/bex/output/BexAdmittedValue.java (100%) rename {src => blue-bex-core/src}/main/java/blue/bex/output/BexEstablishedIdentity.java (100%) create mode 100644 blue-bex-core/src/main/java/blue/bex/output/BexFailurePolicy.java rename {src => blue-bex-core/src}/main/java/blue/bex/output/BexOutputAdmission.java (86%) rename {src => blue-bex-core/src}/main/java/blue/bex/output/BexOutputKind.java (100%) rename {src => blue-bex-core/src}/main/java/blue/bex/output/BexSemanticIdentityBoundary.java (100%) create mode 100644 blue-bex-core/src/main/java/blue/bex/output/package-info.java create mode 100644 blue-bex-core/src/main/java/blue/bex/package-info.java rename {src => blue-bex-core/src}/main/java/blue/bex/pointer/BexPointer.java (100%) rename {src => blue-bex-core/src}/main/java/blue/bex/pointer/BexPointerCache.java (95%) create mode 100644 blue-bex-core/src/main/java/blue/bex/pointer/package-info.java rename {src => blue-bex-core/src}/main/java/blue/bex/result/BexChangeset.java (91%) rename {src => blue-bex-core/src}/main/java/blue/bex/result/BexEvents.java (92%) rename {src => blue-bex-core/src}/main/java/blue/bex/result/BexExecutionResult.java (84%) create mode 100644 blue-bex-core/src/main/java/blue/bex/result/BexMetrics.java create mode 100644 blue-bex-core/src/main/java/blue/bex/result/BexMetricsRecorder.java create mode 100644 blue-bex-core/src/main/java/blue/bex/result/BexMetricsSnapshot.java rename {src => blue-bex-core/src}/main/java/blue/bex/result/BexPatchEntry.java (95%) rename {src => blue-bex-core/src}/main/java/blue/bex/result/BexResultOverlay.java (92%) create mode 100644 blue-bex-core/src/main/java/blue/bex/result/package-info.java rename {src => blue-bex-core/src}/main/java/blue/bex/runtime/BexExecutionAccumulator.java (100%) create mode 100644 blue-bex-core/src/main/java/blue/bex/runtime/BexRuntime.java create mode 100644 blue-bex-core/src/main/java/blue/bex/runtime/BexRuntimeContext.java create mode 100644 blue-bex-core/src/main/java/blue/bex/runtime/BexRuntimeGasSession.java create mode 100644 blue-bex-core/src/main/java/blue/bex/runtime/BexRuntimeIntrinsics.java create mode 100644 blue-bex-core/src/main/java/blue/bex/runtime/BexStepResultView.java create mode 100644 blue-bex-core/src/main/java/blue/bex/runtime/package-info.java create mode 100644 blue-bex-core/src/main/java/blue/bex/spi/BexDocumentAccess.java create mode 100644 blue-bex-core/src/main/java/blue/bex/spi/package-info.java create mode 100644 blue-bex-core/src/main/java/blue/bex/type/BexBlueTypeMatcher.java create mode 100644 blue-bex-core/src/main/java/blue/bex/type/BexFrozenTypeMatcher.java create mode 100644 blue-bex-core/src/main/java/blue/bex/type/BexPatternValidator.java create mode 100644 blue-bex-core/src/main/java/blue/bex/type/BexTypeMatchWorkRecorder.java create mode 100644 blue-bex-core/src/main/java/blue/bex/type/BexTypeMatcher.java create mode 100644 blue-bex-core/src/main/java/blue/bex/type/package-info.java rename {src => blue-bex-core/src}/main/java/blue/bex/value/AbstractBexValue.java (100%) rename {src => blue-bex-core/src}/main/java/blue/bex/value/AdmittedExactBexValue.java (100%) rename {src => blue-bex-core/src}/main/java/blue/bex/value/BexBlueNodeWriter.java (100%) rename {src => blue-bex-core/src}/main/java/blue/bex/value/BexBlueValueImporter.java (100%) create mode 100644 blue-bex-core/src/main/java/blue/bex/value/BexChangesetValueView.java rename {src => blue-bex-core/src}/main/java/blue/bex/value/BexEquality.java (100%) create mode 100644 blue-bex-core/src/main/java/blue/bex/value/BexEventsValueView.java rename {src => blue-bex-core/src}/main/java/blue/bex/value/BexFrozenWriter.java (82%) rename {src => blue-bex-core/src}/main/java/blue/bex/value/BexNodeWriter.java (100%) create mode 100644 blue-bex-core/src/main/java/blue/bex/value/BexPatchValueView.java rename {src => blue-bex-core/src}/main/java/blue/bex/value/BexSimpleWriter.java (100%) rename {src => blue-bex-core/src}/main/java/blue/bex/value/BexTruthiness.java (100%) rename {src => blue-bex-core/src}/main/java/blue/bex/value/BexUnicodeOrder.java (100%) rename {src => blue-bex-core/src}/main/java/blue/bex/value/BexValue.java (91%) create mode 100644 blue-bex-core/src/main/java/blue/bex/value/BexValueKind.java create mode 100644 blue-bex-core/src/main/java/blue/bex/value/BexValueMetrics.java rename {src => blue-bex-core/src}/main/java/blue/bex/value/BexValues.java (94%) rename {src => blue-bex-core/src}/main/java/blue/bex/value/ChangesetBexValue.java (87%) rename {src => blue-bex-core/src}/main/java/blue/bex/value/EventsBexValue.java (88%) rename {src => blue-bex-core/src}/main/java/blue/bex/value/FrozenNodeBexValue.java (100%) rename {src => blue-bex-core/src}/main/java/blue/bex/value/ListBexValue.java (100%) rename {src => blue-bex-core/src}/main/java/blue/bex/value/MapBexValue.java (100%) rename {src => blue-bex-core/src}/main/java/blue/bex/value/NodeBexValue.java (100%) rename {src => blue-bex-core/src}/main/java/blue/bex/value/NullBexValue.java (100%) rename {src => blue-bex-core/src}/main/java/blue/bex/value/OverlayListBexValue.java (100%) rename {src => blue-bex-core/src}/main/java/blue/bex/value/OverlayMapBexValue.java (100%) rename {src => blue-bex-core/src}/main/java/blue/bex/value/PatchEntryBexValue.java (90%) rename {src => blue-bex-core/src}/main/java/blue/bex/value/PointerSetBexValue.java (100%) rename {src => blue-bex-core/src}/main/java/blue/bex/value/ScalarBexValue.java (100%) rename {src => blue-bex-core/src}/main/java/blue/bex/value/UndefinedBexValue.java (100%) create mode 100644 blue-bex-core/src/main/java/blue/bex/value/package-info.java rename {src => blue-bex-core/src}/main/resources/blue/bex/gas/blue-bex-gas-2.0.yaml (100%) create mode 100644 blue-bex-java/build.gradle.kts create mode 100644 blue-bex-java/src/main/java/.gitkeep create mode 100644 build-logic/build.gradle.kts create mode 100644 build-logic/settings.gradle.kts create mode 100644 build-logic/src/main/java/blue/bex/buildlogic/ApiEvidencePlugin.java create mode 100644 build-logic/src/main/java/blue/bex/buildlogic/ArchitectureVerificationPlugin.java create mode 100644 build-logic/src/main/java/blue/bex/buildlogic/ConformanceConventionsPlugin.java create mode 100644 build-logic/src/main/java/blue/bex/buildlogic/Java8LibraryConventionsPlugin.java create mode 100644 build-logic/src/main/java/blue/bex/buildlogic/JmhConventionsPlugin.java create mode 100644 build-logic/src/main/java/blue/bex/buildlogic/LanguageDependencyModeExtension.java create mode 100644 build-logic/src/main/java/blue/bex/buildlogic/LanguageDependencyModePlugin.java create mode 100644 build-logic/src/main/java/blue/bex/buildlogic/PublicationConventionsPlugin.java create mode 100644 build-logic/src/main/java/blue/bex/buildlogic/ReleaseEvidencePlugin.java create mode 100644 build-logic/src/main/java/blue/bex/buildlogic/ReproducibleArchivesPlugin.java create mode 100644 build-logic/src/main/java/blue/bex/buildlogic/RootOrchestrationPlugin.java create mode 100644 build-logic/src/main/java/blue/bex/buildlogic/package-info.java create mode 100644 build-logic/src/main/java/blue/bex/buildlogic/tasks/GenerateApiClassificationTask.java create mode 100644 build-logic/src/main/java/blue/bex/buildlogic/tasks/GenerateBenchmarkEnvironmentTask.java create mode 100644 build-logic/src/main/java/blue/bex/buildlogic/tasks/GenerateDependencyEvidenceTask.java create mode 100644 build-logic/src/main/java/blue/bex/buildlogic/tasks/GenerateModernizationReportTask.java create mode 100644 build-logic/src/main/java/blue/bex/buildlogic/tasks/GenerateReleaseReportTask.java create mode 100644 build-logic/src/main/java/blue/bex/buildlogic/tasks/GenerateSourceFingerprintTask.java create mode 100644 build-logic/src/main/java/blue/bex/buildlogic/tasks/GenerateWorkingReportTask.java create mode 100644 build-logic/src/main/java/blue/bex/buildlogic/tasks/VerifyBexArchitectureTask.java create mode 100644 build-logic/src/main/java/blue/bex/buildlogic/tasks/VerifyJava8BytecodeTask.java create mode 100644 build-logic/src/main/java/blue/bex/buildlogic/tasks/VerifyLegacyLanguageImportsTask.java create mode 100644 build-logic/src/main/java/blue/bex/buildlogic/tasks/VerifyPublishedLanguageTask.java create mode 100644 build-logic/src/main/java/blue/bex/buildlogic/tasks/package-info.java create mode 100644 docs/adding-an-operator.md create mode 100644 docs/architecture.md create mode 100644 docs/blue-output-boundary.md create mode 100644 docs/compiler-and-ir.md create mode 100644 docs/conformance.md create mode 100644 docs/contracts-hosting.md create mode 100644 docs/gas-and-exhaustion.md create mode 100644 docs/intrinsics.md create mode 100644 docs/migrating-to-modular-blue-language.md create mode 100644 docs/program-model.md create mode 100644 docs/release.md create mode 100644 docs/runtime-and-context.md create mode 100644 docs/start-here.md create mode 100644 docs/values-and-identity.md create mode 100644 examples/README.md create mode 100644 examples/build.gradle.kts create mode 100644 examples/src/main/java/blue/bex/examples/HostedBexExample.java create mode 100644 examples/src/main/java/blue/bex/examples/HostedConsumerSmoke.java create mode 100644 examples/src/main/java/blue/bex/examples/StandaloneBexExample.java create mode 100644 examples/src/main/java/blue/bex/examples/package-info.java create mode 100644 examples/src/test/java/blue/bex/examples/HostedConsumerSmokeTest.java create mode 100644 gradle/verification/api/modernization-added-descriptors.txt create mode 100644 gradle/verification/api/modernization-removed-descriptors.txt create mode 100644 gradle/verification/api/working-checkpoint-public-api-classification.json create mode 100644 gradle/verification/api/working-checkpoint-public-api.txt delete mode 100644 src/main/java/blue/bex/api/ProcessorExecutionContextBexGasLedgerHost.java delete mode 100644 src/main/java/blue/bex/compile/BexCompiler.java delete mode 100644 src/main/java/blue/bex/compile/BexExpressions.java delete mode 100644 src/main/java/blue/bex/gas/BexGasMeter.java delete mode 100644 src/main/java/blue/bex/result/BexMetrics.java delete mode 100644 src/main/java/blue/bex/runtime/BexRuntime.java delete mode 100644 src/main/java/blue/bex/runtime/CompiledStatement.java delete mode 100644 src/main/java/blue/bex/runtime/Control.java delete mode 100644 src/main/java/blue/bex/type/BexBlueTypeMatcher.java create mode 100644 src/test/java/blue/bex/BexConcurrentEngineIsolationTest.java create mode 100644 src/test/java/blue/bex/compile/BexCompileBoundaryTest.java create mode 100644 src/test/java/blue/bex/compile/BexCompiledIrImmutabilityTest.java create mode 100644 src/test/java/blue/bex/compile/BexOperatorCatalogTest.java create mode 100644 src/test/java/blue/bex/conformance/BexModernizationPropertyTest.java create mode 100644 src/test/java/blue/bex/test/TestGasLedgerCapability.java diff --git a/.github/scripts/compare-independent-builds.mjs b/.github/scripts/compare-independent-builds.mjs new file mode 100644 index 0000000..f225f93 --- /dev/null +++ b/.github/scripts/compare-independent-builds.mjs @@ -0,0 +1,55 @@ +#!/usr/bin/env node + +import { createHash } from 'node:crypto'; +import { readFileSync, writeFileSync } from 'node:fs'; + +const [standaloneOnePath, standaloneTwoPath, localOnePath, localTwoPath, + bexCommit, outputPath] = process.argv.slice(2); +if (!standaloneOnePath || !standaloneTwoPath || !localOnePath || + !localTwoPath || !bexCommit || !outputPath) { + throw new Error( + 'usage: compare-independent-builds.mjs S1 S2 L1 L2 COMMIT OUTPUT' + ); +} + +function load(path) { + const text = readFileSync(path, 'utf8'); + if (!text.trim()) { + throw new Error(`empty artifact manifest: ${path}`); + } + return text; +} + +function digest(text) { + return createHash('sha256').update(text).digest('hex'); +} + +function pair(firstPath, secondPath) { + const first = load(firstPath); + const second = load(secondPath); + const passed = first === second; + return { + status: passed ? 'passed' : 'failed', + firstManifestSha256: digest(first), + secondManifestSha256: digest(second), + exactArtifactBytesMatch: passed, + artifactCount: first.trim().split(/\r?\n/).length + }; +} + +const standalonePublished = pair(standaloneOnePath, standaloneTwoPath); +const localComposite = pair(localOnePath, localTwoPath); +const passed = standalonePublished.status === 'passed' && + localComposite.status === 'passed'; +const report = { + schema: 'blue-bex-independent-clean-builds/1.0', + status: passed ? 'passed' : 'failed', + bexCommit, + isolatedGradleHomes: 4, + standalonePublished, + localComposite +}; +writeFileSync(outputPath, `${JSON.stringify(report, null, 2)}\n`); +if (!passed) { + process.exitCode = 1; +} diff --git a/.github/scripts/compare-local-published-evidence.mjs b/.github/scripts/compare-local-published-evidence.mjs new file mode 100644 index 0000000..228b12f --- /dev/null +++ b/.github/scripts/compare-local-published-evidence.mjs @@ -0,0 +1,99 @@ +#!/usr/bin/env node + +import { createHash } from 'node:crypto'; +import { readFileSync, writeFileSync } from 'node:fs'; + +const [localPath, publishedPath, bexCommit, outputPath] = process.argv.slice(2); +if (!localPath || !publishedPath || !bexCommit || !outputPath) { + throw new Error( + 'usage: compare-local-published-evidence.mjs LOCAL PUBLISHED COMMIT OUTPUT' + ); +} + +const local = JSON.parse(readFileSync(localPath, 'utf8')); +const published = JSON.parse(readFileSync(publishedPath, 'utf8')); + +const semanticKeys = [ + 'identities', + 'finalTotals', + 'normativeVectorCoverage', + 'operatorCoverage', + 'representationMatrix', + 'representationMatrixResult', + 'cacheMatrix', + 'recursionEvidence', + 'finiteLoopEvidence', + 'semanticBoundaryInvocationEvidence', + 'cyclicProofEvidence', + 'cyclicProofUnavailabilityCapability', + 'intrinsicEvidence', + 'referenceEvidenceClassificationEvidence', + 'hostedLocalLimitCapability', + 'ledgerLifecycleEvidence' +]; +const gasKeys = [ + 'identities', + 'finalTotals', + 'counterCoverage', + 'gasExhaustionEvidence', + 'gasExhaustionTraceExamples', + 'maximumObservedOrderedTraceEntries' +]; + +function selected(report, keys) { + return Object.fromEntries(keys.map((key) => [key, report[key] ?? null])); +} + +function canonical(value) { + if (Array.isArray(value)) { + return `[${value.map(canonical).join(',')}]`; + } + if (value && typeof value === 'object') { + return `{${Object.keys(value).sort().map( + (key) => `${JSON.stringify(key)}:${canonical(value[key])}` + ).join(',')}}`; + } + return JSON.stringify(value); +} + +function digest(value) { + return createHash('sha256').update(canonical(value)).digest('hex'); +} + +const localSemantic = selected(local, semanticKeys); +const publishedSemantic = selected(published, semanticKeys); +const localGas = selected(local, gasKeys); +const publishedGas = selected(published, gasKeys); +const semanticAndGasParity = + canonical(localSemantic) === canonical(publishedSemantic); +const exactGasTraceParity = canonical(localGas) === canonical(publishedGas); +const sourceBound = local.commit === bexCommit && published.commit === bexCommit; +const dependenciesDistinct = + local.dependency?.mode === 'local-composite' && + published.dependency?.mode === 'standalone-published'; +const passed = semanticAndGasParity && exactGasTraceParity && sourceBound && + dependenciesDistinct; + +const report = { + schema: 'blue-bex-local-published-differential/1.0', + status: passed ? 'passed' : 'failed', + bexCommit, + localMode: local.dependency?.mode ?? 'missing', + publishedMode: published.dependency?.mode ?? 'missing', + sourceBound, + dependenciesDistinct, + semanticAndGasParity: semanticAndGasParity ? 'passed' : 'failed', + exactGasTraceParity: exactGasTraceParity ? 'passed' : 'failed', + semanticEvidenceSha256: { + local: digest(localSemantic), + published: digest(publishedSemantic) + }, + gasEvidenceSha256: { + local: digest(localGas), + published: digest(publishedGas) + } +}; +writeFileSync(outputPath, `${JSON.stringify(report, null, 2)}\n`); +if (!passed) { + process.exitCode = 1; +} diff --git a/.github/scripts/run-final-publication-gates.sh b/.github/scripts/run-final-publication-gates.sh old mode 100755 new mode 100644 index 394381a..2f683b9 --- a/.github/scripts/run-final-publication-gates.sh +++ b/.github/scripts/run-final-publication-gates.sh @@ -1,174 +1,257 @@ #!/usr/bin/env bash set -euo pipefail -readonly SCRIPT_DIR="$( - cd "$(dirname "${BASH_SOURCE[0]}")" - pwd -)" -readonly BEX_REPOSITORY="$( - cd "$SCRIPT_DIR/../.." - pwd -)" +readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +readonly BEX_REPOSITORY="$(cd "$SCRIPT_DIR/../.." && pwd)" readonly INSPECTION_FILE="$BEX_REPOSITORY/src/test/resources/hosted-release/published-api-inspection.properties" readonly LANGUAGE_REPOSITORY_URL="${BLUE_LANGUAGE_REPOSITORY_URL:-https://github.com/bluecontract/blue-language-java.git}" -readonly BEX_RELEASE_TEMP_ROOT="$( - mktemp -d "${RUNNER_TEMP:-${TMPDIR:-/tmp}}/blue-bex-publication.XXXXXX" -)" -readonly LANGUAGE_CHECKOUT="$BEX_RELEASE_TEMP_ROOT/blue-language-java" -readonly FIRST_BEX_CHECKOUT="$BEX_RELEASE_TEMP_ROOT/blue-bex-clean-one" -readonly SECOND_BEX_CHECKOUT="$BEX_RELEASE_TEMP_ROOT/blue-bex-clean-two" -readonly LOCAL_FIRST_BEX_CHECKOUT="$BEX_RELEASE_TEMP_ROOT/blue-bex-local-clean-one" -readonly LOCAL_SECOND_BEX_CHECKOUT="$BEX_RELEASE_TEMP_ROOT/blue-bex-local-clean-two" -readonly RECEIPT_ROOT="$BEX_RELEASE_TEMP_ROOT/receipts" -readonly STANDALONE_FIRST_RECEIPT="$RECEIPT_ROOT/standalone-first.properties" -readonly STANDALONE_SECOND_RECEIPT="$RECEIPT_ROOT/standalone-second.properties" -readonly LOCAL_FIRST_RECEIPT="$RECEIPT_ROOT/local-composite-first.properties" -readonly LOCAL_SECOND_RECEIPT="$RECEIPT_ROOT/local-composite-second.properties" +readonly RELEASE_ROOT="$(mktemp -d "${RUNNER_TEMP:-${TMPDIR:-/tmp}}/blue-bex-publication.XXXXXX")" +readonly LANGUAGE_CHECKOUT="$RELEASE_ROOT/blue-language-java" +readonly RECEIPT_ROOT="$RELEASE_ROOT/receipts" +readonly INDEPENDENT_REPORT="$RECEIPT_ROOT/independent-clean-builds.json" +readonly DIFFERENTIAL_REPORT="$RECEIPT_ROOT/local-published-differential.json" +readonly RETAINED_INPUT_ROOT="$BEX_REPOSITORY/build/reports/bex-release/inputs" +readonly RETAINED_ARTIFACT_ROOT="$RETAINED_INPUT_ROOT/published-artifacts" +readonly BEX_COMMIT="$(git -C "$BEX_REPOSITORY" rev-parse HEAD)" +readonly SOURCE_COMMIT_EPOCH="$(git -C "$BEX_REPOSITORY" show -s --format=%ct "$BEX_COMMIT")" +release_succeeded=false + +cleanup() { + if [[ "$release_succeeded" != true || -z "${GITHUB_ENV:-}" ]]; then + rm -rf "$RELEASE_ROOT" + fi +} +trap cleanup EXIT property_value() { local key="$1" - sed -n "s/^${key}=//p" "$INSPECTION_FILE" | head -n 1 + awk -F= -v requested="$key" '$1 == requested { + sub(/^[^=]*=/, "") + print + exit + }' "$INSPECTION_FILE" } -readonly LANGUAGE_COMMIT="$(property_value "source.commit")" -readonly LANGUAGE_TAG="$(property_value "source.tag")" -readonly LANGUAGE_API_STATUS="$(property_value "status")" -readonly BEX_COMMIT="$(git -C "$BEX_REPOSITORY" rev-parse HEAD)" -readonly SOURCE_COMMIT_EPOCH="$( - git -C "$BEX_REPOSITORY" show -s --format=%ct "$BEX_COMMIT" -)" +sha256_file() { + local path="$1" + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$path" | awk '{print $1}' + else + shasum -a 256 "$path" | awk '{print $1}' + fi +} -if [[ ! "$LANGUAGE_COMMIT" =~ ^[0-9a-fA-F]{40}$ ]]; then - echo "Recorded final Blue Language commit is unavailable." >&2 +resolve_reviewed_aggregate() { + local repository="$1" + local coordinate="$2" + local output="$3" + local group artifact version remainder group_path url + IFS=: read -r group artifact version remainder <<< "$coordinate" + if [[ -z "$group" || -z "$artifact" || -z "$version" || -n "${remainder:-}" ]]; then + echo "Reviewed Language coordinate is not group:artifact:version: $coordinate" >&2 + exit 1 + fi + group_path="${group//./\/}" + url="${repository%/}/$group_path/$artifact/$version/$artifact-$version.jar" + mkdir -p "$(dirname "$output")" + curl --fail --silent --show-error --location --retry 3 \ + --output "$output" "$url" +} + +artifact_manifest() { + local checkout="$1" + local output="$2" + local file + : > "$output" + while IFS= read -r file; do + printf '%s %s\n' \ + "$(sha256_file "$file")" \ + "${file#"$checkout"/}" >> "$output" + done < <(find \ + "$checkout/blue-bex-core/build/libs" \ + "$checkout/blue-bex-contracts/build/libs" \ + "$checkout/blue-bex-java/build/libs" \ + "$checkout/build/distributions" \ + -type f \( -name '*.jar' -o -name '*.zip' \) -print | LC_ALL=C sort) + if [[ ! -s "$output" ]]; then + echo "No release artifacts were produced by $checkout" >&2 + exit 1 + fi +} + +clone_bex() { + local destination="$1" + git clone --quiet --no-hardlinks "$BEX_REPOSITORY" "$destination" + git -C "$destination" checkout --quiet --detach "$BEX_COMMIT" +} + +run_isolated_build() { + local checkout="$1" + local gradle_home="$2" + local mode="$3" + local arguments=( + --no-daemon + -p "$checkout" + clean + assemble + bexConformance + sourceReleaseArchive + ) + if [[ "$mode" == "local-composite" ]]; then + arguments+=("-PblueLanguageCompositePath=$LANGUAGE_CHECKOUT") + fi + GRADLE_USER_HOME="$gradle_home" \ + "$checkout/gradlew" "${arguments[@]}" +} + +readonly LANGUAGE_COMMIT="$(property_value source.commit)" +readonly LANGUAGE_TAG="$(property_value source.tag)" +readonly LANGUAGE_ARTIFACT_REPOSITORY="$(property_value repository)" +readonly LANGUAGE_COORDINATE="$(property_value coordinate)" +readonly LANGUAGE_ARTIFACT_SHA256="$(property_value artifact.sha256)" +readonly LANGUAGE_API_STATUS="$(property_value status)" +readonly EXPECTED_RELEASE_TAG="v$(sed -n 's/^version = "\([^"]*\)"/\1/p' "$BEX_REPOSITORY/.cz.toml" | head -n 1)" + +if [[ "$LANGUAGE_API_STATUS" != "compatible-with-final-hosted-adapter" ]]; then + echo "Published Language inspection is not compatible: $LANGUAGE_API_STATUS" >&2 exit 1 fi -if [[ ! "$LANGUAGE_TAG" =~ ^[A-Za-z0-9][A-Za-z0-9._/-]*$ || - "$LANGUAGE_TAG" == *..* ]]; then - echo "Recorded final Blue Language tag is unavailable or invalid." >&2 +if [[ ! "$LANGUAGE_COMMIT" =~ ^[0-9a-fA-F]{40}$ ]]; then + echo "Published Language source commit is missing." >&2 exit 1 fi -if [[ "$LANGUAGE_API_STATUS" != "compatible-with-final-hosted-adapter" ]]; then - echo "Recorded Blue Language artifact is not final-host compatible: $LANGUAGE_API_STATUS" >&2 +if [[ ! "$LANGUAGE_ARTIFACT_SHA256" =~ ^[0-9a-f]{64}$ ]]; then + echo "Published Language artifact SHA-256 is missing." >&2 exit 1 fi if [[ -n "$(git -C "$BEX_REPOSITORY" status --porcelain --untracked-files=all)" ]]; then echo "Publication requires a completely clean BEX checkout." >&2 exit 1 fi +if ! git -C "$BEX_REPOSITORY" tag --points-at HEAD | grep -Fxq "$EXPECTED_RELEASE_TAG"; then + echo "Publication requires exact tag $EXPECTED_RELEASE_TAG at HEAD." >&2 + exit 1 +fi -mkdir -p "$LANGUAGE_CHECKOUT" -git -C "$LANGUAGE_CHECKOUT" init -git -C "$LANGUAGE_CHECKOUT" remote add origin "$LANGUAGE_REPOSITORY_URL" -git -C "$LANGUAGE_CHECKOUT" fetch \ - --depth=1 \ - origin \ +git clone --quiet --filter=blob:none --no-checkout \ + "$LANGUAGE_REPOSITORY_URL" "$LANGUAGE_CHECKOUT" +git -C "$LANGUAGE_CHECKOUT" fetch --quiet --depth=1 origin \ "refs/tags/$LANGUAGE_TAG:refs/tags/$LANGUAGE_TAG" -git -C "$LANGUAGE_CHECKOUT" checkout --detach "refs/tags/$LANGUAGE_TAG" - +git -C "$LANGUAGE_CHECKOUT" checkout --quiet --detach "refs/tags/$LANGUAGE_TAG" if [[ "$(git -C "$LANGUAGE_CHECKOUT" rev-parse HEAD)" != "$LANGUAGE_COMMIT" ]]; then - echo "Blue Language checkout does not match the recorded commit." >&2 + echo "Published Language tag does not resolve to the reviewed commit." >&2 exit 1 fi if [[ -n "$(git -C "$LANGUAGE_CHECKOUT" status --porcelain --untracked-files=all)" ]]; then - echo "Publication requires a clean Blue Language composite checkout." >&2 + echo "Language release checkout is dirty." >&2 exit 1 fi export CI=true export SOURCE_DATE_EPOCH="$SOURCE_COMMIT_EPOCH" -if [[ -n "${GITHUB_ENV:-}" ]]; then - printf 'SOURCE_DATE_EPOCH=%s\n' "$SOURCE_COMMIT_EPOCH" >> "$GITHUB_ENV" -fi +mkdir -p "$RECEIPT_ROOT" cd "$BEX_REPOSITORY" +./gradlew --no-daemon clean bexWorkingVerification \ + "-PblueLanguageCompositePath=$LANGUAGE_CHECKOUT" +./gradlew --no-daemon bexModernizationVerification \ + "-PblueLanguageCompositePath=$LANGUAGE_CHECKOUT" -# Assemble each dependency mode twice from its own pair of clean checkouts of -# this exact BEX commit. Keeping four roots preserves the receipt-owned -# Language JAR and BEX artifacts until every later report has re-hashed them. -git clone --no-hardlinks "$BEX_REPOSITORY" "$FIRST_BEX_CHECKOUT" -git clone --no-hardlinks "$BEX_REPOSITORY" "$SECOND_BEX_CHECKOUT" -git clone --no-hardlinks "$BEX_REPOSITORY" "$LOCAL_FIRST_BEX_CHECKOUT" -git clone --no-hardlinks "$BEX_REPOSITORY" "$LOCAL_SECOND_BEX_CHECKOUT" -git -C "$FIRST_BEX_CHECKOUT" checkout --detach "$BEX_COMMIT" -git -C "$SECOND_BEX_CHECKOUT" checkout --detach "$BEX_COMMIT" -git -C "$LOCAL_FIRST_BEX_CHECKOUT" checkout --detach "$BEX_COMMIT" -git -C "$LOCAL_SECOND_BEX_CHECKOUT" checkout --detach "$BEX_COMMIT" -mkdir -p "$RECEIPT_ROOT" +declare -a checkouts=( + "$RELEASE_ROOT/standalone-one" + "$RELEASE_ROOT/standalone-two" + "$RELEASE_ROOT/local-one" + "$RELEASE_ROOT/local-two" +) +for checkout in "${checkouts[@]}"; do + clone_bex "$checkout" +done + +run_isolated_build "${checkouts[0]}" "$RELEASE_ROOT/gradle-standalone-one" standalone-published +run_isolated_build "${checkouts[1]}" "$RELEASE_ROOT/gradle-standalone-two" standalone-published +run_isolated_build "${checkouts[2]}" "$RELEASE_ROOT/gradle-local-one" local-composite +run_isolated_build "${checkouts[3]}" "$RELEASE_ROOT/gradle-local-two" local-composite + +readonly STANDALONE_ONE_MANIFEST="$RECEIPT_ROOT/standalone-one.sha256" +readonly STANDALONE_TWO_MANIFEST="$RECEIPT_ROOT/standalone-two.sha256" +readonly LOCAL_ONE_MANIFEST="$RECEIPT_ROOT/local-one.sha256" +readonly LOCAL_TWO_MANIFEST="$RECEIPT_ROOT/local-two.sha256" +artifact_manifest "${checkouts[0]}" "$STANDALONE_ONE_MANIFEST" +artifact_manifest "${checkouts[1]}" "$STANDALONE_TWO_MANIFEST" +artifact_manifest "${checkouts[2]}" "$LOCAL_ONE_MANIFEST" +artifact_manifest "${checkouts[3]}" "$LOCAL_TWO_MANIFEST" +node "$SCRIPT_DIR/compare-independent-builds.mjs" \ + "$STANDALONE_ONE_MANIFEST" "$STANDALONE_TWO_MANIFEST" \ + "$LOCAL_ONE_MANIFEST" "$LOCAL_TWO_MANIFEST" \ + "$BEX_COMMIT" "$INDEPENDENT_REPORT" + +node "$SCRIPT_DIR/compare-local-published-evidence.mjs" \ + "${checkouts[2]}/blue-bex-conformance/build/reports/bex-conformance/report.json" \ + "${checkouts[0]}/blue-bex-conformance/build/reports/bex-conformance/report.json" \ + "$BEX_COMMIT" "$DIFFERENTIAL_REPORT" + +readonly REVIEWED_AGGREGATE_ARTIFACT="$RECEIPT_ROOT/published-language-aggregate.jar" +resolve_reviewed_aggregate \ + "$LANGUAGE_ARTIFACT_REPOSITORY" \ + "$LANGUAGE_COORDINATE" \ + "$REVIEWED_AGGREGATE_ARTIFACT" +if [[ "$(sha256_file "$REVIEWED_AGGREGATE_ARTIFACT")" != "$LANGUAGE_ARTIFACT_SHA256" ]]; then + echo "Resolved aggregate Language artifact does not match the reviewed SHA-256." >&2 + exit 1 +fi -GRADLE_USER_HOME="$BEX_RELEASE_TEMP_ROOT/gradle-clean-one" \ - "$FIRST_BEX_CHECKOUT/gradlew" \ - --no-daemon \ - -p "$FIRST_BEX_CHECKOUT" \ - clean test writeCleanBuildArtifactHashes -GRADLE_USER_HOME="$BEX_RELEASE_TEMP_ROOT/gradle-clean-two" \ - "$SECOND_BEX_CHECKOUT/gradlew" \ - --no-daemon \ - -p "$SECOND_BEX_CHECKOUT" \ - clean test writeCleanBuildArtifactHashes - -cp \ - "$FIRST_BEX_CHECKOUT/build/reports/bex-release/clean-build-artifacts.properties" \ - "$STANDALONE_FIRST_RECEIPT" -cp \ - "$SECOND_BEX_CHECKOUT/build/reports/bex-release/clean-build-artifacts.properties" \ - "$STANDALONE_SECOND_RECEIPT" - -GRADLE_USER_HOME="$BEX_RELEASE_TEMP_ROOT/gradle-evidence-verifier" \ - ./gradlew --no-daemon verifyIndependentCleanBuildReproducibility \ - -PcleanBuildEvidenceOne="$STANDALONE_FIRST_RECEIPT" \ - -PcleanBuildEvidenceTwo="$STANDALONE_SECOND_RECEIPT" - -# Record each supported dependency mode separately after the independent -# archive evidence exists. Fresh Gradle homes make standalone dependency-cache -# provenance explicit and prevent one mode from borrowing resolution state -# from the other. -GRADLE_USER_HOME="$BEX_RELEASE_TEMP_ROOT/gradle-standalone-mode" \ - ./gradlew --no-daemon clean test \ - -PblueLanguageRequireFreshModuleCache=true - -GRADLE_USER_HOME="$BEX_RELEASE_TEMP_ROOT/gradle-local-clean-one" \ - "$LOCAL_FIRST_BEX_CHECKOUT/gradlew" \ - --no-daemon \ - -p "$LOCAL_FIRST_BEX_CHECKOUT" \ - clean test writeCleanBuildArtifactHashes \ - -PblueLanguageCompositePath="$LANGUAGE_CHECKOUT" -GRADLE_USER_HOME="$BEX_RELEASE_TEMP_ROOT/gradle-local-clean-two" \ - "$LOCAL_SECOND_BEX_CHECKOUT/gradlew" \ - --no-daemon \ - -p "$LOCAL_SECOND_BEX_CHECKOUT" \ - clean test writeCleanBuildArtifactHashes \ - -PblueLanguageCompositePath="$LANGUAGE_CHECKOUT" - -cp \ - "$LOCAL_FIRST_BEX_CHECKOUT/build/reports/bex-release/clean-build-artifacts.properties" \ - "$LOCAL_FIRST_RECEIPT" -cp \ - "$LOCAL_SECOND_BEX_CHECKOUT/build/reports/bex-release/clean-build-artifacts.properties" \ - "$LOCAL_SECOND_RECEIPT" - -GRADLE_USER_HOME="$BEX_RELEASE_TEMP_ROOT/gradle-local-evidence-verifier" \ - ./gradlew --no-daemon verifyIndependentCleanBuildReproducibility \ - -PblueLanguageCompositePath="$LANGUAGE_CHECKOUT" \ - -PcleanBuildEvidenceOne="$LOCAL_FIRST_RECEIPT" \ - -PcleanBuildEvidenceTwo="$LOCAL_SECOND_RECEIPT" - -GRADLE_USER_HOME="$BEX_RELEASE_TEMP_ROOT/gradle-local-mode" \ - ./gradlew --no-daemon clean test \ - -PblueLanguageCompositePath="$LANGUAGE_CHECKOUT" - -# Rebuild the exact standalone publication outputs from a fresh dependency -# cache and make the single fail-closed readiness decision consumed below by -# the publication workflows. -GRADLE_USER_HOME="$BEX_RELEASE_TEMP_ROOT/gradle-final-standalone" \ - ./gradlew --no-daemon clean bexReleaseEvidence - -readonly CLEAN_BUILD_EVIDENCE_ARCHIVE="$BEX_REPOSITORY/build/reports/bex-release/independent-clean-builds" -mkdir -p "$CLEAN_BUILD_EVIDENCE_ARCHIVE" -cp "$STANDALONE_FIRST_RECEIPT" \ - "$CLEAN_BUILD_EVIDENCE_ARCHIVE/standalone-first.properties" -cp "$STANDALONE_SECOND_RECEIPT" \ - "$CLEAN_BUILD_EVIDENCE_ARCHIVE/standalone-second.properties" -cp "$LOCAL_FIRST_RECEIPT" \ - "$CLEAN_BUILD_EVIDENCE_ARCHIVE/local-composite-first.properties" -cp "$LOCAL_SECOND_RECEIPT" \ - "$CLEAN_BUILD_EVIDENCE_ARCHIVE/local-composite-second.properties" +published_artifacts=("$REVIEWED_AGGREGATE_ARTIFACT") +while IFS= read -r -d '' artifact; do + published_artifacts+=("$artifact") +done < <(find \ + "$RELEASE_ROOT/gradle-standalone-one/caches/modules-2/files-2.1/blue.language" \ + -type f -name '*.jar' -print0) +if [[ "${#published_artifacts[@]}" -eq 1 ]]; then + echo "No focused published Language module artifacts were resolved in the isolated cache." >&2 + exit 1 +fi +artifact_match=false +for artifact in "${published_artifacts[@]}"; do + if [[ "$(sha256_file "$artifact")" == "$LANGUAGE_ARTIFACT_SHA256" ]]; then + artifact_match=true + fi +done +if [[ "$artifact_match" != true ]]; then + echo "No resolved Language artifact matches the reviewed aggregate SHA-256." >&2 + exit 1 +fi +mkdir -p "$RETAINED_ARTIFACT_ROOT" +cp "$INDEPENDENT_REPORT" \ + "$RETAINED_INPUT_ROOT/independent-clean-builds.json" +cp "$DIFFERENTIAL_REPORT" \ + "$RETAINED_INPUT_ROOT/local-published-differential.json" +for artifact in "${published_artifacts[@]}"; do + cp "$artifact" "$RETAINED_ARTIFACT_ROOT/$(basename "$artifact")" +done +retained_artifacts=() +while IFS= read -r -d '' artifact; do + retained_artifacts+=("$artifact") +done < <(find "$RETAINED_ARTIFACT_ROOT" -type f -name '*.jar' -print0) +readonly RETAINED_ARTIFACT_PATHS="$(IFS=:; echo "${retained_artifacts[*]}")" + +# Re-run the root conformance surface in an exact-version empty cache so its +# detailed mode matrix observes both local-composite and published execution. +GRADLE_USER_HOME="$RELEASE_ROOT/gradle-root-standalone" \ + ./gradlew --no-daemon bexConformance +./gradlew --no-daemon generateBexModernizationReport + +./gradlew --no-daemon bexReleaseVerify \ + "-PblueLanguageCompositePath=$LANGUAGE_CHECKOUT" \ + "-PbexPublishedLanguageCoordinate=$LANGUAGE_COORDINATE" \ + "-PbexPublishedLanguageSha256=$LANGUAGE_ARTIFACT_SHA256" \ + "-PbexPublishedLanguageArtifacts=$RETAINED_ARTIFACT_PATHS" \ + "-PbexLocalPublishedDifferential=$RETAINED_INPUT_ROOT/local-published-differential.json" \ + "-PbexIndependentCleanBuildReport=$RETAINED_INPUT_ROOT/independent-clean-builds.json" + +jq -e '.releaseReady == true' \ + "$BEX_REPOSITORY/build/reports/bex-release/final.json" >/dev/null + +if [[ -n "${GITHUB_ENV:-}" ]]; then + printf 'BLUE_LANGUAGE_COMPOSITE_PATH=%s\n' "$LANGUAGE_CHECKOUT" >> "$GITHUB_ENV" +fi +release_succeeded=true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3edcad4..a9213fe 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,44 +7,67 @@ on: - next - 'feature/*' - 'fix/*' - - 'hotix/*' + - 'hotfix/*' - 'release/*' + pull_request: jobs: Build: runs-on: ubuntu-latest env: CI: true + defaults: + run: + working-directory: blue-bex-java steps: - - uses: actions/checkout@v4 + - name: Check out BEX + uses: actions/checkout@v4 + with: + path: blue-bex-java + fetch-depth: 0 - - name: Set up Java 8 test runtime - uses: actions/setup-java@v3 + - name: Check out exact verified Blue Language + uses: actions/checkout@v4 with: - java-version: '8' - distribution: 'corretto' + repository: bluecontract/blue-language-java + ref: 9a607e584ff5dd973684d35d71eb4022d946b760 + path: blue-language-java - name: Set up JDK 25 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: '25' distribution: 'corretto' - - name: Setup Gradle - uses: gradle/gradle-build-action@v2 + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Run clean local-composite working gate + run: >- + ./gradlew --no-daemon clean bexWorkingVerification + -PblueLanguageCompositePath=../blue-language-java - - name: Execute Gradle build - run: ./gradlew clean build + - name: Run modernization and serious benchmark gate + run: >- + ./gradlew --no-daemon bexModernizationVerification + -PblueLanguageCompositePath=../blue-language-java - - name: Archive test results + - name: Archive reports and test results uses: actions/upload-artifact@v4 - if: always() # run even if build failed + if: always() with: - name: test-results - path: build/reports + name: bex-evidence + path: | + blue-bex-java/**/build/reports/** + blue-bex-java/**/build/test-results/** - - name: Archive libs + - name: Archive modular release artifacts uses: actions/upload-artifact@v4 + if: always() with: - name: libs - path: build/libs + name: bex-artifacts + path: | + blue-bex-java/blue-bex-core/build/libs/** + blue-bex-java/blue-bex-contracts/build/libs/** + blue-bex-java/blue-bex-java/build/libs/** + blue-bex-java/build/distributions/** diff --git a/.github/workflows/release-rc.yml b/.github/workflows/release-rc.yml index 8be875d..3ea67f7 100644 --- a/.github/workflows/release-rc.yml +++ b/.github/workflows/release-rc.yml @@ -12,6 +12,14 @@ on: - 'gradle/wrapper/**' - 'gradlew' - 'gradlew.bat' + - 'build-logic/**' + - 'blue-bex-core/**' + - 'blue-bex-contracts/**' + - 'blue-bex-conformance/**' + - 'blue-bex-java/**' + - 'examples/**' + - 'docs/**' + - 'specifications/**' - 'src/**' env: @@ -73,7 +81,9 @@ jobs: run: bash .github/scripts/run-final-publication-gates.sh - name: Execute Gradle publish - run: ./gradlew publish + run: >- + ./gradlew publish + -PblueLanguageCompositePath="$BLUE_LANGUAGE_COMPOSITE_PATH" - name: Execute Gradle release env: @@ -83,7 +93,9 @@ jobs: JRELEASER_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} JRELEASER_GPG_PUBLIC_KEY: ${{ secrets.GPG_PUBLIC_KEY }} JRELEASER_GPG_SECRET_KEY: ${{ secrets.GPG_SECRET_KEY }} - run: ./gradlew jreleaserFullRelease + run: >- + ./gradlew jreleaserFullRelease + -PblueLanguageCompositePath="$BLUE_LANGUAGE_COMPOSITE_PATH" - name: Push release commit and tag run: git push origin HEAD:next --follow-tags @@ -95,9 +107,13 @@ jobs: name: rc-artifacts path: | build/distributions - build/libs - build/publications + blue-bex-core/build/libs + blue-bex-contracts/build/libs + blue-bex-java/build/libs + blue-bex-core/build/publications + blue-bex-contracts/build/publications + blue-bex-java/build/publications build/reports - build/test-results + **/build/reports + **/build/test-results build/jreleaser - .gradle/bex-hosted-release diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7d98e3e..a049b70 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -43,7 +43,9 @@ jobs: run: bash .github/scripts/run-final-publication-gates.sh - name: Execute Gradle publish - run: ./gradlew publish + run: >- + ./gradlew publish + -PblueLanguageCompositePath="$BLUE_LANGUAGE_COMPOSITE_PATH" - name: Execute Gradle release env: @@ -53,7 +55,9 @@ jobs: JRELEASER_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} JRELEASER_GPG_PUBLIC_KEY: ${{ secrets.GPG_PUBLIC_KEY }} JRELEASER_GPG_SECRET_KEY: ${{ secrets.GPG_SECRET_KEY }} - run: ./gradlew jreleaserFullRelease + run: >- + ./gradlew jreleaserFullRelease + -PblueLanguageCompositePath="$BLUE_LANGUAGE_COMPOSITE_PATH" - name: Archive artifacts uses: actions/upload-artifact@v4 @@ -62,9 +66,13 @@ jobs: name: artifacts path: | build/distributions - build/libs - build/publications + blue-bex-core/build/libs + blue-bex-contracts/build/libs + blue-bex-java/build/libs + blue-bex-core/build/publications + blue-bex-contracts/build/publications + blue-bex-java/build/publications build/reports - build/test-results + **/build/reports + **/build/test-results build/jreleaser - .gradle/bex-hosted-release diff --git a/README.md b/README.md index bb47bf9..1edc7ce 100644 --- a/README.md +++ b/README.md @@ -1,929 +1,125 @@ # blue-bex-java -`blue-bex-java` is a compiled Java engine for **Blue Expression Objects** -(**BEX**): a deterministic scripting model written as Blue data. +`blue-bex-java` compiles and executes Blue Expression Objects (BEX): +deterministic programs represented as Blue object trees. A BEX program reads a +host-supplied document view and bindings, computes Blue-compatible values, and +returns a `BexExecutionResult`. It never applies patches, emits workflow events, +or performs I/O by itself. -BEX is for logic that should live inside Blue documents instead of host code. -A BEX program can read a Blue document view, host-provided bindings, constants, -variables, and prior results, then compute structured Blue-compatible data. -Because BEX programs are Blue data, they can participate in the same content -identity and BlueId model as the documents that carry them. +BEX is deliberately not JavaScript, a contract processor, or a general plugin +runtime. Portable programs have no clock, randomness, network, filesystem, or +implicit host authority. The one controlled extension point is `$intrinsic`, +whose processor, BlueId, counter vocabulary, and weights are registered by the +host. -BEX is not JavaScript, WASM, a network runtime, an LLM extension point, or a -contract processor. It does not mutate documents or perform external actions. -It computes a deterministic `BexExecutionResult`; the host decides how to use -that result. +## Five-minute example -## What BEX Is - -BEX programs are Blue object trees. An object with exactly one key beginning -with `$` is an operator. Objects with normal keys are literal Blue-compatible -objects. Lists and scalar values are literal values unless nested inside an -operator body. - -BEX is useful when a host wants deterministic, document-owned logic for policy -checks, projections, validation rules, event payload construction, patch -construction, fixture generation, or other Blue-native computation. - -## Why BEX Exists - -BEX keeps logic close to the data it describes. That matters when logic should -be versioned, hashed, inspected, signed, transported, or reproduced as Blue -data instead of hidden in host application code. - -The runtime is intentionally small: no time, random, network, filesystem, -JavaScript, or WASM in the core language. Host capabilities are available only -through explicitly registered `$intrinsic` processors keyed by BlueId. - -## Basic Example - -```yaml -do: - - $let: - name: status - expr: - $document: /status - - $let: - name: limit - expr: - $integer: - $binding: - name: policy - path: /maxAmount - - $return: - approved: - $and: - - $eq: - - $var: status - - active - - $gte: - - $var: limit - - 1000 - message: - $concat: - - "Status is " - - $var: status -``` - -With `/status = active` and a `policy` binding containing -`/maxAmount = 1000`, the result value is: - -```yaml -approved: true -message: Status is active -``` - -## Java Usage - -```java -FrozenNode programNode = FrozenNode.fromResolvedNode(program); -FrozenNode documentNode = FrozenNode.fromResolvedNode(document); - -Map policyMap = new LinkedHashMap<>(); -policyMap.put("maxAmount", 1000); - -BexExecutionContext context = BexExecutionContext.builder() - .document(new FrozenBexDocumentView(documentNode)) - .binding("policy", BexValues.fromSimple(policyMap)) - .binding("event", BexValues.nodeSnapshot(eventNode)) - .gasLimit(100_000) - .build(); - -BexExecutionResult result = BexEngine.builder() - .build() - .compileAndExecute(BexProgramSource.inline(programNode), context); -``` - -Programs that use shared functions/constants can use a definition node: - -```java -BexProgramSource source = BexProgramSource.withDefinition( - programNode, - definitionNode, - "entryFunction" -); -``` - -Programs that use host capabilities register intrinsic processors on the -engine. The BEX program names the operation with a normal Blue type/BlueId; the -processor registry decides whether that operation is supported: - -```java -BexEngine engine = BexEngine.builder() - .intrinsic( - CommonCryptoEd25519Verify.class, - COMMON_CRYPTO_REGISTRY_IDENTITY, - Collections.singletonMap("signatureVerification", 500L), - invocation -> { - invocation.charge( - "signatureVerification", - 1, - "ed25519-verification"); - // Read invocation.field("publicKey"), - // invocation.field("message"), and - // invocation.field("signature"), then return a boolean. - return BexValues.scalar(verifySignature(invocation)); - }) - .build(); -``` - -## Constants - -`$const` must reference a declared program constant. Unknown constants fail at -compile time instead of evaluating to undefined: - -```yaml -constants: - amount: 400 -expr: - $const: amount -``` - -## Function Arguments And Blue Patterns - -BEX function arguments may declare Blue type or shape patterns. BEX does not -have a separate type enum or type system; declared argument patterns are Blue -nodes and runtime values are checked with Blue's node/type matcher. - -```yaml -functions: - capture: - args: - amount: - type: Integer - hotelOrder: - blueId: HotelOrderBlueId - request: - customerName: - type: Text - schema: - required: true - nights: - type: Integer - schema: - required: true - expr: - amount: - $var: amount - order: - $var: hotelOrder -``` - -All declared function arguments are required by the function call ABI for now. -Unknown functions, extra argument names, and missing declared arguments fail at -compile time. Typed arguments are checked after their call expressions are -evaluated; a runtime mismatch throws `BexException`. - -Text `"400"` does not match the Blue `Integer` pattern. Use an explicit -conversion when conversion is intended: - -```yaml -$call: - function: capture - args: - amount: - $integer: - $event: /message/request/amount -``` - -Untyped required arguments remain supported by declaring an empty pattern: - -```yaml -args: - input: {} -``` - -When BEX converts computed values back to Blue nodes for `$is`, function -argument checks, or output conversion, Blue language keys keep their Blue -meaning. For example, this computed value is a node with a `type` field and a -`status` property, not an object with an ordinary property named `type`: - -```yaml -type: - blueId: HotelOrderType -status: confirmed -``` - -A bare `blueId` object is a Blue reference pattern: - -```yaml -blueId: HotelOrderType -``` - -Do not combine `blueId` with sibling fields to describe a typed instance. Use -`type: { blueId: ... }` for typed values. - -BEX programs are Blue documents, so BEX syntax must use valid Blue authoring -forms. For user-defined name containers such as `functions`, `constants`, -`args`, and `$call.args`, do not use Blue language keys or invalid reserved keys -as names. This includes `value`, `items`, `blueId`, `type`, `schema`, `name`, -`description`, -`itemType`, `keyType`, `valueType`, `mergePolicy`, `constraints`, `contracts`, -`properties`, `$previous`, and `$pos`. - -For operator bodies, payload/reference/control keys such as `value`, `items`, -`blueId`, `contracts`, `properties`, `$previous`, and `$pos` cannot be used as ordinary -multi-field operands. Use BEX operand names such as `node`, `list`, `input`, -`pattern`, `object`, `key`, `path`, `val`, `cond`, `then`, and `else`. - -Metadata keys such as `name`, `description`, `type`, `schema`, `itemType`, -`keyType`, `valueType`, and `contracts` are legal Blue language fields, but they are not -ordinary object properties. `constraints` is invalid in Blue Language 1.0. An -operator may use one of the legal metadata fields only when the BEX compiler -explicitly supports that field. - -Function argument patterns and `$is.pattern` are static Blue patterns. BEX does -not evaluate expressions inside those patterns, and it does not emulate Blue -authoring sugar such as inline `type: Integer` preprocessing for computed type -fields. - -## Document Views - -`BexDocumentView` owns document pointer resolution, canonical reads, resolved -reads, and the current scope path. - -```java -FrozenBexDocumentView view = new FrozenBexDocumentView( - canonicalRoot, - resolvedRoot, - "/orders/123" -); -``` - -`$document` reads the canonical view by default: +This expression returns `42`: ```yaml -$document: /status +$add: + - 40 + - 2 ``` -Resolved view reads are explicit: - -```yaml -$document: - path: /status - view: resolved -``` - -## Runtime Bindings - -Hosts can provide arbitrary named bindings: +The standalone API has three inputs: a selected `BexProgramSource`, an immutable +`BexExecutionContext`, and a reusable `BexEngine`. ```java -BexExecutionContext.builder() - .document(view) - .binding("policy", policyValue) - .binding("actor", actorValue) - .binding("event", eventValue) - .build(); -``` - -For a host value that is expensive to construct and may not be read, use a -lazy binding. Its supplier runs only when BEX first reads that name, at most -once per execution context; BEX then receives the supplied concrete value. - -```java -BexExecutionContext.builder() - .document(view) - .lazyBinding("expensiveSnapshot", this::createSnapshotValue) - .build(); -``` - -`event`, `steps`, and `currentContract` are standard eager bindings. Calling -`context.bindings()` intentionally materializes all lazy bindings in insertion -order and returns an unmodifiable map of concrete values. Cyclic lazy-binding -reads fail deterministically and memoize their failure. - -Use `$binding` for arbitrary host bindings: - -```yaml -$binding: - name: policy - path: /maxAmount -``` - -Short form is supported: - -```yaml -$binding: actor/name -``` - -Hosts often provide common bindings such as `event`, `steps`, and -`currentContract`. BEX includes shortcuts for those common names: - -- `$event`; -- `$steps`; -- `$currentContract`. - -For every other host binding, use `$binding`. - -## Pointer Kinds - -BEX has two pointer contexts. - -Document pointers are resolved relative to the current document scope: - -- `$document`; -- `$resultValue`; -- `$appendChange.path`; -- `$appendChanges` entry `path`. - -Value pointers are resolved inside the selected value and are not affected by -the document scope: - -- `$event`; -- `$currentContract`; -- `$steps.path`; -- `$binding.path`; -- `$pointerGet.path`; -- `$pointerSet.path`. - -Static omitted/default paths may intentionally read a root/default location. -Dynamic pointer expressions that evaluate to `null` or undefined fail instead -of silently becoming the current document scope or root value. - -Dynamic text operands also reject `null` and undefined. This applies to -operator fields such as `$get.key`, `$objectSet.key`, `$hasKey.key`, -`$binding.name`, `$steps.step`, `$appendChange.op`, and `$pointerSet.op`. A -static empty string is still allowed when it is explicitly authored. - -Use `$pointerJoin` when building document paths from dynamic path segments. Each -item is treated as one JSON Pointer segment and escaped safely: - -```yaml -$pointerJoin: - - orders - - $var: orderId - - status -``` - -If `orderId` is `abc/def~ghi`, the result is -`/orders/abc~1def~0ghi/status`. - -## Expression Operators - -BEX operators are Blue objects whose single key starts with `$`. - -### Reading Data - -| Operator | Purpose | -|---|---| -| `$document` | Read from the Blue document view. | -| `$binding` | Read a host-provided runtime binding. | -| `$event` | Shortcut for the common `event` binding. | -| `$steps` | Shortcut for prior step/result bindings when a host provides them. | -| `$currentContract` | Shortcut for a host-provided current contract binding. | -| `$var` | Read a local variable. | -| `$const` | Read a program constant. | -| `$get` | Read an object field by key. | - -`$var` and `$const` support object-form value paths. The `name` is static, so -variables and constants still compile to fixed slots/lookups, while `path` is a -value-local JSON Pointer inside the selected value: - -```yaml -$var: - name: request - path: /summary -``` - -```yaml -$const: - name: Policy/minimum - path: /amount -``` - -The path may be static text or an expression that evaluates to pointer text. -Missing path targets return `undefined`, matching `$pointerGet`/value reads. -There is intentionally no `$var: request/summary` shorthand; existing variable -and constant names may contain `/`. Dynamic variable or constant names are not -supported; use `$get` or `$pointerGet` for dynamic object lookup. - -### Type Helpers - -| Operator | Purpose | -|---|---| -| `$unwrap` | Unwrap Blue scalar wrapper values. | -| `$is` | Return whether a value matches a Blue pattern. | -| `$kind`, `$isKind` | Inspect the visible BEX runtime value kind. | -| `$text` | Convert to text. | -| `$integer` | Convert to exact integer. | -| `$number` | Convert to exact decimal/number. | -| `$boolean` | Convert to boolean. | -| `$object` | Require an object, or default undefined to an empty object. | -| `$list` | Require a list, or default undefined to an empty list. | - -`$is.pattern` is static Blue pattern data, not a BEX expression: - -```yaml -$is: - node: - $event: /message/request/amount - pattern: - type: Integer -``` - -`$kind` returns one of `undefined`, `null`, `text`, `integer`, `double`, -`boolean`, `object`, or `list`. This is BEX runtime shape, not Blue type -conformance; use Blue schema/type validation or `$is` for Blue type semantics. - -```yaml -$isKind: - val: - $event: /message/request/amount - kind: [integer, double] -``` - -`$isKind.kind` may be a single kind or a list of kinds. - -### Strings - -| Operator | Purpose | -|---|---| -| `$concat` | Concatenate text. | -| `$pointerJoin` | Safely build a JSON Pointer from path segments. | -| `$join` | Join list items with a separator. | -| `$split` | Split text. | -| `$startsWith` | Check a prefix. | -| `$sliceAfter` | Return text after a prefix. | - -```yaml -$join: - list: - - a - - b - separator: ":" -``` - -### Logic And Comparison - -| Operator | Purpose | -|---|---| -| `$eq`, `$ne` | Equality and inequality. | -| `$gt`, `$gte`, `$lt`, `$lte` | Numeric comparisons. | -| `$and`, `$or`, `$not` | Boolean logic with short-circuiting. | -| `$truthy`, `$empty`, `$isEmpty` | Truthiness checks. `$isEmpty` is an alias that avoids Blue list-placeholder syntax in list operands. | -| `$exists` | Return false only for undefined values. | -| `$coalesce`, `$default` | First non-empty value. `$default` is an alias. | - -`$exists` is useful for optional-field validation because it distinguishes a -missing value from present falsy values. It returns `true` for `null`, `false`, -`0`, empty text, and empty list/object values: - -```yaml -$or: - - $not: - $exists: - $event: /message/request/note - - $is: - node: - $event: /message/request/note - pattern: - type: Text -``` - -### Numeric - -| Operator | Purpose | -|---|---| -| `$add` | Add exact integers. | -| `$subtract` | Subtract exact integers. | -| `$multiply` | Multiply exact integers. | -| `$divide` | Divide exact integers; non-exact division fails. | - -### Objects And Lists - -| Operator | Purpose | -|---|---| -| `$keys` | Sorted object keys. | -| `$entries` | Sorted object entries as `{ key, val }`. | -| `$size` | Size of a list, object, or scalar value. | -| `$listGet` | Read a list item. | -| `$listConcat` | Concatenate lists. | -| `$merge` | Shallow object merge. | -| `$objectSet` | Set a dynamic object key without mutating the input. | -| `$pointerGet` | Read by JSON Pointer from any value. | -| `$pointerSet` | Return a value with a JSON Pointer update. | -| `$map`, `$filter`, `$flatMap`, `$reduce` | Deterministic collection projection, selection, fanout, and aggregation. | -| `$some`, `$find`, `$findEntry` | Short-circuit collection queries. | -| `$includes` | List membership using BEX equality. | -| `$hasKey` | Object key membership. | -| `$objectFromEntries` | Build an object from `{ key, val }` entries. | - -Collection expressions accept lists or objects. Lists iterate in item order. -Objects iterate in sorted key order, matching `$keys`, `$entries`, and -`$forEach` object behavior. `item`, optional `key`, and optional `index` -bindings are local to the query expression and are restored afterwards, so they -do not overwrite outer variables with the same names. - -```yaml -$map: - in: - $event: /message/request/changeset - item: patch - expr: - op: - $var: - name: patch - path: /op - path: - $var: - name: patch - path: /path -``` - -Collection operator shapes and results: - -| Operator | Shape | Result | -|---|---|---| -| `$map` | `in`, `item`, optional `key`/`index`, `expr` | List of `expr` results. Object input still returns a list in sorted-key order. | -| `$filter` | `in`, `item`, optional `key`/`index`, `where` | Filtered list for list input; filtered object for object input. | -| `$flatMap` | `in`, `item`, optional `key`/`index`, `expr` | Concatenated list. Each `expr` result must be a list. | -| `$reduce` | `in`, `acc`, `init`, `item`, optional `key`/`index`, `expr` | Final accumulator. `init` is evaluated once before iteration. | -| `$some` | `in`, `item`, optional `key`/`index`, `where` | Boolean, short-circuiting at the first truthy `where`. | -| `$find` | `in`, `item`, optional `key`/`index`, `where` | First matching item/value, or `undefined`. | -| `$findEntry` | `in`, `item`, optional `key`/`index`, `where` | First matching entry object, or `undefined`. | - -For list input, `$findEntry` returns: - -```yaml -val: -index: -``` - -For object input, `$findEntry` returns: - -```yaml -key: -val: -index: -``` - -`$includes` has shape `{ list, val }`, requires `list` to evaluate to a list, -uses BEX equality, and short-circuits on the first equal item. `$hasKey` has -shape `{ object, key }`; it returns false for non-object inputs and true when -the object has a non-undefined value for the key. - -`$objectFromEntries` expects a list of objects with `key` and `val` fields: - -```yaml -$objectFromEntries: - $map: - in: - $entries: - b: 2 - a: 1 - item: entry - expr: - key: - $var: - name: entry - path: /key - val: - $multiply: - - $var: - name: entry - path: /val - - 10 -``` - -Entry keys cannot be `undefined` or `null`; those fail. Entry values may be -`undefined`, which omits/removes that key from the result. Duplicate keys use -the last non-undefined value, unless a later undefined value removes the key. - -### Result Helpers - -| Operator | Purpose | -|---|---| -| `$changeset` | Return accumulated patch entries. | -| `$events` | Return accumulated event/data entries. | -| `$resultValue` | Read the document value implied by accumulated changes. | - -### Other Expressions - -| Operator | Purpose | -|---|---| -| `$choose` | Conditional expression. | -| `$call` | Call a local function. | -| `$intrinsic` | Invoke a registered host intrinsic by the BlueId of its static `type`. | -| `$literal` | Return payload without compiling nested operators. | -| `$null`, `$emptyObject`, `$emptyList` | Emit explicit null, empty object, or empty list values after Blue source normalization. | - -`$literal` prevents normal expression compilation, but BEX still rejects -BEX-looking operators inside Blue type-definition fields such as `type`, -`itemType`, `keyType`, `valueType`, `blue`, and `schema`. - -### Intrinsics - -`$intrinsic` is the only host-extension expression. It does not require a -special `Blue/BEX Intrinsic` supertype. The `type` may be any static Blue type -or Blue value whose BlueId can be resolved. BEX takes that BlueId and looks up -a registered processor. - -```yaml -$intrinsic: - type: - blueId: CTkdsd4MNjiFA13MFeAx34jnnBLfGzn7HfP6fx1dV43s - publicKey: - $const: trustedSignerPublicKey - message: - $event: /message/canonicalBytes - signature: - $event: /message/signature -``` - -Rules: - -- `type` is static authored Blue data. BEX expressions inside `type` are - rejected, because the compiler must know the intrinsic BlueId before - execution. -- Hosts can register processors directly by BlueId or by a Java class with a - resolvable `@TypeBlueId`. -- The payload is the normal fields of the typed operation object. There is no - `args` or `params` wrapper. -- The `type` field itself is not passed as a payload field. Processors receive - `type` separately as `invocation.type()`. -- Payload field expressions are evaluated normally. Fields that evaluate to - `undefined` are omitted, matching object literal behavior. -- Compilation fails if the active engine has no intrinsic processor registered - for the resolved BlueId. Execution checks the same support boundary again so - a shared compiled-program cache cannot bypass it. -- The processor returns a `BexValue`. A `null` Java return is normalized to BEX - `undefined`. -- The processor charges only counters declared by its exact registration, - using `invocation.charge(counter, quantity, reason)`. Arbitrary aggregate - intrinsic gas is not accepted. -- Intrinsic namespaces are disjoint physical runtime-session children; they - are never flattened into `bex`. The `/` separator is reserved so several - BEX executions in one host session cannot produce ambiguous namespaces. -- A compiled program opens only the intrinsic namespaces it statically - requires. Success, deterministic failure, evidence unavailability, and - exhaustion are separate host-ledger lifecycle callbacks. - -The Blue type definition and its description/spec text define what the -operation means. For standard intrinsics, keep conformance vectors beside the -spec text so independent processors can prove they implemented the same -behavior. For Ed25519, the definition must be explicit about what bytes are -signed; do not describe verification over a generic object without also naming -the canonical byte representation. - -## Statement Operators - -| Statement | Purpose | -|---|---| -| `$let` | Define or initialize a local variable. | -| `$set` | Update an existing local variable. | -| `$if` | Conditional branch. | -| `$forEach` | Iterate list items or object entries. | -| `$appendChange` | Append a patch entry to the result changeset. | -| `$appendChanges` | Append many patch entries. | -| `$appendEvent` | Append an event/data value. | -| `$appendEvents` | Append many event/data values. | -| `$call` | Call a local function for side effects and return handling. | -| `$return` | Return the result value. | -| `$returnIf` | Return early when a condition is truthy. | -| `$fail` | Fail deterministically. | -| `$failIf` | Fail deterministically when a condition is truthy. | - -`$returnIf` returns from the current function/root when `cond` is truthy: - -```yaml -$returnIf: - cond: - $empty: - $event: /message/request/summary - expr: - changeset: [] - events: - - type: Conversation/Proposed Change Invalid - reason: summary is missing -``` - -`expr` is optional. When omitted, `$returnIf` returns the default result value, -the same as bare `$return`. The payload field is named `expr`; `value` is not -accepted because `value` is a Blue scalar-wrapper field and cannot safely carry -an object payload in authored Blue YAML. - -`$failIf` fails deterministically when `cond` is truthy: - -```yaml -$failIf: - cond: - $not: - $exists: - $event: /message/request/id - message: request id is required -``` - -The `expr` and `$failIf.message` operands are lazy; they are evaluated only -when the guard condition is truthy. - -`$let` also supports a multi-bind form. Without `order`, the bindings are -parallel: all expressions read the frame as it existed before the `$let`, then -all variables are assigned. Unordered bindings are sorted only to make execution -deterministic; they do not create dependencies by name. - -With `order`, bindings are sequential and later bindings may read earlier ones. -`order` must list every key in `vars` exactly once: - -```yaml -$let: - order: [request, summary] - vars: - request: - $event: /message/request - summary: - $var: - name: request - path: /summary -``` - -In unordered form, a binding cannot read another new binding from the same -`vars` block unless that variable already existed before the block. Use `order` -when one binding depends on another. - -`$forEach` can bind list indexes and object keys when those are needed for -patch paths: - -```yaml -$forEach: - in: - $event: /message/request/orders - item: order - index: i - do: - - $appendChange: - op: replace - path: - $pointerJoin: - - orders - - $var: i - - status - val: received -``` - -For object iteration, use `key` and `item` to bind the object key and value -separately. The older form with only `item` still binds `{ key, val }`. - -## Results And Accumulators - -`BexExecutionResult` contains: - -- `value`, the primary return value; -- `changeset`, the standard patch accumulator; -- `events`, the standard event/data accumulator; -- `gasLedger`, the canonical ordered named child ledger (`gasUsed` is a - trace-derived convenience total); -- `metrics`. - -BEX computes these values only. The host decides whether patches are applied, -events are emitted, or accumulators are treated as ordinary data. - -`$resultValue` reads a transient overlay of the document after applying -accumulated patches in order. Parent reads reflect descendant object patches, -so reading `/hotelOrder` after replacing `/hotelOrder/status` returns the -original `hotelOrder` object with the updated status. A list-index removal is -non-shifting only in this `$resultValue` overlay: the removed index reads as -`undefined`, later indexes keep their positions, and converting the whole -sparse overlay list to Blue output fails. This sparse-slot rule does not apply -to ordinary BEX list values, `$pointerSet`, or the host's eventual application -of the changeset. - -`$appendChanges` validates each patch entry the same way as `$appendChange`. -Supported patch operations are `add`, `replace`, and `remove`. `add` and -`replace` require a non-undefined `val`; `remove` does not include a value. - -`$appendEvents` validates each item the same way as `$appendEvent`. Undefined -event values are rejected. BEX core does not require events to be objects; hosts -decide what event shape they accept. - -## Determinism - -BEX execution is deterministic for a fixed program, context, document view, -bindings, gas schedule, and immutable host boundary values. - -Use `BexValues.nodeSnapshot(node)` for untrusted mutable `Node` values. Use -`BexValues.nodeCursorTrustedImmutable(node)` only when the host can guarantee -the node will not be mutated during execution. - -## Performance Model - -The engine is compiled-first: - -- selected programs compile lazily; -- compiled programs are cacheable by stable Blue node identity and entry name; -- variables use slot frames; -- static pointers are parsed at compile time; -- document and binding reads use cursor-backed values where possible; -- `$objectSet` and `$pointerSet` use overlay values; -- `$resultValue` materializes accumulated patch overlays for reads; -- output conversion to `Node`, `FrozenNode`, or simple Java values is explicit. - -Every result includes `BexMetrics`: - -```java -BexMetrics metrics = result.metrics(); -metrics.compiledExecutions(); -metrics.compileCacheHits(); -metrics.frozenDocumentReads(); -metrics.nodeMaterializations(); -``` - -The deterministic gas rules are specified in [docs/GAS.md](docs/GAS.md). The -closed BEX 2.0 fixture format is documented in -[docs/FIXTURES.md](docs/FIXTURES.md). The normative package under -`src/test/resources/conformance/bex/` executes 105 behavior cases and 30 exact -named-counter microfixtures, with direct coverage for all 86 published -operators. Its inventory and package identities are verified before execution. - -The translated corpus strategy is documented in -[docs/TRANSLATED_CORPUS.md](docs/TRANSLATED_CORPUS.md). It contains 80 -representative Kyverno, JMESPath/JSONata, and JSON Patch-style cases translated -into BEX to test whether the small query/operator core is sufficient for common -policy, transform, and patch-emission workflows: 30 Kyverno validate-style -cases, 20 Kyverno mutate/generate-style cases, 20 JMESPath/JSONata-style -query/transform cases, and 10 JSON Patch emission edge cases. - -## Tests - -The local Blue Language composite is opt-in. Use the explicit property when -developing against the sibling checkout: - -```bash -./gradlew -PblueLanguageCompositePath=../blue-language-java test -./gradlew -PblueLanguageCompositePath=../blue-language-java build -``` - -With no property, Gradle uses the declared published dependency -`blue.language:blue-language-java:3.1.0-rc.19`. Resolution is Maven -Central-only—`mavenLocal` is not a dependency repository—and the resolved JAR -must match the recorded Maven Central SHA-256: - -```bash -CI=true ./gradlew clean bexReleaseEvidence -``` - -Each report invocation records its current dependency mode under -`.gradle/bex-hosted-release/`. Publication therefore records standalone and -local-composite runs separately, then makes one final standalone -`bexReleaseEvidence` decision. The release workflows automate this sequence -with `.github/scripts/run-final-publication-gates.sh`. - -Artifact evidence must use the non-snapshot CI version and the same source -epoch in two clean checkouts. Every `GRADLE_USER_HOME` below must be a distinct -fresh empty directory. For the required standalone publication pair: - -```bash -export CI=true -export SOURCE_DATE_EPOCH="$(git show -s --format=%ct HEAD)" - -(cd /first/clean/blue-bex-java && \ - GRADLE_USER_HOME=/tmp/blue-bex-gradle-one \ - ./gradlew --no-daemon clean test writeCleanBuildArtifactHashes) - -(cd /second/clean/blue-bex-java && \ - GRADLE_USER_HOME=/tmp/blue-bex-gradle-two \ - ./gradlew --no-daemon clean test writeCleanBuildArtifactHashes) - -./gradlew verifyIndependentCleanBuildReproducibility \ - -PcleanBuildEvidenceOne=/first/clean/blue-bex-java/build/reports/bex-release/clean-build-artifacts.properties \ - -PcleanBuildEvidenceTwo=/second/clean/blue-bex-java/build/reports/bex-release/clean-build-artifacts.properties - -GRADLE_USER_HOME=/tmp/blue-bex-standalone-mode \ - ./gradlew --no-daemon clean test \ - -PblueLanguageRequireFreshModuleCache=true -GRADLE_USER_HOME=/tmp/blue-bex-local-mode \ - ./gradlew --no-daemon clean test \ - -PblueLanguageCompositePath=/absolute/path/to/clean/blue-language-java -GRADLE_USER_HOME=/tmp/blue-bex-final-standalone \ - ./gradlew --no-daemon clean bexReleaseEvidence -``` - -For an additional local-composite reproducibility pair, add the same explicit -`-PblueLanguageCompositePath=/absolute/path/to/clean/blue-language-java` -argument to both builds in a second pair of clean BEX checkouts. Keep all four -checkout roots intact until the final report has re-hashed their BEX outputs -and receipt-owned Language JAR copies. Never use one standalone build and one -local-composite build as a two-run pair. The verifier rejects dirty checkouts, -different commits, versions or dependency modes, and any mismatch among the -four artifact hashes. Local-composite mode evidence must also resolve from the -exact published Language source commit at the recorded -`v` tag; a different clean Language checkout is rejected. -Its commit-bound evidence is also compared with the -artifacts from the reporting build. The same-working-tree archive gate remains -a separate packaging check. - -The API gate compares the packaged JAR’s complete generated descriptor -manifest with the exact source-controlled first-public BEX 2.0 baseline. -Removals, descriptor changes, reordering, and unexpected public/protected -additions all fail the gate. A clean dependency-cache run is separate -commit-bound evidence and is recorded only when a controlled isolated run -uses `-PblueLanguageRequireFreshModuleCache=true`. Later publication -invocations authenticate and reuse that mode evidence instead of incorrectly -requiring the same cache path to be absent again. At this source state, the current -hosted runtime session APIs exist only in the sibling working tree and are not -present in the published `3.1.0-rc.19` JAR, so standalone release evidence is -expected to remain blocked until Blue Language publishes that API surface. +FrozenNode expression = FrozenNode.fromResolvedNode( + new Node().properties("$add", new Node().items( + new Node().value(40L), + new Node().value(2L)))); +FrozenNode document = FrozenNode.fromResolvedNode(new Node()); + +BexExecutionResult result = BexEngine.builder().build().compileAndExecute( + BexProgramSource.expression(expression), + BexExecutionContext.builder() + .document(new FrozenBexDocumentView(document)) + .gasLimit(10_000L) + .build()); + +System.out.println(result.value().toSimple()); // 42 +System.out.println(result.gasLedger().trace()); +``` + +A runnable version lives in +[`StandaloneBexExample.java`](examples/src/main/java/blue/bex/examples/StandaloneBexExample.java). + +## Standalone use + +The aggregate coordinate is `blue.bex:blue-bex-java`; select an actual version +from release metadata rather than copying a snapshot version from this checkout. +Standalone execution supplies `FrozenBexDocumentView`, ordinary bindings, and a +local gas limit. One engine can be reused; each execution context belongs to one +run. + +Existing exact Blue values should enter through `BexValues.frozen(...)` or an +equivalent exact-value boundary. Transient Java maps/lists can enter through +`BexValues.fromSimple(...)`. They have different construction costs but the same +portable observable behavior once they represent the same exact value. + +## Contracts-hosted use + +The `blue.bex.contracts` adapter is the only layer that should know +`ProcessorExecutionContext`. It binds the canonical/resolved document views, +standard event and contract bindings, the live shared gas budget, failure +translation, and the host-owned semantic-output boundary. See +[`HostedBexExample.java`](examples/src/main/java/blue/bex/examples/HostedBexExample.java) +and [Contracts hosting](docs/contracts-hosting.md). + +Hosted execution does not give BEX contract-processing authority. BEX returns +data; Contracts decides whether and how patches, events, scopes, and processor +effects are committed. + +## Semantics at a glance + +- There is one ordinary BlueId algorithm. `$nodeBlueId` returns an established + exact identity or directly establishes identity for a strictly admitted + transient value. It never preprocesses, resolves, canonicalizes, or minimizes + a Source document. +- Exact values cross BEX boundaries by identity. Portable operators cannot + distinguish inline/reference, eager/lazy, warm/cold, or provider segmentation. +- Gas is named logical work. A charge is admitted before its work; a rejected + charge is absent, and no later work or buffered effect commits. +- `collectionPaths`, Timeline, Mandate, Coordination, `Process Embedded`, + persistence, and collection activation are host/Contracts concerns, not BEX + semantics. + +## Read next + +- [Start here](docs/start-here.md) — mental model and first integration +- [Program model](docs/program-model.md) — source trees, functions, errors, and + evaluation order +- [Values and identity](docs/values-and-identity.md) — exact/transient values and + representation blindness +- [Gas and exhaustion](docs/gas-and-exhaustion.md) — the canonical named ledger +- [Architecture](docs/architecture.md) — modules and ownership boundaries +- [Conformance](docs/conformance.md) — normative counts and identities +- [Release](docs/release.md) — local working gate versus strict publication gate +- [BEX 2.0 specification](specifications/blue-bex-specification-2.0.md) — normative + language definition + +## Current release state + +Two fail-closed gates intentionally answer different questions: + +```text +bexWorkingVerification exact local modular Blue Language checkout +bexReleaseVerify independently reproducible published dependencies +``` + +The working gate may pass before compatible Language modules are published. A +public release is eligible only when `bexReleaseVerify` records +`releaseReady = true`; missing published artifacts or unexecuted differential +evidence must remain red or `not-executed`. This README does not claim that an +unexecuted gate, benchmark, or release has passed. Consult the current generated +reports under `build/reports`. + +The normative package currently contains 60 vectors, 105 behavior fixtures, 30 +gas microfixtures, and coverage for 86 operators. Exact identities are recorded +in [Conformance](docs/conformance.md). ## License -MIT. See [LICENSE](LICENSE). +MIT License. See [LICENSE](LICENSE). diff --git a/blue-bex-conformance/build.gradle.kts b/blue-bex-conformance/build.gradle.kts new file mode 100644 index 0000000..ac67bd4 --- /dev/null +++ b/blue-bex-conformance/build.gradle.kts @@ -0,0 +1,248 @@ +import org.gradle.api.tasks.JavaExec +import org.gradle.api.tasks.Copy +import org.gradle.api.tasks.bundling.Jar +import java.security.MessageDigest + +plugins { + id("blue.bex.conformance") + id("blue.bex.language-dependencies") + id("blue.bex.api-evidence") + id("blue.bex.jmh") +} + +description = "BEX fixtures, package integrity, properties, and release evidence" + +val languageVersion = extensions + .getByType() + .version.get() + +dependencies { + testImplementation(project(":blue-bex-core")) + testImplementation(project(":blue-bex-contracts")) + testImplementation("blue.language:blue-language-model:$languageVersion") + testImplementation("blue.language:blue-language-core:$languageVersion") + testImplementation("blue.language:blue-contracts-core:$languageVersion") + // Explicit aggregate compatibility smoke and publication provenance. + testRuntimeOnly("blue.language:blue-language-java:$languageVersion") + testImplementation("org.yaml:snakeyaml:1.31") +} + +tasks.named( + "writeLanguageDependencyEvidence") { + val evidenceRuntime = configurations.testRuntimeClasspath.get() + artifacts.setFrom(evidenceRuntime) + resolvedComponents.set(provider { + evidenceRuntime.incoming.resolutionResult.allComponents + .map { it.id.displayName } + .sorted() + }) +} + +sourceSets { + test { + java.setSrcDirs(listOf(rootProject.file("src/test/java"))) + resources.setSrcDirs(listOf(rootProject.file("src/test/resources"))) + } +} + +tasks.test { + workingDir = rootProject.projectDir +} + +tasks.named( + "java8BytecodeCheck") { + allowEmpty.set(true) +} + +val apiInspectionJar = tasks.register("apiInspectionJar") { + group = "verification" + archiveClassifier.set("api-inspection") + dependsOn( + project(":blue-bex-core").tasks.named("jar"), + project(":blue-bex-contracts").tasks.named("jar") + ) + from({ + zipTree(project(":blue-bex-core").tasks.named("jar").get() + .archiveFile.get().asFile) + }) + from({ + zipTree(project(":blue-bex-contracts").tasks.named("jar").get() + .archiveFile.get().asFile) + }) + duplicatesStrategy = DuplicatesStrategy.EXCLUDE +} + +val generateBinaryApiManifest = tasks.register( + "generateBinaryApiManifest") { + group = "verification" + dependsOn(tasks.named("testClasses"), apiInspectionJar) + classpath = sourceSets.test.get().runtimeClasspath + mainClass.set("blue.bex.conformance.BexBinaryApiManifestMain") + doFirst { + setArgs(listOf( + apiInspectionJar.get().archiveFile.get().asFile.absolutePath, + layout.buildDirectory.file("reports/bex-release/public-api.txt") + .get().asFile.absolutePath + )) + } +} + +val binaryApiCheck = tasks.register("binaryApiCheck") { + group = "verification" + dependsOn(generateBinaryApiManifest, "generateApiClassification") + val generated = layout.buildDirectory.file( + "reports/bex-release/public-api.txt") + val generatedClassification = layout.buildDirectory.file( + "reports/bex-release/public-api-classification.json") + val required = rootProject.layout.projectDirectory.file( + "src/test/resources/hosted-release/required-public-api.txt") + val checkpoint = rootProject.layout.projectDirectory.file( + "gradle/verification/api/working-checkpoint-public-api.txt") + val checkpointClassification = rootProject.layout.projectDirectory.file( + "gradle/verification/api/working-checkpoint-public-api-classification.json") + val reviewedClassification = rootProject.layout.projectDirectory.file( + "docs/public-api-classification.json") + val removedDescriptors = rootProject.layout.projectDirectory.file( + "gradle/verification/api/modernization-removed-descriptors.txt") + val addedDescriptors = rootProject.layout.projectDirectory.file( + "gradle/verification/api/modernization-added-descriptors.txt") + val migrationLedger = rootProject.layout.projectDirectory.file( + "docs/latest-language-api-migration.json") + inputs.files( + generated, + generatedClassification, + required, + checkpoint, + checkpointClassification, + reviewedClassification, + removedDescriptors, + addedDescriptors, + migrationLedger + ) + doLast { + check(generated.get().asFile.readBytes() + .contentEquals(required.asFile.readBytes())) { + "Public API differs from the reviewed baseline; regenerate only " + + "with an exact migration-ledger update" + } + check(generatedClassification.get().asFile.readBytes() + .contentEquals(reviewedClassification.asFile.readBytes())) { + "Public API classification differs from same-run generation" + } + fun sha256(file: File): String = + MessageDigest.getInstance("SHA-256") + .digest(file.readBytes()) + .joinToString("") { "%02x".format(it) } + fun ownedDescriptors(file: File): java.util.SortedSet { + val descriptors = sortedSetOf() + var owner = "" + file.forEachLine { line -> + when { + line.startsWith("class ") -> { + owner = line + descriptors.add(line) + } + line.startsWith(" ") -> { + check(owner.isNotEmpty()) { + "Member descriptor appears before its owner in $file" + } + descriptors.add("$owner :: ${line.substring(2)}") + } + } + } + return descriptors + } + val beforeLines = ownedDescriptors(checkpoint.asFile) + val afterLines = ownedDescriptors(required.asFile) + val removed = (beforeLines - afterLines).toList() + val added = (afterLines - beforeLines).toList() + check(removed == removedDescriptors.asFile.readLines()) { + "Complete removed-descriptor ledger is stale" + } + check(added == addedDescriptors.asFile.readLines()) { + "Complete added-descriptor ledger is stale" + } + val ledger = migrationLedger.asFile.readText() + val checkpointHash = sha256(checkpoint.asFile) + val afterHash = sha256(required.asFile) + check(checkpointClassification.asFile.readText() + .contains(checkpointHash)) { + "Checkpoint classification is not bound to its manifest" + } + check(ledger.contains(checkpointHash) + && ledger.contains(afterHash) + && ledger.contains(sha256(removedDescriptors.asFile)) + && ledger.contains(sha256(addedDescriptors.asFile)) + && ledger.contains("\"removedDescriptorLines\": ${removed.size}") + && ledger.contains("\"addedDescriptorLines\": ${added.size}")) { + "Migration ledger does not authenticate the complete API delta" + } + } +} + +tasks.named("bexApiEvidence") { + dependsOn(binaryApiCheck) +} + +tasks.check { + dependsOn(binaryApiCheck) +} + +val syncConformanceEvidenceArtifacts = tasks.register( + "syncConformanceEvidenceArtifacts") { + group = "verification" + dependsOn( + project(":blue-bex-java").tasks.named("assemble"), + rootProject.tasks.named("sourceReleaseArchive") + ) + into(layout.buildDirectory.dir("conformance-artifacts")) + from(project(":blue-bex-java").layout.buildDirectory.dir("libs")) { + include("*.jar") + into("libs") + } + from(rootProject.layout.buildDirectory.dir("distributions")) { + include("*-source-release.zip") + into("distributions") + } +} + +val writeBexConformanceReport = tasks.register( + "writeBexConformanceReport") { + group = "verification" + description = "Writes same-run BEX test, fixture, gas, and release evidence." + dependsOn( + tasks.test, + syncConformanceEvidenceArtifacts, + tasks.named("writeLanguageDependencyEvidence") + ) + classpath = sourceSets.test.get().runtimeClasspath + mainClass.set("blue.bex.conformance.BexConformanceReportMain") + val composite = providers.gradleProperty("blueLanguageCompositePath") + .orElse("") + doFirst { + val compositePath = composite.get() + setArgs(listOf( + rootProject.projectDir.absolutePath, + layout.buildDirectory.get().asFile.absolutePath, + gradle.gradleVersion, + project.version.toString(), + if (compositePath.isBlank()) + "standalone-published" else "local-composite", + "blue.language:blue-language-java:$languageVersion", + rootProject.layout.projectDirectory.dir( + ".gradle/bex-hosted-release").asFile.absolutePath, + compositePath, + layout.buildDirectory.dir("conformance-artifacts") + .get().asFile.absolutePath + )) + } +} + +tasks.register("bexConformanceReport") { + group = "verification" + dependsOn(writeBexConformanceReport) +} + +tasks.named("bexConformance") { + dependsOn(writeBexConformanceReport) +} diff --git a/blue-bex-conformance/src/jmh/java/blue/bex/benchmark/BexBenchmarkSupport.java b/blue-bex-conformance/src/jmh/java/blue/bex/benchmark/BexBenchmarkSupport.java new file mode 100644 index 0000000..ea55833 --- /dev/null +++ b/blue-bex-conformance/src/jmh/java/blue/bex/benchmark/BexBenchmarkSupport.java @@ -0,0 +1,172 @@ +package blue.bex.benchmark; + +import blue.bex.api.BexExecutionContext; +import blue.bex.api.FrozenBexDocumentView; +import blue.bex.gas.BexGasLedger; +import blue.bex.result.BexExecutionResult; +import blue.bex.value.BexValue; +import blue.bex.value.BexValues; +import blue.language.model.Node; +import blue.language.snapshot.FrozenNode; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** Shared correctness and exact-gas checks used by the JMH corpus. */ +public final class BexBenchmarkSupport { + private static final FrozenNode EMPTY_DOCUMENT = + FrozenNode.fromResolvedNode(new Node()); + + private BexBenchmarkSupport() { + } + + public static BexExecutionContext context() { + return BexExecutionContext.builder() + .document(new FrozenBexDocumentView(EMPTY_DOCUMENT)) + .gasLimit(100_000_000L) + .build(); + } + + public static BexExecutionContext context( + String name, BexValue value) { + return BexExecutionContext.builder() + .document(new FrozenBexDocumentView(EMPTY_DOCUMENT)) + .binding(name, value) + .gasLimit(100_000_000L) + .build(); + } + + public static ExpectedOutcome expected( + BexExecutionResult result) { + if (result == null || result.output() == null) { + throw new IllegalStateException( + "benchmark scenario did not admit root output"); + } + return new ExpectedOutcome( + result.output().nodeBlueId(), + result.gasLedger()); + } + + public static BexExecutionResult verify( + BexExecutionResult result, + ExpectedOutcome expected) { + if (result == null || result.output() == null) { + throw new IllegalStateException( + "benchmark execution did not admit root output"); + } + if (!expected.outputBlueId.equals( + result.output().nodeBlueId())) { + throw new IllegalStateException( + "benchmark result identity changed: expected " + + expected.outputBlueId + " but was " + + result.output().nodeBlueId()); + } + if (!expected.gasLedger.equals(result.gasLedger())) { + throw new IllegalStateException( + "benchmark gas identity changed: expected " + + expected.gasLedger + " but was " + + result.gasLedger()); + } + return result; + } + + public static Node op(String name, Object body) { + return obj(name, body); + } + + public static Node obj(Object... keysAndValues) { + return new Node().properties(props(keysAndValues)); + } + + public static Node list(Object... values) { + List items = new ArrayList(values.length); + for (Object value : values) { + items.add(node(value)); + } + return new Node().items(items); + } + + public static Node integerList(int size) { + List items = new ArrayList(size); + for (int index = 0; index < size; index++) { + items.add(new Node().value((long) index)); + } + return new Node().items(items); + } + + public static FrozenNode frozen(Node node) { + return FrozenNode.fromResolvedNode(node); + } + + public static Node node(Object value) { + if (value instanceof Node) { + return (Node) value; + } + if (value == null) { + return new Node(); + } + if (value instanceof Byte + || value instanceof Short + || value instanceof Integer + || value instanceof Long) { + return new Node().value(((Number) value).longValue()); + } + if (value instanceof String + || value instanceof Boolean + || value instanceof BigInteger + || value instanceof BigDecimal) { + return new Node().value(value); + } + throw new IllegalArgumentException( + "unsupported benchmark node value " + + value.getClass().getName()); + } + + private static Map props( + Object... keysAndValues) { + if ((keysAndValues.length & 1) != 0) { + throw new IllegalArgumentException( + "property keys and values must be paired"); + } + LinkedHashMap properties = + new LinkedHashMap(); + for (int index = 0; + index < keysAndValues.length; + index += 2) { + properties.put( + Objects.requireNonNull( + (String) keysAndValues[index], + "property name"), + node(keysAndValues[index + 1])); + } + return properties; + } + + /** Exact benchmark oracle: admitted result BlueId plus full named trace. */ + public static final class ExpectedOutcome { + private final String outputBlueId; + private final BexGasLedger gasLedger; + + private ExpectedOutcome( + String outputBlueId, + BexGasLedger gasLedger) { + this.outputBlueId = Objects.requireNonNull( + outputBlueId, "outputBlueId"); + this.gasLedger = Objects.requireNonNull( + gasLedger, "gasLedger"); + } + + public String outputBlueId() { + return outputBlueId; + } + + public BexGasLedger gasLedger() { + return gasLedger; + } + } +} diff --git a/blue-bex-conformance/src/jmh/java/blue/bex/benchmark/BexCoreBenchmark.java b/blue-bex-conformance/src/jmh/java/blue/bex/benchmark/BexCoreBenchmark.java new file mode 100644 index 0000000..76fe87a --- /dev/null +++ b/blue-bex-conformance/src/jmh/java/blue/bex/benchmark/BexCoreBenchmark.java @@ -0,0 +1,483 @@ +package blue.bex.benchmark; + +import blue.bex.api.BexEngine; +import blue.bex.api.BexExecutionContext; +import blue.bex.api.BexProgramSource; +import blue.bex.compile.BexCompiledProgram; +import blue.bex.compile.LruBexCompiledProgramCache; +import blue.bex.result.BexExecutionResult; +import blue.bex.value.BexValue; +import blue.bex.value.BexValues; +import blue.language.model.Node; +import blue.language.snapshot.FrozenNode; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Collections; +import java.util.concurrent.TimeUnit; + +import static blue.bex.benchmark.BexBenchmarkSupport.context; +import static blue.bex.benchmark.BexBenchmarkSupport.expected; +import static blue.bex.benchmark.BexBenchmarkSupport.frozen; +import static blue.bex.benchmark.BexBenchmarkSupport.integerList; +import static blue.bex.benchmark.BexBenchmarkSupport.list; +import static blue.bex.benchmark.BexBenchmarkSupport.obj; +import static blue.bex.benchmark.BexBenchmarkSupport.op; +import static blue.bex.benchmark.BexBenchmarkSupport.verify; + +/** + * BEX compile/runtime JMH matrix. Every measured invocation checks the admitted + * result BlueId and the complete immutable named-gas ledger before returning. + */ +@BenchmarkMode({Mode.Throughput, Mode.AverageTime}) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 8, time = 1) +@Fork(2) +public class BexCoreBenchmark { + @Benchmark + public BexExecutionResult coldCompileAndExecute( + BasicState state) { + BexExecutionResult result = BexEngine.builder() + .cache(new LruBexCompiledProgramCache()) + .build() + .compileAndExecute( + state.coldSource, + context()); + return verify(result, state.coldExpected); + } + + @Benchmark + public BexExecutionResult compileCacheHit( + BasicState state) { + BexExecutionResult result = state.cacheEngine + .compileAndExecute( + state.cacheSource, + context()); + return verify(result, state.cacheExpected); + } + + @Benchmark + public BexExecutionResult smallExecution( + BasicState state) { + return verify( + state.engine.execute( + state.smallProgram, + context()), + state.smallExpected); + } + + @Benchmark + public BexExecutionResult functionExecution( + BasicState state) { + return verify( + state.engine.execute( + state.functionProgram, + context()), + state.functionExpected); + } + + @Benchmark + public BexExecutionResult standaloneGas( + BasicState state) { + return verify( + state.engine.execute( + state.standaloneGasProgram, + context()), + state.standaloneGasExpected); + } + + @Benchmark + public BexExecutionResult collectionMap( + CollectionState state) { + return verify( + state.engine.execute( + state.program, + context()), + state.expected); + } + + @Benchmark + public BexExecutionResult pointerDepth( + PointerState state) { + return verify( + state.engine.execute( + state.program, + context()), + state.expected); + } + + @Benchmark + public BexExecutionResult textWork( + TextNumericState state) { + return verify( + state.engine.execute( + state.textProgram, + context()), + state.textExpected); + } + + @Benchmark + public BexExecutionResult integerAndDecimalWork( + TextNumericState state) { + return verify( + state.engine.execute( + state.numericProgram, + context()), + state.numericExpected); + } + + @Benchmark + public BexExecutionResult exactOutput( + OutputState state) { + return verify( + state.engine.execute( + state.exactOutputProgram, + context("subject", state.exactValue)), + state.exactOutputExpected); + } + + @Benchmark + public BexExecutionResult transientOutput( + OutputState state) { + return verify( + state.engine.execute( + state.transientOutputProgram, + context()), + state.transientOutputExpected); + } + + @Benchmark + public BexExecutionResult nodeBlueIdExact( + OutputState state) { + return verify( + state.engine.execute( + state.exactIdentityProgram, + context("subject", state.exactValue)), + state.exactIdentityExpected); + } + + @Benchmark + public BexExecutionResult nodeBlueIdTransient( + OutputState state) { + return verify( + state.engine.execute( + state.transientIdentityProgram, + context()), + state.transientIdentityExpected); + } + + @Benchmark + public BexExecutionResult intrinsicInvocation( + IntrinsicState state) { + return verify( + state.engine.execute( + state.program, + context()), + state.expected); + } + + @State(Scope.Thread) + public static class BasicState { + private BexEngine engine; + private BexProgramSource coldSource; + private BexBenchmarkSupport.ExpectedOutcome coldExpected; + private BexEngine cacheEngine; + private BexProgramSource cacheSource; + private BexBenchmarkSupport.ExpectedOutcome cacheExpected; + private BexCompiledProgram smallProgram; + private BexBenchmarkSupport.ExpectedOutcome smallExpected; + private BexCompiledProgram functionProgram; + private BexBenchmarkSupport.ExpectedOutcome functionExpected; + private BexCompiledProgram standaloneGasProgram; + private BexBenchmarkSupport.ExpectedOutcome standaloneGasExpected; + + @Setup(Level.Trial) + public void setup() { + engine = BexEngine.builder().build(); + + BexProgramSource smallSource = BexProgramSource.expression( + frozen(op("$add", list(40, 2)))); + smallProgram = engine.compile(smallSource); + smallExpected = expected( + engine.execute(smallProgram, context())); + + Node function = obj( + "args", obj("left", obj(), "right", obj()), + "expr", op("$add", list( + op("$var", "left"), + op("$var", "right")))); + BexProgramSource functionSource = BexProgramSource.inline( + frozen(obj( + "functions", obj("sum", function), + "expr", op("$call", obj( + "function", "sum", + "args", obj( + "left", 19, + "right", 23)))))); + functionProgram = engine.compile(functionSource); + functionExpected = expected( + engine.execute(functionProgram, context())); + + coldSource = BexProgramSource.inline( + frozen(obj( + "constants", obj("limit", 41), + "expr", obj( + "answer", op("$add", list( + op("$const", "limit"), + 1)), + "label", op("$concat", list( + "cold-", "compile")))))); + coldExpected = expected(BexEngine.builder().build() + .compileAndExecute(coldSource, context())); + + cacheSource = BexProgramSource.expression( + frozen(op("$pointerGet", obj( + "object", obj( + "nested", obj("answer", 42)), + "path", "/nested/answer")))); + cacheEngine = BexEngine.builder() + .cache(new LruBexCompiledProgramCache()) + .build(); + cacheEngine.compile(cacheSource); + cacheExpected = expected(cacheEngine.compileAndExecute( + cacheSource, context())); + + BexProgramSource gasSource = BexProgramSource.expression( + frozen(op("$map", obj( + "in", integerList(128), + "item", "item", + "expr", op("$add", list( + op("$var", "item"), + 1)))))); + standaloneGasProgram = engine.compile(gasSource); + standaloneGasExpected = expected( + engine.execute( + standaloneGasProgram, + context())); + } + } + + @State(Scope.Thread) + public static class CollectionState { + @Param({"10", "100", "1000", "10000"}) + public int size; + + private BexEngine engine; + private BexCompiledProgram program; + private BexBenchmarkSupport.ExpectedOutcome expected; + + @Setup(Level.Trial) + public void setup() { + engine = BexEngine.builder().build(); + BexProgramSource source = BexProgramSource.expression( + frozen(op("$map", obj( + "in", integerList(size), + "item", "item", + "index", "index", + "expr", op("$add", list( + op("$var", "item"), + op("$var", "index"))))))); + program = engine.compile(source); + expected = expected(engine.execute(program, context())); + } + } + + @State(Scope.Thread) + public static class PointerState { + @Param({"1", "8", "32", "128"}) + public int depth; + + private BexEngine engine; + private BexCompiledProgram program; + private BexBenchmarkSupport.ExpectedOutcome expected; + + @Setup(Level.Trial) + public void setup() { + Node nested = new Node().value("leaf"); + StringBuilder pointer = new StringBuilder(); + for (int index = depth - 1; index >= 0; index--) { + String key = "level-" + index; + nested = obj(key, nested); + } + for (int index = 0; index < depth; index++) { + pointer.append('/').append("level-").append(index); + } + engine = BexEngine.builder().build(); + BexProgramSource source = BexProgramSource.expression( + frozen(op("$pointerGet", obj( + "object", nested, + "path", pointer.toString())))); + program = engine.compile(source); + expected = expected(engine.execute(program, context())); + } + } + + @State(Scope.Thread) + public static class TextNumericState { + private BexEngine engine; + private BexCompiledProgram textProgram; + private BexBenchmarkSupport.ExpectedOutcome textExpected; + private BexCompiledProgram numericProgram; + private BexBenchmarkSupport.ExpectedOutcome numericExpected; + + @Setup(Level.Trial) + public void setup() { + engine = BexEngine.builder().build(); + String text = repeat("Blue-\uD83D\uDE80-", 512); + BexProgramSource textSource = BexProgramSource.expression( + frozen(op("$concat", list( + text, + "|", + text, + "|tail")))); + textProgram = engine.compile(textSource); + textExpected = expected( + engine.execute(textProgram, context())); + + BigInteger left = BigInteger.ONE.shiftLeft(4096) + .add(BigInteger.valueOf(17L)); + BigInteger right = BigInteger.ONE.shiftLeft(2048) + .add(BigInteger.valueOf(31L)); + BexProgramSource numericSource = BexProgramSource.expression( + frozen(obj( + "integer", op("$multiply", list( + left, right)), + "decimalCompare", op("$gt", list( + new BigDecimal( + "98765432109876543210.875"), + new BigDecimal( + "12345678901234567890.125"))), + "decimalEqual", op("$eq", list( + new BigDecimal( + "12345678901234567890.125"), + new BigDecimal( + "12345678901234567890.125"))), + "decimalKind", op("$kind", + new BigDecimal("1.2500")), + "decimalToInteger", op("$integer", + new BigDecimal( + "12345678901234567890.000"))))); + numericProgram = engine.compile(numericSource); + numericExpected = expected( + engine.execute(numericProgram, context())); + } + + private static String repeat(String value, int count) { + StringBuilder repeated = new StringBuilder( + value.length() * count); + for (int index = 0; index < count; index++) { + repeated.append(value); + } + return repeated.toString(); + } + } + + @State(Scope.Thread) + public static class OutputState { + private BexEngine engine; + private BexValue exactValue; + private BexCompiledProgram exactOutputProgram; + private BexBenchmarkSupport.ExpectedOutcome exactOutputExpected; + private BexCompiledProgram transientOutputProgram; + private BexBenchmarkSupport.ExpectedOutcome transientOutputExpected; + private BexCompiledProgram exactIdentityProgram; + private BexBenchmarkSupport.ExpectedOutcome exactIdentityExpected; + private BexCompiledProgram transientIdentityProgram; + private BexBenchmarkSupport.ExpectedOutcome transientIdentityExpected; + + @Setup(Level.Trial) + public void setup() { + engine = BexEngine.builder().build(); + FrozenNode exactNode = frozen(obj( + "status", "exact", + "values", list(1, 2, 3))); + exactValue = BexValues.frozen(exactNode); + + exactOutputProgram = engine.compile( + BexProgramSource.expression( + frozen(op("$binding", "subject")))); + exactOutputExpected = expected(engine.execute( + exactOutputProgram, + context("subject", exactValue))); + + Node transientValue = obj( + "status", op("$concat", list( + "trans", "ient")), + "values", op("$listConcat", list( + list(1, 2), + list(3, 4)))); + transientOutputProgram = engine.compile( + BexProgramSource.expression( + frozen(transientValue))); + transientOutputExpected = expected(engine.execute( + transientOutputProgram, context())); + + exactIdentityProgram = engine.compile( + BexProgramSource.expression( + frozen(op("$nodeBlueId", + op("$binding", "subject"))))); + exactIdentityExpected = expected(engine.execute( + exactIdentityProgram, + context("subject", exactValue))); + + transientIdentityProgram = engine.compile( + BexProgramSource.expression( + frozen(op("$nodeBlueId", obj( + "status", op("$concat", list( + "trans", "ient")), + "values", list(1, 2, 3, 4)))))); + transientIdentityExpected = expected(engine.execute( + transientIdentityProgram, context())); + } + } + + @State(Scope.Thread) + public static class IntrinsicState { + private static final String BLUE_ID = + "BexBenchmarkIntrinsic"; + private static final String REGISTRY_IDENTITY = + "blue-bex-benchmark-intrinsics/1.0"; + + private BexEngine engine; + private BexCompiledProgram program; + private BexBenchmarkSupport.ExpectedOutcome expected; + + @Setup(Level.Trial) + public void setup() { + engine = BexEngine.builder() + .intrinsic( + BLUE_ID, + REGISTRY_IDENTITY, + Collections.singletonMap( + "work", 7L), + invocation -> { + invocation.charge( + "work", 3L, + "benchmark-intrinsic-work"); + return invocation.field("payload"); + }) + .build(); + BexProgramSource source = BexProgramSource.expression( + frozen(op("$intrinsic", obj( + "type", obj("blueId", BLUE_ID), + "payload", obj( + "answer", 42, + "label", "intrinsic"))))); + program = engine.compile(source); + expected = expected(engine.execute(program, context())); + } + } +} diff --git a/blue-bex-conformance/src/jmh/java/blue/bex/benchmark/package-info.java b/blue-bex-conformance/src/jmh/java/blue/bex/benchmark/package-info.java new file mode 100644 index 0000000..8dc9ad6 --- /dev/null +++ b/blue-bex-conformance/src/jmh/java/blue/bex/benchmark/package-info.java @@ -0,0 +1,9 @@ +/** + * JMH benchmarks that validate BEX results and exact named gas traces before + * recording throughput and allocation evidence. + * + *

Benchmark states are fork-owned and never shared with production + * invocations. Parameters are non-null and an identity or gas mismatch aborts + * the run, preventing performance numbers from masking semantic drift.

+ */ +package blue.bex.benchmark; diff --git a/blue-bex-conformance/src/jmh/java/blue/language/processor/BexHostedGasBenchmark.java b/blue-bex-conformance/src/jmh/java/blue/language/processor/BexHostedGasBenchmark.java new file mode 100644 index 0000000..538e8c2 --- /dev/null +++ b/blue-bex-conformance/src/jmh/java/blue/language/processor/BexHostedGasBenchmark.java @@ -0,0 +1,140 @@ +package blue.language.processor; + +import blue.bex.api.BexEngine; +import blue.bex.api.BexExecutionContext; +import blue.bex.api.BexProgramSource; +import blue.bex.api.FrozenBexDocumentView; +import blue.bex.benchmark.BexBenchmarkSupport; +import blue.bex.compile.BexCompiledProgram; +import blue.bex.contracts.BexContractsFailureBoundary; +import blue.bex.contracts.ProcessorExecutionContextBexGasLedgerHost; +import blue.bex.gas.BexGasCharge; +import blue.bex.output.BexSemanticIdentityBoundary; +import blue.bex.result.BexExecutionResult; +import blue.language.model.Node; +import blue.language.snapshot.FrozenNode; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static blue.bex.benchmark.BexBenchmarkSupport.expected; +import static blue.bex.benchmark.BexBenchmarkSupport.frozen; +import static blue.bex.benchmark.BexBenchmarkSupport.integerList; +import static blue.bex.benchmark.BexBenchmarkSupport.list; +import static blue.bex.benchmark.BexBenchmarkSupport.obj; +import static blue.bex.benchmark.BexBenchmarkSupport.op; +import static blue.bex.benchmark.BexBenchmarkSupport.verify; + +/** Contracts RuntimeWorkSession child-ledger benchmark with exact trace checks. */ +@BenchmarkMode({Mode.Throughput, Mode.AverageTime}) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 8, time = 1) +@Fork(2) +public class BexHostedGasBenchmark { + @Benchmark + public BexExecutionResult contractsHostedGas( + HostedState state) { + return state.executeAndVerify(); + } + + @State(Scope.Thread) + public static class HostedState { + private static final long HOST_BUDGET = + 100_000L; + + private BexEngine engine; + private BexCompiledProgram program; + private FrozenNode document; + private BexBenchmarkSupport.ExpectedOutcome expected; + + @Setup(Level.Trial) + public void setup() { + engine = BexEngine.builder().build(); + document = FrozenNode.fromResolvedNode(new Node()); + BexProgramSource source = BexProgramSource.expression( + frozen(op("$map", obj( + "in", integerList(128), + "item", "item", + "expr", op("$add", list( + op("$var", "item"), + 1)))))); + program = engine.compile(source); + expected = expected(executeHosted()); + } + + private BexExecutionResult executeAndVerify() { + return verify(executeHosted(), expected); + } + + private BexExecutionResult executeHosted() { + GasMeter parent = new GasMeter( + GasSchedule.contracts10(), + HOST_BUDGET); + RuntimeWorkSession session = new RuntimeWorkSession( + parent, + RuntimeWorkSession.Mode.PROCESSING); + ProcessorExecutionContextBexGasLedgerHost host = + new ProcessorExecutionContextBexGasLedgerHost( + session, + "benchmarkHosted"); + BexExecutionContext context = BexExecutionContext.builder() + .document(new FrozenBexDocumentView(document)) + .gasLedgerHost(host) + .semanticIdentityBoundary( + BexSemanticIdentityBoundary.STANDALONE) + .failureBoundary( + BexContractsFailureBoundary.INSTANCE) + .gasLimit(HOST_BUDGET) + .build(); + + BexExecutionResult result = engine.execute( + program, context); + assertHostedTrace( + result.gasTrace(), + session.stagedTrace()); + session.complete(); + assertHostedTrace( + result.gasTrace(), + parent.trace()); + if (parent.totalGas() != result.gasUsed()) { + throw new IllegalStateException( + "hosted parent gas differs from BEX child total"); + } + return result; + } + + private static void assertHostedTrace( + List bex, + List hosted) { + if (bex.size() != hosted.size()) { + throw new IllegalStateException( + "hosted trace size differs from BEX trace"); + } + for (int index = 0; index < bex.size(); index++) { + BexGasCharge child = bex.get(index); + GasTraceEntry parent = hosted.get(index); + if (child.sequence() != parent.sequence() + || !child.counterName().equals( + parent.counter()) + || child.quantity() != parent.quantity() + || child.weight() != parent.weight() + || child.gas() != parent.subtotal()) { + throw new IllegalStateException( + "hosted trace differs at entry " + index); + } + } + } + } +} diff --git a/blue-bex-conformance/src/main/java/.gitkeep b/blue-bex-conformance/src/main/java/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/blue-bex-conformance/src/main/java/.gitkeep @@ -0,0 +1 @@ + diff --git a/blue-bex-contracts/build.gradle.kts b/blue-bex-contracts/build.gradle.kts new file mode 100644 index 0000000..2ce8a16 --- /dev/null +++ b/blue-bex-contracts/build.gradle.kts @@ -0,0 +1,21 @@ +plugins { + id("blue.bex.java8-library") + id("blue.bex.language-dependencies") + id("blue.bex.reproducible-archives") + id("blue.bex.publication") +} + +description = "Contracts-hosted adapters for the Blue BEX runtime" + +base { + archivesName.set("blue-bex-contracts") +} + +val languageVersion = extensions + .getByType() + .version.get() + +dependencies { + api(project(":blue-bex-core")) + api("blue.language:blue-contracts-core:$languageVersion") +} diff --git a/blue-bex-contracts/src/main/java/blue/bex/contracts/BexContractsExecutionContext.java b/blue-bex-contracts/src/main/java/blue/bex/contracts/BexContractsExecutionContext.java new file mode 100644 index 0000000..a8c4347 --- /dev/null +++ b/blue-bex-contracts/src/main/java/blue/bex/contracts/BexContractsExecutionContext.java @@ -0,0 +1,69 @@ +package blue.bex.contracts; + +import blue.bex.api.BexExecutionContext; +import blue.bex.gas.BexGasCounter; +import blue.bex.value.BexValues; +import blue.language.processor.ProcessorExecutionContext; +import blue.language.snapshot.FrozenNode; + +import java.util.Objects; + +/** + * Invocation-scoped composition point for the Contracts-hosted BEX adapters. + */ +public final class BexContractsExecutionContext { + private BexContractsExecutionContext() { + } + + public static BexExecutionContext.Builder builder( + ProcessorExecutionContext context) { + return configure( + BexExecutionContext.builder(), + context, + BexGasCounter.NAMESPACE); + } + + public static BexExecutionContext.Builder builder( + ProcessorExecutionContext context, + String runtimeNamespace) { + return configure( + BexExecutionContext.builder(), + context, + runtimeNamespace); + } + + public static BexExecutionContext.Builder configure( + BexExecutionContext.Builder builder, + ProcessorExecutionContext context) { + return configure(builder, context, BexGasCounter.NAMESPACE); + } + + public static BexExecutionContext.Builder configure( + BexExecutionContext.Builder builder, + ProcessorExecutionContext context, + String runtimeNamespace) { + BexExecutionContext.Builder exactBuilder = + Objects.requireNonNull(builder, "builder"); + ProcessorExecutionContext exactContext = + Objects.requireNonNull(context, "context"); + exactBuilder.document( + new ProcessorExecutionContextBexDocumentView(exactContext)); + exactBuilder.gasLedgerHost( + new ProcessorExecutionContextBexGasLedgerHost( + exactContext, runtimeNamespace)); + exactBuilder.semanticIdentityBoundary( + new ProcessorExecutionContextBexSemanticIdentityBoundary( + exactContext)); + exactBuilder.failureBoundary(BexContractsFailureBoundary.INSTANCE); + exactBuilder.event(BexValues.nodeSnapshot(exactContext.event())); + FrozenNode processEvent = exactContext.frozenProcessEvent(); + exactBuilder.processingEvent(processEvent != null + ? BexValues.frozen(processEvent) + : BexValues.undefined()); + FrozenNode contract = exactContext.frozenContractNode(); + exactBuilder.currentContract(contract != null + ? BexValues.frozen(contract) + : BexValues.undefined()); + return exactBuilder; + } +} diff --git a/blue-bex-contracts/src/main/java/blue/bex/contracts/BexContractsFailureBoundary.java b/blue-bex-contracts/src/main/java/blue/bex/contracts/BexContractsFailureBoundary.java new file mode 100644 index 0000000..b1ae56f --- /dev/null +++ b/blue-bex-contracts/src/main/java/blue/bex/contracts/BexContractsFailureBoundary.java @@ -0,0 +1,80 @@ +package blue.bex.contracts; + +import blue.bex.BexException; +import blue.bex.BexExecutionEvidenceUnavailableException; +import blue.bex.BexInvalidExecutionEvidenceException; +import blue.bex.api.BexFailureBoundary; +import blue.bex.gas.BexGasLimitExceededException; +import blue.bex.gas.BexHostGasExhaustion; +import blue.language.processor.ExecutionEvidenceUnavailableException; +import blue.language.processor.GasLimitExceededException; +import blue.language.processor.InvalidExecutionEvidenceException; +import blue.language.processor.PortableLimitExceededException; +import blue.language.processor.ProcessorFailureException; + +/** Contracts exception classification and translation at the adapter edge. */ +public final class BexContractsFailureBoundary + implements BexFailureBoundary { + public static final BexContractsFailureBoundary INSTANCE = + new BexContractsFailureBoundary(); + + private BexContractsFailureBoundary() { + } + + @Override + public Classification classify(Throwable failure) { + Throwable current = failure; + boolean genericBexFailure = false; + while (current != null) { + if (current instanceof ExecutionEvidenceUnavailableException + || current + instanceof BexExecutionEvidenceUnavailableException) { + return Classification.EVIDENCE_UNAVAILABLE; + } + if (current instanceof ProcessorFailureException + || current instanceof InvalidExecutionEvidenceException + || current instanceof PortableLimitExceededException + || current instanceof GasLimitExceededException + || current instanceof BexHostGasExhaustion + || current instanceof BexGasLimitExceededException + || current instanceof BexInvalidExecutionEvidenceException) { + return Classification.DETERMINISTIC; + } + if (current instanceof BexException) { + genericBexFailure = true; + } + Throwable cause = current.getCause(); + if (cause == current) { + break; + } + current = cause; + } + return genericBexFailure + ? Classification.DETERMINISTIC + : Classification.UNCLASSIFIED; + } + + @Override + public RuntimeException translate(RuntimeException failure) { + Throwable current = failure; + while (current != null) { + if (current instanceof BexExecutionEvidenceUnavailableException) { + BexExecutionEvidenceUnavailableException unavailable = + (BexExecutionEvidenceUnavailableException) current; + return new ExecutionEvidenceUnavailableException( + unavailable.getMessage(), + unavailable.requiredExactBlueIds()); + } + if (current instanceof BexInvalidExecutionEvidenceException) { + return new InvalidExecutionEvidenceException( + current.getMessage()); + } + Throwable cause = current.getCause(); + if (cause == current) { + break; + } + current = cause; + } + return failure; + } +} diff --git a/src/main/java/blue/bex/api/ProcessorExecutionContextBexDocumentView.java b/blue-bex-contracts/src/main/java/blue/bex/contracts/ProcessorExecutionContextBexDocumentView.java similarity index 79% rename from src/main/java/blue/bex/api/ProcessorExecutionContextBexDocumentView.java rename to blue-bex-contracts/src/main/java/blue/bex/contracts/ProcessorExecutionContextBexDocumentView.java index 34e9bf4..0a320b5 100644 --- a/src/main/java/blue/bex/api/ProcessorExecutionContextBexDocumentView.java +++ b/blue-bex-contracts/src/main/java/blue/bex/contracts/ProcessorExecutionContextBexDocumentView.java @@ -1,20 +1,21 @@ -package blue.bex.api; +package blue.bex.contracts; +import blue.bex.api.BexDocumentView; import blue.bex.value.BexValue; import blue.bex.value.BexValues; -import blue.language.snapshot.FrozenNode; -import blue.language.processor.ProcessorExecutionContext; import blue.language.model.wire.JsonPointer; +import blue.language.processor.ProcessorExecutionContext; +import blue.language.snapshot.FrozenNode; import java.util.Objects; -/** - * Adapter from blue-language-java processor execution context to BEX document view. - */ -public final class ProcessorExecutionContextBexDocumentView implements BexDocumentView { +/** Contracts adapter from a processor invocation to a BEX document view. */ +public final class ProcessorExecutionContextBexDocumentView + implements BexDocumentView { private final ProcessorExecutionContext context; - public ProcessorExecutionContextBexDocumentView(ProcessorExecutionContext context) { + public ProcessorExecutionContextBexDocumentView( + ProcessorExecutionContext context) { this.context = Objects.requireNonNull(context, "context"); } diff --git a/blue-bex-contracts/src/main/java/blue/bex/contracts/ProcessorExecutionContextBexGasLedgerHost.java b/blue-bex-contracts/src/main/java/blue/bex/contracts/ProcessorExecutionContextBexGasLedgerHost.java new file mode 100644 index 0000000..0918ad9 --- /dev/null +++ b/blue-bex-contracts/src/main/java/blue/bex/contracts/ProcessorExecutionContextBexGasLedgerHost.java @@ -0,0 +1,255 @@ +package blue.bex.contracts; + +import blue.bex.api.BexGasLedgerHost; +import blue.bex.gas.BexGasChargeContext; +import blue.bex.gas.BexGasCounter; +import blue.bex.gas.BexGasLedgerCapability; +import blue.bex.gas.BexGasLimitExceededException; +import blue.bex.gas.BexHostGasExhaustion; +import blue.bex.gas.BexSharedGasBudget; +import blue.language.processor.GasChargeContext; +import blue.language.processor.GasLimitExceededException; +import blue.language.processor.GasMeter; +import blue.language.processor.ProcessorErrorCategory; +import blue.language.processor.ProcessorExecutionContext; +import blue.language.processor.ProcessorFailureException; +import blue.language.processor.RuntimeWorkBudget; +import blue.language.processor.RuntimeWorkSession; + +import java.util.Map; +import java.util.Objects; + +/** Contracts 1.0 adapter for BEX's host-neutral gas capabilities. */ +public final class ProcessorExecutionContextBexGasLedgerHost + implements BexGasLedgerHost { + private final ProcessorExecutionContext context; + private final RuntimeWorkSession session; + private final String runtimeNamespace; + + public ProcessorExecutionContextBexGasLedgerHost( + ProcessorExecutionContext context) { + this(context, BexGasCounter.NAMESPACE); + } + + public ProcessorExecutionContextBexGasLedgerHost( + ProcessorExecutionContext context, + String runtimeNamespace) { + this.context = Objects.requireNonNull(context, "context"); + this.session = null; + this.runtimeNamespace = requireRuntimeNamespace(runtimeNamespace); + } + + public ProcessorExecutionContextBexGasLedgerHost( + RuntimeWorkSession session, + String runtimeNamespace) { + this.context = null; + this.session = Objects.requireNonNull(session, "session"); + this.runtimeNamespace = requireRuntimeNamespace(runtimeNamespace); + } + + @Override + public BexGasLedgerCapability open( + String namespace, + Map counterWeights) { + return open(namespace, counterWeights, null); + } + + @Override + public BexSharedGasBudget openSharedBudget(long maximumGas) { + return session != null + ? new ContractsSharedGasBudget( + this, session.openSharedBudget(maximumGas)) + : BexGasLedgerHost.super.openSharedBudget(maximumGas); + } + + @Override + public BexGasLedgerCapability open( + String namespace, + Map counterWeights, + BexSharedGasBudget sharedBudget) { + String logicalNamespace = requireRuntimeNamespace(namespace); + String physicalNamespace = physicalNamespace(logicalNamespace); + GasMeter.ChildGasLedger ledger; + if (session != null) { + RuntimeWorkBudget budget = sharedBudget == null + ? null + : requireSharedBudget(sharedBudget).delegate; + ledger = session.openLedger( + physicalNamespace, counterWeights, budget); + } else { + if (sharedBudget != null) { + throw new IllegalArgumentException( + "ProcessorExecutionContext does not expose shared runtime budgets"); + } + ledger = context.newRuntimeGasLedger( + physicalNamespace, counterWeights); + } + return new ContractsGasLedger(this, ledger); + } + + @Override + public void submit(BexGasLedgerCapability ledger) { + GasMeter.ChildGasLedger exact = requireLedger(ledger).delegate; + if (session != null) { + session.submit(exact); + } else { + context.submitRuntimeGasLedger(exact); + } + } + + @Override + public boolean separatesRuntimeNamespaces() { + return true; + } + + @Override + public void failedDeterministically(BexGasLedgerCapability ledger) { + requireLedger(ledger); + } + + @Override + public void evidenceUnavailable(BexGasLedgerCapability ledger) { + requireLedger(ledger); + } + + @Override + public RuntimeException localGasLimitExceeded( + BexGasLimitExceededException exhaustion, + RuntimeException originalFailure) { + BexGasLimitExceededException exact = + Objects.requireNonNull(exhaustion, "exhaustion"); + Objects.requireNonNull(originalFailure, "originalFailure"); + return new ProcessorFailureException( + ProcessorErrorCategory.GasLimitExceeded, + exact.getMessage(), + exact); + } + + @Override + public void propagateGasExhaustion( + BexGasLedgerCapability ledger, + BexHostGasExhaustion exhaustion) { + requireLedger(ledger); + RuntimeException nativeFailure = Objects.requireNonNull( + exhaustion, "exhaustion").hostFailure(); + if (!(nativeFailure instanceof GasLimitExceededException)) { + throw new IllegalArgumentException( + "Contracts gas exhaustion must retain its exact host rejection", + nativeFailure); + } + GasLimitExceededException exact = + (GasLimitExceededException) nativeFailure; + if (session != null) { + session.propagateGasExhaustion(exact); + } + throw exact; + } + + public String runtimeNamespace() { + return runtimeNamespace; + } + + public String physicalNamespace(String logicalNamespace) { + String exactLogical = requireRuntimeNamespace(logicalNamespace); + return BexGasCounter.NAMESPACE.equals(exactLogical) + ? runtimeNamespace + : runtimeNamespace + "/" + exactLogical; + } + + private ContractsGasLedger requireLedger( + BexGasLedgerCapability ledger) { + if (!(ledger instanceof ContractsGasLedger) + || ((ContractsGasLedger) ledger).owner != this) { + throw new IllegalArgumentException( + "Gas ledger was not opened by this Contracts adapter"); + } + return (ContractsGasLedger) ledger; + } + + private ContractsSharedGasBudget requireSharedBudget( + BexSharedGasBudget budget) { + if (!(budget instanceof ContractsSharedGasBudget) + || ((ContractsSharedGasBudget) budget).owner != this) { + throw new IllegalArgumentException( + "Shared gas budget was not opened by this Contracts adapter"); + } + return (ContractsSharedGasBudget) budget; + } + + private static String requireRuntimeNamespace(String value) { + if (value == null || value.trim().isEmpty()) { + throw new IllegalArgumentException("runtimeNamespace is required"); + } + if (value.indexOf('/') >= 0) { + throw new IllegalArgumentException( + "runtimeNamespace must not contain the reserved '/' separator"); + } + return value; + } + + private static final class ContractsSharedGasBudget + implements BexSharedGasBudget { + private final ProcessorExecutionContextBexGasLedgerHost owner; + private final RuntimeWorkBudget delegate; + + private ContractsSharedGasBudget( + ProcessorExecutionContextBexGasLedgerHost owner, + RuntimeWorkBudget delegate) { + this.owner = owner; + this.delegate = Objects.requireNonNull(delegate, "delegate"); + } + + @Override public long maximumGas() { return delegate.maximumGas(); } + @Override public long admittedGas() { return delegate.admittedGas(); } + @Override public long remainingGas() { return delegate.remainingGas(); } + } + + private static final class ContractsGasLedger + implements BexGasLedgerCapability { + private final ProcessorExecutionContextBexGasLedgerHost owner; + private final GasMeter.ChildGasLedger delegate; + + private ContractsGasLedger( + ProcessorExecutionContextBexGasLedgerHost owner, + GasMeter.ChildGasLedger delegate) { + this.owner = owner; + this.delegate = Objects.requireNonNull(delegate, "delegate"); + } + + @Override public String namespace() { return delegate.namespace(); } + @Override public long totalGas() { return delegate.totalGas(); } + @Override public long remainingGas() { return delegate.remainingGas(); } + @Override public long effectiveBudget() { return delegate.effectiveBudget(); } + @Override public Map counterWeights() { + return delegate.counterWeights(); + } + + @Override + public void charge( + String counter, + long quantity, + BexGasChargeContext context) { + BexGasChargeContext exact = context != null + ? context : BexGasChargeContext.empty(); + try { + delegate.charge( + counter, + quantity, + GasChargeContext.of( + exact.scopePath(), + exact.contractKey(), + exact.logicalPath(), + exact.reason())); + } catch (GasLimitExceededException exhausted) { + throw new BexHostGasExhaustion( + exhausted.namespace(), + exhausted.counter(), + exhausted.quantity(), + exhausted.weight(), + exhausted.admittedGas(), + exhausted.effectiveBudget(), + exhausted); + } + } + } +} diff --git a/src/main/java/blue/bex/output/ProcessorExecutionContextBexSemanticIdentityBoundary.java b/blue-bex-contracts/src/main/java/blue/bex/contracts/ProcessorExecutionContextBexSemanticIdentityBoundary.java similarity index 80% rename from src/main/java/blue/bex/output/ProcessorExecutionContextBexSemanticIdentityBoundary.java rename to blue-bex-contracts/src/main/java/blue/bex/contracts/ProcessorExecutionContextBexSemanticIdentityBoundary.java index 73d9b93..cf760b4 100644 --- a/src/main/java/blue/bex/output/ProcessorExecutionContextBexSemanticIdentityBoundary.java +++ b/blue-bex-contracts/src/main/java/blue/bex/contracts/ProcessorExecutionContextBexSemanticIdentityBoundary.java @@ -1,15 +1,14 @@ -package blue.bex.output; +package blue.bex.contracts; +import blue.bex.output.BexEstablishedIdentity; +import blue.bex.output.BexSemanticIdentityBoundary; import blue.language.model.Node; import blue.language.processor.ExactBlueValue; import blue.language.processor.ProcessorExecutionContext; import java.util.Objects; -/** - * Hosted BEX semantic identity boundary backed by the active processor - * invocation. - */ +/** Hosted semantic-output boundary owned by one processor invocation. */ public final class ProcessorExecutionContextBexSemanticIdentityBoundary implements BexSemanticIdentityBoundary { private final ProcessorExecutionContext context; diff --git a/blue-bex-contracts/src/main/java/blue/bex/contracts/package-info.java b/blue-bex-contracts/src/main/java/blue/bex/contracts/package-info.java new file mode 100644 index 0000000..b7a00be --- /dev/null +++ b/blue-bex-contracts/src/main/java/blue/bex/contracts/package-info.java @@ -0,0 +1,14 @@ +/** + * BEX-owned adapters from one Contracts {@code ProcessorExecutionContext} to + * portable BEX document, exact-value, gas, semantic-output, evidence, and + * failure boundaries. + * + *

Adapter instances are invocation-owned and must not outlive or be shared + * across their processor work session. Processor contexts, builders, namespaces, + * and admitted nodes are non-null. Host evidence and processor failures retain + * their Contracts classification rather than becoming undefined or generic BEX + * success. Hosted gas uses a live parent-bounded child capability, admits before + * work, omits rejected charges, and submits/merges each child ledger exactly + * once.

+ */ +package blue.bex.contracts; diff --git a/blue-bex-core/build.gradle.kts b/blue-bex-core/build.gradle.kts new file mode 100644 index 0000000..4abcdf7 --- /dev/null +++ b/blue-bex-core/build.gradle.kts @@ -0,0 +1,21 @@ +plugins { + id("blue.bex.java8-library") + id("blue.bex.language-dependencies") + id("blue.bex.reproducible-archives") + id("blue.bex.publication") +} + +description = "Host-neutral Blue Expression Object compiler and runtime" + +base { + archivesName.set("blue-bex-core") +} + +val languageVersion = extensions + .getByType() + .version.get() + +dependencies { + api("blue.language:blue-language-model:$languageVersion") + api("blue.language:blue-language-core:$languageVersion") +} diff --git a/src/main/java/blue/bex/BexException.java b/blue-bex-core/src/main/java/blue/bex/BexException.java similarity index 100% rename from src/main/java/blue/bex/BexException.java rename to blue-bex-core/src/main/java/blue/bex/BexException.java diff --git a/blue-bex-core/src/main/java/blue/bex/BexExecutionEvidenceUnavailableException.java b/blue-bex-core/src/main/java/blue/bex/BexExecutionEvidenceUnavailableException.java new file mode 100644 index 0000000..17b5f06 --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/BexExecutionEvidenceUnavailableException.java @@ -0,0 +1,43 @@ +package blue.bex; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.TreeSet; + +/** + * Retryable BEX suspension caused by unavailable exact Blue evidence. + */ +public final class BexExecutionEvidenceUnavailableException + extends BexException { + private static final long serialVersionUID = 1L; + + private final List requiredExactBlueIds; + + public BexExecutionEvidenceUnavailableException(String message) { + this(message, Collections.emptyList()); + } + + public BexExecutionEvidenceUnavailableException( + String message, + Collection requiredExactBlueIds) { + super(Objects.requireNonNull(message, "message")); + Objects.requireNonNull(requiredExactBlueIds, "requiredExactBlueIds"); + TreeSet sorted = new TreeSet<>(); + for (String blueId : requiredExactBlueIds) { + if (blueId == null || blueId.isEmpty()) { + throw new IllegalArgumentException( + "Required exact BlueIds must be non-empty"); + } + sorted.add(blueId); + } + this.requiredExactBlueIds = Collections.unmodifiableList( + new ArrayList<>(sorted)); + } + + public List requiredExactBlueIds() { + return requiredExactBlueIds; + } +} diff --git a/blue-bex-core/src/main/java/blue/bex/BexInvalidExecutionEvidenceException.java b/blue-bex-core/src/main/java/blue/bex/BexInvalidExecutionEvidenceException.java new file mode 100644 index 0000000..0639606 --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/BexInvalidExecutionEvidenceException.java @@ -0,0 +1,10 @@ +package blue.bex; + +/** Deterministic rejection of invalid exact Blue execution evidence. */ +public final class BexInvalidExecutionEvidenceException extends BexException { + private static final long serialVersionUID = 1L; + + public BexInvalidExecutionEvidenceException(String message) { + super(message); + } +} diff --git a/src/main/java/blue/bex/BexSourcePath.java b/blue-bex-core/src/main/java/blue/bex/BexSourcePath.java similarity index 100% rename from src/main/java/blue/bex/BexSourcePath.java rename to blue-bex-core/src/main/java/blue/bex/BexSourcePath.java diff --git a/src/main/java/blue/bex/api/BexDocumentView.java b/blue-bex-core/src/main/java/blue/bex/api/BexDocumentView.java similarity index 83% rename from src/main/java/blue/bex/api/BexDocumentView.java rename to blue-bex-core/src/main/java/blue/bex/api/BexDocumentView.java index cd882d2..37e3693 100644 --- a/src/main/java/blue/bex/api/BexDocumentView.java +++ b/blue-bex-core/src/main/java/blue/bex/api/BexDocumentView.java @@ -1,6 +1,7 @@ package blue.bex.api; import blue.bex.value.BexValue; +import blue.bex.spi.BexDocumentAccess; /** * Immutable document access boundary for BEX execution. @@ -9,7 +10,7 @@ * document reads call either {@link #canonicalAt(String)} or * {@link #resolvedAt(String)} with an absolute pointer.

*/ -public interface BexDocumentView { +public interface BexDocumentView extends BexDocumentAccess { String resolvePointer(String authoredPointer); BexValue canonicalAt(String absolutePointer); BexValue resolvedAt(String absolutePointer); diff --git a/src/main/java/blue/bex/api/BexEngine.java b/blue-bex-core/src/main/java/blue/bex/api/BexEngine.java similarity index 74% rename from src/main/java/blue/bex/api/BexEngine.java rename to blue-bex-core/src/main/java/blue/bex/api/BexEngine.java index b6d62d0..8a4083e 100644 --- a/src/main/java/blue/bex/api/BexEngine.java +++ b/blue-bex-core/src/main/java/blue/bex/api/BexEngine.java @@ -4,12 +4,13 @@ import blue.bex.compile.BexCompiledProgram; import blue.bex.compile.BexCompiledProgramCache; import blue.bex.compile.BexCompiledProgramKey; -import blue.bex.compile.BexCompiler; +import blue.bex.compile.BexCompilerRuntimeAccess; import blue.bex.compile.LruBexCompiledProgramCache; import blue.bex.gas.BexGasSchedule; import blue.bex.pointer.BexPointerCache; import blue.bex.result.BexExecutionResult; -import blue.bex.result.BexMetrics; +import blue.bex.result.BexMetricsRecorder; +import blue.bex.result.BexMetricsSnapshot; import blue.bex.runtime.BexRuntime; import blue.language.registry.BlueCoreTypeRegistry; import blue.language.runtime.BlueLanguage; @@ -24,8 +25,9 @@ * against a {@link BexExecutionContext}. It does not apply document patches, * emit events, or perform host actions.

*/ -public final class BexEngine { +public final class BexEngine implements AutoCloseable { private final BlueLanguage blue; + private final boolean ownsBlue; private final BexGasSchedule gasSchedule; private final BexCompiledProgramCache cache; private final BexMetricsSink metricsSink; @@ -33,7 +35,10 @@ public final class BexEngine { private final BexPointerCache pointerCache = new BexPointerCache(); private BexEngine(Builder builder) { - this.blue = builder.blue; + this.ownsBlue = builder.blue == null; + this.blue = ownsBlue + ? BlueLanguage.builder().build() + : builder.blue; this.gasSchedule = builder.gasSchedule; this.cache = builder.cache; this.metricsSink = builder.metricsSink; @@ -45,24 +50,28 @@ public static Builder builder() { } public BexCompiledProgram compile(BexProgramSource source) { - BexMetrics metrics = new BexMetrics(); + BexMetricsRecorder metrics = new BexMetricsRecorder(); BexCompiledProgram program = compile(source, metrics); - metricsSink.accept(metrics); + publishMetrics(metrics.snapshot()); return program; } - private BexCompiledProgram compile(BexProgramSource source, BexMetrics metrics) { + private BexCompiledProgram compile( + BexProgramSource source, + BexMetricsRecorder metrics) { long start = System.nanoTime(); try { BexCompiledProgramKey key = key(source); BexCompiledProgram cached = cache.get(key); if (cached != null) { metrics.incrementCompileCacheHits(); + validateCompilationEnvironment(cached); validateIntrinsicSupport(cached); return cached; } metrics.incrementCompileCacheMisses(); - BexCompiledProgram program = new BexCompiler(metrics, intrinsics).compile(source); + BexCompiledProgram program = BexCompilerRuntimeAccess.compile( + source, metrics, intrinsics, compileEnvironmentIdentity()); validateIntrinsicSupport(program); cache.put(key, program); return program; @@ -72,14 +81,18 @@ private BexCompiledProgram compile(BexProgramSource source, BexMetrics metrics) } public BexExecutionResult execute(BexCompiledProgram program, BexExecutionContext context) { - BexMetrics metrics = new BexMetrics(); + BexMetricsRecorder metrics = new BexMetricsRecorder(); BexExecutionResult result = execute(program, context, metrics); - metricsSink.accept(result.metrics()); + publishMetrics(result.metricsSnapshot()); return result; } - private BexExecutionResult execute(BexCompiledProgram program, BexExecutionContext context, BexMetrics metrics) { + private BexExecutionResult execute( + BexCompiledProgram program, + BexExecutionContext context, + BexMetricsRecorder metrics) { long start = System.nanoTime(); + validateCompilationEnvironment(program); validateIntrinsicSupport(program); BexRuntime runtime = new BexRuntime(program, context, blue, gasSchedule, metrics, pointerCache, intrinsics); BexExecutionResult result = runtime.execute(); @@ -88,15 +101,15 @@ private BexExecutionResult execute(BexCompiledProgram program, BexExecutionConte result.changeset(), result.events(), result.gasLedger(), - metrics, + metrics.snapshot(), result.output()); } public BexExecutionResult compileAndExecute(BexProgramSource source, BexExecutionContext context) { - BexMetrics metrics = new BexMetrics(); + BexMetricsRecorder metrics = new BexMetricsRecorder(); BexCompiledProgram program = compile(source, metrics); BexExecutionResult result = execute(program, context, metrics); - metricsSink.accept(result.metrics()); + publishMetrics(result.metricsSnapshot()); return result; } @@ -123,16 +136,41 @@ private void validateIntrinsicSupport(BexCompiledProgram program) { } } + private void validateCompilationEnvironment(BexCompiledProgram program) { + String expected = compileEnvironmentIdentity(); + if (!expected.equals(program.compilationEnvironmentIdentity())) { + throw new BexException( + "Compiled BEX environment identity mismatch: expected " + + expected + " but program was compiled with " + + program.compilationEnvironmentIdentity()); + } + } + + private void publishMetrics(BexMetricsSnapshot metrics) { + try { + metricsSink.accept(metrics); + } catch (RuntimeException ignored) { + // Diagnostics must never alter compilation, execution, gas, or cache semantics. + } + } + + /** Closes only the default Language runtime created and owned by this engine. */ + @Override + public void close() { + if (ownsBlue) { + blue.close(); + } + } + public static final class Builder { - private BlueLanguage blue = BlueLanguage.builder().build(); + private BlueLanguage blue; private BexGasSchedule gasSchedule = BexGasSchedule.defaults(); private BexCompiledProgramCache cache = new LruBexCompiledProgramCache(); private BexMetricsSink metricsSink = BexMetricsSink.NOOP; private BexIntrinsicRegistry intrinsics = BexIntrinsicRegistry.empty(); public Builder language(BlueLanguage blue) { - this.blue = blue != null - ? blue : BlueLanguage.builder().build(); + this.blue = blue; return this; } diff --git a/src/main/java/blue/bex/api/BexExecutionContext.java b/blue-bex-core/src/main/java/blue/bex/api/BexExecutionContext.java similarity index 91% rename from src/main/java/blue/bex/api/BexExecutionContext.java rename to blue-bex-core/src/main/java/blue/bex/api/BexExecutionContext.java index 4346370..465fa93 100644 --- a/src/main/java/blue/bex/api/BexExecutionContext.java +++ b/blue-bex-core/src/main/java/blue/bex/api/BexExecutionContext.java @@ -1,13 +1,10 @@ package blue.bex.api; import blue.bex.output.BexSemanticIdentityBoundary; -import blue.bex.output.ProcessorExecutionContextBexSemanticIdentityBoundary; import blue.bex.gas.BexGasMeter; -import blue.bex.gas.BexGasCounter; import blue.bex.value.BexValue; import blue.bex.value.BexValues; -import blue.language.processor.ProcessorExecutionContext; -import blue.language.snapshot.FrozenNode; +import blue.bex.runtime.BexRuntimeContext; import java.util.ArrayDeque; import java.util.Collections; @@ -26,7 +23,7 @@ * the configured {@link BexDocumentView}; callers should not maintain a * separate scope value.

*/ -public final class BexExecutionContext { +public final class BexExecutionContext implements BexRuntimeContext { private final BexDocumentView document; private final Map bindingSlots; private final BexValue event; @@ -37,6 +34,7 @@ public final class BexExecutionContext { private final long gasLimit; private final BexGasLedgerHost gasLedgerHost; private final BexSemanticIdentityBoundary semanticIdentityBoundary; + private final BexFailureBoundary failureBoundary; private final ResolutionCoordinator resolutionCoordinator; private volatile Map materializedBindings; @@ -56,6 +54,9 @@ private BexExecutionContext(Builder builder) { builder.semanticIdentityBoundary != null ? builder.semanticIdentityBoundary : BexSemanticIdentityBoundary.STANDALONE; + this.failureBoundary = builder.failureBoundary != null + ? builder.failureBoundary + : BexFailureBoundary.STANDALONE; if (document == null) { throw new IllegalArgumentException("document is required"); } @@ -154,6 +155,10 @@ public BexSemanticIdentityBoundary semanticIdentityBoundary() { return semanticIdentityBoundary; } + public BexFailureBoundary failureBoundary() { + return failureBoundary; + } + public static final class Builder { private BexDocumentView document; private BexStepResults steps; @@ -161,6 +166,7 @@ public static final class Builder { private long gasLimit = BexGasMeter.NO_LOCAL_LIMIT; private BexGasLedgerHost gasLedgerHost; private BexSemanticIdentityBoundary semanticIdentityBoundary; + private BexFailureBoundary failureBoundary; private final LinkedHashMap bindings = new LinkedHashMap<>(); public Builder document(BexDocumentView document) { @@ -168,48 +174,6 @@ public Builder document(BexDocumentView document) { return this; } - /** - * Configures the standard Contracts 1.0 host views, live gas bridge, - * and invocation-owned semantic identity boundary under the default - * physical runtime namespace {@code bex}. - * - *

Use {@link #processorExecutionContext(ProcessorExecutionContext, - * String)} when one host invocation executes more than one BEX - * program.

- */ - public Builder processorExecutionContext(ProcessorExecutionContext context) { - return processorExecutionContext( - context, BexGasCounter.NAMESPACE); - } - - /** - * Configures hosted execution under an explicit deterministic physical - * runtime namespace. Distinct BEX executions in one processor work - * session must use distinct namespaces so they share one live parent - * budget without producing ambiguous traces. - */ - public Builder processorExecutionContext( - ProcessorExecutionContext context, - String runtimeNamespace) { - Objects.requireNonNull(context, "context"); - document(new ProcessorExecutionContextBexDocumentView(context)); - gasLedgerHost(new ProcessorExecutionContextBexGasLedgerHost( - context, runtimeNamespace)); - semanticIdentityBoundary( - new ProcessorExecutionContextBexSemanticIdentityBoundary( - context)); - event(BexValues.nodeSnapshot(context.event())); - FrozenNode processEvent = context.frozenProcessEvent(); - processingEvent(processEvent != null - ? BexValues.frozen(processEvent) - : BexValues.undefined()); - FrozenNode contract = context.frozenContractNode(); - currentContract(contract != null - ? BexValues.frozen(contract) - : BexValues.undefined()); - return this; - } - public Builder event(BexValue event) { return binding("event", event); } @@ -303,6 +267,11 @@ public Builder semanticIdentityBoundary( return this; } + public Builder failureBoundary(BexFailureBoundary failureBoundary) { + this.failureBoundary = failureBoundary; + return this; + } + public BexExecutionContext build() { return new BexExecutionContext(this); } diff --git a/blue-bex-core/src/main/java/blue/bex/api/BexFailureBoundary.java b/blue-bex-core/src/main/java/blue/bex/api/BexFailureBoundary.java new file mode 100644 index 0000000..81339aa --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/api/BexFailureBoundary.java @@ -0,0 +1,83 @@ +package blue.bex.api; + +import blue.bex.BexException; +import blue.bex.BexExecutionEvidenceUnavailableException; +import blue.bex.output.BexFailurePolicy; + +import java.util.Objects; + +/** + * Host-neutral classification and translation boundary for execution + * failures which cross into or out of BEX core. + * + *

Core never needs to know a host exception hierarchy. A hosting adapter + * can preserve its native exception instances, classify suspension versus + * deterministic failure, and translate BEX-owned evidence failures at the + * outer execution boundary.

+ */ +public interface BexFailureBoundary extends BexFailurePolicy { + /** Host-neutral lifecycle outcome used by the gas-ledger boundary. */ + enum Classification { + DETERMINISTIC, + EVIDENCE_UNAVAILABLE, + UNCLASSIFIED + } + + /** Pure-runtime classification with no Contracts dependency. */ + BexFailureBoundary STANDALONE = new BexFailureBoundary() { + @Override + public Classification classify(Throwable failure) { + Throwable current = failure; + boolean genericBexFailure = false; + while (current != null) { + if (current instanceof BexExecutionEvidenceUnavailableException) { + return Classification.EVIDENCE_UNAVAILABLE; + } + if (current instanceof BexException) { + genericBexFailure = true; + } + Throwable cause = current.getCause(); + if (cause == current) { + break; + } + current = cause; + } + return genericBexFailure + ? Classification.DETERMINISTIC + : Classification.UNCLASSIFIED; + } + }; + + /** Classifies a failure for host-ledger finalization. */ + Classification classify(Throwable failure); + + @Override + default boolean evidenceUnavailable(Throwable failure) { + return classify(failure) == Classification.EVIDENCE_UNAVAILABLE; + } + + /** + * Translates a BEX-owned exception at the outer hosted boundary. + * Existing host exceptions should be returned by identity. + */ + default RuntimeException translate(RuntimeException failure) { + return Objects.requireNonNull(failure, "failure"); + } + + /** + * Preserves a classified host failure and wraps only an unexpected + * implementation failure. + */ + default RuntimeException preserveOrWrap( + String operation, + RuntimeException failure) { + RuntimeException exact = Objects.requireNonNull(failure, "failure"); + if (classify(exact) != Classification.UNCLASSIFIED) { + return exact; + } + return new BexException( + Objects.requireNonNull(operation, "operation") + + ": " + exact.getMessage(), + exact); + } +} diff --git a/src/main/java/blue/bex/api/BexGasLedgerHost.java b/blue-bex-core/src/main/java/blue/bex/api/BexGasLedgerHost.java similarity index 81% rename from src/main/java/blue/bex/api/BexGasLedgerHost.java rename to blue-bex-core/src/main/java/blue/bex/api/BexGasLedgerHost.java index faa9319..28fd80e 100644 --- a/src/main/java/blue/bex/api/BexGasLedgerHost.java +++ b/blue-bex-core/src/main/java/blue/bex/api/BexGasLedgerHost.java @@ -1,9 +1,10 @@ package blue.bex.api; import blue.bex.gas.BexGasLimitExceededException; -import blue.language.processor.GasMeter; -import blue.language.processor.GasLimitExceededException; -import blue.language.processor.RuntimeWorkBudget; +import blue.bex.gas.BexGasLedgerCapability; +import blue.bex.gas.BexHostGasExhaustion; +import blue.bex.gas.BexSharedGasBudget; +import blue.bex.gas.BexGasLedgerLifecycle; import java.util.Map; import java.util.Objects; @@ -11,13 +12,15 @@ /** * Parent-runtime bridge for one live BEX named child ledger. * - *

The ordinary {@link #submit(GasMeter.ChildGasLedger)} callback is the + *

The ordinary {@link #submit(BexGasLedgerCapability)} callback is the * success path only. Failure callbacks are separate because a generic * processor work session owns final retention or discard, while older * detached hosts merged deterministic prefixes directly.

*/ -public interface BexGasLedgerHost { - GasMeter.ChildGasLedger open(String namespace, Map counterWeights); +public interface BexGasLedgerHost extends BexGasLedgerLifecycle { + BexGasLedgerCapability open( + String namespace, + Map counterWeights); /** * Opens an invocation-owned local budget shared by all physical ledgers @@ -32,7 +35,7 @@ public interface BexGasLedgerHost { * @return shared host budget, or {@code null} when this host has no * canonical shared-budget capability */ - default RuntimeWorkBudget openSharedBudget(long maximumGas) { + default BexSharedGasBudget openSharedBudget(long maximumGas) { if (maximumGas < 0L) { throw new IllegalArgumentException( "Shared BEX gas budget must be non-negative"); @@ -54,10 +57,10 @@ default RuntimeWorkBudget openSharedBudget(long maximumGas) { * @throws UnsupportedOperationException if a non-null budget is supplied * to a compatibility host which cannot attach it */ - default GasMeter.ChildGasLedger open( + default BexGasLedgerCapability open( String namespace, Map counterWeights, - RuntimeWorkBudget sharedBudget) { + BexSharedGasBudget sharedBudget) { if (sharedBudget != null) { throw new UnsupportedOperationException( "This gas host cannot attach a shared runtime work budget"); @@ -65,7 +68,7 @@ default GasMeter.ChildGasLedger open( return open(namespace, counterWeights); } - void submit(GasMeter.ChildGasLedger ledger); + void submit(BexGasLedgerCapability ledger); /** * Whether this host can own separate live child ledgers for BEX and each @@ -86,7 +89,7 @@ default boolean separatesRuntimeNamespaces() { * adapters leave the ledger staged because the enclosing processor * failure path retains every admitted prefix exactly once.

*/ - void failedDeterministically(GasMeter.ChildGasLedger ledger); + void failedDeterministically(BexGasLedgerCapability ledger); /** * Reports transient evidence unavailability. @@ -95,7 +98,7 @@ default boolean separatesRuntimeNamespaces() { * leaves the reservation staged so its enclosing suspension can discard * it.

*/ - void evidenceUnavailable(GasMeter.ChildGasLedger ledger); + void evidenceUnavailable(BexGasLedgerCapability ledger); /** * Maps a BEX-local sub-limit rejection after every admitted ledger prefix @@ -118,15 +121,16 @@ default RuntimeException localGasLimitExceeded( * exhaustion path. * *

BEX reports every opened ledger through - * {@link #failedDeterministically(GasMeter.ChildGasLedger)} first, so the + * {@link #failedDeterministically(BexGasLedgerCapability)} first, so the * detached-host default only needs to rethrow the exact exception. * Session-backed hosts delegate to their owning runtime work session, * which validates that the same exception was recorded live.

*/ default void propagateGasExhaustion( - GasMeter.ChildGasLedger ledger, - GasLimitExceededException exhaustion) { + BexGasLedgerCapability ledger, + BexHostGasExhaustion exhaustion) { Objects.requireNonNull(ledger, "ledger"); - throw Objects.requireNonNull(exhaustion, "exhaustion"); + throw Objects.requireNonNull( + exhaustion, "exhaustion").hostFailure(); } } diff --git a/src/main/java/blue/bex/api/BexIntrinsicInvocation.java b/blue-bex-core/src/main/java/blue/bex/api/BexIntrinsicInvocation.java similarity index 100% rename from src/main/java/blue/bex/api/BexIntrinsicInvocation.java rename to blue-bex-core/src/main/java/blue/bex/api/BexIntrinsicInvocation.java diff --git a/src/main/java/blue/bex/api/BexIntrinsicProcessor.java b/blue-bex-core/src/main/java/blue/bex/api/BexIntrinsicProcessor.java similarity index 100% rename from src/main/java/blue/bex/api/BexIntrinsicProcessor.java rename to blue-bex-core/src/main/java/blue/bex/api/BexIntrinsicProcessor.java diff --git a/src/main/java/blue/bex/api/BexIntrinsicRegistry.java b/blue-bex-core/src/main/java/blue/bex/api/BexIntrinsicRegistry.java similarity index 96% rename from src/main/java/blue/bex/api/BexIntrinsicRegistry.java rename to blue-bex-core/src/main/java/blue/bex/api/BexIntrinsicRegistry.java index 9692449..f7ee1a8 100644 --- a/src/main/java/blue/bex/api/BexIntrinsicRegistry.java +++ b/blue-bex-core/src/main/java/blue/bex/api/BexIntrinsicRegistry.java @@ -1,13 +1,15 @@ package blue.bex.api; import blue.bex.BexException; +import blue.bex.compile.BexIntrinsicCatalog; import blue.bex.gas.BexGasMeter; import blue.bex.output.BexOutputAdmission; import blue.bex.output.BexOutputKind; import blue.bex.value.BexUnicodeOrder; import blue.bex.value.BexValue; import blue.bex.value.BexValues; -import blue.language.mapping.TypeClassResolver; +import blue.bex.runtime.BexRuntimeIntrinsics; +import blue.language.model.TypeBlueId; import java.util.Collections; import java.util.LinkedHashMap; @@ -18,7 +20,8 @@ /** * Exact intrinsic registry keyed by static Blue type identity. */ -public final class BexIntrinsicRegistry { +public final class BexIntrinsicRegistry + implements BexIntrinsicCatalog, BexRuntimeIntrinsics { private static final BexIntrinsicRegistry EMPTY = new BexIntrinsicRegistry(Collections.emptyMap()); @@ -131,6 +134,7 @@ public BexIntrinsicRegistry with(Class typeClass, .build(); } + @Override public boolean supports(String blueId) { return registrations.containsKey(blueId); } @@ -264,16 +268,17 @@ private static String namespaceFor(String blueId) { } private static String resolveAnnotatedTypeBlueId(Class typeClass) { - TypeClassResolver resolver = new TypeClassResolver(); - try { - resolver.registerAnnotatedClass(typeClass); - } catch (IllegalArgumentException unannotated) { + TypeBlueId annotation = Objects.requireNonNull( + typeClass, "typeClass").getAnnotation(TypeBlueId.class); + if (annotation == null) { return null; } - for (Map.Entry> entry - : resolver.getBlueIdMap().entrySet()) { - if (typeClass.equals(entry.getValue())) { - return entry.getKey(); + if (!annotation.defaultValue().isEmpty()) { + return annotation.defaultValue(); + } + for (String blueId : annotation.value()) { + if (blueId != null && !blueId.isEmpty()) { + return blueId; } } return null; diff --git a/src/main/java/blue/bex/api/BexMetricsSink.java b/blue-bex-core/src/main/java/blue/bex/api/BexMetricsSink.java similarity index 58% rename from src/main/java/blue/bex/api/BexMetricsSink.java rename to blue-bex-core/src/main/java/blue/bex/api/BexMetricsSink.java index 2b36605..359350b 100644 --- a/src/main/java/blue/bex/api/BexMetricsSink.java +++ b/blue-bex-core/src/main/java/blue/bex/api/BexMetricsSink.java @@ -1,16 +1,16 @@ package blue.bex.api; -import blue.bex.result.BexMetrics; +import blue.bex.result.BexMetricsSnapshot; /** * Optional sink invoked after compilation/execution. */ public interface BexMetricsSink { - void accept(BexMetrics metrics); + void accept(BexMetricsSnapshot metrics); BexMetricsSink NOOP = new BexMetricsSink() { @Override - public void accept(BexMetrics metrics) { + public void accept(BexMetricsSnapshot metrics) { } }; } diff --git a/src/main/java/blue/bex/api/BexProgramSource.java b/blue-bex-core/src/main/java/blue/bex/api/BexProgramSource.java similarity index 91% rename from src/main/java/blue/bex/api/BexProgramSource.java rename to blue-bex-core/src/main/java/blue/bex/api/BexProgramSource.java index 79333c9..35a4386 100644 --- a/src/main/java/blue/bex/api/BexProgramSource.java +++ b/blue-bex-core/src/main/java/blue/bex/api/BexProgramSource.java @@ -1,5 +1,6 @@ package blue.bex.api; +import blue.bex.compile.BexCompilationInput; import blue.language.snapshot.FrozenNode; import java.util.Objects; @@ -13,7 +14,7 @@ * sources combine a selected program node with a shared definition/library node * and entry function.

*/ -public final class BexProgramSource { +public final class BexProgramSource implements BexCompilationInput { public enum Kind { FULL_PROGRAM, EXPRESSION @@ -47,18 +48,22 @@ public Kind kind() { return kind; } + @Override public boolean isExpression() { return kind == Kind.EXPRESSION; } + @Override public FrozenNode programNode() { return programNode; } + @Override public Optional definitionNode() { return Optional.ofNullable(definitionNode); } + @Override public Optional entry() { return Optional.ofNullable(entry); } diff --git a/src/main/java/blue/bex/api/BexStepResults.java b/blue-bex-core/src/main/java/blue/bex/api/BexStepResults.java similarity index 92% rename from src/main/java/blue/bex/api/BexStepResults.java rename to blue-bex-core/src/main/java/blue/bex/api/BexStepResults.java index fcce4c6..901aee9 100644 --- a/src/main/java/blue/bex/api/BexStepResults.java +++ b/blue-bex-core/src/main/java/blue/bex/api/BexStepResults.java @@ -3,6 +3,7 @@ import blue.bex.result.BexExecutionResult; import blue.bex.value.BexValue; import blue.bex.value.BexValues; +import blue.bex.runtime.BexStepResultView; import java.util.Collections; import java.util.LinkedHashMap; @@ -11,7 +12,7 @@ /** * Prior workflow step results keyed by step name. */ -public final class BexStepResults { +public final class BexStepResults implements BexStepResultView { private final Map steps; private BexStepResults(Map steps) { diff --git a/src/main/java/blue/bex/api/BexTypeBlueIdResolver.java b/blue-bex-core/src/main/java/blue/bex/api/BexTypeBlueIdResolver.java similarity index 100% rename from src/main/java/blue/bex/api/BexTypeBlueIdResolver.java rename to blue-bex-core/src/main/java/blue/bex/api/BexTypeBlueIdResolver.java diff --git a/src/main/java/blue/bex/api/FrozenBexDocumentView.java b/blue-bex-core/src/main/java/blue/bex/api/FrozenBexDocumentView.java similarity index 100% rename from src/main/java/blue/bex/api/FrozenBexDocumentView.java rename to blue-bex-core/src/main/java/blue/bex/api/FrozenBexDocumentView.java diff --git a/blue-bex-core/src/main/java/blue/bex/api/package-info.java b/blue-bex-core/src/main/java/blue/bex/api/package-info.java new file mode 100644 index 0000000..6f38e35 --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/api/package-info.java @@ -0,0 +1,14 @@ +/** + * Stable engine/context entry points plus host and intrinsic service-provider + * boundaries. + * + *

An engine and immutable intrinsic registry may be shared; each execution + * context, lazy-binding state, document view, gas capability, and result belongs + * to one run unless its implementation explicitly guarantees otherwise. + * Required builder inputs are non-null, while documented null defaults map only + * to the stated default/undefined behavior. Compilation, runtime, evidence, + * failure-boundary, and exhaustion errors fail closed. Hosts supply gas and + * identity capabilities; they may reduce budgets but must not bypass named + * charge-before-work accounting.

+ */ +package blue.bex.api; diff --git a/blue-bex-core/src/main/java/blue/bex/compile/BexCompilationInput.java b/blue-bex-core/src/main/java/blue/bex/compile/BexCompilationInput.java new file mode 100644 index 0000000..f950ccc --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/compile/BexCompilationInput.java @@ -0,0 +1,28 @@ +package blue.bex.compile; + +import blue.language.snapshot.FrozenNode; + +import java.util.Optional; + +/** + * Immutable compiler view of a selected BEX program. + * + *

Implementations must keep every returned value stable for their entire + * lifetime. The compiler deliberately owns this narrow boundary so that its + * implementation does not depend on the public host API package.

+ */ +public interface BexCompilationInput { + /** Source shape used by cache identity and root compilation. */ + enum Kind { + FULL_PROGRAM, + EXPRESSION + } + + boolean isExpression(); + + FrozenNode programNode(); + + Optional definitionNode(); + + Optional entry(); +} diff --git a/src/main/java/blue/bex/compile/BexCompiledProgram.java b/blue-bex-core/src/main/java/blue/bex/compile/BexCompiledProgram.java similarity index 58% rename from src/main/java/blue/bex/compile/BexCompiledProgram.java rename to blue-bex-core/src/main/java/blue/bex/compile/BexCompiledProgram.java index 12da1c7..0839bb8 100644 --- a/src/main/java/blue/bex/compile/BexCompiledProgram.java +++ b/blue-bex-core/src/main/java/blue/bex/compile/BexCompiledProgram.java @@ -2,15 +2,11 @@ import blue.bex.BexException; import blue.bex.gas.BexGasCounter; -import blue.bex.runtime.BexRuntime; -import blue.bex.runtime.CompiledFrame; -import blue.bex.runtime.CompiledStatement; -import blue.bex.runtime.Control; -import blue.bex.runtime.CompiledExpression; import blue.bex.value.BexValue; import blue.bex.value.BexValues; import blue.language.snapshot.FrozenNode; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; @@ -23,27 +19,42 @@ * Lazy-compiled BEX program. */ public final class BexCompiledProgram { + private static final String UNBOUND_ENVIRONMENT_IDENTITY = + "blue-bex/unbound-compile-environment"; + private final CompiledFunction entry; private final Map functions; private final Map constants; private final int rootFrameSize; private final String programBlueId; private final Set requiredIntrinsicBlueIds; + private final String compilationEnvironmentIdentity; - public BexCompiledProgram(CompiledFunction entry, - Map functions, - Map constants, - int rootFrameSize, - String programBlueId) { + BexCompiledProgram(CompiledFunction entry, + Map functions, + Map constants, + int rootFrameSize, + String programBlueId) { this(entry, functions, constants, rootFrameSize, programBlueId, Collections.emptySet()); } - public BexCompiledProgram(CompiledFunction entry, - Map functions, - Map constants, - int rootFrameSize, - String programBlueId, - Set requiredIntrinsicBlueIds) { + BexCompiledProgram(CompiledFunction entry, + Map functions, + Map constants, + int rootFrameSize, + String programBlueId, + Set requiredIntrinsicBlueIds) { + this(entry, functions, constants, rootFrameSize, programBlueId, + requiredIntrinsicBlueIds, UNBOUND_ENVIRONMENT_IDENTITY); + } + + BexCompiledProgram(CompiledFunction entry, + Map functions, + Map constants, + int rootFrameSize, + String programBlueId, + Set requiredIntrinsicBlueIds, + String compilationEnvironmentIdentity) { this.entry = entry; this.functions = Collections.unmodifiableMap(new LinkedHashMap<>(functions)); this.constants = Collections.unmodifiableMap(new LinkedHashMap<>(constants)); @@ -52,18 +63,25 @@ public BexCompiledProgram(CompiledFunction entry, this.requiredIntrinsicBlueIds = Collections.unmodifiableSet(new LinkedHashSet<>(requiredIntrinsicBlueIds != null ? requiredIntrinsicBlueIds : Collections.emptySet())); + this.compilationEnvironmentIdentity = java.util.Objects.requireNonNull( + compilationEnvironmentIdentity, + "compilationEnvironmentIdentity"); } - public BexValue execute(BexRuntime runtime) { - return entry.invokeRoot(runtime); + BexValue execute(BexExecutionMachine machine) { + return entry.invokeRoot(machine); } - public CompiledFunction entry() { return entry; } - public Map functions() { return functions; } - public Map constants() { return constants; } - public int rootFrameSize() { return rootFrameSize; } + CompiledFunction entry() { return entry; } + Map functions() { return functions; } + Map constants() { return constants; } + int rootFrameSize() { return rootFrameSize; } public String programBlueId() { return programBlueId; } public Set requiredIntrinsicBlueIds() { return requiredIntrinsicBlueIds; } + /** Exact registry, gas, Language, and compiler identity used to compile. */ + public String compilationEnvironmentIdentity() { + return compilationEnvironmentIdentity; + } public BexValue constant(String name) { BexValue value = constants.get(name); @@ -76,7 +94,7 @@ public BexValue constant(String name) { /** * Compiled function or root body. */ - public static final class CompiledFunction { + static final class CompiledFunction { private final String name; private final List args; private final Map argByName; @@ -85,23 +103,24 @@ public static final class CompiledFunction { private final CompiledExpression expression; private final int frameSize; - public CompiledFunction(String name, - List args, - List statements, - CompiledExpression expression, - int frameSize) { + CompiledFunction(String name, + List args, + List statements, + CompiledExpression expression, + int frameSize) { this.name = name; - this.args = Collections.unmodifiableList(args); + this.args = Collections.unmodifiableList(new ArrayList<>(args)); LinkedHashMap argSpecs = new LinkedHashMap<>(); this.argBySlot = new ArgSpec[frameSize]; - for (ArgSpec arg : args) { + for (ArgSpec arg : this.args) { argSpecs.put(arg.name(), arg); if (arg.slot() >= 0 && arg.slot() < argBySlot.length) { argBySlot[arg.slot()] = arg; } } this.argByName = Collections.unmodifiableMap(argSpecs); - this.statements = statements; + this.statements = Collections.unmodifiableList( + new ArrayList<>(statements)); this.expression = expression; this.frameSize = frameSize; } @@ -116,26 +135,31 @@ public int argSlot(String name) { return arg != null ? arg.slot() : -1; } - public BexValue invokeRoot(BexRuntime runtime) { - return invokePrepared(runtime, null, new int[0], new BexValue[0]); + public BexValue invokeRoot(BexExecutionMachine machine) { + return invokePrepared( + machine, null, new int[0], new BexValue[0]); } - public BexValue invokePrepared(BexRuntime runtime, CompiledFrame parent, int[] slots, BexValue[] values) { - runtime.gas().charge(BexGasCounter.FUNCTION_CALLED); - runtime.metrics().incrementFunctionCalls(); + public BexValue invokePrepared( + BexExecutionMachine machine, + CompiledFrame parent, + int[] slots, + BexValue[] values) { + machine.gas().charge(BexGasCounter.FUNCTION_CALLED); + machine.metrics().incrementFunctionCalls(); if (parent == null) { - runtime.metrics().incrementCompiledExecutions(); + machine.metrics().incrementCompiledExecutions(); } - CompiledFrame frame = new CompiledFrame(runtime, frameSize, parent); + CompiledFrame frame = new CompiledFrame( + machine, frameSize, parent); for (int i = 0; i < slots.length; i++) { if (slots[i] >= 0) { BexValue value = values[i]; ArgSpec arg = slots[i] < argBySlot.length ? argBySlot[slots[i]] : null; if (arg != null && arg.typed() - && !runtime.typeMatcher().matches( + && !machine.matchesType( value, arg.pattern(), - runtime.gas(), parent != null ? parent.sourcePath() : null)) { throw new BexException("Function " + name + " argument " + arg.name() @@ -150,21 +174,23 @@ public BexValue invokePrepared(BexRuntime runtime, CompiledFrame parent, int[] s } for (CompiledStatement statement : statements) { if (statement.exec(frame) == Control.RETURN) { - return frame.returnValue() != null ? frame.returnValue() : runtime.defaultResultValue(); + return frame.returnValue() != null + ? frame.returnValue() + : machine.defaultResultValue(); } } - return runtime.defaultResultValue(); + return machine.defaultResultValue(); } } - public static final class ArgSpec { + static final class ArgSpec { private final String name; private final int slot; private final FrozenNode pattern; private final boolean typed; private final String sourcePointer; - public ArgSpec(String name, int slot, FrozenNode pattern, String sourcePointer) { + ArgSpec(String name, int slot, FrozenNode pattern, String sourcePointer) { this.name = name; this.slot = slot; this.pattern = pattern; diff --git a/src/main/java/blue/bex/compile/BexCompiledProgramCache.java b/blue-bex-core/src/main/java/blue/bex/compile/BexCompiledProgramCache.java similarity index 100% rename from src/main/java/blue/bex/compile/BexCompiledProgramCache.java rename to blue-bex-core/src/main/java/blue/bex/compile/BexCompiledProgramCache.java diff --git a/src/main/java/blue/bex/compile/BexCompiledProgramKey.java b/blue-bex-core/src/main/java/blue/bex/compile/BexCompiledProgramKey.java similarity index 79% rename from src/main/java/blue/bex/compile/BexCompiledProgramKey.java rename to blue-bex-core/src/main/java/blue/bex/compile/BexCompiledProgramKey.java index 9128a7c..a75bafd 100644 --- a/src/main/java/blue/bex/compile/BexCompiledProgramKey.java +++ b/blue-bex-core/src/main/java/blue/bex/compile/BexCompiledProgramKey.java @@ -1,7 +1,5 @@ package blue.bex.compile; -import blue.bex.api.BexProgramSource; - import java.util.Objects; /** @@ -12,22 +10,22 @@ public final class BexCompiledProgramKey { public static final String BEX_RUNTIME_REGISTRY_IDENTITY = "sha256:23d282ec1c0bb016263922b1b49c369fdd537efdcf23e005eceeb888d7763fe1"; - private final BexProgramSource.Kind kind; + private final BexCompilationInput.Kind kind; private final String programIdentity; private final String definitionIdentity; private final String entryName; private final String compileEnvironmentIdentity; public BexCompiledProgramKey(String programIdentity, String definitionIdentity, String entryName) { - this(BexProgramSource.Kind.FULL_PROGRAM, programIdentity, definitionIdentity, entryName, + this(BexCompilationInput.Kind.FULL_PROGRAM, programIdentity, definitionIdentity, entryName, COMPILER_IDENTITY); } - public BexCompiledProgramKey(BexProgramSource.Kind kind, String programIdentity, String definitionIdentity, String entryName) { + public BexCompiledProgramKey(BexCompilationInput.Kind kind, String programIdentity, String definitionIdentity, String entryName) { this(kind, programIdentity, definitionIdentity, entryName, COMPILER_IDENTITY); } - public BexCompiledProgramKey(BexProgramSource.Kind kind, + public BexCompiledProgramKey(BexCompilationInput.Kind kind, String programIdentity, String definitionIdentity, String entryName, @@ -40,20 +38,23 @@ public BexCompiledProgramKey(BexProgramSource.Kind kind, compileEnvironmentIdentity, "compileEnvironmentIdentity"); } - public static BexCompiledProgramKey from(BexProgramSource source) { + public static BexCompiledProgramKey from(BexCompilationInput source) { return from(source, COMPILER_IDENTITY); } - public static BexCompiledProgramKey from(BexProgramSource source, + public static BexCompiledProgramKey from(BexCompilationInput source, String compileEnvironmentIdentity) { - return new BexCompiledProgramKey(source.kind(), + return new BexCompiledProgramKey( + source.isExpression() + ? BexCompilationInput.Kind.EXPRESSION + : BexCompilationInput.Kind.FULL_PROGRAM, BexNodeIdentity.stable(source.programNode()), source.definitionNode().map(BexNodeIdentity::stable).orElse("none"), source.entry().orElse(null), compileEnvironmentIdentity); } - public BexProgramSource.Kind kind() { return kind; } + public BexCompilationInput.Kind kind() { return kind; } public String programIdentity() { return programIdentity; } public String definitionIdentity() { return definitionIdentity; } public String entryName() { return entryName; } diff --git a/blue-bex-core/src/main/java/blue/bex/compile/BexCompiledProgramRuntimeAccess.java b/blue-bex-core/src/main/java/blue/bex/compile/BexCompiledProgramRuntimeAccess.java new file mode 100644 index 0000000..2f3475e --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/compile/BexCompiledProgramRuntimeAccess.java @@ -0,0 +1,20 @@ +package blue.bex.compile; + +import blue.bex.value.BexValue; + +/** + * Internal bridge that executes an opaque compiled-program handle. + * + *

Public visibility is required only across BEX implementation packages; + * this type is classified as internal implementation and is not a host SPI.

+ */ +public final class BexCompiledProgramRuntimeAccess { + private BexCompiledProgramRuntimeAccess() { + } + + public static BexValue execute( + BexCompiledProgram program, + BexExecutionMachine machine) { + return program.execute(machine); + } +} diff --git a/blue-bex-core/src/main/java/blue/bex/compile/BexCompiler.java b/blue-bex-core/src/main/java/blue/bex/compile/BexCompiler.java new file mode 100644 index 0000000..19be986 --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/compile/BexCompiler.java @@ -0,0 +1,22 @@ +package blue.bex.compile; + +import blue.bex.result.BexMetricsRecorder; + +/** Compiler from frozen BEX Blue data to specialized runtime objects. */ +final class BexCompiler { + private final BexProgramCompiler delegate; + + BexCompiler(BexMetricsRecorder metrics, BexIntrinsicCatalog intrinsics) { + this.delegate = new BexProgramCompiler(metrics, intrinsics); + } + + /** + * Compiles and permanently binds the program to every supplied + * compilation-affecting identity. + */ + BexCompiledProgram compile( + BexCompilationInput source, + String compilationEnvironmentIdentity) { + return delegate.compileProgram(source, compilationEnvironmentIdentity); + } +} diff --git a/blue-bex-core/src/main/java/blue/bex/compile/BexCompilerRuntimeAccess.java b/blue-bex-core/src/main/java/blue/bex/compile/BexCompilerRuntimeAccess.java new file mode 100644 index 0000000..522d860 --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/compile/BexCompilerRuntimeAccess.java @@ -0,0 +1,18 @@ +package blue.bex.compile; + +import blue.bex.result.BexMetricsRecorder; + +/** Internal bridge for engine-owned compilation diagnostics and identity. */ +public final class BexCompilerRuntimeAccess { + private BexCompilerRuntimeAccess() { + } + + public static BexCompiledProgram compile( + BexCompilationInput source, + BexMetricsRecorder metrics, + BexIntrinsicCatalog intrinsics, + String compilationEnvironmentIdentity) { + return new BexCompiler(metrics, intrinsics).compile( + source, compilationEnvironmentIdentity); + } +} diff --git a/blue-bex-core/src/main/java/blue/bex/compile/BexCompilerSupport.java b/blue-bex-core/src/main/java/blue/bex/compile/BexCompilerSupport.java new file mode 100644 index 0000000..5df95ef --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/compile/BexCompilerSupport.java @@ -0,0 +1,412 @@ +package blue.bex.compile; + +import blue.bex.BexException; +import blue.bex.BexSourcePath; +import blue.bex.value.BexValue; +import blue.bex.value.BexValues; +import blue.bex.result.BexMetricsRecorder; +import blue.language.model.Node; +import blue.language.model.Schema; +import blue.language.registry.BlueCoreTypeRegistry; +import blue.language.snapshot.FrozenNode; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + + +/** Shared grammar, validation primitives, and compiler state. */ +abstract class BexCompilerSupport { + static final String TEXT_TYPE_BLUE_ID = + BlueCoreTypeRegistry.INSTANCE.blueId("Text"); + static final String INTEGER_TYPE_BLUE_ID = + BlueCoreTypeRegistry.INSTANCE.blueId("Integer"); + static final String DOUBLE_TYPE_BLUE_ID = + BlueCoreTypeRegistry.INSTANCE.blueId("Double"); + static final String BOOLEAN_TYPE_BLUE_ID = + BlueCoreTypeRegistry.INSTANCE.blueId("Boolean"); + static final Set RESERVED_BLUE_KEYS = reservedBlueKeys(); + + final BexContainsCache containsCache = new BexContainsCache(); + final BexMetricsRecorder metrics; + final BexIntrinsicCatalog intrinsics; + final Set requiredIntrinsicBlueIds = new LinkedHashSet<>(); + Map functionSignatures = Collections.emptyMap(); + Map constants = Collections.emptyMap(); + String currentFunction = "$root"; + + BexCompilerSupport(BexMetricsRecorder metrics, BexIntrinsicCatalog intrinsics) { + this.metrics = metrics; + this.intrinsics = intrinsics != null + ? intrinsics + : blueId -> false; + } + + abstract CompiledExpression compileExpr( + FrozenNode node, CompileScope scope, String pointer); + + FrozenNode prop(FrozenNode node, String key) { + if (node == null) { + return null; + } + if (node.getProperties() != null && node.getProperties().containsKey(key)) { + return node.getProperties().get(key); + } + if ("name".equals(key) && node.getName() != null) { + return scalarNode(node.getName()); + } + if ("description".equals(key) && node.getDescription() != null) { + return scalarNode(node.getDescription()); + } + if ("type".equals(key) && node.getType() != null) { + return node.getType(); + } + if ("itemType".equals(key) && node.getItemType() != null) { + return node.getItemType(); + } + if ("keyType".equals(key) && node.getKeyType() != null) { + return node.getKeyType(); + } + if ("valueType".equals(key) && node.getValueType() != null) { + return node.getValueType(); + } + if ("value".equals(key) && node.getValue() != null) { + return scalarNode(node.getValue()); + } + if ("blueId".equals(key) && node.getReferenceBlueId() != null) { + return scalarNode(node.getReferenceBlueId()); + } + if ("blue".equals(key) && node.getBlue() != null) { + return node.getBlue(); + } + if ("contracts".equals(key) && node.getContracts() != null) { + return node.getContracts(); + } + return null; + } + + FrozenNode explicitProp(FrozenNode node, String key) { + return node != null && node.getProperties() != null + ? node.getProperties().get(key) + : null; + } + + boolean hasExplicitProperty(FrozenNode node, String key) { + return node != null && node.getProperties() != null && node.getProperties().containsKey(key); + } + + boolean hasAuthoredField(FrozenNode node, String key) { + return authoredFieldNames(node).contains(key); + } + + Set authoredFieldNames(FrozenNode node) { + Set fields = new LinkedHashSet<>(); + if (node == null) { + return fields; + } + if (node.getProperties() != null) { + fields.addAll(node.getProperties().keySet()); + } + if (node.getName() != null) fields.add("name"); + if (node.getDescription() != null) fields.add("description"); + if (node.getType() != null) fields.add("type"); + if (node.getItemType() != null) fields.add("itemType"); + if (node.getKeyType() != null) fields.add("keyType"); + if (node.getValueType() != null) fields.add("valueType"); + if (node.getValue() != null) fields.add("value"); + if (node.getItems() != null) fields.add("items"); + if (node.getReferenceBlueId() != null) fields.add("blueId"); + if (node.getBlue() != null) fields.add("blue"); + if (node.getSchema() != null) fields.add("schema"); + if (node.getMergePolicy() != null) fields.add("mergePolicy"); + if (node.getContracts() != null) fields.add("contracts"); + if (node.getPreviousBlueId() != null) fields.add("$previous"); + if (node.getPosition() != null) fields.add("$pos"); + return fields; + } + + FrozenNode required(FrozenNode node, String label) { + if (node == null) { + throw new BexException("Missing required field: " + label); + } + return node; + } + + String requiredText(FrozenNode node, String label) { + String value = text(node); + if (value == null) { + throw new BexException("Missing required text field: " + label); + } + return value; + } + + String requiredNonEmptyText(FrozenNode node, String label) { + String value = requiredText(node, label); + if (value.isEmpty()) { + throw new BexException("Required text field is empty: " + label); + } + return value; + } + + String text(FrozenNode node) { + return node != null && node.getValue() instanceof String + ? (String) node.getValue() + : null; + } + + boolean isExpressionOperatorShape(FrozenNode node) { + if (node == null + || node.getProperties() == null + || node.getProperties().size() != 1 + || authoredFieldNames(node).size() != 1) { + return false; + } + String key = node.getProperties().keySet().iterator().next(); + return key.startsWith("$"); + } + + boolean isOperator(FrozenNode node, String op) { + return isExpressionOperatorShape(node) && node.getProperties().containsKey(op); + } + + FrozenNode onlyValue(FrozenNode node) { + return node.getProperties().values().iterator().next(); + } + + void addMetadataFields(Map fields, FrozenNode node, CompileScope scope, String pointer) { + if (node.getName() != null) { + fields.put("name", new TransientLiteralExpr(node.getName())); + } + if (node.getDescription() != null) { + fields.put("description", new TransientLiteralExpr(node.getDescription())); + } + if (node.getType() != null) { + fields.put("type", compileExpr(node.getType(), scope, pointer + "/type")); + } + if (node.getItemType() != null) { + fields.put("itemType", compileExpr(node.getItemType(), scope, pointer + "/itemType")); + } + if (node.getKeyType() != null) { + fields.put("keyType", compileExpr(node.getKeyType(), scope, pointer + "/keyType")); + } + if (node.getValueType() != null) { + fields.put("valueType", compileExpr(node.getValueType(), scope, pointer + "/valueType")); + } + if (node.getValue() != null) { + fields.put("value", new TransientLiteralExpr(node.getValue())); + } + if (node.getReferenceBlueId() != null) { + fields.put("blueId", new TransientLiteralExpr(node.getReferenceBlueId())); + } + if (node.getBlue() != null) { + fields.put("blue", compileExpr(node.getBlue(), scope, pointer + "/blue")); + } + if (node.getContracts() != null) { + fields.put("contracts", compileExpr(node.getContracts(), scope, pointer + "/contracts")); + } + if (node.getSchema() != null) { + fields.put("schema", new LiteralExpr(BexValues.nodeSnapshot(new blue.language.model.Node().schema(node.getSchema())))); + } + if (node.getMergePolicy() != null) { + fields.put("mergePolicy", new TransientLiteralExpr(node.getMergePolicy())); + } + } + + boolean hasLanguageFields(FrozenNode node) { + return node.getName() != null + || node.getDescription() != null + || node.getType() != null + || node.getItemType() != null + || node.getKeyType() != null + || node.getValueType() != null + || node.getReferenceBlueId() != null + || node.getBlue() != null + || node.getContracts() != null + || node.getSchema() != null + || node.getMergePolicy() != null; + } + + FrozenNode scalarNode(Object value) { + return FrozenNode.fromResolvedNode(new blue.language.model.Node().value(value)); + } + + CompiledExpression sourceExpr(String functionName, String pointer, String operator, CompiledExpression expression) { + return new SourceExpr(BexSourcePath.of(functionName, pointer, operator), expression); + } + + CompiledStatement sourceStatement(String functionName, String pointer, String operator, CompiledStatement statement) { + return new SourceStatement(BexSourcePath.of(functionName, pointer, operator), statement); + } + + void validateDistinctForEachBindings(String itemName, String keyName, String indexName) { + if (keyName != null && keyName.equals(itemName)) { + throw new BexException("$forEach.key must use a different binding name than $forEach.item"); + } + if (indexName != null && indexName.equals(itemName)) { + throw new BexException("$forEach.index must use a different binding name than $forEach.item"); + } + if (keyName != null && indexName != null && keyName.equals(indexName)) { + throw new BexException("$forEach.key must use a different binding name than $forEach.index"); + } + } + + String escape(String segment) { + return segment.replace("~", "~0").replace("/", "~1"); + } + + void validatePlainObjectContainer(FrozenNode node, String label) { + if (node == null) { + return; + } + if (node.getValue() != null || node.getItems() != null || node.getReferenceBlueId() != null) { + throw new BexException(label + " must be a plain object with non-reserved field names"); + } + if (node.getPreviousBlueId() != null || node.getPosition() != null) { + throw new BexException(label + " contains a Blue list-control key; use non-reserved names"); + } + if (node.getContracts() != null) { + throw new BexException(label + " contains reserved Blue key: contracts"); + } + if (node.getName() != null + || node.getDescription() != null + || node.getType() != null + || node.getItemType() != null + || node.getKeyType() != null + || node.getValueType() != null + || node.getBlue() != null + || node.getSchema() != null + || node.getMergePolicy() != null) { + throw new BexException(label + " contains a Blue language key; use non-reserved names"); + } + if (node.getProperties() != null) { + for (String key : node.getProperties().keySet()) { + if (RESERVED_BLUE_KEYS.contains(key)) { + throw new BexException(label + " contains reserved Blue key: " + key); + } + } + } + } + + void rejectBexAnywhereInStaticPattern(FrozenNode pattern, String pointer) { + if (pattern != null && containsCache.containsBex(pattern, metrics)) { + throw new BexException("BEX expressions inside static Blue patterns are not supported at " + pointer); + } + rejectBexInStaticBlueDefinitionFields(pattern, pointer); + } + + void rejectBexInStaticBlueDefinitionFields(FrozenNode node, String pointer) { + if (node == null) { + return; + } + rejectBexInStaticField(node.getType(), pointer + "/type", "type"); + rejectBexInStaticField(node.getItemType(), pointer + "/itemType", "itemType"); + rejectBexInStaticField(node.getKeyType(), pointer + "/keyType", "keyType"); + rejectBexInStaticField(node.getValueType(), pointer + "/valueType", "valueType"); + rejectBexInStaticField(node.getBlue(), pointer + "/blue", "blue"); + rejectBexInStaticField(node.getContracts(), pointer + "/contracts", "contracts"); + if (node.getSchema() != null) { + rejectBexInSchema(node.getSchema(), pointer + "/schema"); + } + if (node.getItems() != null) { + for (int i = 0; i < node.getItems().size(); i++) { + rejectBexInStaticBlueDefinitionFields(node.getItems().get(i), pointer + "/" + i); + } + } + if (node.getProperties() != null) { + for (Map.Entry entry : node.getProperties().entrySet()) { + rejectBexInStaticBlueDefinitionFields(entry.getValue(), pointer + "/" + escape(entry.getKey())); + } + } + } + + void rejectBexInStaticField(FrozenNode field, String pointer, String fieldName) { + if (field != null && containsCache.containsBex(field, metrics)) { + throw new BexException("BEX expressions inside Blue " + fieldName + + " fields are not supported at " + pointer); + } + rejectBexInStaticBlueDefinitionFields(field, pointer); + } + + void rejectBexInSchema(Schema schema, String pointer) { + rejectSchemaNode(schema.getRequired(), pointer); + rejectSchemaNode(schema.getMinLength(), pointer); + rejectSchemaNode(schema.getMaxLength(), pointer); + rejectSchemaNode(schema.getMinimum(), pointer); + rejectSchemaNode(schema.getMaximum(), pointer); + rejectSchemaNode(schema.getExclusiveMinimum(), pointer); + rejectSchemaNode(schema.getExclusiveMaximum(), pointer); + rejectSchemaNode(schema.getMultipleOf(), pointer); + rejectSchemaNode(schema.getMinItems(), pointer); + rejectSchemaNode(schema.getMaxItems(), pointer); + rejectSchemaNode(schema.getUniqueItems(), pointer); + rejectSchemaNode(schema.getMinFields(), pointer); + rejectSchemaNode(schema.getMaxFields(), pointer); + if (schema.getEnum() != null) { + for (Node node : schema.getEnum()) { + rejectSchemaNode(node, pointer); + } + } + } + + void rejectSchemaNode(Node node, String pointer) { + if (node == null) { + return; + } + FrozenNode frozen = FrozenNode.fromResolvedNode(node); + if (containsCache.containsBex(frozen, metrics)) { + throw new BexException("BEX expressions inside schema are not supported at " + pointer); + } + rejectBexInStaticBlueDefinitionFields(frozen, pointer); + } + + static Set reservedBlueKeys() { + Set keys = new LinkedHashSet<>(); + Collections.addAll(keys, + "name", + "description", + "type", + "itemType", + "keyType", + "valueType", + "value", + "items", + "blueId", + "blue", + "schema", + "constraints", + "mergePolicy", + "properties", + "contracts", + "$previous", + "$pos", + "$replace", + "$empty"); + return Collections.unmodifiableSet(keys); + } + + static final class FunctionSignature { + private final List args; + private final Map argsByName; + + FunctionSignature(List args) { + this.args = Collections.unmodifiableList(new ArrayList<>(args)); + Map byName = new LinkedHashMap<>(); + for (BexCompiledProgram.ArgSpec arg : this.args) { + byName.put(arg.name(), arg); + } + this.argsByName = Collections.unmodifiableMap(byName); + } + + List args() { + return args; + } + + BexCompiledProgram.ArgSpec arg(String name) { + return argsByName.get(name); + } + } +} diff --git a/src/main/java/blue/bex/compile/BexContainsCache.java b/blue-bex-core/src/main/java/blue/bex/compile/BexContainsCache.java similarity index 98% rename from src/main/java/blue/bex/compile/BexContainsCache.java rename to blue-bex-core/src/main/java/blue/bex/compile/BexContainsCache.java index 6187053..65e47d1 100644 --- a/src/main/java/blue/bex/compile/BexContainsCache.java +++ b/blue-bex-core/src/main/java/blue/bex/compile/BexContainsCache.java @@ -1,6 +1,6 @@ package blue.bex.compile; -import blue.bex.result.BexMetrics; +import blue.bex.result.BexMetricsRecorder; import blue.language.snapshot.FrozenNode; import java.util.IdentityHashMap; @@ -27,7 +27,7 @@ protected boolean removeEldestEntry(Map.Entry eldest) { }; } - public synchronized boolean containsBex(FrozenNode node, BexMetrics metrics) { + public synchronized boolean containsBex(FrozenNode node, BexMetricsRecorder metrics) { if (node == null) { return false; } diff --git a/blue-bex-core/src/main/java/blue/bex/compile/BexExecutionMachine.java b/blue-bex-core/src/main/java/blue/bex/compile/BexExecutionMachine.java new file mode 100644 index 0000000..9bda0b9 --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/compile/BexExecutionMachine.java @@ -0,0 +1,73 @@ +package blue.bex.compile; + +import blue.bex.BexSourcePath; +import blue.bex.gas.BexGasMeter; +import blue.bex.result.BexMetricsRecorder; +import blue.bex.result.BexPatchEntry; +import blue.bex.value.BexValue; +import blue.language.snapshot.FrozenNode; + +import java.util.List; +import java.util.Map; + +/** + * Per-execution machine operations required by compiled BEX IR. + * + *

This port is intentionally owned by the compiler package: compiled nodes + * can execute without depending on a concrete runtime implementation. A + * machine is invocation-scoped and must not be shared between executions.

+ */ +public interface BexExecutionMachine { + BexCompiledProgram program(); + + BexGasMeter gas(); + + BexMetricsRecorder metrics(); + + BexValue readDocument( + String absolutePointer, + List precompiledSegments, + boolean resolved); + + BexValue readEvent(List precompiledSegments); + + BexValue readProcessingEvent(List precompiledSegments); + + BexValue readCurrentContract(List precompiledSegments); + + BexValue readBinding(String name, List pathSegments); + + BexValue readSteps(String step, List pathSegments); + + BexValue readResultValue(String absolutePointer, List segments); + + BexValue readValuePointer(BexValue root, List segments); + + BexValue defaultResultValue(); + + BexValue invokeIntrinsic( + String blueId, + BexValue type, + Map fields); + + BexValue nodeBlueId(BexValue value); + + boolean matchesType( + BexValue value, + FrozenNode pattern, + BexSourcePath sourcePath); + + String resolvePointer(String authoredPointer); + + String canonicalPointer(String pointer); + + List parseDynamicPointer(String pointer); + + void appendChange(BexPatchEntry entry); + + void appendEvent(BexValue event); + + BexValue changesetValue(); + + BexValue eventsValue(); +} diff --git a/blue-bex-core/src/main/java/blue/bex/compile/BexExpressionCompiler.java b/blue-bex-core/src/main/java/blue/bex/compile/BexExpressionCompiler.java new file mode 100644 index 0000000..ce7abcf --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/compile/BexExpressionCompiler.java @@ -0,0 +1,693 @@ +package blue.bex.compile; + +import blue.bex.BexException; +import blue.bex.BexSourcePath; +import blue.bex.value.BexValue; +import blue.bex.value.BexUnicodeOrder; +import blue.bex.value.BexValues; +import blue.bex.result.BexMetricsRecorder; +import blue.language.snapshot.FrozenNode; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + + +/** Expression recognition, validation, and IR construction. */ +abstract class BexExpressionCompiler extends BexCompilerSupport { + BexExpressionCompiler(BexMetricsRecorder metrics, BexIntrinsicCatalog intrinsics) { + super(metrics, intrinsics); + } + + @Override + final CompiledExpression compileExpr(FrozenNode node, CompileScope scope, String pointer) { + if (node == null) { + return sourceExpr(currentFunction, pointer, null, new LiteralExpr(BexValues.nullValue())); + } + rejectBexInStaticBlueDefinitionFields(node, pointer); + if (isExpressionOperatorShape(node)) { + String op = node.getProperties().keySet().iterator().next(); + FrozenNode body = node.getProperties().values().iterator().next(); + BexSourcePath sourcePath = BexSourcePath.of(currentFunction, pointer + "/" + escape(op), op); + try { + return new SourceExpr(sourcePath, compileOperator(op, body, scope, pointer + "/" + escape(op))); + } catch (BexException ex) { + throw ex.withSourcePath(sourcePath); + } + } + if (node.isEmptyNode()) { + return sourceExpr(currentFunction, pointer, null, new LiteralExpr(BexValues.nullValue())); + } + if (isScalarNode(node)) { + return sourceExpr(currentFunction, pointer, null, + new TransientLiteralExpr(node.getValue())); + } + if (node.getItems() != null && !hasLanguageFields(node)) { + List items = new ArrayList<>(); + for (int i = 0; i < node.getItems().size(); i++) { + items.add(compileExpr(node.getItems().get(i), scope, pointer + "/" + i)); + } + return sourceExpr(currentFunction, pointer, null, new ListExpr(items)); + } + if (node.getProperties() != null || hasLanguageFields(node)) { + Map fields = new LinkedHashMap<>(); + addMetadataFields(fields, node, scope, pointer); + if (node.getItems() != null) { + List items = new ArrayList<>(); + for (int i = 0; i < node.getItems().size(); i++) { + items.add(compileExpr(node.getItems().get(i), scope, pointer + "/" + i)); + } + fields.put("items", new ListExpr(items)); + } + if (node.getProperties() != null) { + for (String key : BexUnicodeOrder.sortedCopy(node.getProperties().keySet())) { + fields.put(key, compileExpr(node.getProperties().get(key), scope, + pointer + "/" + escape(key))); + } + } + return sourceExpr(currentFunction, pointer, null, new ObjectExpr(fields)); + } + return sourceExpr(currentFunction, pointer, null, + new TransientLiteralExpr(node.getValue())); + } + + final CompiledExpression compileOperator(String op, FrozenNode body, CompileScope scope, String pointer) { + if (!BexOperatorCatalog.supportsExpression(op)) { + throw new BexException("Unknown expression operator: " + op); + } + validateExpressionBody(op, body); + if ("$literal".equals(op)) return new LiteralExpr(BexValues.frozen(body)); + if ("$null".equals(op)) return new LiteralExpr(BexValues.nullValue()); + if ("$emptyObject".equals(op)) return new LiteralExpr(BexValues.map(Collections.emptyMap())); + if ("$emptyList".equals(op)) return new LiteralExpr(BexValues.list(Collections.emptyList())); + if ("$document".equals(op)) return documentExpr(body, scope, pointer); + if ("$binding".equals(op)) return bindingExpr(body, scope, pointer); + if ("$event".equals(op)) return contextPointerExpr(body, scope, ContextKind.EVENT, pointer); + if ("$processingEvent".equals(op)) return contextPointerExpr(body, scope, ContextKind.PROCESSING_EVENT, pointer); + if ("$steps".equals(op)) return stepsExpr(body, scope, pointer); + if ("$currentContract".equals(op)) return contextPointerExpr(body, scope, ContextKind.CURRENT_CONTRACT, pointer); + if ("$var".equals(op)) return varExpr(body, scope, pointer); + if ("$const".equals(op)) { + String name = constName(body); + if (!constants.containsKey(name)) { + throw new BexException("Unknown constant: " + name); + } + return new ConstExpr(name, pathOperandOrNull(body, scope, pointer)); + } + if ("$get".equals(op)) return new GetExpr(compileExpr(required(prop(body, "object"), "$get.object"), scope, pointer + "/object"), textOrExpr(required(prop(body, "key"), "$get.key"), scope, null, pointer + "/key")); + if ("$changeset".equals(op)) return new ChangesetExpr(); + if ("$events".equals(op)) return new EventsExpr(); + if ("$resultValue".equals(op)) return new ResultValueExpr(pointerOperand(body, scope, pointer)); + if ("$unwrap".equals(op)) return new UnaryExpr(compileExpr(body, scope, pointer), UnaryOp.UNWRAP); + if ("$is".equals(op)) return isExpr(body, scope, pointer); + if ("$text".equals(op)) return new UnaryExpr(compileExpr(body, scope, pointer), UnaryOp.TEXT); + if ("$integer".equals(op)) return new UnaryExpr(compileExpr(body, scope, pointer), UnaryOp.INTEGER); + if ("$number".equals(op)) return new UnaryExpr(compileExpr(body, scope, pointer), UnaryOp.NUMBER); + if ("$boolean".equals(op)) return new UnaryExpr(compileExpr(body, scope, pointer), UnaryOp.BOOLEAN); + if ("$object".equals(op)) return new UnaryExpr(compileExpr(body, scope, pointer), UnaryOp.OBJECT); + if ("$list".equals(op)) return new UnaryExpr(compileExpr(body, scope, pointer), UnaryOp.LIST); + if ("$concat".equals(op)) return new VariadicExpr(compileExprList(body, scope, pointer), VariadicOp.CONCAT); + if ("$pointerJoin".equals(op)) return new PointerJoinExpr(compileExprList(body, scope, pointer)); + if ("$join".equals(op)) return new JoinExpr(compileExpr(required(prop(body, "list"), "$join.list"), scope, pointer + "/list"), compileExpr(required(prop(body, "separator"), "$join.separator"), scope, pointer + "/separator")); + if ("$split".equals(op)) return new SplitExpr(compileExpr(required(prop(body, "text"), "$split.text"), scope, pointer + "/text"), compileExpr(required(prop(body, "separator"), "$split.separator"), scope, pointer + "/separator"), prop(body, "limit") != null ? compileExpr(prop(body, "limit"), scope, pointer + "/limit") : null); + if ("$startsWith".equals(op)) return new BinaryTextExpr(compileExprList(body, scope, pointer), BinaryTextOp.STARTS_WITH); + if ("$sliceAfter".equals(op)) return new BinaryTextExpr(compileExprList(body, scope, pointer), BinaryTextOp.SLICE_AFTER); + if ("$eq".equals(op)) return new CompareExpr(compileExprList(body, scope, pointer), CompareOp.EQ); + if ("$ne".equals(op)) return new CompareExpr(compileExprList(body, scope, pointer), CompareOp.NE); + if ("$gt".equals(op)) return new CompareExpr(compileExprList(body, scope, pointer), CompareOp.GT); + if ("$gte".equals(op)) return new CompareExpr(compileExprList(body, scope, pointer), CompareOp.GTE); + if ("$lt".equals(op)) return new CompareExpr(compileExprList(body, scope, pointer), CompareOp.LT); + if ("$lte".equals(op)) return new CompareExpr(compileExprList(body, scope, pointer), CompareOp.LTE); + if ("$and".equals(op)) return new LogicalExpr(compileExprList(body, scope, pointer), true); + if ("$or".equals(op)) return new LogicalExpr(compileExprList(body, scope, pointer), false); + if ("$not".equals(op)) return new NotExpr(compileExpr(body, scope, pointer)); + if ("$truthy".equals(op)) return new UnaryExpr(compileExpr(body, scope, pointer), UnaryOp.TRUTHY); + if ("$empty".equals(op) || "$isEmpty".equals(op)) return new UnaryExpr(compileExpr(body, scope, pointer), UnaryOp.EMPTY); + if ("$exists".equals(op)) return new UnaryExpr(compileExpr(body, scope, pointer), UnaryOp.EXISTS); + if ("$kind".equals(op)) return new KindExpr(compileExpr(body, scope, pointer)); + if ("$isKind".equals(op)) return new IsKindExpr(compileExpr(required(prop(body, "val"), "$isKind.val"), scope, pointer + "/val"), kindSet(required(prop(body, "kind"), "$isKind.kind"))); + if ("$nodeBlueId".equals(op)) return new NodeBlueIdExpr(compileExpr(body, scope, pointer)); + if ("$coalesce".equals(op)) return new CoalesceExpr(compileExprList(body, scope, pointer)); + if ("$default".equals(op)) return new CoalesceExpr(compileExprList(body, scope, pointer)); + if ("$add".equals(op)) return new NumericExpr(compileExprList(body, scope, pointer), NumericOp.ADD); + if ("$subtract".equals(op)) return new NumericExpr(compileExprList(body, scope, pointer), NumericOp.SUBTRACT); + if ("$multiply".equals(op)) return new NumericExpr(compileExprList(body, scope, pointer), NumericOp.MULTIPLY); + if ("$divide".equals(op)) return new NumericExpr(compileExprList(body, scope, pointer), NumericOp.DIVIDE); + if ("$keys".equals(op)) return new UnaryExpr(compileExpr(body, scope, pointer), UnaryOp.KEYS); + if ("$entries".equals(op)) return new UnaryExpr(compileExpr(body, scope, pointer), UnaryOp.ENTRIES); + if ("$size".equals(op)) return new UnaryExpr(compileExpr(body, scope, pointer), UnaryOp.SIZE); + if ("$listGet".equals(op)) return new ListGetExpr(compileExpr(required(prop(body, "list"), "$listGet.list"), scope, pointer + "/list"), compileExpr(required(prop(body, "index"), "$listGet.index"), scope, pointer + "/index"), prop(body, "default") != null ? compileExpr(prop(body, "default"), scope, pointer + "/default") : null); + if ("$listConcat".equals(op)) return new VariadicExpr(compileExprList(body, scope, pointer), VariadicOp.LIST_CONCAT); + if ("$merge".equals(op)) return new VariadicExpr(compileExprList(body, scope, pointer), VariadicOp.MERGE); + if ("$objectSet".equals(op)) return new ObjectSetExpr(compileExpr(required(prop(body, "object"), "$objectSet.object"), scope, pointer + "/object"), textOrExpr(required(prop(body, "key"), "$objectSet.key"), scope, null, pointer + "/key"), compileExpr(required(prop(body, "val"), "$objectSet.val"), scope, pointer + "/val")); + if ("$pointerGet".equals(op)) return new PointerGetExpr(compileExpr(required(prop(body, "object"), "$pointerGet.object"), scope, pointer + "/object"), valuePointerOperand(required(prop(body, "path"), "$pointerGet.path"), scope, pointer + "/path"), prop(body, "default") != null ? compileExpr(prop(body, "default"), scope, pointer + "/default") : null); + if ("$pointerSet".equals(op)) return new PointerSetExpr(compileExpr(required(prop(body, "object"), "$pointerSet.object"), scope, pointer + "/object"), textOrExpr(prop(body, "op"), scope, "set", pointer + "/op"), valuePointerOperand(required(prop(body, "path"), "$pointerSet.path"), scope, pointer + "/path"), prop(body, "val") != null ? compileExpr(prop(body, "val"), scope, pointer + "/val") : null); + if ("$map".equals(op)) return collectionExpr(body, scope, pointer, CollectionOp.MAP, "expr"); + if ("$filter".equals(op)) return collectionExpr(body, scope, pointer, CollectionOp.FILTER, "where"); + if ("$flatMap".equals(op)) return collectionExpr(body, scope, pointer, CollectionOp.FLAT_MAP, "expr"); + if ("$reduce".equals(op)) return reduceExpr(body, scope, pointer); + if ("$some".equals(op)) return collectionExpr(body, scope, pointer, CollectionOp.SOME, "where"); + if ("$find".equals(op)) return collectionExpr(body, scope, pointer, CollectionOp.FIND, "where"); + if ("$findEntry".equals(op)) return collectionExpr(body, scope, pointer, CollectionOp.FIND_ENTRY, "where"); + if ("$includes".equals(op)) return new IncludesExpr(compileExpr(required(prop(body, "list"), "$includes.list"), scope, pointer + "/list"), + compileExpr(required(prop(body, "val"), "$includes.val"), scope, pointer + "/val")); + if ("$hasKey".equals(op)) return new HasKeyExpr(compileExpr(required(prop(body, "object"), "$hasKey.object"), scope, pointer + "/object"), + textOrExpr(required(prop(body, "key"), "$hasKey.key"), scope, null, pointer + "/key")); + if ("$objectFromEntries".equals(op)) return new ObjectFromEntriesExpr(compileExpr(body, scope, pointer)); + if ("$intrinsic".equals(op)) return intrinsicExpr(body, scope, pointer); + if ("$fail".equals(op)) return new FailExpr(failMessageExpr(body, scope, pointer)); + if ("$choose".equals(op)) return new ChooseExpr(compileExpr(required(prop(body, "cond"), "$choose.cond"), scope, pointer + "/cond"), compileExpr(required(prop(body, "then"), "$choose.then"), scope, pointer + "/then"), prop(body, "else") != null ? compileExpr(prop(body, "else"), scope, pointer + "/else") : new LiteralExpr(BexValues.undefined())); + if ("$call".equals(op)) return compileCall(body, scope, pointer); + throw new BexException("Catalogued expression operator has no compiler implementation: " + op); + } + + CompiledExpression failMessageExpr(FrozenNode body, + CompileScope scope, + String pointer) { + boolean messageWrapper = body != null + && body.getProperties() != null + && hasExplicitProperty(body, "message"); + return compileExpr(messageWrapper ? explicitProp(body, "message") : body, + scope, + messageWrapper ? pointer + "/message" : pointer); + } + + void validateExpressionBody(String op, FrozenNode body) { + if ("$concat".equals(op) + || "$pointerJoin".equals(op) + || "$listConcat".equals(op) + || "$merge".equals(op) + || "$and".equals(op) + || "$or".equals(op) + || "$coalesce".equals(op) + || "$default".equals(op)) { + requireListBody(body, op); + return; + } + if ("$eq".equals(op) + || "$ne".equals(op) + || "$gt".equals(op) + || "$gte".equals(op) + || "$lt".equals(op) + || "$lte".equals(op) + || "$startsWith".equals(op) + || "$sliceAfter".equals(op)) { + requireListArity(body, op, 2); + return; + } + if ("$add".equals(op) + || "$subtract".equals(op) + || "$multiply".equals(op) + || "$divide".equals(op)) { + requireListBody(body, op); + if (body.getItems().isEmpty()) { + throw new BexException(op + " requires at least one operand"); + } + return; + } + if ("$get".equals(op)) { + requireObjectBody(body, op, "object", "key"); + } else if ("$is".equals(op)) { + requireObjectBody(body, op, "node", "pattern"); + } else if ("$isKind".equals(op)) { + requireObjectBody(body, op, "val", "kind"); + } else if ("$join".equals(op)) { + requireObjectBody(body, op, "list", "separator"); + } else if ("$split".equals(op)) { + requireObjectBody(body, op, "text", "separator", "limit"); + } else if ("$listGet".equals(op)) { + requireObjectBody(body, op, "list", "index", "default"); + } else if ("$objectSet".equals(op)) { + requireObjectBody(body, op, "object", "key", "val"); + } else if ("$pointerGet".equals(op)) { + requireObjectBody(body, op, "object", "path", "default"); + } else if ("$pointerSet".equals(op)) { + requireObjectBody(body, op, "object", "path", "op", "val"); + } else if ("$map".equals(op) || "$flatMap".equals(op)) { + requireObjectBody(body, op, "in", "item", "key", "index", "expr"); + } else if ("$filter".equals(op) + || "$some".equals(op) + || "$find".equals(op) + || "$findEntry".equals(op)) { + requireObjectBody(body, op, "in", "item", "key", "index", "where"); + } else if ("$reduce".equals(op)) { + requireObjectBody(body, op, "in", "acc", "init", "item", "key", "index", "expr"); + } else if ("$includes".equals(op)) { + requireObjectBody(body, op, "list", "val"); + } else if ("$hasKey".equals(op)) { + requireObjectBody(body, op, "object", "key"); + } else if ("$choose".equals(op)) { + requireObjectBody(body, op, "cond", "then", "else"); + } else if ("$call".equals(op)) { + requireObjectBody(body, op, "function", "args"); + } else if ("$intrinsic".equals(op)) { + requireObjectNode(body, op); + } else if ("$binding".equals(op)) { + if (!isScalarBody(body)) { + requireObjectBody(body, op, "name", "path"); + } + } else if ("$steps".equals(op)) { + if (!isScalarBody(body)) { + requireObjectBody(body, op, "step", "path"); + } + } else if ("$var".equals(op) || "$const".equals(op)) { + if (!isScalarBody(body)) { + requireObjectBody(body, op, "name", "path"); + } + } else if ("$document".equals(op) + && body != null + && hasAuthoredField(body, "path") + && !isExpressionOperatorShape(body)) { + requireObjectBody(body, op, "path", "view"); + } + } + + + void requireListBody(FrozenNode body, String op) { + if (body == null || body.getItems() == null || hasNonListPayload(body)) { + throw new BexException(op + " expects a list body"); + } + } + + void requireListArity(FrozenNode body, String op, int expected) { + requireListBody(body, op); + if (body.getItems().size() != expected) { + throw new BexException(op + " expects exactly " + expected + " operands"); + } + } + + void requireObjectBody(FrozenNode body, String op, String... allowedFields) { + requireObjectNode(body, op); + Set allowed = new LinkedHashSet<>(); + Collections.addAll(allowed, allowedFields); + for (String field : authoredFieldNames(body)) { + if (!allowed.contains(field)) { + throw new BexException(op + " has unknown body field: " + field); + } + } + } + + void requireObjectNode(FrozenNode body, String op) { + if (body == null + || body.getItems() != null + || body.getValue() != null + || body.getReferenceBlueId() != null + || body.getPreviousBlueId() != null + || body.getPosition() != null) { + throw new BexException(op + " expects an object body"); + } + } + + void requireProgramNode(FrozenNode node, String label) { + if (node == null + || node.getValue() != null + || node.getItems() != null + || node.getReferenceBlueId() != null + || node.getPreviousBlueId() != null + || node.getPosition() != null) { + throw new BexException("BEX " + label + " must be an object node"); + } + } + + boolean hasNonListPayload(FrozenNode node) { + return node.getValue() != null + || node.getProperties() != null + || hasLanguageFields(node) + || node.getPreviousBlueId() != null + || node.getPosition() != null; + } + + boolean isScalarBody(FrozenNode body) { + return isScalarNode(body); + } + + /** + * Blue preprocessing adds an exact core-type reference to an authored + * scalar. That inferred metadata is part of the scalar representation, not + * a BEX object literal field. Keep the exception deliberately narrow: + * computed or additional Blue metadata must still compile as an object. + */ + boolean isScalarNode(FrozenNode node) { + if (node == null + || node.getValue() == null + || node.getItems() != null + || node.getProperties() != null + || node.getName() != null + || node.getDescription() != null + || node.getItemType() != null + || node.getKeyType() != null + || node.getValueType() != null + || node.getReferenceBlueId() != null + || node.getBlue() != null + || node.getContracts() != null + || node.getSchema() != null + || node.getMergePolicy() != null + || node.getPreviousBlueId() != null + || node.getPosition() != null) { + return false; + } + FrozenNode type = node.getType(); + if (type == null) { + return true; + } + String expectedTypeBlueId = scalarTypeBlueId(node.getValue()); + return expectedTypeBlueId != null + && type.isReferenceOnly() + && expectedTypeBlueId.equals(type.getReferenceBlueId()); + } + + String scalarTypeBlueId(Object value) { + if (value instanceof String) { + return TEXT_TYPE_BLUE_ID; + } + if (value instanceof Boolean) { + return BOOLEAN_TYPE_BLUE_ID; + } + if (value instanceof java.math.BigDecimal + || value instanceof Float + || value instanceof Double) { + return DOUBLE_TYPE_BLUE_ID; + } + if (value instanceof java.math.BigInteger + || value instanceof Byte + || value instanceof Short + || value instanceof Integer + || value instanceof Long) { + return INTEGER_TYPE_BLUE_ID; + } + return null; + } + + CompiledExpression intrinsicExpr(FrozenNode body, CompileScope scope, String pointer) { + if (body == null || body.getValue() != null || body.getItems() != null) { + throw new BexException("$intrinsic expects an object body"); + } + FrozenNode typeNode = required(prop(body, "type"), "$intrinsic.type"); + rejectBexAnywhereInStaticPattern(typeNode, pointer + "/type"); + String blueId = intrinsicTypeBlueId(typeNode); + BexValue typeValue = BexValues.frozen(typeNode); + if (blueId == null || blueId.isEmpty()) { + throw new BexException("$intrinsic.type must resolve to a BlueId"); + } + if (!intrinsics.supports(blueId)) { + throw new BexException("Unsupported intrinsic BlueId: " + blueId); + } + requiredIntrinsicBlueIds.add(blueId); + + Map fields = new LinkedHashMap<>(); + if (body.getProperties() != null) { + for (String fieldName : BexUnicodeOrder.sortedCopy(body.getProperties().keySet())) { + if ("type".equals(fieldName)) { + continue; + } + fields.put(fieldName, compileExpr(body.getProperties().get(fieldName), scope, + pointer + "/" + escape(fieldName))); + } + } + return new IntrinsicExpr(blueId, typeValue, fields); + } + + String intrinsicTypeBlueId(FrozenNode typeNode) { + if (hasExplicitProperty(typeNode, "blueId")) { + return text(explicitProp(typeNode, "blueId")); + } + if (typeNode.getReferenceBlueId() != null && !typeNode.getReferenceBlueId().isEmpty()) { + return typeNode.getReferenceBlueId(); + } + String blueId = BexNodeIdentity.safeBlueId(typeNode); + if (blueId != null && !blueId.isEmpty()) { + return blueId; + } + return text(typeNode); + } + + CompiledExpression varExpr(FrozenNode body, CompileScope scope, String pointer) { + if (body != null && body.getProperties() != null) { + String name = requiredText(prop(body, "name"), "$var.name"); + return new VarExpr(scope.resolveSlot(name), pathOperandOrNull(body, scope, pointer)); + } + return new VarExpr(scope.resolveSlot(requiredText(body, "$var"))); + } + + String constName(FrozenNode body) { + if (body != null && body.getProperties() != null) { + return requiredText(prop(body, "name"), "$const.name"); + } + return requiredText(body, "$const"); + } + + PointerOperand pathOperandOrNull(FrozenNode body, CompileScope scope, String pointer) { + if (body == null || body.getProperties() == null || prop(body, "path") == null) { + return null; + } + return valuePointerOperand(prop(body, "path"), scope, pointer + "/path"); + } + + Set kindSet(FrozenNode node) { + Set kinds = new LinkedHashSet<>(); + if (node.getItems() != null) { + for (FrozenNode item : node.getItems()) { + kinds.add(validateKind(requiredText(item, "$isKind.kind item"))); + } + } else { + kinds.add(validateKind(requiredText(node, "$isKind.kind"))); + } + return Collections.unmodifiableSet(kinds); + } + + String validateKind(String kind) { + if ("undefined".equals(kind) + || "null".equals(kind) + || "text".equals(kind) + || "integer".equals(kind) + || "double".equals(kind) + || "boolean".equals(kind) + || "object".equals(kind) + || "list".equals(kind)) { + return kind; + } + throw new BexException("Unknown BEX kind: " + kind); + } + + CompiledExpression collectionExpr(FrozenNode body, CompileScope scope, String pointer, + CollectionOp op, String bodyField) { + String operator = collectionOperatorName(op); + CompiledExpression input = compileExpr(required(prop(body, "in"), operator + ".in"), scope, pointer + "/in"); + String itemName = requiredText(prop(body, "item"), operator + ".item"); + String keyName = prop(body, "key") != null ? requiredText(prop(body, "key"), operator + ".key") : null; + String indexName = prop(body, "index") != null ? requiredText(prop(body, "index"), operator + ".index") : null; + validateDistinctCollectionBindings(operator, itemName, keyName, indexName); + CompileScope.Visibility visibility = scope.captureVisibility(); + try { + int itemSlot = scope.declareOrGetSlot(itemName); + int keySlot = keyName != null ? scope.declareOrGetSlot(keyName) : -1; + int indexSlot = indexName != null ? scope.declareOrGetSlot(indexName) : -1; + CompiledExpression expr = compileExpr(required(prop(body, bodyField), operator + "." + bodyField), + scope, pointer + "/" + bodyField); + return new CollectionQueryExpr(input, itemSlot, keySlot, indexSlot, expr, op); + } finally { + scope.restoreVisibility(visibility); + } + } + + CompiledExpression reduceExpr(FrozenNode body, CompileScope scope, String pointer) { + CompiledExpression input = compileExpr(required(prop(body, "in"), "$reduce.in"), scope, pointer + "/in"); + String accName = requiredText(prop(body, "acc"), "$reduce.acc"); + String itemName = requiredText(prop(body, "item"), "$reduce.item"); + String keyName = prop(body, "key") != null ? requiredText(prop(body, "key"), "$reduce.key") : null; + String indexName = prop(body, "index") != null ? requiredText(prop(body, "index"), "$reduce.index") : null; + validateDistinctCollectionBindings("$reduce", itemName, keyName, indexName); + if (accName.equals(itemName) || accName.equals(keyName) || accName.equals(indexName)) { + throw new BexException("$reduce.acc must use a different binding name"); + } + CompiledExpression init = compileExpr(required(prop(body, "init"), "$reduce.init"), scope, pointer + "/init"); + CompileScope.Visibility visibility = scope.captureVisibility(); + try { + int accSlot = scope.declareOrGetSlot(accName); + int itemSlot = scope.declareOrGetSlot(itemName); + int keySlot = keyName != null ? scope.declareOrGetSlot(keyName) : -1; + int indexSlot = indexName != null ? scope.declareOrGetSlot(indexName) : -1; + CompiledExpression expr = compileExpr(required(prop(body, "expr"), "$reduce.expr"), scope, pointer + "/expr"); + return new ReduceExpr(input, accSlot, init, itemSlot, keySlot, indexSlot, expr); + } finally { + scope.restoreVisibility(visibility); + } + } + + String collectionOperatorName(CollectionOp op) { + switch (op) { + case MAP: + return "$map"; + case FILTER: + return "$filter"; + case FLAT_MAP: + return "$flatMap"; + case SOME: + return "$some"; + case FIND: + return "$find"; + case FIND_ENTRY: + return "$findEntry"; + default: + return "collection operator"; + } + } + + void validateDistinctCollectionBindings(String operator, String itemName, String keyName, String indexName) { + if (keyName != null && keyName.equals(itemName)) { + throw new BexException(operator + ".key must use a different binding name than " + operator + ".item"); + } + if (indexName != null && indexName.equals(itemName)) { + throw new BexException(operator + ".index must use a different binding name than " + operator + ".item"); + } + if (keyName != null && indexName != null && keyName.equals(indexName)) { + throw new BexException(operator + ".key must use a different binding name than " + operator + ".index"); + } + } + + CompiledExpression isExpr(FrozenNode body, CompileScope scope, String pointer) { + if (body == null || body.getProperties() == null) { + throw new BexException("$is expects an object body"); + } + FrozenNode pattern = required(prop(body, "pattern"), "$is.pattern"); + rejectBexAnywhereInStaticPattern(pattern, pointer + "/pattern"); + return new IsExpr( + compileExpr(required(prop(body, "node"), "$is.node"), scope, pointer + "/node"), + pattern); + } + + CompiledExpression documentExpr(FrozenNode body, CompileScope scope, String pointer) { + boolean resolved = false; + FrozenNode pointerNode = body; + if (body != null && body.getProperties() != null && body.getProperties().containsKey("path")) { + pointerNode = prop(body, "path"); + String view = text(prop(body, "view")); + resolved = "resolved".equals(view); + } + return new DocumentExpr(pointerOperand(pointerNode, scope, pointer), resolved); + } + + CompiledExpression contextPointerExpr(FrozenNode body, CompileScope scope, ContextKind kind, String pointer) { + return new ContextPointerExpr(valuePointerOperand(body, scope, pointer), kind); + } + + CompiledExpression bindingExpr(FrozenNode body, CompileScope scope, String pointer) { + if (body != null && body.getValue() != null && body.getProperties() == null && body.getItems() == null) { + String selector = String.valueOf(body.getValue()); + int slash = selector.indexOf('/'); + String name = slash >= 0 ? selector.substring(0, slash) : selector; + if (name.isEmpty()) { + throw new BexException("$binding short form requires a binding name"); + } + String path = slash >= 0 ? selector.substring(slash) : "/"; + return new BindingExpr(new StaticTextExpr(name), StaticValuePointerOperand.of(path)); + } + if (body == null || body.getProperties() == null) { + throw new BexException("$binding expects a binding name or object form"); + } + TextOperand name = textOrExpr(required(prop(body, "name"), "$binding.name"), scope, null, pointer + "/name"); + FrozenNode path = prop(body, "path") != null ? prop(body, "path") : scalarNode("/"); + return new BindingExpr(name, valuePointerOperand(path, scope, pointer + "/path")); + } + + CompiledExpression stepsExpr(FrozenNode body, CompileScope scope, String pointer) { + if (body.getValue() != null) { + String selector = String.valueOf(body.getValue()); + int dot = selector.indexOf('.'); + String step = dot >= 0 ? selector.substring(0, dot) : selector; + String path = dot >= 0 ? "/" + selector.substring(dot + 1) : "/"; + return new StepsExpr(new StaticTextExpr(step), StaticValuePointerOperand.of(path)); + } + return new StepsExpr(textOrExpr(required(prop(body, "step"), "$steps.step"), scope, null, pointer + "/step"), + valuePointerOperand(prop(body, "path") != null ? prop(body, "path") : scalarNode("/"), scope, pointer + "/path")); + } + + CallExpr compileCall(FrozenNode body, CompileScope scope, String pointer) { + String function = requiredText(prop(body, "function"), "$call.function"); + FunctionSignature signature = functionSignatures.get(function); + if (signature == null) { + throw new BexException("Unknown function: " + function); + } + List argExpressions = new ArrayList<>(); + List targetSlots = new ArrayList<>(); + Set providedArgs = new LinkedHashSet<>(); + FrozenNode argsNode = prop(body, "args"); + if (argsNode != null) { + validatePlainObjectContainer(argsNode, "$call.args"); + if (argsNode.getProperties() == null) { + if (!argsNode.isEmptyNode()) { + throw new BexException("$call.args must be an object at " + pointer + "/args"); + } + } else { + for (String argName : BexUnicodeOrder.sortedCopy(argsNode.getProperties().keySet())) { + BexCompiledProgram.ArgSpec arg = signature.arg(argName); + if (arg == null) { + throw new BexException("Unknown argument " + argName + " for function " + function); + } + providedArgs.add(argName); + targetSlots.add(arg.slot()); + argExpressions.add(compileExpr(argsNode.getProperties().get(argName), scope, + pointer + "/args/" + escape(argName))); + } + } + } + for (BexCompiledProgram.ArgSpec arg : signature.args()) { + if (!providedArgs.contains(arg.name())) { + throw new BexException("Missing argument " + arg.name() + " for function " + function); + } + } + int[] slots = new int[targetSlots.size()]; + for (int i = 0; i < targetSlots.size(); i++) { + slots[i] = targetSlots.get(i); + } + return new CallExpr(function, + slots, + argExpressions.toArray(new CompiledExpression[0])); + } + + List compileExprList(FrozenNode node, CompileScope scope, String pointer) { + if (node == null || node.getItems() == null) { + throw new BexException("Operator expects a list"); + } + List expressions = new ArrayList<>(); + for (int i = 0; i < node.getItems().size(); i++) { + expressions.add(compileExpr(node.getItems().get(i), scope, pointer + "/" + i)); + } + return expressions; + } + + TextOperand textOrExpr(FrozenNode node, CompileScope scope, String defaultText, String pointer) { + if (node == null) { + return new StaticTextExpr(defaultText); + } + if (node.getValue() != null && node.getProperties() == null && node.getItems() == null) { + return new StaticTextExpr(String.valueOf(node.getValue())); + } + return new DynamicTextExpr(compileExpr(node, scope, pointer), pointer); + } + + PointerOperand pointerOperand(FrozenNode node, CompileScope scope, String pointer) { + if (node != null && node.getProperties() != null && node.getProperties().containsKey("path")) { + node = prop(node, "path"); + } + if (node != null && node.getValue() != null && node.getProperties() == null && node.getItems() == null) { + return StaticPointerOperand.of(String.valueOf(node.getValue())); + } + return new DynamicPointerOperand(compileExpr(node, scope, pointer)); + } + + PointerOperand valuePointerOperand(FrozenNode node, CompileScope scope, String pointer) { + if (node != null && node.getProperties() != null && node.getProperties().containsKey("path")) { + node = prop(node, "path"); + } + if (node != null && node.getValue() != null && node.getProperties() == null && node.getItems() == null) { + return StaticValuePointerOperand.of(String.valueOf(node.getValue())); + } + return new DynamicValuePointerOperand(compileExpr(node, scope, pointer)); + } + +} diff --git a/blue-bex-core/src/main/java/blue/bex/compile/BexGasWork.java b/blue-bex-core/src/main/java/blue/bex/compile/BexGasWork.java new file mode 100644 index 0000000..2ff59b2 --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/compile/BexGasWork.java @@ -0,0 +1,779 @@ +package blue.bex.compile; + +import blue.bex.BexException; +import blue.bex.BexSourcePath; +import blue.bex.gas.BexGasCounter; +import blue.bex.value.BexValue; +import blue.bex.value.BexValues; + +import java.math.BigDecimal; +import java.math.BigInteger; + +final class BexGasWork { + private static final int TEXT_BLOCK_CODE_POINTS = 64; + + private BexGasWork() { + } + + static void charge(CompiledFrame frame, BexGasCounter counter) { + charge(frame, counter, 1L); + } + + static void charge(CompiledFrame frame, BexGasCounter counter, long quantity) { + if (quantity <= 0L) { + return; + } + BexSourcePath source = frame.sourcePath(); + frame.machine().gas().charge(counter, + quantity, + source, + source != null ? source.operator() : null, + counter.canonicalName()); + } + + static long textBlocks(String text) { + return textBlocksForCodePoints(textCodePoints(text)); + } + + static long textCodePoints(String text) { + return text == null || text.isEmpty() + ? 0L + : text.codePointCount(0, text.length()); + } + + static long textBlocksForCodePoints(long codePoints) { + if (codePoints < 0L) { + throw new IllegalArgumentException( + "Text code-point count must be non-negative"); + } + return codePoints == 0L + ? 0L + : 1L + (codePoints - 1L) / TEXT_BLOCK_CODE_POINTS; + } + + /** + * Converts one scalar to canonical text while admitting every full-scan + * block before the conversion or scan which consumes that block. + */ + static MeteredText fullText( + CompiledFrame frame, + BexValue value) { + return fullText(frame, value, false); + } + + /** + * `$text` additionally constructs a new BEX Text value. Each output + * block's examination and construction units are admitted before the + * scalar formatter computes that block. + */ + static MeteredText constructedText( + CompiledFrame frame, + BexValue value) { + return fullText(frame, value, true); + } + + private static MeteredText fullText( + CompiledFrame frame, + BexValue value, + boolean constructed) { + BexValue exactValue = + value != null ? value : BexValues.undefined(); + if (exactValue.isUndefined() || exactValue.isNull()) { + return new MeteredText("", 0L, 0L); + } + if (!exactValue.isScalar()) { + throw new BexException("Value cannot be converted to text"); + } + + Object raw = exactValue.toSimple(); + if (raw instanceof String) { + TextScan scan = BexGasWorkSupport.chargeTextScan( + frame, + BexGasCounter.TEXT_BLOCK_EXAMINED, + (String) raw, + 0, + ((String) raw).length()); + if (constructed) { + charge( + frame, + BexGasCounter.TEXT_BLOCK_CONSTRUCTED, + textBlocksForCodePoints(scan.codePoints)); + } + return new MeteredText( + (String) raw, + scan.codePoints, + scan.pointerEscapeExpansions); + } + + BexGasWorkSupport.ScalarTextCursor cursor = BexGasWorkSupport.scalarTextCursor(raw); + StringBuilder text = null; + long codePoints = 0L; + while (cursor.hasNext()) { + charge( + frame, + BexGasCounter.TEXT_BLOCK_EXAMINED); + if (constructed) { + charge( + frame, + BexGasCounter.TEXT_BLOCK_CONSTRUCTED); + } + String block = cursor.nextBlock( + TEXT_BLOCK_CODE_POINTS); + if (block.isEmpty()) { + throw new BexException( + "Scalar text conversion made no progress"); + } + if (text == null) { + text = new StringBuilder(); + } + text.append(block); + codePoints += block.codePointCount( + 0, block.length()); + } + return new MeteredText( + text != null ? text.toString() : "", + codePoints, + 0L); + } + + /** + * Charges one construction unit before scanning each output block and + * creates the requested substring only after its final unit is admitted. + */ + static String constructSubstring( + CompiledFrame frame, + String text, + int start, + int end) { + chargeSubstringConstruction( + frame, text, start, end); + return text.substring(start, end); + } + + static void chargeSubstringConstruction( + CompiledFrame frame, + String text, + int start, + int end) { + BexGasWorkSupport.chargeTextScan( + frame, + BexGasCounter.TEXT_BLOCK_CONSTRUCTED, + text, + start, + end); + } + + static void chargeTextConstruction( + CompiledFrame frame, + String text) { + chargeSubstringConstruction( + frame, text, 0, text.length()); + } + + /** + * Canonical Unicode code-point comparison with block-pair admission before + * each compared block is read. + */ + static int compareText( + CompiledFrame frame, + String left, + String right) { + int leftOffset = 0; + int rightOffset = 0; + while (leftOffset < left.length() && rightOffset < right.length()) { + charge( + frame, + BexGasCounter.TEXT_BLOCK_EXAMINED, + 2L); + int inBlock = 0; + while (inBlock < TEXT_BLOCK_CODE_POINTS + && leftOffset < left.length() + && rightOffset < right.length()) { + int leftCodePoint = BexGasWorkSupport.codePointAt( + left, leftOffset, left.length()); + int rightCodePoint = BexGasWorkSupport.codePointAt( + right, rightOffset, right.length()); + leftOffset += BexGasWorkSupport.charCountAt( + left, leftOffset, left.length()); + rightOffset += BexGasWorkSupport.charCountAt( + right, rightOffset, right.length()); + if (leftCodePoint != rightCodePoint) { + return Integer.compare( + leftCodePoint, rightCodePoint); + } + inBlock++; + } + } + return Integer.compare( + left.length() - leftOffset, + right.length() - rightOffset); + } + + /** + * Scalar Text equality with each pair of source blocks admitted before it + * is read. This deliberately avoids eagerly materializing formatted + * numeric scalars, even though equality currently calls it only for Text. + */ + static boolean equalText( + CompiledFrame frame, + BexValue left, + BexValue right) { + BexGasWorkSupport.ScalarTextCursor leftCursor = + BexGasWorkSupport.scalarTextCursor(left); + BexGasWorkSupport.ScalarTextCursor rightCursor = + BexGasWorkSupport.scalarTextCursor(right); + while (leftCursor.hasNext() + && rightCursor.hasNext()) { + charge( + frame, + BexGasCounter.TEXT_BLOCK_EXAMINED, + 2L); + String leftBlock = leftCursor.nextBlock( + TEXT_BLOCK_CODE_POINTS); + String rightBlock = rightCursor.nextBlock( + TEXT_BLOCK_CODE_POINTS); + if (!leftBlock.equals(rightBlock)) { + return false; + } + } + return !leftCursor.hasNext() + && !rightCursor.hasNext(); + } + + /** + * Lazily converts and compares prefix blocks. Non-Text scalar formatting + * is deferred until after the exact block-pair charge which consumes it. + */ + static PrefixResult comparePrefix( + CompiledFrame frame, + BexValue text, + BexValue prefix) { + BexGasWorkSupport.ScalarTextCursor textCursor = + BexGasWorkSupport.scalarTextCursor(text); + BexGasWorkSupport.ScalarTextCursor prefixCursor = + BexGasWorkSupport.scalarTextCursor(prefix); + while (prefixCursor.hasNext()) { + if (!textCursor.hasNext()) { + return new PrefixResult( + false, textCursor); + } + charge( + frame, + BexGasCounter.TEXT_BLOCK_EXAMINED, + 2L); + String prefixBlock = prefixCursor.nextBlock( + TEXT_BLOCK_CODE_POINTS); + int comparedCodePoints = prefixBlock.codePointCount( + 0, prefixBlock.length()); + String textBlock = textCursor.nextBlock( + comparedCodePoints); + if (!textBlock.equals(prefixBlock)) { + return new PrefixResult( + false, textCursor); + } + } + return new PrefixResult( + true, textCursor); + } + + static long integerLimbs(BigInteger value) { + if (value == null) { + return 1L; + } + int bits = value.abs().bitLength(); + return Math.max(1L, (bits + 31L) / 32L); + } + + /** + * Exact Integer conversion with admission before parsing or decimal + * conversion work. + */ + static BigInteger convertInteger( + CompiledFrame frame, + BexValue value) { + Object raw = BexGasWorkSupport.numericScalar(value, "integer"); + if (raw instanceof BigInteger) { + BigInteger integer = (BigInteger) raw; + charge( + frame, + BexGasCounter.INTEGER_LIMB_OPERATION, + integerLimbs(integer)); + return integer; + } + if (BexGasWorkSupport.isIntegralPrimitive(raw)) { + long primitive = ((Number) raw).longValue(); + long limbs = primitive == Long.MIN_VALUE + || primitive > 0xFFFFFFFFL + || primitive < -0xFFFFFFFFL + ? 2L + : 1L; + charge( + frame, + BexGasCounter.INTEGER_LIMB_OPERATION, + limbs); + return BigInteger.valueOf(primitive); + } + if (raw instanceof String) { + return parseIntegerText( + frame, (String) raw); + } + if (raw instanceof BigDecimal) { + return exactDecimalInteger( + frame, (BigDecimal) raw); + } + if (raw instanceof Float || raw instanceof Double) { + charge( + frame, + BexGasCounter.INTEGER_LIMB_OPERATION); + BigDecimal decimal; + try { + decimal = BigDecimal.valueOf( + ((Number) raw).doubleValue()); + } catch (NumberFormatException ex) { + throw new BexException( + "Value cannot be converted to integer"); + } + charge( + frame, + BexGasCounter.INTEGER_LIMB_OPERATION, + integerLimbs(decimal.unscaledValue())); + BigInteger integer; + try { + integer = decimal.toBigIntegerExact(); + } catch (ArithmeticException ex) { + throw new BexException( + "Value cannot be converted to integer"); + } + return integer; + } + throw new BexException( + "Value cannot be converted to integer"); + } + + /** + * Reads an already-integer operand without adding a conversion charge, + * while routing Text and decimal coercion through the metered conversion + * path before arithmetic begins. + */ + static BigInteger integerOperand( + CompiledFrame frame, + BexValue value) { + Object raw = BexGasWorkSupport.numericScalar(value, "integer"); + if (raw instanceof BigInteger) { + return (BigInteger) raw; + } + if (BexGasWorkSupport.isIntegralPrimitive(raw)) { + return BigInteger.valueOf( + ((Number) raw).longValue()); + } + return convertInteger(frame, value); + } + + /** + * Decimal conversion with one scale-alignment unit and every unscaled + * magnitude limb admitted before parsing or allocation. + */ + static BigDecimal convertNumber( + CompiledFrame frame, + BexValue value) { + Object raw = BexGasWorkSupport.numericScalar(value, "number"); + if (raw instanceof String) { + charge( + frame, + BexGasCounter.INTEGER_LIMB_OPERATION, + 2L); + return parseDecimalText( + frame, (String) raw, 1L); + } + if (raw instanceof BigDecimal) { + BigDecimal decimal = (BigDecimal) raw; + charge( + frame, + BexGasCounter.INTEGER_LIMB_OPERATION, + integerLimbs(decimal.unscaledValue()) + 1L); + return decimal; + } + if (raw instanceof BigInteger) { + BigInteger integer = (BigInteger) raw; + charge( + frame, + BexGasCounter.INTEGER_LIMB_OPERATION, + integerLimbs(integer) + 1L); + return new BigDecimal(integer); + } + if (BexGasWorkSupport.isIntegralPrimitive(raw)) { + BigInteger integer = BigInteger.valueOf( + ((Number) raw).longValue()); + charge( + frame, + BexGasCounter.INTEGER_LIMB_OPERATION, + integerLimbs(integer) + 1L); + return new BigDecimal(integer); + } + if (raw instanceof Float || raw instanceof Double) { + charge( + frame, + BexGasCounter.INTEGER_LIMB_OPERATION, + 2L); + BigDecimal decimal; + try { + decimal = BigDecimal.valueOf( + ((Number) raw).doubleValue()); + } catch (NumberFormatException ex) { + throw new BexException( + "Value cannot be converted to number"); + } + charge( + frame, + BexGasCounter.INTEGER_LIMB_OPERATION, + integerLimbs(decimal.unscaledValue()) - 1L); + return decimal; + } + throw new BexException( + "Value cannot be converted to number"); + } + + /** + * Numeric equality/ordering conversion and comparison. The guaranteed + * first limb of each operand (plus the canonical decimal alignment unit) + * is admitted before any Text parse or Integer-to-decimal allocation. + */ + static int compareNumbers( + CompiledFrame frame, + BexValue left, + BexValue right, + boolean decimalAlignment) { + Object leftRaw = BexGasWorkSupport.numericScalar(left, "number"); + Object rightRaw = BexGasWorkSupport.numericScalar(right, "number"); + long base = 2L; + if (decimalAlignment + && (BexGasWorkSupport.isDecimalRaw(leftRaw) + || BexGasWorkSupport.isDecimalRaw(rightRaw))) { + base++; + } + charge( + frame, + BexGasCounter.INTEGER_LIMB_OPERATION, + base); + BigDecimal leftNumber = comparisonNumber( + frame, leftRaw); + BigDecimal rightNumber = comparisonNumber( + frame, rightRaw); + return leftNumber.compareTo(rightNumber); + } + + private static BigDecimal comparisonNumber( + CompiledFrame frame, + Object raw) { + if (raw instanceof BigDecimal) { + BigDecimal decimal = (BigDecimal) raw; + charge( + frame, + BexGasCounter.INTEGER_LIMB_OPERATION, + integerLimbs(decimal.unscaledValue()) - 1L); + return decimal; + } + if (raw instanceof BigInteger) { + BigInteger integer = (BigInteger) raw; + charge( + frame, + BexGasCounter.INTEGER_LIMB_OPERATION, + integerLimbs(integer) - 1L); + return new BigDecimal(integer); + } + if (BexGasWorkSupport.isIntegralPrimitive(raw)) { + BigInteger integer = BigInteger.valueOf( + ((Number) raw).longValue()); + charge( + frame, + BexGasCounter.INTEGER_LIMB_OPERATION, + integerLimbs(integer) - 1L); + return new BigDecimal(integer); + } + if (raw instanceof String) { + return parseDecimalText( + frame, (String) raw, 1L); + } + if (raw instanceof Float || raw instanceof Double) { + BigDecimal decimal; + try { + decimal = BigDecimal.valueOf( + ((Number) raw).doubleValue()); + } catch (NumberFormatException ex) { + throw new BexException( + "Value cannot be converted to number"); + } + charge( + frame, + BexGasCounter.INTEGER_LIMB_OPERATION, + integerLimbs(decimal.unscaledValue()) - 1L); + return decimal; + } + throw new BexException( + "Value cannot be converted to number"); + } + + private static BigInteger parseIntegerText( + CompiledFrame frame, + String text) { + charge( + frame, + BexGasCounter.INTEGER_LIMB_OPERATION); + if (text == null || text.isEmpty()) { + throw new BexException( + "Value cannot be converted to integer"); + } + int offset = 0; + boolean negative = false; + if (text.charAt(0) == '-') { + negative = true; + offset = 1; + } + if (offset == text.length()) { + throw new BexException( + "Value cannot be converted to integer"); + } + + BexGasWorkSupport.IncrementalMagnitude magnitude = + new BexGasWorkSupport.IncrementalMagnitude(frame, 1L); + while (offset < text.length()) { + char character = text.charAt(offset++); + if (character < '0' || character > '9') { + throw new BexException( + "Value cannot be converted to integer"); + } + magnitude.append(character - '0'); + } + BigInteger integer = magnitude.value(); + return negative ? integer.negate() : integer; + } + + private static BigInteger exactDecimalInteger( + CompiledFrame frame, + BigDecimal decimal) { + int scale = decimal.scale(); + BigInteger unscaled = decimal.unscaledValue(); + if (scale <= 0) { + long admittedMagnitudeLimbs = + integerLimbs(unscaled); + charge( + frame, + BexGasCounter.INTEGER_LIMB_OPERATION, + admittedMagnitudeLimbs + 1L); + boolean negative = unscaled.signum() < 0; + BexGasWorkSupport.IncrementalMagnitude magnitude = + new BexGasWorkSupport.IncrementalMagnitude( + frame, + unscaled.abs(), + admittedMagnitudeLimbs); + for (long remaining = -(long) scale; + remaining > 0L; + remaining--) { + magnitude.append(0); + } + BigInteger integer = magnitude.value(); + return negative ? integer.negate() : integer; + } + + /* + * Exact scale reduction must inspect the unscaled magnitude. Admit + * every such limb before BigDecimal performs that work; charging only + * the eventual result limbs would allow a large fractional magnitude + * to be inspected before a later admission. + */ + charge( + frame, + BexGasCounter.INTEGER_LIMB_OPERATION, + integerLimbs(unscaled) + 1L); + BigInteger integer; + try { + integer = decimal.toBigIntegerExact(); + } catch (ArithmeticException ex) { + throw new BexException( + "Value cannot be converted to integer"); + } + return integer; + } + + private static BigDecimal parseDecimalText( + CompiledFrame frame, + String text, + long admittedLimbs) { + if (text == null || text.isEmpty()) { + throw new BexException( + "Value cannot be converted to number"); + } + int offset = 0; + boolean negative = false; + char first = text.charAt(offset); + if (first == '+' || first == '-') { + negative = first == '-'; + offset++; + } + + BexGasWorkSupport.IncrementalMagnitude magnitude = + new BexGasWorkSupport.IncrementalMagnitude( + frame, admittedLimbs); + boolean decimalPoint = false; + boolean digitSeen = false; + long fractionalDigits = 0L; + while (offset < text.length()) { + char character = text.charAt(offset); + if (character == 'e' || character == 'E') { + break; + } + if (character == '.') { + if (decimalPoint) { + throw new BexException( + "Value cannot be converted to number"); + } + decimalPoint = true; + offset++; + continue; + } + int digit = Character.digit(character, 10); + if (digit < 0) { + throw new BexException( + "Value cannot be converted to number"); + } + digitSeen = true; + magnitude.append(digit); + if (decimalPoint) { + fractionalDigits++; + } + offset++; + } + if (!digitSeen) { + throw new BexException( + "Value cannot be converted to number"); + } + + long exponent = 0L; + if (offset < text.length()) { + offset++; + boolean exponentNegative = false; + if (offset < text.length() + && (text.charAt(offset) == '+' + || text.charAt(offset) == '-')) { + exponentNegative = text.charAt(offset) == '-'; + offset++; + } + if (offset == text.length()) { + throw new BexException( + "Value cannot be converted to number"); + } + while (offset < text.length()) { + int digit = Character.digit( + text.charAt(offset++), 10); + if (digit < 0 + || exponent > (Long.MAX_VALUE - digit) / 10L) { + throw new BexException( + "Value cannot be converted to number"); + } + exponent = exponent * 10L + digit; + } + if (exponentNegative) { + exponent = -exponent; + } + } + + long scale; + try { + scale = Math.subtractExact( + fractionalDigits, exponent); + } catch (ArithmeticException overflow) { + throw new BexException( + "Value cannot be converted to number"); + } + if (scale < Integer.MIN_VALUE + || scale > Integer.MAX_VALUE) { + throw new BexException( + "Value cannot be converted to number"); + } + BigInteger unscaled = magnitude.value(); + if (negative) { + unscaled = unscaled.negate(); + } + return new BigDecimal(unscaled, (int) scale); + } + + static final class MeteredText { + private final String text; + private final long codePoints; + private final long pointerEscapeExpansions; + + private MeteredText( + String text, + long codePoints, + long pointerEscapeExpansions) { + this.text = text; + this.codePoints = codePoints; + this.pointerEscapeExpansions = + pointerEscapeExpansions; + } + + String text() { + return text; + } + + long codePoints() { + return codePoints; + } + + long pointerEscapeExpansions() { + return pointerEscapeExpansions; + } + } + + static final class TextScan { + private final long codePoints; + private final long pointerEscapeExpansions; + + TextScan( + long codePoints, + long pointerEscapeExpansions) { + this.codePoints = codePoints; + this.pointerEscapeExpansions = + pointerEscapeExpansions; + } + } + + static final class PrefixResult { + private final boolean matched; + private final BexGasWorkSupport.ScalarTextCursor text; + + private PrefixResult( + boolean matched, + BexGasWorkSupport.ScalarTextCursor text) { + this.matched = matched; + this.text = text; + } + + boolean matched() { + return matched; + } + + String constructSuffix( + CompiledFrame frame) { + if (!matched) { + return ""; + } + StringBuilder suffix = null; + while (text.hasNext()) { + charge( + frame, + BexGasCounter.TEXT_BLOCK_CONSTRUCTED); + String block = text.nextBlock( + TEXT_BLOCK_CODE_POINTS); + if (suffix == null) { + suffix = new StringBuilder(); + } + suffix.append(block); + } + return suffix != null + ? suffix.toString() + : ""; + } + } + +} diff --git a/blue-bex-core/src/main/java/blue/bex/compile/BexGasWorkSupport.java b/blue-bex-core/src/main/java/blue/bex/compile/BexGasWorkSupport.java new file mode 100644 index 0000000..275bfb5 --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/compile/BexGasWorkSupport.java @@ -0,0 +1,592 @@ +package blue.bex.compile; + +import blue.bex.BexException; +import blue.bex.gas.BexGasCounter; +import blue.bex.value.BexValue; +import blue.bex.value.BexValues; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.List; + +/** Scalar conversion and incremental text cursor support for gas work. */ +final class BexGasWorkSupport { + private static final int TEXT_BLOCK_CODE_POINTS = 64; + private static final BigInteger TEN = BigInteger.TEN; + + private BexGasWorkSupport() { + } + + static Object numericScalar( + BexValue value, + String target) { + BexValue exactValue = + value != null ? value : BexValues.undefined(); + if (!exactValue.isScalar()) { + throw new BexException( + "Value cannot be converted to " + target); + } + return exactValue.toSimple(); + } + + static ScalarTextCursor scalarTextCursor( + BexValue value) { + BexValue exactValue = + value != null ? value : BexValues.undefined(); + if (exactValue.isUndefined() || exactValue.isNull()) { + return new RawStringCursor(""); + } + if (!exactValue.isScalar()) { + throw new BexException( + "Value cannot be converted to text"); + } + return scalarTextCursor(exactValue.toSimple()); + } + + static ScalarTextCursor scalarTextCursor( + Object raw) { + if (raw instanceof String) { + return new RawStringCursor((String) raw); + } + if (raw instanceof BigInteger) { + return new IntegerTextCursor( + (BigInteger) raw); + } + if (raw instanceof BigDecimal) { + return new DecimalTextCursor( + (BigDecimal) raw); + } + if (isIntegralPrimitive(raw) + || raw instanceof Float + || raw instanceof Double + || raw instanceof Boolean) { + return new FixedScalarTextCursor(raw); + } + throw new BexException( + "Value cannot be converted to text"); + } + + static boolean isIntegralPrimitive(Object raw) { + return raw instanceof Byte + || raw instanceof Short + || raw instanceof Integer + || raw instanceof Long; + } + + static boolean isDecimalRaw(Object raw) { + return raw instanceof BigDecimal + || raw instanceof Float + || raw instanceof Double; + } + + static int decimalDigits(BigInteger magnitude) { + if (magnitude.signum() == 0) { + return 1; + } + int bitLength = magnitude.bitLength(); + int estimate = Math.max( + 1, + (int) Math.floor( + (bitLength - 1) + * 0.3010299956639812d) + + 1); + BigInteger lower = TEN.pow(estimate - 1); + while (magnitude.compareTo(lower) < 0) { + estimate--; + lower = lower.divide(TEN); + } + BigInteger upper = lower.multiply(TEN); + while (magnitude.compareTo(upper) >= 0) { + estimate++; + lower = upper; + upper = upper.multiply(TEN); + } + return estimate; + } + + static int decimalDigits(long value) { + long remaining = value < 0L ? -value : value; + int digits = 1; + while (remaining >= 10L) { + remaining /= 10L; + digits++; + } + return digits; + } + + static BexGasWork.TextScan chargeTextScan( + CompiledFrame frame, + BexGasCounter counter, + String text, + int start, + int end) { + int offset = start; + long codePoints = 0L; + long pointerEscapeExpansions = 0L; + while (offset < end) { + BexGasWork.charge(frame, counter); + int inBlock = 0; + while (inBlock < TEXT_BLOCK_CODE_POINTS + && offset < end) { + char character = text.charAt(offset); + if (character == '~' || character == '/') { + pointerEscapeExpansions++; + } + offset += charCountAt(text, offset, end); + codePoints++; + inBlock++; + } + } + return new BexGasWork.TextScan( + codePoints, + pointerEscapeExpansions); + } + + static int codePointAt( + String text, + int offset, + int end) { + char first = text.charAt(offset); + if (Character.isHighSurrogate(first) + && offset + 1 < end) { + char second = text.charAt(offset + 1); + if (Character.isLowSurrogate(second)) { + return Character.toCodePoint( + first, second); + } + } + return first; + } + + static int charCountAt( + String text, + int offset, + int end) { + char first = text.charAt(offset); + return Character.isHighSurrogate(first) + && offset + 1 < end + && Character.isLowSurrogate( + text.charAt(offset + 1)) + ? 2 + : 1; + } + + + interface ScalarTextCursor { + boolean hasNext(); + + String nextBlock(int maxCodePoints); + } + + private static final class RawStringCursor + implements ScalarTextCursor { + private final String text; + private int offset; + + private RawStringCursor(String text) { + this.text = text; + } + + @Override + public boolean hasNext() { + return offset < text.length(); + } + + @Override + public String nextBlock(int maxCodePoints) { + int start = offset; + int consumed = 0; + while (consumed < maxCodePoints + && offset < text.length()) { + offset += charCountAt( + text, offset, text.length()); + consumed++; + } + return text.substring(start, offset); + } + } + + private static final class FixedScalarTextCursor + implements ScalarTextCursor { + private final Object raw; + private String text; + private int offset; + + private FixedScalarTextCursor(Object raw) { + this.raw = raw; + } + + @Override + public boolean hasNext() { + return text == null || offset < text.length(); + } + + @Override + public String nextBlock(int maxCodePoints) { + if (text == null) { + text = String.valueOf(raw); + } + int end = Math.min( + text.length(), + offset + maxCodePoints); + String block = text.substring(offset, end); + offset = end; + return block; + } + } + + private static final class IntegerTextCursor + implements ScalarTextCursor { + private final boolean negative; + private final DecimalDigitsCursor digits; + private boolean signPending; + + private IntegerTextCursor(BigInteger integer) { + this.negative = integer.signum() < 0; + this.signPending = negative; + this.digits = new DecimalDigitsCursor(integer); + } + + @Override + public boolean hasNext() { + return signPending || digits.hasNext(); + } + + @Override + public String nextBlock(int maxCodePoints) { + StringBuilder block = + new StringBuilder(maxCodePoints); + if (signPending && block.length() < maxCodePoints) { + block.append('-'); + signPending = false; + } + if (block.length() < maxCodePoints + && digits.hasNext()) { + block.append(digits.nextDigits( + maxCodePoints - block.length())); + } + return block.toString(); + } + } + + private static final class DecimalTextCursor + implements ScalarTextCursor { + private static final int LITERAL = 0; + private static final int DIGITS = 1; + private static final int ZEROS = 2; + private static final int EXPONENT = 3; + + private final BigDecimal decimal; + private final DecimalDigitsCursor digits; + private List parts; + private int partIndex; + + private DecimalTextCursor(BigDecimal decimal) { + this.decimal = decimal; + this.digits = new DecimalDigitsCursor( + decimal.unscaledValue()); + } + + @Override + public boolean hasNext() { + if (parts == null) { + return true; + } + skipEmptyParts(); + return partIndex < parts.size(); + } + + @Override + public String nextBlock(int maxCodePoints) { + if (parts == null) { + initialize(); + } + StringBuilder block = + new StringBuilder(maxCodePoints); + while (block.length() < maxCodePoints) { + skipEmptyParts(); + if (partIndex >= parts.size()) { + break; + } + TextPart part = parts.get(partIndex); + int capacity = + maxCodePoints - block.length(); + if (part.kind == DIGITS) { + int take = (int) Math.min( + part.remaining, + (long) capacity); + block.append( + digits.nextDigits(take)); + part.remaining -= take; + } else if (part.kind == ZEROS) { + int take = (int) Math.min( + part.remaining, + (long) capacity); + for (int index = 0; + index < take; + index++) { + block.append('0'); + } + part.remaining -= take; + } else { + if (part.text == null) { + part.text = part.kind == EXPONENT + ? Long.toString(part.exponent) + : ""; + } + int take = Math.min( + capacity, + part.text.length() + - part.textOffset); + block.append( + part.text, + part.textOffset, + part.textOffset + take); + part.textOffset += take; + part.remaining -= take; + } + } + return block.toString(); + } + + private void initialize() { + parts = new ArrayList<>(); + if (decimal.signum() < 0) { + parts.add(TextPart.literal("-")); + } + long precision = digits.digitCount(); + long scale = decimal.scale(); + long adjustedExponent = + -scale + precision - 1L; + if (scale >= 0L + && adjustedExponent >= -6L) { + if (scale == 0L) { + parts.add(TextPart.digits( + precision)); + } else if (scale < precision) { + parts.add(TextPart.digits( + precision - scale)); + parts.add(TextPart.literal(".")); + parts.add(TextPart.digits(scale)); + } else { + parts.add(TextPart.literal("0.")); + parts.add(TextPart.zeros( + scale - precision)); + parts.add(TextPart.digits( + precision)); + } + return; + } + + parts.add(TextPart.digits(1L)); + if (precision > 1L) { + parts.add(TextPart.literal(".")); + parts.add(TextPart.digits( + precision - 1L)); + } + parts.add(TextPart.literal( + adjustedExponent >= 0L + ? "E+" + : "E-")); + parts.add(TextPart.exponent( + Math.abs(adjustedExponent))); + } + + private void skipEmptyParts() { + while (partIndex < parts.size() + && parts.get(partIndex).remaining == 0L) { + partIndex++; + } + } + } + + private static final class DecimalDigitsCursor { + private final BigInteger signed; + private BigInteger magnitude; + private BigInteger emittedPrefix = BigInteger.ZERO; + private int totalDigits; + private int emittedDigits; + private boolean initialized; + + private DecimalDigitsCursor(BigInteger signed) { + this.signed = signed; + } + + private boolean hasNext() { + return !initialized || emittedDigits < totalDigits; + } + + private int digitCount() { + initialize(); + return totalDigits; + } + + private String nextDigits(int maximum) { + if (maximum <= 0) { + throw new IllegalArgumentException( + "Decimal digit block must be positive"); + } + initialize(); + int take = Math.min( + maximum, totalDigits - emittedDigits); + int after = totalDigits + - emittedDigits + - take; + + /* + * Extract only the prefix ending at this admitted output block. + * In particular, do not use divideAndRemainder here: its remainder + * materializes every lower digit even though a later text-block + * charge may still reject before those digits are examined. + * + * Recomputing the progressively longer quotient from the original + * magnitude is deliberate. Work for a lower block begins only + * when nextDigits is called for that block, after its caller has + * admitted the corresponding text counter. + */ + BigInteger divisor = TEN.pow(after); + BigInteger prefixThroughBlock = + magnitude.divide(divisor); + BigInteger blockMagnitude = + prefixThroughBlock.subtract( + emittedPrefix.multiply( + TEN.pow(take))); + String digits = blockMagnitude.toString(); + emittedPrefix = prefixThroughBlock; + emittedDigits += take; + if (digits.length() == take) { + return digits; + } + StringBuilder padded = + new StringBuilder(take); + for (int index = digits.length(); + index < take; + index++) { + padded.append('0'); + } + padded.append(digits); + return padded.toString(); + } + + private void initialize() { + if (initialized) { + return; + } + magnitude = signed.signum() < 0 + ? signed.negate() + : signed; + totalDigits = decimalDigits( + magnitude); + initialized = true; + } + } + + private static final class TextPart { + private final int kind; + private long remaining; + private String text; + private int textOffset; + private long exponent; + + private TextPart( + int kind, + long remaining, + String text, + long exponent) { + this.kind = kind; + this.remaining = remaining; + this.text = text; + this.exponent = exponent; + } + + private static TextPart literal(String value) { + return new TextPart( + DecimalTextCursor.LITERAL, + value.length(), + value, + 0L); + } + + private static TextPart digits(long count) { + return new TextPart( + DecimalTextCursor.DIGITS, + count, + null, + 0L); + } + + private static TextPart zeros(long count) { + return new TextPart( + DecimalTextCursor.ZEROS, + count, + null, + 0L); + } + + private static TextPart exponent(long value) { + return new TextPart( + DecimalTextCursor.EXPONENT, + decimalDigits(value), + null, + value); + } + } + + static final class IncrementalMagnitude { + private final CompiledFrame frame; + private BigInteger value; + private long admittedLimbs; + + IncrementalMagnitude( + CompiledFrame frame, + long admittedLimbs) { + this(frame, BigInteger.ZERO, admittedLimbs); + } + + IncrementalMagnitude( + CompiledFrame frame, + BigInteger value, + long admittedLimbs) { + this.frame = frame; + this.value = value; + this.admittedLimbs = admittedLimbs; + } + + void append(int digit) { + if (wouldGrow(digit)) { + BexGasWork.charge( + frame, + BexGasCounter.INTEGER_LIMB_OPERATION); + admittedLimbs++; + } + value = value.multiply(TEN) + .add(BigInteger.valueOf(digit)); + } + + private boolean wouldGrow(int digit) { + long admittedBits = admittedLimbs * 32L; + if (admittedBits > Integer.MAX_VALUE) { + throw new BexException( + "Integer magnitude exceeds supported range"); + } + if ((long) value.bitLength() + 4L + <= admittedBits) { + return false; + } + BigInteger boundary = BigInteger.ONE.shiftLeft( + (int) admittedBits); + BigInteger largestWithoutGrowth = + boundary.subtract( + BigInteger.ONE + .add(BigInteger.valueOf(digit))) + .divide(TEN); + return value.compareTo( + largestWithoutGrowth) > 0; + } + + BigInteger value() { + return value; + } + } +} diff --git a/blue-bex-core/src/main/java/blue/bex/compile/BexIntrinsicCatalog.java b/blue-bex-core/src/main/java/blue/bex/compile/BexIntrinsicCatalog.java new file mode 100644 index 0000000..c05ab8a --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/compile/BexIntrinsicCatalog.java @@ -0,0 +1,13 @@ +package blue.bex.compile; + +/** + * Immutable compile-time view of the intrinsic types available to a host. + * + *

The compiler needs only membership, not processors, gas ledgers, or any + * other runtime capability. Implementations must return a stable answer for + * their entire lifetime.

+ */ +@FunctionalInterface +public interface BexIntrinsicCatalog { + boolean supports(String blueId); +} diff --git a/src/main/java/blue/bex/compile/BexNodeFingerprint.java b/blue-bex-core/src/main/java/blue/bex/compile/BexNodeFingerprint.java similarity index 100% rename from src/main/java/blue/bex/compile/BexNodeFingerprint.java rename to blue-bex-core/src/main/java/blue/bex/compile/BexNodeFingerprint.java diff --git a/src/main/java/blue/bex/compile/BexNodeIdentity.java b/blue-bex-core/src/main/java/blue/bex/compile/BexNodeIdentity.java similarity index 100% rename from src/main/java/blue/bex/compile/BexNodeIdentity.java rename to blue-bex-core/src/main/java/blue/bex/compile/BexNodeIdentity.java diff --git a/src/main/java/blue/bex/compile/BexOperands.java b/blue-bex-core/src/main/java/blue/bex/compile/BexOperands.java similarity index 89% rename from src/main/java/blue/bex/compile/BexOperands.java rename to blue-bex-core/src/main/java/blue/bex/compile/BexOperands.java index 7146f7c..338ca30 100644 --- a/src/main/java/blue/bex/compile/BexOperands.java +++ b/blue-bex-core/src/main/java/blue/bex/compile/BexOperands.java @@ -2,10 +2,10 @@ import blue.bex.BexException; import blue.bex.pointer.BexPointer; -import blue.bex.runtime.CompiledExpression; -import blue.bex.runtime.CompiledFrame; import blue.bex.value.BexValue; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; interface TextOperand { @@ -36,7 +36,8 @@ final class ResolvedPointer { ResolvedPointer(String authored, String absolute, List segments) { this.authored = authored; this.absolute = absolute; - this.segments = segments; + this.segments = Collections.unmodifiableList( + new ArrayList<>(segments)); } String authored() { @@ -104,9 +105,9 @@ public ResolvedPointer resolve(CompiledFrame frame) { if (absolute) { return new ResolvedPointer(authored, pointer.text(), pointer.segments()); } - String resolved = frame.runtime().resolvePointer(authored); + String resolved = frame.machine().resolvePointer(authored); return new ResolvedPointer(authored, resolved, - frame.runtime().parseDynamicPointer(resolved)); + frame.machine().parseDynamicPointer(resolved)); } } @@ -120,9 +121,9 @@ final class DynamicPointerOperand implements PointerOperand { @Override public ResolvedPointer resolve(CompiledFrame frame) { String authored = PointerOperands.pointerText(expr.eval(frame)); - String absolute = frame.runtime().resolvePointer(authored); + String absolute = frame.machine().resolvePointer(authored); return new ResolvedPointer(authored, absolute, - frame.runtime().parseDynamicPointer(absolute)); + frame.machine().parseDynamicPointer(absolute)); } } @@ -161,9 +162,9 @@ final class DynamicValuePointerOperand implements PointerOperand { public ResolvedPointer resolve(CompiledFrame frame) { String authored = StaticValuePointerOperand.normalize( PointerOperands.pointerText(expr.eval(frame))); - String absolute = frame.runtime().canonicalPointer(authored); + String absolute = frame.machine().canonicalPointer(authored); return new ResolvedPointer(authored, absolute, - frame.runtime().parseDynamicPointer(absolute)); + frame.machine().parseDynamicPointer(absolute)); } } diff --git a/blue-bex-core/src/main/java/blue/bex/compile/BexOperatorCatalog.java b/blue-bex-core/src/main/java/blue/bex/compile/BexOperatorCatalog.java new file mode 100644 index 0000000..a653f03 --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/compile/BexOperatorCatalog.java @@ -0,0 +1,784 @@ +package blue.bex.compile; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Closed, immutable compiler catalog for the BEX 2.0 operator surface. + * + *

The catalog is deliberately package-private: it is compiler metadata, + * not an extension registry. Adding an entry is therefore a specification and + * conformance change, not a host-side registration operation.

+ */ +final class BexOperatorCatalog { + static final int OPERATOR_COUNT = 86; + + enum Role { + EXPRESSION(true, false), + STATEMENT(false, true), + EXPRESSION_AND_STATEMENT(true, true); + + private final boolean expression; + private final boolean statement; + + Role(boolean expression, boolean statement) { + this.expression = expression; + this.statement = statement; + } + + boolean supportsExpression() { + return expression; + } + + boolean supportsStatement() { + return statement; + } + } + + enum CompilerFamily { + LITERAL, + READ, + TYPE_AND_IDENTITY, + TEXT, + LOGIC_AND_COMPARISON, + NUMERIC, + OBJECT_AND_LIST, + COLLECTION_QUERY, + RESULT, + CONTROL, + INTRINSIC, + LOCAL_STATEMENT, + CONTROL_STATEMENT, + EFFECT_STATEMENT + } + + enum RuntimeFamily { + LITERAL_VALUE, + HOST_VIEW, + FRAME_LOOKUP, + TYPE_MATCHING, + SCALAR_CONVERSION, + IDENTITY, + TEXT_PROCESSING, + LOGICAL, + NUMERIC, + VALUE_COLLECTION, + COLLECTION_QUERY, + POINTER, + RESULT_ACCUMULATOR, + FUNCTION, + INTRINSIC, + FRAME_MUTATION, + STATEMENT_CONTROL, + RESULT_EFFECT, + FAILURE + } + + static final class Entry { + private final String canonicalName; + private final Role role; + private final String specificationSection; + private final String operandGrammar; + private final Set staticOperands; + private final Set dynamicOperands; + private final String evaluationContract; + private final CompilerFamily compilerFamily; + private final RuntimeFamily runtimeFamily; + private final Set fixturePaths; + private final Set vectorIdentifiers; + + private Entry(String canonicalName, + Role role, + String specificationSection, + String operandGrammar, + String evaluationContract, + CompilerFamily compilerFamily, + RuntimeFamily runtimeFamily) { + this.canonicalName = required(canonicalName, "canonicalName"); + this.role = required(role, "role"); + this.specificationSection = required( + specificationSection, "specificationSection"); + this.operandGrammar = required( + operandGrammar, "operandGrammar"); + OperandMetadata operands = operandMetadata(canonicalName); + this.staticOperands = operands.staticOperands; + this.dynamicOperands = operands.dynamicOperands; + this.evaluationContract = required( + evaluationContract, "evaluationContract"); + this.compilerFamily = required( + compilerFamily, "compilerFamily"); + this.runtimeFamily = required(runtimeFamily, "runtimeFamily"); + Coverage coverage = coverage(canonicalName); + this.fixturePaths = coverage.fixturePaths; + this.vectorIdentifiers = coverage.vectorIdentifiers; + } + + String canonicalName() { + return canonicalName; + } + + Role role() { + return role; + } + + String specificationSection() { + return specificationSection; + } + + String operandGrammar() { + return operandGrammar; + } + + Set staticOperands() { + return staticOperands; + } + + Set dynamicOperands() { + return dynamicOperands; + } + + String evaluationContract() { + return evaluationContract; + } + + CompilerFamily compilerFamily() { + return compilerFamily; + } + + RuntimeFamily runtimeFamily() { + return runtimeFamily; + } + + Set fixturePaths() { + return fixturePaths; + } + + Set vectorIdentifiers() { + return vectorIdentifiers; + } + } + + private static final class OperandMetadata { + private final Set staticOperands; + private final Set dynamicOperands; + + private OperandMetadata(String staticOperands, + String dynamicOperands) { + this.staticOperands = immutableNames(staticOperands); + this.dynamicOperands = immutableNames(dynamicOperands); + } + } + + private static final class Coverage { + private final Set fixturePaths; + private final Set vectorIdentifiers; + + private Coverage(String fixturePaths, String vectorIdentifiers) { + this.fixturePaths = immutableNames(fixturePaths); + this.vectorIdentifiers = immutableNames(vectorIdentifiers); + if (this.fixturePaths.isEmpty()) { + throw new ExceptionInInitializerError( + "BEX operator coverage must name an executable fixture"); + } + if (this.vectorIdentifiers.isEmpty()) { + throw new ExceptionInInitializerError( + "BEX operator coverage must name a normative vector"); + } + } + } + + private static final Map BY_NAME; + private static final List ENTRIES; + + static { + LinkedHashMap entries = new LinkedHashMap<>(); + add(entries, "$add", Role.EXPRESSION, "6.7", + "[expression, expression+]", "eager-left-to-right", + CompilerFamily.NUMERIC, RuntimeFamily.NUMERIC); + add(entries, "$and", Role.EXPRESSION, "6.6", + "[expression*]", "short-circuit-left-to-right", + CompilerFamily.LOGIC_AND_COMPARISON, RuntimeFamily.LOGICAL); + add(entries, "$appendChange", Role.STATEMENT, "7.6", + "{op, path, val?}", "op-and-path-eager; val-skipped-for-remove", + CompilerFamily.EFFECT_STATEMENT, RuntimeFamily.RESULT_EFFECT); + add(entries, "$appendChanges", Role.STATEMENT, "7.7", + "expression", "eager", + CompilerFamily.EFFECT_STATEMENT, RuntimeFamily.RESULT_EFFECT); + add(entries, "$appendEvent", Role.STATEMENT, "7.8", + "expression", "eager", + CompilerFamily.EFFECT_STATEMENT, RuntimeFamily.RESULT_EFFECT); + add(entries, "$appendEvents", Role.STATEMENT, "7.8", + "expression", "eager", + CompilerFamily.EFFECT_STATEMENT, RuntimeFamily.RESULT_EFFECT); + add(entries, "$binding", Role.EXPRESSION, "6.3", + "name/path | {name, path?}", "eager", + CompilerFamily.READ, RuntimeFamily.HOST_VIEW); + add(entries, "$boolean", Role.EXPRESSION, "6.4", + "expression", "eager", + CompilerFamily.TYPE_AND_IDENTITY, RuntimeFamily.SCALAR_CONVERSION); + add(entries, "$call", Role.EXPRESSION_AND_STATEMENT, "6.10; 7.2", + "{function, args}", "args-eager-in-canonical-name-order", + CompilerFamily.CONTROL, RuntimeFamily.FUNCTION); + add(entries, "$changeset", Role.EXPRESSION, "6.9", + "ignored", "no-runtime-operands", + CompilerFamily.RESULT, RuntimeFamily.RESULT_ACCUMULATOR); + add(entries, "$choose", Role.EXPRESSION, "6.10", + "{cond, then, else?}", "condition-eager; selected-branch-only", + CompilerFamily.CONTROL, RuntimeFamily.LOGICAL); + add(entries, "$coalesce", Role.EXPRESSION, "6.6", + "[expression*]", "short-circuit-left-to-right", + CompilerFamily.LOGIC_AND_COMPARISON, RuntimeFamily.LOGICAL); + add(entries, "$concat", Role.EXPRESSION, "6.5", + "[expression*]", "eager-left-to-right", + CompilerFamily.TEXT, RuntimeFamily.TEXT_PROCESSING); + add(entries, "$const", Role.EXPRESSION, "6.3", + "name | {name, path?}", "static-name; dynamic-path-eager", + CompilerFamily.READ, RuntimeFamily.FRAME_LOOKUP); + add(entries, "$currentContract", Role.EXPRESSION, "6.3", + "pointer", "eager", + CompilerFamily.READ, RuntimeFamily.HOST_VIEW); + add(entries, "$default", Role.EXPRESSION, "6.6", + "[expression*]", "short-circuit-left-to-right", + CompilerFamily.LOGIC_AND_COMPARISON, RuntimeFamily.LOGICAL); + add(entries, "$divide", Role.EXPRESSION, "6.7", + "[expression, expression+]", "eager-left-to-right", + CompilerFamily.NUMERIC, RuntimeFamily.NUMERIC); + add(entries, "$document", Role.EXPRESSION, "6.3", + "pointer | {path, view?}", "eager", + CompilerFamily.READ, RuntimeFamily.HOST_VIEW); + add(entries, "$empty", Role.EXPRESSION, "6.6", + "expression", "eager", + CompilerFamily.LOGIC_AND_COMPARISON, RuntimeFamily.LOGICAL); + add(entries, "$emptyList", Role.EXPRESSION, "6.2", + "ignored", "no-runtime-operands", + CompilerFamily.LITERAL, RuntimeFamily.LITERAL_VALUE); + add(entries, "$emptyObject", Role.EXPRESSION, "6.2", + "ignored", "no-runtime-operands", + CompilerFamily.LITERAL, RuntimeFamily.LITERAL_VALUE); + add(entries, "$entries", Role.EXPRESSION, "6.8", + "expression", "eager", + CompilerFamily.OBJECT_AND_LIST, RuntimeFamily.VALUE_COLLECTION); + add(entries, "$eq", Role.EXPRESSION, "6.6", + "[expression, expression]", "eager-left-to-right", + CompilerFamily.LOGIC_AND_COMPARISON, RuntimeFamily.LOGICAL); + add(entries, "$event", Role.EXPRESSION, "6.3", + "pointer", "eager", + CompilerFamily.READ, RuntimeFamily.HOST_VIEW); + add(entries, "$events", Role.EXPRESSION, "6.9", + "ignored", "no-runtime-operands", + CompilerFamily.RESULT, RuntimeFamily.RESULT_ACCUMULATOR); + add(entries, "$exists", Role.EXPRESSION, "6.6", + "expression", "eager", + CompilerFamily.LOGIC_AND_COMPARISON, RuntimeFamily.LOGICAL); + add(entries, "$fail", Role.EXPRESSION_AND_STATEMENT, "7.11", + "expression | {message}", "message-eager-then-terminal", + CompilerFamily.CONTROL, RuntimeFamily.FAILURE); + add(entries, "$failIf", Role.STATEMENT, "7.10", + "{cond, message}", "condition-eager; message-only-when-truthy", + CompilerFamily.CONTROL_STATEMENT, RuntimeFamily.FAILURE); + add(entries, "$filter", Role.EXPRESSION, "6.8.1", + "{in, item, key?, index?, where}", "input-eager; where-per-item", + CompilerFamily.COLLECTION_QUERY, RuntimeFamily.COLLECTION_QUERY); + add(entries, "$find", Role.EXPRESSION, "6.8.1", + "{in, item, key?, index?, where}", "short-circuit-per-item", + CompilerFamily.COLLECTION_QUERY, RuntimeFamily.COLLECTION_QUERY); + add(entries, "$findEntry", Role.EXPRESSION, "6.8.1", + "{in, item, key?, index?, where}", "short-circuit-per-item", + CompilerFamily.COLLECTION_QUERY, RuntimeFamily.COLLECTION_QUERY); + add(entries, "$flatMap", Role.EXPRESSION, "6.8.1", + "{in, item, key?, index?, expr}", "input-eager; expr-per-item", + CompilerFamily.COLLECTION_QUERY, RuntimeFamily.COLLECTION_QUERY); + add(entries, "$forEach", Role.STATEMENT, "7.5", + "{in, item, key?, index?, do}", "input-eager; body-per-item", + CompilerFamily.CONTROL_STATEMENT, RuntimeFamily.STATEMENT_CONTROL); + add(entries, "$get", Role.EXPRESSION, "6.3", + "{object, key}", "eager-object-then-key", + CompilerFamily.READ, RuntimeFamily.VALUE_COLLECTION); + add(entries, "$gt", Role.EXPRESSION, "6.6", + "[expression, expression]", "eager-left-to-right", + CompilerFamily.LOGIC_AND_COMPARISON, RuntimeFamily.LOGICAL); + add(entries, "$gte", Role.EXPRESSION, "6.6", + "[expression, expression]", "eager-left-to-right", + CompilerFamily.LOGIC_AND_COMPARISON, RuntimeFamily.LOGICAL); + add(entries, "$hasKey", Role.EXPRESSION, "6.8", + "{object, key}", "eager-object-then-key", + CompilerFamily.OBJECT_AND_LIST, RuntimeFamily.VALUE_COLLECTION); + add(entries, "$if", Role.STATEMENT, "7.4", + "{cond, then?, else?}", "condition-eager; selected-branch-only", + CompilerFamily.CONTROL_STATEMENT, RuntimeFamily.STATEMENT_CONTROL); + add(entries, "$includes", Role.EXPRESSION, "6.8.1", + "{list, val}", "operands-eager; comparison-short-circuit", + CompilerFamily.OBJECT_AND_LIST, RuntimeFamily.COLLECTION_QUERY); + add(entries, "$integer", Role.EXPRESSION, "6.4", + "expression", "eager", + CompilerFamily.TYPE_AND_IDENTITY, RuntimeFamily.SCALAR_CONVERSION); + add(entries, "$intrinsic", Role.EXPRESSION, "6.11", + "{type: static-blue, payload-field: expression*}", + "static-type; payload-eager-in-canonical-name-order; host-dispatch-last", + CompilerFamily.INTRINSIC, RuntimeFamily.INTRINSIC); + add(entries, "$is", Role.EXPRESSION, "6.4", + "{node, pattern: static-blue}", "node-eager; pattern-static", + CompilerFamily.TYPE_AND_IDENTITY, RuntimeFamily.TYPE_MATCHING); + add(entries, "$isEmpty", Role.EXPRESSION, "6.6", + "expression", "eager", + CompilerFamily.LOGIC_AND_COMPARISON, RuntimeFamily.LOGICAL); + add(entries, "$isKind", Role.EXPRESSION, "6.4", + "{val, kind: static-kind-or-list}", "val-eager; kind-static", + CompilerFamily.TYPE_AND_IDENTITY, RuntimeFamily.TYPE_MATCHING); + add(entries, "$join", Role.EXPRESSION, "6.5", + "{list, separator}", "eager-list-then-separator", + CompilerFamily.TEXT, RuntimeFamily.TEXT_PROCESSING); + add(entries, "$keys", Role.EXPRESSION, "6.8", + "expression", "eager", + CompilerFamily.OBJECT_AND_LIST, RuntimeFamily.VALUE_COLLECTION); + add(entries, "$kind", Role.EXPRESSION, "6.4", + "expression", "eager", + CompilerFamily.TYPE_AND_IDENTITY, RuntimeFamily.TYPE_MATCHING); + add(entries, "$let", Role.STATEMENT, "7.3", + "{name, expr} | {vars, order?}", + "single-eager; multi-parallel-unless-explicitly-ordered", + CompilerFamily.LOCAL_STATEMENT, RuntimeFamily.FRAME_MUTATION); + add(entries, "$list", Role.EXPRESSION, "6.4", + "expression", "eager", + CompilerFamily.TYPE_AND_IDENTITY, RuntimeFamily.SCALAR_CONVERSION); + add(entries, "$listConcat", Role.EXPRESSION, "6.8", + "[expression*]", "eager-left-to-right", + CompilerFamily.OBJECT_AND_LIST, RuntimeFamily.VALUE_COLLECTION); + add(entries, "$listGet", Role.EXPRESSION, "6.8", + "{list, index, default?}", "list-and-index-eager; default-only-when-missing", + CompilerFamily.OBJECT_AND_LIST, RuntimeFamily.VALUE_COLLECTION); + add(entries, "$literal", Role.EXPRESSION, "2.6; 6.10", + "static-blue-value", "no-nested-runtime-evaluation", + CompilerFamily.LITERAL, RuntimeFamily.LITERAL_VALUE); + add(entries, "$lt", Role.EXPRESSION, "6.6", + "[expression, expression]", "eager-left-to-right", + CompilerFamily.LOGIC_AND_COMPARISON, RuntimeFamily.LOGICAL); + add(entries, "$lte", Role.EXPRESSION, "6.6", + "[expression, expression]", "eager-left-to-right", + CompilerFamily.LOGIC_AND_COMPARISON, RuntimeFamily.LOGICAL); + add(entries, "$map", Role.EXPRESSION, "6.8.1", + "{in, item, key?, index?, expr}", "input-eager; expr-per-item", + CompilerFamily.COLLECTION_QUERY, RuntimeFamily.COLLECTION_QUERY); + add(entries, "$merge", Role.EXPRESSION, "6.8", + "[expression*]", "eager-left-to-right", + CompilerFamily.OBJECT_AND_LIST, RuntimeFamily.VALUE_COLLECTION); + add(entries, "$multiply", Role.EXPRESSION, "6.7", + "[expression, expression+]", "eager-left-to-right", + CompilerFamily.NUMERIC, RuntimeFamily.NUMERIC); + add(entries, "$ne", Role.EXPRESSION, "6.6", + "[expression, expression]", "eager-left-to-right", + CompilerFamily.LOGIC_AND_COMPARISON, RuntimeFamily.LOGICAL); + add(entries, "$nodeBlueId", Role.EXPRESSION, "6.4; 11.4", + "expression", "eager", + CompilerFamily.TYPE_AND_IDENTITY, RuntimeFamily.IDENTITY); + add(entries, "$not", Role.EXPRESSION, "6.6", + "expression", "eager", + CompilerFamily.LOGIC_AND_COMPARISON, RuntimeFamily.LOGICAL); + add(entries, "$null", Role.EXPRESSION, "6.2", + "ignored", "no-runtime-operands", + CompilerFamily.LITERAL, RuntimeFamily.LITERAL_VALUE); + add(entries, "$number", Role.EXPRESSION, "6.4", + "expression", "eager", + CompilerFamily.TYPE_AND_IDENTITY, RuntimeFamily.SCALAR_CONVERSION); + add(entries, "$object", Role.EXPRESSION, "6.4", + "expression", "eager", + CompilerFamily.TYPE_AND_IDENTITY, RuntimeFamily.SCALAR_CONVERSION); + add(entries, "$objectFromEntries", Role.EXPRESSION, "6.8", + "expression", "eager", + CompilerFamily.OBJECT_AND_LIST, RuntimeFamily.VALUE_COLLECTION); + add(entries, "$objectSet", Role.EXPRESSION, "6.8", + "{object, key, val}", "eager-object-then-key-then-val", + CompilerFamily.OBJECT_AND_LIST, RuntimeFamily.VALUE_COLLECTION); + add(entries, "$or", Role.EXPRESSION, "6.6", + "[expression*]", "short-circuit-left-to-right", + CompilerFamily.LOGIC_AND_COMPARISON, RuntimeFamily.LOGICAL); + add(entries, "$pointerGet", Role.EXPRESSION, "6.8", + "{object, path, default?}", "object-and-path-eager; default-only-when-missing", + CompilerFamily.OBJECT_AND_LIST, RuntimeFamily.POINTER); + add(entries, "$pointerJoin", Role.EXPRESSION, "6.5; 9.5", + "[expression*]", "eager-left-to-right", + CompilerFamily.TEXT, RuntimeFamily.POINTER); + add(entries, "$pointerSet", Role.EXPRESSION, "6.8", + "{object, path, op?, val?}", "object-path-op-eager; val-skipped-for-remove", + CompilerFamily.OBJECT_AND_LIST, RuntimeFamily.POINTER); + add(entries, "$processingEvent", Role.EXPRESSION, "6.3", + "pointer", "eager", + CompilerFamily.READ, RuntimeFamily.HOST_VIEW); + add(entries, "$reduce", Role.EXPRESSION, "6.8.1", + "{in, acc, init, item, key?, index?, expr}", + "input-then-init-eager; expr-per-item", + CompilerFamily.COLLECTION_QUERY, RuntimeFamily.COLLECTION_QUERY); + add(entries, "$resultValue", Role.EXPRESSION, "6.9; 10.4", + "pointer", "eager", + CompilerFamily.RESULT, RuntimeFamily.RESULT_ACCUMULATOR); + add(entries, "$return", Role.STATEMENT, "7.9", + "expression | empty", "expression-eager-when-present; terminal", + CompilerFamily.CONTROL_STATEMENT, RuntimeFamily.STATEMENT_CONTROL); + add(entries, "$returnIf", Role.STATEMENT, "7.10", + "{cond, expr?}", "condition-eager; expr-only-when-truthy", + CompilerFamily.CONTROL_STATEMENT, RuntimeFamily.STATEMENT_CONTROL); + add(entries, "$set", Role.STATEMENT, "7.3", + "{name, expr}", "expression-eager-before-assignment", + CompilerFamily.LOCAL_STATEMENT, RuntimeFamily.FRAME_MUTATION); + add(entries, "$size", Role.EXPRESSION, "6.8", + "expression", "eager", + CompilerFamily.OBJECT_AND_LIST, RuntimeFamily.VALUE_COLLECTION); + add(entries, "$sliceAfter", Role.EXPRESSION, "6.5", + "[expression, expression]", "eager-left-to-right", + CompilerFamily.TEXT, RuntimeFamily.TEXT_PROCESSING); + add(entries, "$some", Role.EXPRESSION, "6.8.1", + "{in, item, key?, index?, where}", "short-circuit-per-item", + CompilerFamily.COLLECTION_QUERY, RuntimeFamily.COLLECTION_QUERY); + add(entries, "$split", Role.EXPRESSION, "6.5", + "{text, separator, limit?}", "eager-text-then-separator-then-limit", + CompilerFamily.TEXT, RuntimeFamily.TEXT_PROCESSING); + add(entries, "$startsWith", Role.EXPRESSION, "6.5", + "[expression, expression]", "eager-left-to-right", + CompilerFamily.TEXT, RuntimeFamily.TEXT_PROCESSING); + add(entries, "$steps", Role.EXPRESSION, "6.3", + "step.path | {step, path?}", "eager", + CompilerFamily.READ, RuntimeFamily.HOST_VIEW); + add(entries, "$subtract", Role.EXPRESSION, "6.7", + "[expression, expression+]", "eager-left-to-right", + CompilerFamily.NUMERIC, RuntimeFamily.NUMERIC); + add(entries, "$text", Role.EXPRESSION, "6.4", + "expression", "eager", + CompilerFamily.TYPE_AND_IDENTITY, RuntimeFamily.SCALAR_CONVERSION); + add(entries, "$truthy", Role.EXPRESSION, "6.6", + "expression", "eager", + CompilerFamily.LOGIC_AND_COMPARISON, RuntimeFamily.LOGICAL); + add(entries, "$unwrap", Role.EXPRESSION, "6.4", + "expression", "eager", + CompilerFamily.TYPE_AND_IDENTITY, RuntimeFamily.SCALAR_CONVERSION); + add(entries, "$var", Role.EXPRESSION, "6.3", + "name | {name, path?}", "static-name; dynamic-path-eager", + CompilerFamily.READ, RuntimeFamily.FRAME_LOOKUP); + + if (entries.size() != OPERATOR_COUNT) { + throw new ExceptionInInitializerError( + "BEX operator catalog must contain exactly " + + OPERATOR_COUNT + " entries, found " + entries.size()); + } + BY_NAME = Collections.unmodifiableMap(entries); + ENTRIES = Collections.unmodifiableList( + new ArrayList<>(entries.values())); + } + + private BexOperatorCatalog() { + } + + static List entries() { + return ENTRIES; + } + + static Entry find(String canonicalName) { + return BY_NAME.get(canonicalName); + } + + static boolean supportsExpression(String canonicalName) { + Entry entry = find(canonicalName); + return entry != null && entry.role().supportsExpression(); + } + + static boolean supportsStatement(String canonicalName) { + Entry entry = find(canonicalName); + return entry != null && entry.role().supportsStatement(); + } + + private static void add(Map entries, + String canonicalName, + Role role, + String specificationSection, + String operandGrammar, + String evaluationContract, + CompilerFamily compilerFamily, + RuntimeFamily runtimeFamily) { + if (!canonicalName.startsWith("$")) { + throw new ExceptionInInitializerError( + "BEX operator name must start with $: " + canonicalName); + } + Entry entry = new Entry(canonicalName, role, specificationSection, + operandGrammar, evaluationContract, compilerFamily, + runtimeFamily); + if (entries.put(canonicalName, entry) != null) { + throw new ExceptionInInitializerError( + "Duplicate BEX operator catalog entry: " + canonicalName); + } + } + + /** + * Returns the authored operands that are static and/or evaluated at + * runtime. A name present in both sets accepts either authored scalar data + * or a dynamic BEX expression. Empty sets are intentional for operators + * whose bodies are ignored. + */ + private static OperandMetadata operandMetadata(String operator) { + switch (operator) { + case "$changeset": + case "$emptyList": + case "$emptyObject": + case "$events": + case "$null": + return operands("", ""); + case "$literal": + return operands("body", ""); + case "$binding": + return operands("selector,name,path", "name,path"); + case "$const": + case "$var": + return operands("name,path", "path"); + case "$document": + return operands("path,view", "path"); + case "$currentContract": + case "$event": + case "$processingEvent": + case "$resultValue": + return operands("path", "path"); + case "$steps": + return operands("selector,step,path", "step,path"); + case "$get": + case "$hasKey": + return operands("key", "object,key"); + case "$is": + return operands("pattern", "node"); + case "$isKind": + return operands("kind", "val"); + case "$call": + return operands("function,args.keys", "args.values"); + case "$intrinsic": + return operands("type,payload.keys", "payload.values"); + case "$let": + return operands("name,vars.keys,order", "expr,vars.values"); + case "$set": + return operands("name", "expr"); + case "$forEach": + return operands("item,key,index", "in,do"); + case "$filter": + case "$find": + case "$findEntry": + case "$some": + return operands("item,key,index", "in,where"); + case "$flatMap": + case "$map": + return operands("item,key,index", "in,expr"); + case "$reduce": + return operands("acc,item,key,index", "in,init,expr"); + case "$appendChange": + return operands("op,path", "op,path,val"); + case "$pointerSet": + return operands("op,path", "object,op,path,val"); + case "$pointerGet": + return operands("path", "object,path,default"); + case "$objectSet": + return operands("key", "object,key,val"); + case "$if": + case "$choose": + return operands("", "cond,then,else"); + case "$returnIf": + return operands("", "cond,expr"); + case "$failIf": + return operands("", "cond,message"); + case "$listGet": + return operands("", "list,index,default"); + case "$split": + return operands("", "text,separator,limit"); + case "$join": + return operands("", "list,separator"); + case "$includes": + return operands("", "list,val"); + case "$appendChanges": + case "$appendEvent": + case "$appendEvents": + case "$boolean": + case "$empty": + case "$entries": + case "$exists": + case "$integer": + case "$isEmpty": + case "$keys": + case "$kind": + case "$list": + case "$nodeBlueId": + case "$not": + case "$number": + case "$object": + case "$objectFromEntries": + case "$return": + case "$size": + case "$text": + case "$truthy": + case "$unwrap": + case "$fail": + return operands("", "value"); + case "$add": + case "$and": + case "$coalesce": + case "$concat": + case "$default": + case "$divide": + case "$eq": + case "$gt": + case "$gte": + case "$listConcat": + case "$lt": + case "$lte": + case "$merge": + case "$multiply": + case "$ne": + case "$or": + case "$pointerJoin": + case "$sliceAfter": + case "$startsWith": + case "$subtract": + return operands("", "operands"); + default: + throw new ExceptionInInitializerError( + "Missing operand metadata for " + operator); + } + } + + private static OperandMetadata operands(String staticOperands, + String dynamicOperands) { + return new OperandMetadata(staticOperands, dynamicOperands); + } + + private static Coverage coverage(String operator) { + switch (operator) { + case "$add": return coverage("operators/bex-op-add.yaml,operators/bex-op-reduce.yaml", "BEX-E-08,BEX-E-11"); + case "$and": return coverage("operators/bex-op-and.yaml", "BEX-E-07"); + case "$appendChange": return coverage("h/bex-h-04.yaml,operators/bex-op-changeset.yaml,s/bex-s-02.yaml,s/bex-s-03.yaml,s/bex-s-05.yaml", "BEX-H-04,BEX-S-05,BEX-S-02,BEX-S-03"); + case "$appendChanges": return coverage("operators/bex-op-appendchanges.yaml", "BEX-S-02"); + case "$appendEvent": return coverage("g/bex-g-10.yaml,operators/bex-op-events.yaml,r/bex-r-06.yaml,s/bex-s-01.yaml,s/bex-s-04.yaml", "BEX-G-10,BEX-S-04,BEX-R-06,BEX-S-01"); + case "$appendEvents": return coverage("operators/bex-op-appendevents.yaml", "BEX-S-04"); + case "$binding": return coverage("e/bex-e-02.yaml", "BEX-E-02"); + case "$boolean": return coverage("operators/bex-op-boolean.yaml", "BEX-E-08"); + case "$call": return coverage("c/bex-c-04.yaml", "BEX-C-04"); + case "$changeset": return coverage("operators/bex-op-changeset.yaml", "BEX-S-05"); + case "$choose": return coverage("operators/bex-op-choose.yaml", "BEX-E-07"); + case "$coalesce": return coverage("operators/bex-op-coalesce.yaml", "BEX-E-07"); + case "$concat": return coverage("g/bex-g-02.yaml,g/bex-g-05.yaml,r/bex-r-07.yaml", "BEX-G-02,BEX-G-05,BEX-R-07"); + case "$const": return coverage("c/bex-c-06.yaml,e/bex-e-02.yaml", "BEX-C-06,BEX-E-02"); + case "$currentContract": return coverage("e/bex-e-02.yaml", "BEX-E-02"); + case "$default": return coverage("operators/bex-op-default.yaml", "BEX-E-07"); + case "$divide": return coverage("e/bex-e-08.yaml", "BEX-E-08"); + case "$document": return coverage("e/bex-e-01.yaml,e/bex-e-05.yaml,e/bex-e-10.yaml,g/bex-g-11.yaml,h/bex-h-01.yaml,h/bex-h-02.yaml,h/bex-h-05.yaml,operators/bex-op-coalesce.yaml,operators/bex-op-default.yaml,r/bex-r-01.yaml,r/bex-r-02.yaml,r/bex-r-03.yaml,r/bex-r-04.yaml,r/bex-r-06.yaml,r/bex-r-08.yaml", "BEX-E-01,BEX-E-05,BEX-E-10,BEX-G-11,BEX-H-01,BEX-H-02,BEX-H-05,BEX-E-07,BEX-R-01,BEX-R-02,BEX-R-03,BEX-R-04,BEX-R-06,BEX-R-08"); + case "$empty": return coverage("operators/bex-op-empty.yaml", "BEX-E-06"); + case "$emptyList": return coverage("operators/bex-op-emptylist.yaml", "BEX-E-06"); + case "$emptyObject": return coverage("operators/bex-op-emptyobject.yaml", "BEX-E-06"); + case "$entries": return coverage("operators/bex-op-entries.yaml", "BEX-E-09"); + case "$eq": return coverage("e/bex-e-14.yaml,g/bex-g-08.yaml", "BEX-E-14,BEX-G-08"); + case "$event": return coverage("e/bex-e-02.yaml,h/bex-h-06.yaml", "BEX-E-02,BEX-H-06"); + case "$events": return coverage("operators/bex-op-events.yaml", "BEX-S-04"); + case "$exists": return coverage("e/bex-e-05.yaml,r/bex-r-02.yaml,r/bex-r-03.yaml", "BEX-E-05,BEX-R-02,BEX-R-03"); + case "$fail": return coverage("e/bex-e-07.yaml,g/bex-g-03.yaml,operators/bex-op-and.yaml,operators/bex-op-choose.yaml,operators/bex-op-coalesce.yaml,s/bex-s-02.yaml,s/bex-s-06.yaml", "BEX-E-07,BEX-G-03,BEX-S-02,BEX-S-06"); + case "$failIf": return coverage("operators/bex-op-failif.yaml", "BEX-S-06"); + case "$filter": return coverage("operators/bex-op-filter.yaml", "BEX-E-11"); + case "$find": return coverage("operators/bex-op-find.yaml", "BEX-E-11"); + case "$findEntry": return coverage("operators/bex-op-findentry.yaml", "BEX-E-11"); + case "$flatMap": return coverage("operators/bex-op-flatmap.yaml", "BEX-E-11"); + case "$forEach": return coverage("s/bex-s-01.yaml", "BEX-S-01"); + case "$get": return coverage("operators/bex-op-get.yaml", "BEX-E-02"); + case "$gt": return coverage("operators/bex-op-filter.yaml,operators/bex-op-find.yaml,operators/bex-op-findentry.yaml,operators/bex-op-gt.yaml,operators/bex-op-some.yaml", "BEX-E-11,BEX-E-08"); + case "$gte": return coverage("operators/bex-op-gte.yaml", "BEX-E-08"); + case "$hasKey": return coverage("operators/bex-op-haskey.yaml", "BEX-E-11"); + case "$if": return coverage("s/bex-s-01.yaml", "BEX-S-01"); + case "$includes": return coverage("operators/bex-op-includes.yaml", "BEX-E-11"); + case "$integer": return coverage("e/bex-e-08.yaml", "BEX-E-08"); + case "$intrinsic": return coverage("g/bex-g-09.yaml,g/bex-g-14.yaml", "BEX-G-09,BEX-G-14"); + case "$is": return coverage("c/bex-c-06.yaml", "BEX-C-06"); + case "$isEmpty": return coverage("operators/bex-op-isempty.yaml", "BEX-E-06"); + case "$isKind": return coverage("operators/bex-op-iskind.yaml", "BEX-E-10"); + case "$join": return coverage("operators/bex-op-join.yaml", "BEX-E-08"); + case "$keys": return coverage("e/bex-e-09.yaml,r/bex-r-02.yaml", "BEX-E-09,BEX-R-02"); + case "$kind": return coverage("e/bex-e-10.yaml,r/bex-r-02.yaml", "BEX-E-10,BEX-R-02"); + case "$let": return coverage("e/bex-e-02.yaml,e/bex-e-13.yaml,s/bex-s-07.yaml", "BEX-E-02,BEX-E-13,BEX-S-07"); + case "$list": return coverage("operators/bex-op-list.yaml", "BEX-E-08"); + case "$listConcat": return coverage("operators/bex-op-listconcat.yaml", "BEX-E-11"); + case "$listGet": return coverage("operators/bex-op-listget.yaml", "BEX-E-11"); + case "$literal": return coverage("c/bex-c-03.yaml", "BEX-C-03"); + case "$lt": return coverage("operators/bex-op-lt.yaml", "BEX-E-08"); + case "$lte": return coverage("operators/bex-op-lte.yaml", "BEX-E-08"); + case "$map": return coverage("e/bex-e-11.yaml,g/bex-g-07.yaml", "BEX-E-11,BEX-G-07"); + case "$merge": return coverage("operators/bex-op-merge.yaml", "BEX-E-11"); + case "$multiply": return coverage("g/bex-g-06.yaml", "BEX-G-06"); + case "$ne": return coverage("operators/bex-op-ne.yaml", "BEX-E-14"); + case "$nodeBlueId": return coverage("e/bex-e-14.yaml,r/bex-r-04.yaml,r/bex-r-05.yaml", "BEX-E-14,BEX-R-04,BEX-R-05"); + case "$not": return coverage("operators/bex-op-not.yaml", "BEX-E-06"); + case "$null": return coverage("e/bex-e-03.yaml,e/bex-e-05.yaml,e/bex-e-06.yaml", "BEX-E-03,BEX-E-05,BEX-E-06"); + case "$number": return coverage("h/bex-h-05.yaml", "BEX-H-05"); + case "$object": return coverage("operators/bex-op-object.yaml", "BEX-E-08"); + case "$objectFromEntries": return coverage("operators/bex-op-objectfromentries.yaml", "BEX-E-11"); + case "$objectSet": return coverage("operators/bex-op-objectset.yaml", "BEX-E-12"); + case "$or": return coverage("e/bex-e-07.yaml,g/bex-g-03.yaml", "BEX-E-07,BEX-G-03"); + case "$pointerGet": return coverage("e/bex-e-03.yaml", "BEX-E-03"); + case "$pointerJoin": return coverage("e/bex-e-04.yaml", "BEX-E-04"); + case "$pointerSet": return coverage("e/bex-e-12.yaml,g/bex-g-04.yaml", "BEX-E-12,BEX-G-04"); + case "$processingEvent": return coverage("e/bex-e-02.yaml,h/bex-h-06.yaml", "BEX-E-02,BEX-H-06"); + case "$reduce": return coverage("operators/bex-op-reduce.yaml", "BEX-E-11"); + case "$resultValue": return coverage("h/bex-h-04.yaml,s/bex-s-05.yaml", "BEX-H-04,BEX-S-05"); + case "$return": return coverage("e/bex-e-02.yaml,e/bex-e-13.yaml,h/bex-h-04.yaml,operators/bex-op-changeset.yaml,operators/bex-op-events.yaml,s/bex-s-05.yaml", "BEX-E-02,BEX-E-13,BEX-H-04,BEX-S-05,BEX-S-04"); + case "$returnIf": return coverage("s/bex-s-06.yaml", "BEX-S-06"); + case "$set": return coverage("c/bex-c-07.yaml", "BEX-C-07"); + case "$size": return coverage("r/bex-r-02.yaml", "BEX-R-02"); + case "$sliceAfter": return coverage("operators/bex-op-sliceafter.yaml", "BEX-E-08"); + case "$some": return coverage("operators/bex-op-some.yaml", "BEX-E-11"); + case "$split": return coverage("operators/bex-op-split.yaml", "BEX-E-08"); + case "$startsWith": return coverage("operators/bex-op-startswith.yaml", "BEX-E-08"); + case "$steps": return coverage("e/bex-e-02.yaml", "BEX-E-02"); + case "$subtract": return coverage("operators/bex-op-subtract.yaml", "BEX-E-08"); + case "$text": return coverage("e/bex-e-08.yaml", "BEX-E-08"); + case "$truthy": return coverage("e/bex-e-06.yaml", "BEX-E-06"); + case "$unwrap": return coverage("operators/bex-op-unwrap.yaml", "BEX-E-08"); + case "$var": return coverage("e/bex-e-02.yaml,e/bex-e-11.yaml,e/bex-e-13.yaml,g/bex-g-07.yaml,operators/bex-op-filter.yaml,operators/bex-op-find.yaml,operators/bex-op-findentry.yaml,operators/bex-op-flatmap.yaml,operators/bex-op-reduce.yaml,operators/bex-op-some.yaml,s/bex-s-01.yaml,s/bex-s-07.yaml", "BEX-E-02,BEX-E-11,BEX-E-13,BEX-G-07,BEX-S-01,BEX-S-07"); + default: + throw new ExceptionInInitializerError( + "Missing fixture/vector coverage for " + operator); + } + } + + private static Coverage coverage(String fixturePaths, + String vectorIdentifiers) { + return new Coverage(fixturePaths, vectorIdentifiers); + } + + private static Set immutableNames(String csv) { + if (csv.isEmpty()) { + return Collections.emptySet(); + } + List names = Arrays.asList(csv.split(",", -1)); + LinkedHashSet uniqueNames = new LinkedHashSet<>(); + for (String name : names) { + if (name.isEmpty()) { + throw new ExceptionInInitializerError( + "BEX operator metadata contains an empty name"); + } + if (!uniqueNames.add(name)) { + throw new ExceptionInInitializerError( + "Duplicate BEX operator metadata name: " + name); + } + } + return Collections.unmodifiableSet(uniqueNames); + } + + private static String required(String value, String field) { + if (value == null || value.isEmpty()) { + throw new ExceptionInInitializerError( + "BEX operator catalog " + field + " must be non-empty"); + } + return value; + } + + private static T required(T value, String field) { + if (value == null) { + throw new ExceptionInInitializerError( + "BEX operator catalog " + field + " must be non-null"); + } + return value; + } +} diff --git a/src/main/java/blue/bex/compile/BexPatchEntryParser.java b/blue-bex-core/src/main/java/blue/bex/compile/BexPatchEntryParser.java similarity index 95% rename from src/main/java/blue/bex/compile/BexPatchEntryParser.java rename to blue-bex-core/src/main/java/blue/bex/compile/BexPatchEntryParser.java index eab4c06..11d1f78 100644 --- a/src/main/java/blue/bex/compile/BexPatchEntryParser.java +++ b/blue-bex-core/src/main/java/blue/bex/compile/BexPatchEntryParser.java @@ -2,7 +2,6 @@ import blue.bex.BexException; import blue.bex.result.BexPatchEntry; -import blue.bex.runtime.CompiledFrame; import blue.bex.value.BexValue; import blue.bex.value.BexValues; @@ -26,7 +25,7 @@ static BexPatchEntry fromFields(CompiledFrame frame, } normalizedVal = val; } - String absolute = frame.runtime().resolvePointer(authoredPath); + String absolute = frame.machine().resolvePointer(authoredPath); return new BexPatchEntry(op, authoredPath, absolute, normalizedVal); } diff --git a/blue-bex-core/src/main/java/blue/bex/compile/BexProgramCompiler.java b/blue-bex-core/src/main/java/blue/bex/compile/BexProgramCompiler.java new file mode 100644 index 0000000..dd15636 --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/compile/BexProgramCompiler.java @@ -0,0 +1,363 @@ +package blue.bex.compile; + +import blue.bex.BexException; +import blue.bex.BexSourcePath; +import blue.bex.value.BexValue; +import blue.bex.value.BexUnicodeOrder; +import blue.bex.value.BexValues; +import blue.bex.result.BexMetricsRecorder; +import blue.language.snapshot.FrozenNode; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + + +/** Program selection, functions, constants, and recursion validation. */ +final class BexProgramCompiler extends BexStatementCompiler { + BexProgramCompiler(BexMetricsRecorder metrics, BexIntrinsicCatalog intrinsics) { + super(metrics, intrinsics); + } + + final BexCompiledProgram compileProgram( + BexCompilationInput source, + String compilationEnvironmentIdentity) { + requiredIntrinsicBlueIds.clear(); + FrozenNode step = source.programNode(); + FrozenNode definition = source.definitionNode().orElse(null); + if (source.isExpression()) { + if (definition != null) { + throw new BexException("Expression BEX source cannot use a definition"); + } + if (source.entry().isPresent()) { + throw new BexException("Expression BEX source cannot use an entry"); + } + constants = Collections.emptyMap(); + functionSignatures = Collections.emptyMap(); + currentFunction = "$root"; + CompileScope scope = new CompileScope(); + BexCompiledProgram.CompiledFunction root = new BexCompiledProgram.CompiledFunction("$root", + Collections.emptyList(), + Collections.emptyList(), + compileExpr(step, scope, "/expr"), + scope.frameSize()); + return new BexCompiledProgram(root, Collections.emptyMap(), constants, scope.frameSize(), + BexNodeIdentity.safeBlueId(step), requiredIntrinsicBlueIds, + compilationEnvironmentIdentity); + } + requireProgramNode(step, "program"); + if (definition != null) { + requireProgramNode(definition, "definition"); + } + + Map loadedConstants = new LinkedHashMap<>(); + loadConstants(loadedConstants, explicitProp(definition, "constants")); + loadConstants(loadedConstants, explicitProp(step, "constants")); + constants = Collections.unmodifiableMap(new LinkedHashMap<>(loadedConstants)); + + Map functionNodes = new LinkedHashMap<>(); + loadFunctions(functionNodes, explicitProp(definition, "functions")); + loadFunctions(functionNodes, explicitProp(step, "functions")); + rejectRecursion(functionNodes); + functionSignatures = compileFunctionSignatures(functionNodes); + + Map compiledFunctions = new LinkedHashMap<>(); + for (String name : BexUnicodeOrder.sortedCopy(functionNodes.keySet())) { + compiledFunctions.put(name, compileFunction(name, functionNodes.get(name), functionSignatures.get(name))); + } + + boolean hasStepEntry = hasExplicitProperty(step, "entry"); + boolean hasStepExpr = hasExplicitProperty(step, "expr"); + boolean hasStepDo = hasExplicitProperty(step, "do"); + String entryName = source.entry().isPresent() + ? requiredNonEmptyText(scalarNode(source.entry().get()), "entry") + : hasStepEntry + ? requiredNonEmptyText(explicitProp(step, "entry"), "entry") + : null; + BexCompiledProgram.CompiledFunction root; + int rootFrameSize = 0; + if (entryName != null) { + BexCompiledProgram.CompiledFunction entry = compiledFunctions.get(entryName); + if (entry == null) { + throw new BexException("Unknown entry function: " + entryName); + } + if (!entry.args().isEmpty()) { + throw new BexException("Entry function " + entryName + " declares arguments but entry invocation provides none"); + } + root = new BexCompiledProgram.CompiledFunction("$root", Collections.emptyList(), + Collections.singletonList(sourceStatement("$root", "/entry", "$return", + new ReturnStatement(sourceExpr("$root", "/entry/$call", "$call", + new CallExpr(entryName, new int[0], new CompiledExpression[0]))))), + null, 0); + validateUnselectedRoot(step, hasStepExpr, hasStepDo); + } else if (hasStepExpr) { + currentFunction = "$root"; + CompileScope scope = new CompileScope(); + CompiledExpression expression = compileExpr(explicitProp(step, "expr"), scope, "/expr"); + rootFrameSize = scope.frameSize(); + root = new BexCompiledProgram.CompiledFunction("$root", Collections.emptyList(), + Collections.emptyList(), expression, rootFrameSize); + if (hasStepDo) { + validateRootStatements(explicitProp(step, "do")); + } + } else { + CompileScope scope = new CompileScope(); + currentFunction = "$root"; + List statements = hasStepDo + ? compileStatements(explicitProp(step, "do"), scope, "/do") + : Collections.emptyList(); + rootFrameSize = scope.frameSize(); + root = new BexCompiledProgram.CompiledFunction("$root", Collections.emptyList(), statements, null, rootFrameSize); + } + + return new BexCompiledProgram(root, compiledFunctions, constants, rootFrameSize, + BexNodeIdentity.safeBlueId(step), requiredIntrinsicBlueIds, + compilationEnvironmentIdentity); + } + + BexCompiledProgram.CompiledFunction compileFunction(String name, FrozenNode functionNode, FunctionSignature signature) { + String previousFunction = currentFunction; + currentFunction = name; + try { + CompileScope scope = functionScope(name, signature); + String basePointer = "/functions/" + escape(name); + boolean hasExpression = hasExplicitProperty(functionNode, "expr"); + boolean hasStatements = hasExplicitProperty(functionNode, "do"); + CompiledExpression expression = hasExpression + ? compileExpr(explicitProp(functionNode, "expr"), scope, basePointer + "/expr") + : null; + List statements = !hasExpression && hasStatements + ? compileStatements(explicitProp(functionNode, "do"), scope, basePointer + "/do") + : Collections.emptyList(); + if (hasExpression && hasStatements) { + CompileScope validationScope = functionScope(name, signature); + compileStatements(explicitProp(functionNode, "do"), validationScope, basePointer + "/do"); + } + return new BexCompiledProgram.CompiledFunction(name, signature.args(), + statements, expression, scope.frameSize()); + } finally { + currentFunction = previousFunction; + } + } + + CompileScope functionScope(String name, FunctionSignature signature) { + CompileScope scope = new CompileScope(); + for (BexCompiledProgram.ArgSpec arg : signature.args()) { + int slot = scope.declareOrGetSlot(arg.name()); + if (slot != arg.slot()) { + throw new BexException("Internal function arg slot mismatch for " + name + "." + arg.name()); + } + } + return scope; + } + + void validateUnselectedRoot(FrozenNode step, boolean hasExpression, boolean hasStatements) { + if (hasExpression) { + currentFunction = "$root"; + compileExpr(explicitProp(step, "expr"), new CompileScope(), "/expr"); + } + if (hasStatements) { + validateRootStatements(explicitProp(step, "do")); + } + } + + void validateRootStatements(FrozenNode statements) { + currentFunction = "$root"; + compileStatements(statements, new CompileScope(), "/do"); + } + + Map compileFunctionSignatures(Map functionNodes) { + Map signatures = new LinkedHashMap<>(); + for (String name : BexUnicodeOrder.sortedCopy(functionNodes.keySet())) { + signatures.put(name, compileFunctionSignature(name, functionNodes.get(name))); + } + return signatures; + } + + FunctionSignature compileFunctionSignature(String name, FrozenNode functionNode) { + requireObjectBody(functionNode, "Function " + name, "args", "expr", "do"); + List names = new ArrayList<>(); + FrozenNode argsNode = prop(functionNode, "args"); + if (argsNode != null) { + validatePlainObjectContainer(argsNode, "Function " + name + " args"); + if (argsNode.getProperties() == null) { + if (!argsNode.isEmptyNode()) { + throw new BexException("Function " + name + " args must be an object"); + } + } else { + names.addAll(argsNode.getProperties().keySet()); + Collections.sort(names, BexUnicodeOrder.CODE_POINT_COMPARATOR); + } + } + List args = new ArrayList<>(); + for (int i = 0; i < names.size(); i++) { + String arg = names.get(i); + FrozenNode pattern = argsNode.getProperties().get(arg); + String sourcePointer = "/functions/" + escape(name) + "/args/" + escape(arg); + rejectBexAnywhereInStaticPattern(pattern, sourcePointer); + args.add(new BexCompiledProgram.ArgSpec(arg, i, + pattern, + sourcePointer)); + } + return new FunctionSignature(Collections.unmodifiableList(args)); + } + + void loadConstants(Map constants, FrozenNode node) { + validatePlainObjectContainer(node, "constants"); + if (node == null || node.getProperties() == null) { + return; + } + for (String name : BexUnicodeOrder.sortedCopy(node.getProperties().keySet())) { + FrozenNode value = node.getProperties().get(name); + rejectBexInStaticBlueDefinitionFields(value, "/constants/" + escape(name)); + constants.put(name, BexValues.frozen(value)); + } + } + + void loadFunctions(Map functions, FrozenNode node) { + validatePlainObjectContainer(node, "functions"); + if (node == null || node.getProperties() == null) { + return; + } + for (String name : BexUnicodeOrder.sortedCopy(node.getProperties().keySet())) { + functions.put(name, node.getProperties().get(name)); + } + } + + void rejectRecursion(Map functions) { + Map> calls = new LinkedHashMap<>(); + for (String name : BexUnicodeOrder.sortedCopy(functions.keySet())) { + List targets = new ArrayList<>(); + FrozenNode function = functions.get(name); + String base = "/functions/" + escape(name); + if (hasExplicitProperty(function, "expr")) { + collectCalls(explicitProp(function, "expr"), name, base + "/expr", targets); + } + if (hasExplicitProperty(function, "do")) { + collectCalls(explicitProp(function, "do"), name, base + "/do", targets); + } + calls.put(name, targets); + } + Map states = new LinkedHashMap<>(); + for (String name : calls.keySet()) { + states.put(name, VisitState.UNVISITED); + } + ArrayDeque stack = new ArrayDeque<>(); + for (String name : calls.keySet()) { + if (states.get(name) == VisitState.UNVISITED) { + detectCycle(name, calls, states, stack); + } + } + } + + void detectCycle(String current, + Map> calls, + Map states, + ArrayDeque stack) { + states.put(current, VisitState.VISITING); + stack.addLast(current); + List edges = new ArrayList<>(calls.get(current)); + Collections.sort(edges, (left, right) -> { + int byTarget = BexUnicodeOrder.compareCodePoints(left.target, right.target); + return byTarget != 0 + ? byTarget + : BexUnicodeOrder.compareCodePoints(left.sourcePath.pointer(), right.sourcePath.pointer()); + }); + for (CallSite edge : edges) { + if (!calls.containsKey(edge.target)) { + continue; + } + VisitState state = states.get(edge.target); + if (state == VisitState.VISITING) { + throw BexException.at(edge.sourcePath, + "Compile error reason=recursive-call-graph: recursive BEX function cycle " + + cycleText(stack, edge.target)); + } + if (state == VisitState.UNVISITED) { + detectCycle(edge.target, calls, states, stack); + } + } + stack.removeLast(); + states.put(current, VisitState.VISITED); + } + + String cycleText(ArrayDeque stack, String target) { + List cycle = new ArrayList<>(); + boolean append = false; + for (String name : stack) { + if (name.equals(target)) { + append = true; + } + if (append) { + cycle.add(name); + } + } + cycle.add(target); + return String.join(" -> ", cycle); + } + + void collectCalls(FrozenNode node, String functionName, String pointer, List calls) { + if (node == null) { + return; + } + if (isOperator(node, "$literal")) { + return; + } + if (isOperator(node, "$is")) { + collectCalls(prop(onlyValue(node), "node"), functionName, + pointer + "/$is/node", calls); + return; + } + if (isOperator(node, "$fail")) { + FrozenNode body = onlyValue(node); + boolean messageWrapper = body != null + && body.getProperties() != null + && hasExplicitProperty(body, "message"); + collectCalls(messageWrapper ? explicitProp(body, "message") : body, + functionName, + messageWrapper ? pointer + "/$fail/message" : pointer + "/$fail", + calls); + return; + } + if (isOperator(node, "$call")) { + FrozenNode body = onlyValue(node); + String function = text(prop(body, "function")); + if (function != null) { + calls.add(new CallSite(function, + BexSourcePath.of(functionName, pointer + "/$call", "$call"))); + } + } + if (node.getProperties() != null) { + for (String key : BexUnicodeOrder.sortedCopy(node.getProperties().keySet())) { + collectCalls(node.getProperties().get(key), functionName, + pointer + "/" + escape(key), calls); + } + } + if (node.getItems() != null) { + for (int i = 0; i < node.getItems().size(); i++) { + collectCalls(node.getItems().get(i), functionName, + pointer + "/" + i, calls); + } + } + } + + + enum VisitState { + UNVISITED, + VISITING, + VISITED + } + + static final class CallSite { + private final String target; + private final BexSourcePath sourcePath; + + private CallSite(String target, BexSourcePath sourcePath) { + this.target = target; + this.sourcePath = sourcePath; + } + } +} diff --git a/blue-bex-core/src/main/java/blue/bex/compile/BexStatementCompiler.java b/blue-bex-core/src/main/java/blue/bex/compile/BexStatementCompiler.java new file mode 100644 index 0000000..93af66f --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/compile/BexStatementCompiler.java @@ -0,0 +1,243 @@ +package blue.bex.compile; + +import blue.bex.BexException; +import blue.bex.BexSourcePath; +import blue.bex.value.BexUnicodeOrder; +import blue.bex.result.BexMetricsRecorder; +import blue.language.snapshot.FrozenNode; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + + +/** Statement recognition, validation, and IR construction. */ +abstract class BexStatementCompiler extends BexExpressionCompiler { + BexStatementCompiler(BexMetricsRecorder metrics, BexIntrinsicCatalog intrinsics) { + super(metrics, intrinsics); + } + + List compileStatements(FrozenNode node, CompileScope scope, String pointer) { + if (node == null) { + return Collections.emptyList(); + } + if (node.getItems() == null) { + throw BexException.at(BexSourcePath.of(currentFunction, pointer, null), + "Compile error: statement body must be a list"); + } + List statements = new ArrayList<>(); + for (int i = 0; i < node.getItems().size(); i++) { + FrozenNode item = node.getItems().get(i); + statements.add(compileStatement(item, scope, pointer + "/" + i)); + } + return statements; + } + + CompiledStatement compileStatement(FrozenNode statement, CompileScope scope, String pointer) { + if (isEmptyStatement(statement, pointer)) { + throw BexException.at(BexSourcePath.of(currentFunction, pointer, null), + "Compile error: null or empty statement item"); + } + if (statement.getProperties() == null) { + throw BexException.at(BexSourcePath.of(currentFunction, pointer, null), + "Compile error: statement must be an operator object"); + } + int count = 0; + String op = null; + FrozenNode body = null; + for (Map.Entry entry : statement.getProperties().entrySet()) { + if (entry.getKey().startsWith("$")) { + count++; + op = entry.getKey(); + body = entry.getValue(); + } + } + if (count != 1 + || statement.getProperties().size() != 1 + || authoredFieldNames(statement).size() != 1) { + throw BexException.at(BexSourcePath.of(currentFunction, pointer, null), + "Compile error: statement must have exactly one $ operator"); + } + String bodyPointer = pointer + "/" + escape(op); + BexSourcePath sourcePath = BexSourcePath.of(currentFunction, bodyPointer, op); + try { + if ("$empty".equals(op)) { + throw new BexException("Compile error: $empty placeholder is not a BEX statement"); + } + if (!BexOperatorCatalog.supportsStatement(op)) { + throw new BexException("Unknown statement operator: " + op); + } + validateStatementBody(op, body); + CompiledStatement compiled; + if ("$let".equals(op)) { + if (prop(body, "vars") != null) { + compiled = compileMultiLet(body, scope, bodyPointer); + } else { + String name = requiredText(prop(body, "name"), "$let.name"); + int slot = scope.declareOrGetSlot(name); + compiled = new LetStatement(slot, compileExpr(required(prop(body, "expr"), "$let.expr"), scope, bodyPointer + "/expr")); + } + } else if ("$set".equals(op)) { + String name = requiredText(prop(body, "name"), "$set.name"); + int slot = scope.resolveSlot(name); + compiled = new SetStatement(slot, compileExpr(required(prop(body, "expr"), "$set.expr"), scope, bodyPointer + "/expr")); + } else if ("$if".equals(op)) { + compiled = new IfStatement(compileExpr(required(prop(body, "cond"), "$if.cond"), scope, bodyPointer + "/cond"), + compileStatements(prop(body, "then"), scope, bodyPointer + "/then"), + compileStatements(prop(body, "else"), scope, bodyPointer + "/else")); + } else if ("$forEach".equals(op)) { + String itemName = requiredText(prop(body, "item"), "$forEach.item"); + String keyName = prop(body, "key") != null ? requiredText(prop(body, "key"), "$forEach.key") : null; + String indexName = prop(body, "index") != null ? requiredText(prop(body, "index"), "$forEach.index") : null; + validateDistinctForEachBindings(itemName, keyName, indexName); + CompiledExpression input = compileExpr(required(prop(body, "in"), "$forEach.in"), + scope, bodyPointer + "/in"); + int slot = scope.declareOrGetSlot(itemName); + int keySlot = keyName != null ? scope.declareOrGetSlot(keyName) : -1; + int indexSlot = indexName != null ? scope.declareOrGetSlot(indexName) : -1; + compiled = new ForEachStatement(input, + slot, keySlot, indexSlot, compileStatements(prop(body, "do"), scope, bodyPointer + "/do")); + } else if ("$appendChange".equals(op)) { + compiled = new AppendChangeStatement(textOrExpr(required(prop(body, "op"), "$appendChange.op"), scope, null, bodyPointer + "/op"), + pointerOperand(required(prop(body, "path"), "$appendChange.path"), scope, bodyPointer + "/path"), + prop(body, "val") != null ? compileExpr(prop(body, "val"), scope, bodyPointer + "/val") : null); + } else if ("$appendChanges".equals(op)) { + compiled = new AppendChangesStatement(compileExpr(body, scope, bodyPointer)); + } else if ("$appendEvent".equals(op)) { + compiled = new AppendEventStatement(compileExpr(body, scope, bodyPointer)); + } else if ("$appendEvents".equals(op)) { + compiled = new AppendEventsStatement(compileExpr(body, scope, bodyPointer)); + } else if ("$call".equals(op)) { + compiled = new CallStatement(compileCall(body, scope, bodyPointer)); + } else if ("$return".equals(op)) { + if (body == null || body.isEmptyNode() || (body.getProperties() != null && body.getProperties().isEmpty())) { + compiled = new ReturnStatement(null); + } else { + compiled = new ReturnStatement(compileExpr(body, scope, bodyPointer)); + } + } else if ("$returnIf".equals(op)) { + if (hasExplicitProperty(body, "value")) { + throw new BexException("$returnIf uses expr for its return payload; value is not supported"); + } + compiled = new ReturnIfStatement( + compileExpr(required(prop(body, "cond"), "$returnIf.cond"), scope, bodyPointer + "/cond"), + hasExplicitProperty(body, "expr") + ? compileExpr(explicitProp(body, "expr"), scope, bodyPointer + "/expr") + : null); + } else if ("$fail".equals(op)) { + compiled = new FailStatement(failMessageExpr(body, scope, bodyPointer)); + } else if ("$failIf".equals(op)) { + compiled = new FailIfStatement( + compileExpr(required(prop(body, "cond"), "$failIf.cond"), scope, bodyPointer + "/cond"), + compileExpr(required(prop(body, "message"), "$failIf.message"), scope, bodyPointer + "/message")); + } else { + throw new BexException("Unknown statement operator: " + op); + } + return new SourceStatement(sourcePath, compiled); + } catch (BexException ex) { + throw ex.withSourcePath(sourcePath); + } + } + + CompiledStatement compileMultiLet(FrozenNode body, CompileScope scope, String pointer) { + FrozenNode varsNode = required(prop(body, "vars"), "$let.vars"); + validatePlainObjectContainer(varsNode, "$let.vars"); + if (varsNode.getProperties() == null) { + if (varsNode.isEmptyNode()) { + return new MultiLetStatement(new int[0], new CompiledExpression[0], false); + } + throw new BexException("$let.vars must be an object"); + } + List names = new ArrayList<>(varsNode.getProperties().keySet()); + boolean sequential = prop(body, "order") != null; + if (sequential) { + names = orderedLetNames(prop(body, "order"), varsNode, pointer + "/order"); + } else { + Collections.sort(names, BexUnicodeOrder.CODE_POINT_COMPARATOR); + } + + int[] slots = new int[names.size()]; + List expressions = new ArrayList<>(); + if (sequential) { + for (int i = 0; i < names.size(); i++) { + String name = names.get(i); + expressions.add(compileExpr(varsNode.getProperties().get(name), scope, + pointer + "/vars/" + escape(name))); + slots[i] = scope.declareOrGetSlot(name); + } + } else { + for (int i = 0; i < names.size(); i++) { + slots[i] = scope.declareOrGetSlot(names.get(i)); + } + for (String name : names) { + expressions.add(compileExpr(varsNode.getProperties().get(name), scope, + pointer + "/vars/" + escape(name))); + } + } + return new MultiLetStatement(slots, expressions.toArray(new CompiledExpression[0]), sequential); + } + + List orderedLetNames(FrozenNode orderNode, FrozenNode varsNode, String pointer) { + if (orderNode == null || orderNode.getItems() == null) { + throw new BexException("$let.order must be a list"); + } + List names = new ArrayList<>(); + Set seen = new LinkedHashSet<>(); + for (int i = 0; i < orderNode.getItems().size(); i++) { + String name = requiredText(orderNode.getItems().get(i), "$let.order item"); + if (!seen.add(name)) { + throw new BexException("$let.order contains duplicate variable: " + name); + } + if (varsNode.getProperties() == null || !varsNode.getProperties().containsKey(name)) { + throw new BexException("$let.order references unknown variable: " + name); + } + names.add(name); + } + if (varsNode.getProperties() != null && seen.size() != varsNode.getProperties().size()) { + for (String name : varsNode.getProperties().keySet()) { + if (!seen.contains(name)) { + throw new BexException("$let.order missing variable: " + name); + } + } + } + return names; + } + + boolean isEmptyStatement(FrozenNode statement, String pointer) { + return statement == null || statement.isEmptyNode(); + } + + + void validateStatementBody(String op, FrozenNode body) { + if ("$let".equals(op)) { + requireObjectBody(body, op, "name", "expr", "vars", "order"); + boolean multi = hasAuthoredField(body, "vars"); + if (multi && (hasAuthoredField(body, "name") || hasAuthoredField(body, "expr"))) { + throw new BexException("$let must use either name/expr or vars/order form"); + } + if (!multi && (!hasAuthoredField(body, "name") || !hasAuthoredField(body, "expr"))) { + throw new BexException("$let single-binding form requires name and expr"); + } + } else if ("$set".equals(op)) { + requireObjectBody(body, op, "name", "expr"); + } else if ("$if".equals(op)) { + requireObjectBody(body, op, "cond", "then", "else"); + } else if ("$forEach".equals(op)) { + requireObjectBody(body, op, "in", "item", "key", "index", "do"); + if (!hasAuthoredField(body, "do")) { + throw new BexException("$forEach.do is required"); + } + } else if ("$appendChange".equals(op)) { + requireObjectBody(body, op, "op", "path", "val"); + } else if ("$call".equals(op)) { + requireObjectBody(body, op, "function", "args"); + } else if ("$returnIf".equals(op)) { + requireObjectBody(body, op, "cond", "expr"); + } else if ("$failIf".equals(op)) { + requireObjectBody(body, op, "cond", "message"); + } + } +} diff --git a/src/main/java/blue/bex/compile/BexStatements.java b/blue-bex-core/src/main/java/blue/bex/compile/BexStatements.java similarity index 92% rename from src/main/java/blue/bex/compile/BexStatements.java rename to blue-bex-core/src/main/java/blue/bex/compile/BexStatements.java index c3fb5fe..1665cb7 100644 --- a/src/main/java/blue/bex/compile/BexStatements.java +++ b/blue-bex-core/src/main/java/blue/bex/compile/BexStatements.java @@ -4,14 +4,12 @@ import blue.bex.BexSourcePath; import blue.bex.gas.BexGasCounter; import blue.bex.result.BexPatchEntry; -import blue.bex.runtime.CompiledExpression; -import blue.bex.runtime.CompiledFrame; -import blue.bex.runtime.CompiledStatement; -import blue.bex.runtime.Control; import blue.bex.value.BexValue; import blue.bex.value.BexValues; import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -20,7 +18,7 @@ abstract class Stmt implements CompiledStatement { @Override public final Control exec(CompiledFrame frame) { BexGasWork.charge(frame, BexGasCounter.STATEMENT_EXECUTED); - frame.runtime().metrics().incrementStatementExecutions(); + frame.machine().metrics().incrementStatementExecutions(); try { return doExec(frame); } catch (BexException ex) { @@ -82,8 +80,8 @@ final class MultiLetStatement extends Stmt { private final boolean sequential; MultiLetStatement(int[] slots, CompiledExpression[] expressions, boolean sequential) { - this.slots = slots; - this.expressions = expressions; + this.slots = slots.clone(); + this.expressions = expressions.clone(); this.sequential = sequential; } @@ -119,8 +117,10 @@ final class IfStatement extends Stmt { IfStatement(CompiledExpression cond, List thenStatements, List elseStatements) { this.cond = cond; - this.thenStatements = thenStatements; - this.elseStatements = elseStatements; + this.thenStatements = Collections.unmodifiableList( + new ArrayList<>(thenStatements)); + this.elseStatements = Collections.unmodifiableList( + new ArrayList<>(elseStatements)); } @Override @@ -145,7 +145,7 @@ final class ForEachStatement extends Stmt { this.itemSlot = itemSlot; this.keySlot = keySlot; this.indexSlot = indexSlot; - this.body = body; + this.body = Collections.unmodifiableList(new ArrayList<>(body)); } @Override @@ -155,7 +155,7 @@ protected Control doExec(CompiledFrame frame) { for (String key : value.keys()) { BexGasWork.charge(frame, BexGasCounter.COLLECTION_ITEM_VISITED); BexGasWork.charge(frame, BexGasCounter.OBJECT_MEMBER_READ); - frame.runtime().metrics().incrementLoopIterations(); + frame.machine().metrics().incrementLoopIterations(); if (keySlot >= 0) { frame.set(keySlot, BexValues.scalar(key)); frame.set(itemSlot, value.get(key)); @@ -177,7 +177,7 @@ protected Control doExec(CompiledFrame frame) { for (int i = 0; i < value.size(); i++) { BexGasWork.charge(frame, BexGasCounter.COLLECTION_ITEM_VISITED); BexGasWork.charge(frame, BexGasCounter.LIST_ITEM_READ); - frame.runtime().metrics().incrementLoopIterations(); + frame.machine().metrics().incrementLoopIterations(); frame.set(itemSlot, value.get(String.valueOf(i))); if (indexSlot >= 0) { frame.set(indexSlot, BexValues.scalar(BigInteger.valueOf(i))); @@ -202,7 +202,7 @@ private BexStatementEffects() { static void appendChange(CompiledFrame frame, BexPatchEntry entry) { BexGasWork.charge(frame, BexGasCounter.PATCH_APPENDED); - frame.accumulator().appendChange(entry); + frame.appendChange(entry); } static void appendEvent(CompiledFrame frame, BexValue value) { @@ -210,7 +210,7 @@ static void appendEvent(CompiledFrame frame, BexValue value) { throw new BexException("Undefined cannot be emitted as an event"); } BexGasWork.charge(frame, BexGasCounter.EVENT_APPENDED); - frame.accumulator().appendEvent(value); + frame.appendEvent(value); } } @@ -321,7 +321,9 @@ final class ReturnStatement extends Stmt { @Override protected Control doExec(CompiledFrame frame) { - frame.returnValue(expr != null ? expr.eval(frame) : frame.runtime().defaultResultValue()); + frame.returnValue(expr != null + ? expr.eval(frame) + : frame.machine().defaultResultValue()); return Control.RETURN; } } @@ -340,7 +342,9 @@ protected Control doExec(CompiledFrame frame) { if (!BexValues.truthy(cond.eval(frame))) { return Control.CONTINUE; } - frame.returnValue(value != null ? value.eval(frame) : frame.runtime().defaultResultValue()); + frame.returnValue(value != null + ? value.eval(frame) + : frame.machine().defaultResultValue()); return Control.RETURN; } } diff --git a/src/main/java/blue/bex/compile/CallExpr.java b/blue-bex-core/src/main/java/blue/bex/compile/CallExpr.java similarity index 67% rename from src/main/java/blue/bex/compile/CallExpr.java rename to blue-bex-core/src/main/java/blue/bex/compile/CallExpr.java index fe794d8..c2a2e4f 100644 --- a/src/main/java/blue/bex/compile/CallExpr.java +++ b/blue-bex-core/src/main/java/blue/bex/compile/CallExpr.java @@ -1,8 +1,6 @@ package blue.bex.compile; import blue.bex.BexException; -import blue.bex.runtime.CompiledExpression; -import blue.bex.runtime.CompiledFrame; import blue.bex.value.BexValue; final class CallExpr extends Expr { @@ -12,13 +10,14 @@ final class CallExpr extends Expr { CallExpr(String function, int[] targetSlots, CompiledExpression[] argExpressions) { this.function = function; - this.targetSlots = targetSlots; - this.argExpressions = argExpressions; + this.targetSlots = targetSlots.clone(); + this.argExpressions = argExpressions.clone(); } @Override protected BexValue doEval(CompiledFrame frame) { - BexCompiledProgram.CompiledFunction compiled = frame.runtime().program().functions().get(function); + BexCompiledProgram.CompiledFunction compiled = + frame.machine().program().functions().get(function); if (compiled == null) { throw new BexException("Unknown function: " + function); } @@ -26,6 +25,7 @@ protected BexValue doEval(CompiledFrame frame) { for (int i = 0; i < argExpressions.length; i++) { values[i] = argExpressions[i].eval(frame); } - return compiled.invokePrepared(frame.runtime(), frame, targetSlots, values); + return compiled.invokePrepared( + frame.machine(), frame, targetSlots, values); } } diff --git a/src/main/java/blue/bex/compile/CollectionExpressions.java b/blue-bex-core/src/main/java/blue/bex/compile/CollectionExpressions.java similarity index 96% rename from src/main/java/blue/bex/compile/CollectionExpressions.java rename to blue-bex-core/src/main/java/blue/bex/compile/CollectionExpressions.java index 756f819..af07d6c 100644 --- a/src/main/java/blue/bex/compile/CollectionExpressions.java +++ b/blue-bex-core/src/main/java/blue/bex/compile/CollectionExpressions.java @@ -2,15 +2,15 @@ import blue.bex.BexException; import blue.bex.gas.BexGasCounter; -import blue.bex.runtime.CompiledExpression; -import blue.bex.runtime.CompiledFrame; import blue.bex.value.BexValue; import blue.bex.value.BexUnicodeOrder; import blue.bex.value.BexValues; import java.math.BigInteger; import java.util.ArrayList; +import java.util.Collections; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -34,7 +34,7 @@ final class IsKindExpr extends Expr { IsKindExpr(CompiledExpression value, Set kinds) { this.value = value; - this.kinds = kinds; + this.kinds = Collections.unmodifiableSet(new LinkedHashSet<>(kinds)); } @Override @@ -89,7 +89,7 @@ private BexValue evalList(CompiledFrame frame, BexValue list) { for (int i = 0; i < list.size(); i++) { BexGasWork.charge(frame, BexGasCounter.COLLECTION_ITEM_VISITED); BexGasWork.charge(frame, BexGasCounter.LIST_ITEM_READ); - frame.runtime().metrics().incrementLoopIterations(); + frame.machine().metrics().incrementLoopIterations(); BexValue item = list.get(String.valueOf(i)); bind(frame, item, BexValues.undefined(), BexValues.scalar(BigInteger.valueOf(i))); BexValue result = body.eval(frame); @@ -142,7 +142,7 @@ private BexValue evalObject(CompiledFrame frame, BexValue object) { String key = keys.get(i); BexGasWork.charge(frame, BexGasCounter.COLLECTION_ITEM_VISITED); BexGasWork.charge(frame, BexGasCounter.OBJECT_MEMBER_READ); - frame.runtime().metrics().incrementLoopIterations(); + frame.machine().metrics().incrementLoopIterations(); BexValue item = object.get(key); bind(frame, item, BexValues.scalar(key), BexValues.scalar(BigInteger.valueOf(i))); BexValue result = body.eval(frame); @@ -294,7 +294,7 @@ protected BexValue doEval(CompiledFrame frame) { for (int i = 0; i < collection.size(); i++) { BexGasWork.charge(frame, BexGasCounter.COLLECTION_ITEM_VISITED); BexGasWork.charge(frame, BexGasCounter.LIST_ITEM_READ); - frame.runtime().metrics().incrementLoopIterations(); + frame.machine().metrics().incrementLoopIterations(); bind(frame, collection.get(String.valueOf(i)), BexValues.undefined(), BexValues.scalar(BigInteger.valueOf(i))); acc = expr.eval(frame); @@ -308,7 +308,7 @@ protected BexValue doEval(CompiledFrame frame) { String key = keys.get(i); BexGasWork.charge(frame, BexGasCounter.COLLECTION_ITEM_VISITED); BexGasWork.charge(frame, BexGasCounter.OBJECT_MEMBER_READ); - frame.runtime().metrics().incrementLoopIterations(); + frame.machine().metrics().incrementLoopIterations(); bind(frame, collection.get(key), BexValues.scalar(key), BexValues.scalar(BigInteger.valueOf(i))); acc = expr.eval(frame); frame.set(accSlot, acc); @@ -338,9 +338,9 @@ final class SlotSnapshot { private final boolean[] initialized; private SlotSnapshot(int[] slots, BexValue[] values, boolean[] initialized) { - this.slots = slots; - this.values = values; - this.initialized = initialized; + this.slots = slots.clone(); + this.values = values.clone(); + this.initialized = initialized.clone(); } static SlotSnapshot capture(CompiledFrame frame, int... candidates) { @@ -395,7 +395,7 @@ protected BexValue doEval(CompiledFrame frame) { for (int i = 0; i < list.size(); i++) { BexGasWork.charge(frame, BexGasCounter.COLLECTION_ITEM_VISITED); BexGasWork.charge(frame, BexGasCounter.LIST_ITEM_READ); - frame.runtime().metrics().incrementLoopIterations(); + frame.machine().metrics().incrementLoopIterations(); if (MeteredEquality.equal( frame, list.get(String.valueOf(i)), val)) { return BexValues.scalar(true); @@ -443,7 +443,7 @@ protected BexValue doEval(CompiledFrame frame) { for (int i = 0; i < entries.size(); i++) { BexGasWork.charge(frame, BexGasCounter.COLLECTION_ITEM_VISITED); BexGasWork.charge(frame, BexGasCounter.LIST_ITEM_READ); - frame.runtime().metrics().incrementLoopIterations(); + frame.machine().metrics().incrementLoopIterations(); BexValue entry = entries.get(String.valueOf(i)); if (!entry.isObject()) { throw new BexException("$objectFromEntries entries must be objects"); diff --git a/src/main/java/blue/bex/runtime/CompileScope.java b/blue-bex-core/src/main/java/blue/bex/compile/CompileScope.java similarity index 82% rename from src/main/java/blue/bex/runtime/CompileScope.java rename to blue-bex-core/src/main/java/blue/bex/compile/CompileScope.java index 941a51e..0d1bae2 100644 --- a/src/main/java/blue/bex/runtime/CompileScope.java +++ b/blue-bex-core/src/main/java/blue/bex/compile/CompileScope.java @@ -1,13 +1,11 @@ -package blue.bex.runtime; +package blue.bex.compile; import blue.bex.BexException; import java.util.LinkedHashMap; import java.util.Map; -/** - * Compile-time slot allocator. - */ +/** Compile-time slot allocator. */ public final class CompileScope { private final CompileScope parent; private final Map slots = new LinkedHashMap<>(); @@ -47,20 +45,22 @@ public int resolveSlot(String name) { } public boolean hasSlot(String name) { - return slots.containsKey(name) || (parent != null && parent.hasSlot(name)); + return slots.containsKey(name) + || (parent != null && parent.hasSlot(name)); } public int frameSize() { - return Math.max(nextSlot, parent != null ? parent.frameSize() : 0); + return Math.max( + nextSlot, + parent != null ? parent.frameSize() : 0); } /** - * Captures which names are visible without rewinding allocated frame slots. + * Captures visible names without rewinding allocated frame slots. * *

Collection-query bindings are lexical only for the query expression. * Restoring visibility removes names introduced by the query while keeping - * the allocated slots in the function frame so the compiled expression can - * still use them at runtime.

+ * allocated slots available to the compiled expression.

*/ public Visibility captureVisibility() { return new Visibility(new LinkedHashMap<>(slots)); diff --git a/src/main/java/blue/bex/runtime/CompiledExpression.java b/blue-bex-core/src/main/java/blue/bex/compile/CompiledExpression.java similarity index 57% rename from src/main/java/blue/bex/runtime/CompiledExpression.java rename to blue-bex-core/src/main/java/blue/bex/compile/CompiledExpression.java index 185c42f..984b15d 100644 --- a/src/main/java/blue/bex/runtime/CompiledExpression.java +++ b/blue-bex-core/src/main/java/blue/bex/compile/CompiledExpression.java @@ -1,7 +1,8 @@ -package blue.bex.runtime; +package blue.bex.compile; import blue.bex.value.BexValue; +/** Immutable expression node in a compiled BEX program. */ public interface CompiledExpression { BexValue eval(CompiledFrame frame); } diff --git a/src/main/java/blue/bex/runtime/CompiledFrame.java b/blue-bex-core/src/main/java/blue/bex/compile/CompiledFrame.java similarity index 53% rename from src/main/java/blue/bex/runtime/CompiledFrame.java rename to blue-bex-core/src/main/java/blue/bex/compile/CompiledFrame.java index eb79cf6..99a6a59 100644 --- a/src/main/java/blue/bex/runtime/CompiledFrame.java +++ b/blue-bex-core/src/main/java/blue/bex/compile/CompiledFrame.java @@ -1,30 +1,32 @@ -package blue.bex.runtime; +package blue.bex.compile; import blue.bex.BexException; import blue.bex.BexSourcePath; +import blue.bex.result.BexPatchEntry; import blue.bex.value.BexValue; import blue.bex.value.BexValues; import java.util.List; -/** - * Slot-based variable frame. - */ +/** Slot-based invocation frame for compiled BEX IR. */ public final class CompiledFrame { - private final BexRuntime runtime; + private final BexExecutionMachine machine; private final BexValue[] slots; private final CompiledFrame parent; private BexValue returnValue; private BexSourcePath sourcePath; - public CompiledFrame(BexRuntime runtime, int frameSize, CompiledFrame parent) { - this.runtime = runtime; + public CompiledFrame( + BexExecutionMachine machine, + int frameSize, + CompiledFrame parent) { + this.machine = machine; this.slots = new BexValue[frameSize]; this.parent = parent; } - public BexRuntime runtime() { - return runtime; + public BexExecutionMachine machine() { + return machine; } public BexValue get(int slot) { @@ -32,9 +34,7 @@ public BexValue get(int slot) { return value != null ? value : BexValues.undefined(); } - /** - * Reads a declared slot and fails when its initializer has not completed. - */ + /** Reads a declared slot and fails while its initializer is incomplete. */ public BexValue getRequired(int slot) { BexValue value = slots[slot]; if (value != null) { @@ -42,7 +42,9 @@ public BexValue getRequired(int slot) { } BexSourcePath path = sourcePath(); String message = "Binding is uninitialized"; - throw path != null ? BexException.at(path, message) : new BexException(message); + throw path != null + ? BexException.at(path, message) + : new BexException(message); } public boolean isInitialized(int slot) { @@ -61,28 +63,45 @@ public CompiledFrame parent() { return parent; } - public BexValue readDocument(String pointer, List precompiledSegments, boolean resolved) { - return runtime.readDocument(pointer, precompiledSegments, resolved); + public BexValue readDocument( + String pointer, + List precompiledSegments, + boolean resolved) { + return machine.readDocument(pointer, precompiledSegments, resolved); } public BexValue readEvent(List precompiledSegments) { - return runtime.readEvent(precompiledSegments); + return machine.readEvent(precompiledSegments); } public BexValue readProcessingEvent(List precompiledSegments) { - return runtime.readProcessingEvent(precompiledSegments); + return machine.readProcessingEvent(precompiledSegments); } public BexValue readCurrentContract(List precompiledSegments) { - return runtime.readCurrentContract(precompiledSegments); + return machine.readCurrentContract(precompiledSegments); } - public BexValue readBinding(String name, List precompiledSegments) { - return runtime.readBinding(name, precompiledSegments); + public BexValue readBinding( + String name, + List precompiledSegments) { + return machine.readBinding(name, precompiledSegments); } - public BexExecutionAccumulator accumulator() { - return runtime.accumulator(); + public void appendChange(BexPatchEntry entry) { + machine.appendChange(entry); + } + + public void appendEvent(BexValue event) { + machine.appendEvent(event); + } + + public BexValue changesetValue() { + return machine.changesetValue(); + } + + public BexValue eventsValue() { + return machine.eventsValue(); } public BexValue returnValue() { @@ -94,7 +113,9 @@ public void returnValue(BexValue returnValue) { } public BexSourcePath sourcePath() { - return sourcePath != null ? sourcePath : parent != null ? parent.sourcePath() : null; + return sourcePath != null + ? sourcePath + : parent != null ? parent.sourcePath() : null; } public BexSourcePath enter(BexSourcePath next) { diff --git a/blue-bex-core/src/main/java/blue/bex/compile/CompiledStatement.java b/blue-bex-core/src/main/java/blue/bex/compile/CompiledStatement.java new file mode 100644 index 0000000..7dcc6d0 --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/compile/CompiledStatement.java @@ -0,0 +1,6 @@ +package blue.bex.compile; + +/** Immutable statement node in a compiled BEX program. */ +public interface CompiledStatement { + Control exec(CompiledFrame frame); +} diff --git a/blue-bex-core/src/main/java/blue/bex/compile/ConstructedExpressions.java b/blue-bex-core/src/main/java/blue/bex/compile/ConstructedExpressions.java new file mode 100644 index 0000000..afd8774 --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/compile/ConstructedExpressions.java @@ -0,0 +1,116 @@ +package blue.bex.compile; + +import blue.bex.BexException; +import blue.bex.gas.BexGasCounter; +import blue.bex.value.BexValue; +import blue.bex.value.BexUnicodeOrder; +import blue.bex.value.BexValues; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +final class ObjectExpr extends Expr { + private final Map fields; + + ObjectExpr(Map fields) { + Map sortedFields = new LinkedHashMap<>(); + for (String key : BexUnicodeOrder.sortedCopy(fields.keySet())) { + sortedFields.put(key, fields.get(key)); + } + this.fields = Collections.unmodifiableMap(sortedFields); + } + + @Override + protected BexValue doEval(CompiledFrame frame) { + Map out = new LinkedHashMap<>(); + for (Map.Entry entry : fields.entrySet()) { + BexValue value = entry.getValue().eval(frame); + if (!value.isUndefined()) { + BexGasWork.charge(frame, BexGasCounter.TRANSIENT_OBJECT_MEMBER_PRODUCED); + out.put(entry.getKey(), value); + } + } + return BexValues.map(out); + } +} + +final class ListExpr extends Expr { + private final List items; + + ListExpr(List items) { + this.items = ImmutableExpressionLists.copyOf(items); + } + + @Override + protected BexValue doEval(CompiledFrame frame) { + List out = new ArrayList<>(); + for (CompiledExpression item : items) { + BexValue value = item.eval(frame); + if (value.isUndefined()) { + throw new BexException( + "Undefined cannot appear in a Blue list"); + } + BexGasWork.charge(frame, BexGasCounter.TRANSIENT_LIST_ITEM_PRODUCED); + out.add(value); + } + return BexValues.list(out); + } +} + +final class IntrinsicExpr extends Expr { + private final String blueId; + private final BexValue type; + private final Map fields; + + IntrinsicExpr(String blueId, BexValue type, Map fields) { + this.blueId = blueId; + this.type = type; + Map sortedFields = new LinkedHashMap<>(); + for (String key : BexUnicodeOrder.sortedCopy(fields.keySet())) { + sortedFields.put(key, fields.get(key)); + } + this.fields = Collections.unmodifiableMap(sortedFields); + } + + @Override + protected BexValue doEval(CompiledFrame frame) { + Map values = new LinkedHashMap<>(); + for (Map.Entry entry : fields.entrySet()) { + BexValue value = entry.getValue().eval(frame); + if (!value.isUndefined()) { + values.put(entry.getKey(), value); + } + } + BexGasWork.charge(frame, BexGasCounter.INTRINSIC_CALLED); + return frame.machine().invokeIntrinsic(blueId, type, values); + } +} + +final class FailExpr extends Expr { + private final CompiledExpression message; + + FailExpr(CompiledExpression message) { + this.message = message; + } + + @Override + protected BexValue doEval(CompiledFrame frame) { + throw new BexException(message.eval(frame).asText()); + } +} + +final class NodeBlueIdExpr extends Expr { + private final CompiledExpression expression; + + NodeBlueIdExpr(CompiledExpression expression) { + this.expression = expression; + } + + @Override + protected BexValue doEval(CompiledFrame frame) { + return frame.machine().nodeBlueId(expression.eval(frame)); + } +} diff --git a/blue-bex-core/src/main/java/blue/bex/compile/Control.java b/blue-bex-core/src/main/java/blue/bex/compile/Control.java new file mode 100644 index 0000000..e0bb09e --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/compile/Control.java @@ -0,0 +1,7 @@ +package blue.bex.compile; + +/** Control outcome of one compiled statement. */ +public enum Control { + CONTINUE, + RETURN +} diff --git a/blue-bex-core/src/main/java/blue/bex/compile/ExpressionBase.java b/blue-bex-core/src/main/java/blue/bex/compile/ExpressionBase.java new file mode 100644 index 0000000..51b1987 --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/compile/ExpressionBase.java @@ -0,0 +1,81 @@ +package blue.bex.compile; + +import blue.bex.BexException; +import blue.bex.BexSourcePath; +import blue.bex.gas.BexGasCounter; +import blue.bex.value.BexValue; +import blue.bex.value.BexValues; + +abstract class Expr implements CompiledExpression { + @Override + public final BexValue eval(CompiledFrame frame) { + BexGasWork.charge(frame, BexGasCounter.EXPRESSION_EVALUATED); + frame.machine().metrics().incrementExpressionEvaluations(); + try { + return doEval(frame); + } catch (BexException ex) { + BexSourcePath sourcePath = frame.sourcePath(); + if (sourcePath != null && !ex.sourcePath().isPresent()) { + throw ex.withSourcePath(sourcePath); + } + throw ex; + } + } + + protected abstract BexValue doEval(CompiledFrame frame); +} + +final class SourceExpr implements CompiledExpression { + private final BexSourcePath sourcePath; + private final CompiledExpression delegate; + + SourceExpr(BexSourcePath sourcePath, CompiledExpression delegate) { + this.sourcePath = sourcePath; + this.delegate = delegate; + } + + @Override + public BexValue eval(CompiledFrame frame) { + BexSourcePath previous = frame.enter(sourcePath); + try { + return delegate.eval(frame); + } catch (BexException ex) { + if (!ex.sourcePath().isPresent()) { + throw ex.withSourcePath(sourcePath); + } + throw ex; + } finally { + frame.restore(previous); + } + } +} + +final class LiteralExpr extends Expr { + private final BexValue value; + + LiteralExpr(BexValue value) { + this.value = value; + } + + @Override + protected BexValue doEval(CompiledFrame frame) { + return value; + } +} + +final class TransientLiteralExpr extends Expr { + private final Object scalar; + + TransientLiteralExpr(Object scalar) { + this.scalar = scalar; + } + + @Override + protected BexValue doEval(CompiledFrame frame) { + if (scalar instanceof String) { + BexGasWork.chargeTextConstruction( + frame, (String) scalar); + } + return BexValues.scalar(scalar); + } +} diff --git a/src/main/java/blue/bex/compile/LogicNumericExpressions.java b/blue-bex-core/src/main/java/blue/bex/compile/LogicNumericExpressions.java similarity index 92% rename from src/main/java/blue/bex/compile/LogicNumericExpressions.java rename to blue-bex-core/src/main/java/blue/bex/compile/LogicNumericExpressions.java index 5ba97cc..7382810 100644 --- a/src/main/java/blue/bex/compile/LogicNumericExpressions.java +++ b/blue-bex-core/src/main/java/blue/bex/compile/LogicNumericExpressions.java @@ -2,12 +2,12 @@ import blue.bex.BexException; import blue.bex.gas.BexGasCounter; -import blue.bex.runtime.CompiledExpression; -import blue.bex.runtime.CompiledFrame; import blue.bex.value.BexValue; import blue.bex.value.BexValues; import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; enum CompareOp { EQ, NE, GT, GTE, LT, LTE } @@ -89,7 +89,7 @@ final class CompareExpr extends Expr { private final CompareOp op; CompareExpr(List expressions, CompareOp op) { - this.expressions = expressions; + this.expressions = ImmutableExpressionLists.copyOf(expressions); this.op = op; } @@ -118,7 +118,7 @@ final class LogicalExpr extends Expr { private final boolean and; LogicalExpr(List expressions, boolean and) { - this.expressions = expressions; + this.expressions = ImmutableExpressionLists.copyOf(expressions); this.and = and; } @@ -150,7 +150,7 @@ final class CoalesceExpr extends Expr { private final List expressions; CoalesceExpr(List expressions) { - this.expressions = expressions; + this.expressions = ImmutableExpressionLists.copyOf(expressions); } @Override @@ -170,7 +170,7 @@ final class NumericExpr extends Expr { private final NumericOp op; NumericExpr(List expressions, NumericOp op) { - this.expressions = expressions; + this.expressions = ImmutableExpressionLists.copyOf(expressions); this.op = op; } @@ -216,3 +216,13 @@ protected BexValue doEval(CompiledFrame frame) { return BexValues.scalar(result); } } + +final class ImmutableExpressionLists { + private ImmutableExpressionLists() { + } + + static List copyOf( + List expressions) { + return Collections.unmodifiableList(new ArrayList<>(expressions)); + } +} diff --git a/src/main/java/blue/bex/compile/LruBexCompiledProgramCache.java b/blue-bex-core/src/main/java/blue/bex/compile/LruBexCompiledProgramCache.java similarity index 100% rename from src/main/java/blue/bex/compile/LruBexCompiledProgramCache.java rename to blue-bex-core/src/main/java/blue/bex/compile/LruBexCompiledProgramCache.java diff --git a/src/main/java/blue/bex/compile/ObjectResultExpressions.java b/blue-bex-core/src/main/java/blue/bex/compile/ObjectResultExpressions.java similarity index 96% rename from src/main/java/blue/bex/compile/ObjectResultExpressions.java rename to blue-bex-core/src/main/java/blue/bex/compile/ObjectResultExpressions.java index 1e443e0..c9d8dd7 100644 --- a/src/main/java/blue/bex/compile/ObjectResultExpressions.java +++ b/blue-bex-core/src/main/java/blue/bex/compile/ObjectResultExpressions.java @@ -2,8 +2,6 @@ import blue.bex.BexException; import blue.bex.gas.BexGasCounter; -import blue.bex.runtime.CompiledExpression; -import blue.bex.runtime.CompiledFrame; import blue.bex.value.BexValue; import blue.bex.value.BexValues; @@ -218,14 +216,14 @@ protected BexValue doEval(CompiledFrame frame) { final class ChangesetExpr extends Expr { @Override protected BexValue doEval(CompiledFrame frame) { - return frame.accumulator().changeset().asValue(); + return frame.changesetValue(); } } final class EventsExpr extends Expr { @Override protected BexValue doEval(CompiledFrame frame) { - return frame.accumulator().events().asValue(); + return frame.eventsValue(); } } @@ -239,6 +237,7 @@ final class ResultValueExpr extends Expr { @Override protected BexValue doEval(CompiledFrame frame) { ResolvedPointer resolved = pointer.resolve(frame); - return frame.runtime().readResultValue(resolved.absolute(), resolved.segments()); + return frame.machine().readResultValue( + resolved.absolute(), resolved.segments()); } } diff --git a/blue-bex-core/src/main/java/blue/bex/compile/ReadExpressions.java b/blue-bex-core/src/main/java/blue/bex/compile/ReadExpressions.java new file mode 100644 index 0000000..3ccbbd4 --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/compile/ReadExpressions.java @@ -0,0 +1,150 @@ +package blue.bex.compile; + +import blue.bex.BexException; +import blue.bex.gas.BexGasCounter; +import blue.bex.value.BexValue; +import java.util.List; + +final class DocumentExpr extends Expr { + private final PointerOperand pointer; + private final boolean resolved; + + DocumentExpr(PointerOperand pointer, boolean resolved) { + this.pointer = pointer; + this.resolved = resolved; + } + + @Override + protected BexValue doEval(CompiledFrame frame) { + ResolvedPointer resolvedPointer = pointer.resolve(frame); + return frame.readDocument(resolvedPointer.absolute(), resolvedPointer.segments(), resolved); + } +} + +enum ContextKind { EVENT, PROCESSING_EVENT, CURRENT_CONTRACT } + +final class ContextPointerExpr extends Expr { + private final PointerOperand pointer; + private final ContextKind kind; + + ContextPointerExpr(PointerOperand pointer, ContextKind kind) { + this.pointer = pointer; + this.kind = kind; + } + + @Override + protected BexValue doEval(CompiledFrame frame) { + List segments = pointer.segments(frame); + if (kind == ContextKind.EVENT) { + return frame.readEvent(segments); + } + if (kind == ContextKind.PROCESSING_EVENT) { + return frame.readProcessingEvent(segments); + } + return frame.readCurrentContract(segments); + } +} + +final class StepsExpr extends Expr { + private final TextOperand step; + private final PointerOperand pointer; + + StepsExpr(TextOperand step, PointerOperand pointer) { + this.step = step; + this.pointer = pointer; + } + + @Override + protected BexValue doEval(CompiledFrame frame) { + String stepName = step.get(frame); + if (stepName.isEmpty()) { + throw new BexException("$steps.step cannot be empty"); + } + return frame.machine().readSteps(stepName, pointer.segments(frame)); + } +} + +final class BindingExpr extends Expr { + private final TextOperand name; + private final PointerOperand pointer; + + BindingExpr(TextOperand name, PointerOperand pointer) { + this.name = name; + this.pointer = pointer; + } + + @Override + protected BexValue doEval(CompiledFrame frame) { + String bindingName = name.get(frame); + if (bindingName.isEmpty()) { + throw new BexException("$binding.name cannot be empty"); + } + return frame.readBinding(bindingName, pointer.segments(frame)); + } +} + +final class VarExpr extends Expr { + private final int slot; + private final PointerOperand pointer; + + VarExpr(int slot) { + this(slot, null); + } + + VarExpr(int slot, PointerOperand pointer) { + this.slot = slot; + this.pointer = pointer; + } + + @Override + protected BexValue doEval(CompiledFrame frame) { + BexGasWork.charge(frame, BexGasCounter.VARIABLE_READ); + BexValue value = frame.getRequired(slot); + return pointer != null + ? frame.machine().readValuePointer(value, pointer.segments(frame)) + : value; + } +} + +final class ConstExpr extends Expr { + private final String name; + private final PointerOperand pointer; + + ConstExpr(String name) { + this(name, null); + } + + ConstExpr(String name, PointerOperand pointer) { + this.name = name; + this.pointer = pointer; + } + + @Override + protected BexValue doEval(CompiledFrame frame) { + BexGasWork.charge(frame, BexGasCounter.CONSTANT_READ); + BexValue value = frame.machine().program().constant(name); + return pointer != null + ? frame.machine().readValuePointer(value, pointer.segments(frame)) + : value; + } +} + +final class GetExpr extends Expr { + private final CompiledExpression object; + private final TextOperand key; + + GetExpr(CompiledExpression object, TextOperand key) { + this.object = object; + this.key = key; + } + + @Override + protected BexValue doEval(CompiledFrame frame) { + BexValue value = object.eval(frame); + String evaluatedKey = key.get(frame); + if (value.isObject()) { + BexGasWork.charge(frame, BexGasCounter.OBJECT_MEMBER_READ); + } + return value.get(evaluatedKey); + } +} diff --git a/src/main/java/blue/bex/compile/TypeStringExpressions.java b/blue-bex-core/src/main/java/blue/bex/compile/TypeStringExpressions.java similarity index 98% rename from src/main/java/blue/bex/compile/TypeStringExpressions.java rename to blue-bex-core/src/main/java/blue/bex/compile/TypeStringExpressions.java index f6aa8bf..fc9d7e0 100644 --- a/src/main/java/blue/bex/compile/TypeStringExpressions.java +++ b/blue-bex-core/src/main/java/blue/bex/compile/TypeStringExpressions.java @@ -2,8 +2,6 @@ import blue.bex.BexException; import blue.bex.gas.BexGasCounter; -import blue.bex.runtime.CompiledExpression; -import blue.bex.runtime.CompiledFrame; import blue.bex.value.BexValue; import blue.bex.value.BexUnicodeOrder; import blue.bex.value.BexValues; @@ -112,10 +110,9 @@ final class IsExpr extends Expr { @Override protected BexValue doEval(CompiledFrame frame) { BexValue value = valueExpression.eval(frame); - return BexValues.scalar(frame.runtime().typeMatcher().matches( + return BexValues.scalar(frame.machine().matchesType( value, pattern, - frame.runtime().gas(), frame.sourcePath())); } } @@ -127,7 +124,7 @@ final class VariadicExpr extends Expr { private final VariadicOp op; VariadicExpr(List expressions, VariadicOp op) { - this.expressions = expressions; + this.expressions = ImmutableExpressionLists.copyOf(expressions); this.op = op; } @@ -264,7 +261,7 @@ final class PointerJoinExpr extends Expr { private final List segments; PointerJoinExpr(List segments) { - this.segments = segments; + this.segments = ImmutableExpressionLists.copyOf(segments); } @Override @@ -382,7 +379,7 @@ final class BinaryTextExpr extends Expr { private final BinaryTextOp op; BinaryTextExpr(List expressions, BinaryTextOp op) { - this.expressions = expressions; + this.expressions = ImmutableExpressionLists.copyOf(expressions); this.op = op; } diff --git a/blue-bex-core/src/main/java/blue/bex/compile/package-info.java b/blue-bex-core/src/main/java/blue/bex/compile/package-info.java new file mode 100644 index 0000000..ffe1a5c --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/compile/package-info.java @@ -0,0 +1,12 @@ +/** + * Deterministic BEX compilation, immutable compiled programs, operator metadata, + * and compiled-program caches. + * + *

Compiled programs and keys are immutable and shareable. Cache + * implementations document their synchronization; compiler instances are + * operation-owned. Required source/configuration arguments are non-null. + * Invalid static source fails compilation rather than becoming runtime + * undefined. Compilation is outside portable runtime gas, while every compiled + * instruction must preserve the named charges and lazy order of its execution.

+ */ +package blue.bex.compile; diff --git a/blue-bex-core/src/main/java/blue/bex/gas/BexGasAdmission.java b/blue-bex-core/src/main/java/blue/bex/gas/BexGasAdmission.java new file mode 100644 index 0000000..69df6b0 --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/gas/BexGasAdmission.java @@ -0,0 +1,145 @@ +package blue.bex.gas; + +/** + * Performs one fail-before-work gas admission against local and hosted + * budgets, then records the admitted charge. + */ +final class BexGasAdmission { + private final BexGasBudget budget; + private final BexGasHostSession hostSession; + private final BexGasTraceRecorder traceRecorder; + + BexGasAdmission( + BexGasBudget budget, + BexGasHostSession hostSession, + BexGasTraceRecorder traceRecorder) { + this.budget = budget; + this.hostSession = hostSession; + this.traceRecorder = traceRecorder; + } + + void admit( + String namespace, + String counterName, + BexGasCounter portableCounter, + long quantity, + long weight, + String sourcePath, + String operator, + String reason) { + hostSession.ensureOpenForCharge(); + if (quantity < 0L) { + throw new IllegalArgumentException( + "Gas quantity must be non-negative"); + } + if (quantity == 0L || weight == 0L) { + return; + } + String exactReason = requireReason(reason); + long gas = multiplyExact(quantity, weight); + + /* + * A hosted meter prechecks only the optional BEX-local sub-limit. The + * processor-owned child ledger remains the sole authority for the live + * parent budget, including reservations consumed after this meter was + * opened. A standalone meter has no such owner and therefore checks + * the complete effective budget itself. + */ + long localAdmissionBudget = budget.admissionLimit( + hostSession.isHosted(), + hostSession.enforcesLocalLimit()); + if (localAdmissionBudget != BexGasMeter.NO_LOCAL_LIMIT + && gas > localAdmissionBudget - traceRecorder.totalGas()) { + throw exhausted( + namespace, + counterName, + portableCounter, + quantity, + weight, + null); + } + + BexGasLedgerCapability hostLedger = hostSession.ledgerFor(namespace); + if (hostLedger != null) { + try { + hostLedger.charge( + hostSession.physicalCounterName( + namespace, counterName), + quantity, + BexGasChargeContext.of( + emptyToNull(sourcePath), + null, + emptyToNull(operator), + exactReason)); + } catch (BexHostGasExhaustion exhaustion) { + /* + * Retain the exact host rejection so its owning work session + * can validate and propagate that same object. The rejected + * entry remains absent from the local trace. + */ + throw exhausted( + namespace, + counterName, + portableCounter, + quantity, + weight, + exhaustion); + } + } + + traceRecorder.append( + namespace, + counterName, + quantity, + weight, + gas, + sourcePath, + operator, + exactReason); + } + + private BexGasLimitExceededException exhausted( + String namespace, + String counterName, + BexGasCounter portableCounter, + long quantity, + long weight, + BexHostGasExhaustion hostGasExhaustion) { + if (portableCounter != null) { + return new BexGasLimitExceededException( + portableCounter, + quantity, + weight, + traceRecorder.totalGas(), + budget.effectiveBudget(), + hostGasExhaustion); + } + return new BexGasLimitExceededException( + namespace, + counterName, + quantity, + weight, + traceRecorder.totalGas(), + budget.effectiveBudget(), + hostGasExhaustion); + } + + private static String emptyToNull(String value) { + return value == null || value.isEmpty() ? null : value; + } + + private static String requireReason(String value) { + if (value == null || value.trim().isEmpty()) { + throw new IllegalArgumentException("Gas charge reason is required"); + } + return value; + } + + private static long multiplyExact(long left, long right) { + if (left != 0L && right > Long.MAX_VALUE / left) { + throw new IllegalArgumentException( + "Gas subtotal exceeds long range"); + } + return left * right; + } +} diff --git a/blue-bex-core/src/main/java/blue/bex/gas/BexGasBudget.java b/blue-bex-core/src/main/java/blue/bex/gas/BexGasBudget.java new file mode 100644 index 0000000..8f14d68 --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/gas/BexGasBudget.java @@ -0,0 +1,42 @@ +package blue.bex.gas; + +/** Immutable parent/local budget configuration for one meter. */ +final class BexGasBudget { + private final long parentRemainingGas; + private final long localLimit; + private final long effectiveBudget; + + BexGasBudget(long parentRemainingGas, long localLimit) { + if (parentRemainingGas < 0L) { + throw new IllegalArgumentException( + "parentRemainingGas must be non-negative"); + } + if (localLimit < BexGasMeter.NO_LOCAL_LIMIT) { + throw new IllegalArgumentException( + "localLimit must be non-negative or NO_LOCAL_LIMIT"); + } + this.parentRemainingGas = parentRemainingGas; + this.localLimit = localLimit; + this.effectiveBudget = localLimit == BexGasMeter.NO_LOCAL_LIMIT + ? parentRemainingGas + : Math.min(parentRemainingGas, localLimit); + } + + long parentRemainingGas() { return parentRemainingGas; } + long localLimit() { return localLimit; } + long effectiveBudget() { return effectiveBudget; } + + long admissionLimit( + boolean hosted, + boolean hostEnforcesLocalLimit) { + return !hosted + ? effectiveBudget + : hostEnforcesLocalLimit + ? BexGasMeter.NO_LOCAL_LIMIT + : localLimit; + } + + long remaining(long admittedGas) { + return effectiveBudget - admittedGas; + } +} diff --git a/src/main/java/blue/bex/gas/BexGasCharge.java b/blue-bex-core/src/main/java/blue/bex/gas/BexGasCharge.java similarity index 100% rename from src/main/java/blue/bex/gas/BexGasCharge.java rename to blue-bex-core/src/main/java/blue/bex/gas/BexGasCharge.java diff --git a/blue-bex-core/src/main/java/blue/bex/gas/BexGasChargeContext.java b/blue-bex-core/src/main/java/blue/bex/gas/BexGasChargeContext.java new file mode 100644 index 0000000..809b778 --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/gas/BexGasChargeContext.java @@ -0,0 +1,41 @@ +package blue.bex.gas; + +/** Immutable host-neutral attribution for a named gas charge. */ +public final class BexGasChargeContext { + private static final BexGasChargeContext EMPTY = + new BexGasChargeContext(null, null, null, "unspecified"); + + private final String scopePath; + private final String contractKey; + private final String logicalPath; + private final String reason; + + private BexGasChargeContext( + String scopePath, + String contractKey, + String logicalPath, + String reason) { + this.scopePath = scopePath; + this.contractKey = contractKey; + this.logicalPath = logicalPath; + this.reason = reason != null ? reason : "unspecified"; + } + + public static BexGasChargeContext empty() { + return EMPTY; + } + + public static BexGasChargeContext of( + String scopePath, + String contractKey, + String logicalPath, + String reason) { + return new BexGasChargeContext( + scopePath, contractKey, logicalPath, reason); + } + + public String scopePath() { return scopePath; } + public String contractKey() { return contractKey; } + public String logicalPath() { return logicalPath; } + public String reason() { return reason; } +} diff --git a/src/main/java/blue/bex/gas/BexGasCounter.java b/blue-bex-core/src/main/java/blue/bex/gas/BexGasCounter.java similarity index 100% rename from src/main/java/blue/bex/gas/BexGasCounter.java rename to blue-bex-core/src/main/java/blue/bex/gas/BexGasCounter.java diff --git a/blue-bex-core/src/main/java/blue/bex/gas/BexGasCounterCatalog.java b/blue-bex-core/src/main/java/blue/bex/gas/BexGasCounterCatalog.java new file mode 100644 index 0000000..7916986 --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/gas/BexGasCounterCatalog.java @@ -0,0 +1,127 @@ +package blue.bex.gas; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; + +/** Immutable portable and intrinsic named-counter catalog. */ +final class BexGasCounterCatalog { + static final class Weight { + final String namespace; + final String counterName; + final BexGasCounter portableCounter; + final long weight; + + private Weight( + String namespace, + String counterName, + BexGasCounter portableCounter, + long weight) { + this.namespace = namespace; + this.counterName = counterName; + this.portableCounter = portableCounter; + this.weight = weight; + } + } + + private final BexGasSchedule schedule; + private final Map registeredWeights; + + BexGasCounterCatalog( + BexGasSchedule schedule, + Map registeredWeights) { + this.schedule = Objects.requireNonNull(schedule, "schedule"); + this.registeredWeights = immutableWeights(registeredWeights); + } + + BexGasSchedule schedule() { + return schedule; + } + + Map registeredWeights() { + return registeredWeights; + } + + Map combinedWeights() { + return combinedWeights(schedule, registeredWeights); + } + + Weight weight(String namespace, String counterName) { + String exactNamespace = requireName(namespace, "Gas namespace"); + String exactCounterName = requireName(counterName, "Gas counter"); + if (BexGasCounter.NAMESPACE.equals(exactNamespace)) { + BexGasCounter portable = + BexGasCounter.fromCanonicalName(exactCounterName); + return new Weight( + exactNamespace, + exactCounterName, + portable, + schedule.weight(portable)); + } + String qualified = qualifiedName( + exactNamespace, exactCounterName); + Long weight = registeredWeights.get(qualified); + if (weight == null) { + throw new IllegalArgumentException( + "Unregistered named gas counter: " + qualified); + } + return new Weight( + exactNamespace, + exactCounterName, + null, + weight); + } + + static String qualifiedName(String namespace, String counterName) { + String exactNamespace = requireName(namespace, "Gas namespace"); + String exactCounter = requireName(counterName, "Gas counter"); + return BexGasCounter.NAMESPACE.equals(exactNamespace) + ? exactCounter + : exactNamespace + "." + exactCounter; + } + + static Map combinedWeights( + BexGasSchedule schedule, + Map registeredWeights) { + LinkedHashMap combined = new LinkedHashMap<>( + Objects.requireNonNull(schedule, "schedule").counterWeights()); + Map registered = immutableWeights(registeredWeights); + for (Map.Entry entry : registered.entrySet()) { + if (combined.containsKey(entry.getKey())) { + throw new IllegalArgumentException( + "Registered gas counter collides with BEX manifest counter: " + + entry.getKey()); + } + combined.put(entry.getKey(), entry.getValue()); + } + return Collections.unmodifiableMap(combined); + } + + private static Map immutableWeights( + Map registeredWeights) { + if (registeredWeights == null || registeredWeights.isEmpty()) { + return Collections.emptyMap(); + } + LinkedHashMap copy = new LinkedHashMap<>(); + for (Map.Entry entry : registeredWeights.entrySet()) { + String counterName = requireName( + entry.getKey(), "Registered gas counter"); + Long weight = Objects.requireNonNull( + entry.getValue(), "Registered gas weight"); + if (weight <= 0L) { + throw new IllegalArgumentException( + "Registered gas weight must be positive"); + } + copy.put(counterName, weight); + } + return Collections.unmodifiableMap(copy); + } + + private static String requireName(String value, String label) { + if (value == null || value.trim().isEmpty()) { + throw new IllegalArgumentException(label + " is required"); + } + return value; + } +} diff --git a/blue-bex-core/src/main/java/blue/bex/gas/BexGasHostSession.java b/blue-bex-core/src/main/java/blue/bex/gas/BexGasHostSession.java new file mode 100644 index 0000000..e23a53b --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/gas/BexGasHostSession.java @@ -0,0 +1,235 @@ +package blue.bex.gas; + +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +/** Validated hosted-ledger set with exactly-once lifecycle finalization. */ +final class BexGasHostSession { + private enum State { + OPEN, + SUBMITTED, + FAILED, + UNAVAILABLE, + EXHAUSTED + } + + private final Map ledgers; + private final boolean qualifiedCounters; + private final boolean enforcesLocalLimit; + private State state = State.OPEN; + + BexGasHostSession( + Map ledgers, + boolean qualifiedCounters, + boolean enforcesLocalLimit, + long localLimit) { + this.ledgers = immutableLedgers(ledgers); + this.qualifiedCounters = qualifiedCounters; + if (enforcesLocalLimit + && (this.ledgers.isEmpty() + || localLimit == BexGasMeter.NO_LOCAL_LIMIT)) { + throw new IllegalArgumentException( + "Host-enforced local limits require hosted ledgers " + + "and a non-negative local limit"); + } + this.enforcesLocalLimit = enforcesLocalLimit; + } + + boolean isHosted() { + return !ledgers.isEmpty(); + } + + boolean enforcesLocalLimit() { + return enforcesLocalLimit; + } + + BexGasLedgerCapability ledgerFor(String logicalNamespace) { + if (ledgers.isEmpty()) { + return null; + } + String selected = qualifiedCounters + ? BexGasCounter.NAMESPACE + : logicalNamespace; + BexGasLedgerCapability ledger = ledgers.get(selected); + if (ledger == null) { + throw new IllegalStateException( + "No live host child ledger for logical namespace " + + logicalNamespace); + } + return ledger; + } + + String physicalCounterName( + String namespace, + String counterName) { + return qualifiedCounters + ? BexGasMeter.qualifiedCounterName(namespace, counterName) + : counterName; + } + + boolean submitted() { + return state == State.SUBMITTED; + } + + boolean finalized() { + return state != State.OPEN; + } + + void ensureOpenForCharge() { + if (state != State.OPEN) { + throw new IllegalStateException( + "Cannot charge a finalized BEX host child ledger"); + } + } + + void submit(Consumer callback) { + finalizeAll( + State.SUBMITTED, + Objects.requireNonNull(callback, "submitter")); + } + + void fail(Consumer callback) { + finalizeAll( + State.FAILED, + Objects.requireNonNull(callback, "failureHandler")); + } + + void unavailable(Consumer callback) { + finalizeAll( + State.UNAVAILABLE, + Objects.requireNonNull(callback, "unavailableHandler")); + } + + void propagateExhaustion( + BexHostGasExhaustion exhaustion, + Consumer prefixHandler, + BiConsumer exhaustionHandler) { + requireHostedOpen(); + state = State.EXHAUSTED; + BexHostGasExhaustion exactExhaustion = + Objects.requireNonNull(exhaustion, "exhaustion"); + Consumer exactPrefixHandler = + Objects.requireNonNull(prefixHandler, "prefixHandler"); + Throwable prefixFailure = null; + for (BexGasLedgerCapability ledger : ledgers.values()) { + try { + exactPrefixHandler.accept(ledger); + } catch (RuntimeException | Error failure) { + prefixFailure = retainFailure(prefixFailure, failure); + } + } + try { + Objects.requireNonNull( + exhaustionHandler, "exhaustionHandler").accept( + rejectionLedger(exactExhaustion), exactExhaustion); + } catch (RuntimeException | Error propagated) { + if (prefixFailure != null && prefixFailure != propagated) { + propagated.addSuppressed(prefixFailure); + } + throw propagated; + } + rethrowFailure(prefixFailure); + } + + private void finalizeAll( + State finalState, + Consumer callback) { + requireHostedOpen(); + state = finalState; + Throwable callbackFailure = null; + for (BexGasLedgerCapability ledger : ledgers.values()) { + try { + callback.accept(ledger); + } catch (RuntimeException | Error failure) { + callbackFailure = retainFailure(callbackFailure, failure); + } + } + rethrowFailure(callbackFailure); + } + + private void requireHostedOpen() { + if (ledgers.isEmpty()) { + throw new IllegalStateException( + "This BEX gas meter has no host child ledgers"); + } + if (state != State.OPEN) { + throw new IllegalStateException( + "BEX host child ledger was already finalized as " + + state); + } + } + + private BexGasLedgerCapability rejectionLedger( + BexHostGasExhaustion exhaustion) { + for (BexGasLedgerCapability ledger : ledgers.values()) { + if (ledger.namespace().equals(exhaustion.namespace())) { + return ledger; + } + } + return ledgers.get(BexGasCounter.NAMESPACE); + } + + private static Throwable retainFailure( + Throwable retained, + Throwable next) { + if (retained == null) { + return next; + } + if (retained != next) { + retained.addSuppressed(next); + } + return retained; + } + + private static void rethrowFailure(Throwable failure) { + if (failure instanceof RuntimeException) { + throw (RuntimeException) failure; + } + if (failure instanceof Error) { + throw (Error) failure; + } + } + + private static Map immutableLedgers( + Map ledgers) { + if (ledgers == null || ledgers.isEmpty()) { + return Collections.emptyMap(); + } + if (!ledgers.containsKey(BexGasCounter.NAMESPACE)) { + throw new IllegalArgumentException( + "Hosted BEX ledger map must contain logical namespace " + + BexGasCounter.NAMESPACE); + } + LinkedHashMap copy = + new LinkedHashMap<>(); + IdentityHashMap identities = + new IdentityHashMap<>(); + for (Map.Entry entry + : ledgers.entrySet()) { + String namespace = requireName( + entry.getKey(), "Logical gas namespace"); + BexGasLedgerCapability ledger = Objects.requireNonNull( + entry.getValue(), "host child ledger"); + if (identities.put(ledger, Boolean.TRUE) != null) { + throw new IllegalArgumentException( + "Each logical runtime namespace requires a distinct " + + "host child ledger"); + } + copy.put(namespace, ledger); + } + return Collections.unmodifiableMap(copy); + } + + private static String requireName(String value, String label) { + if (value == null || value.trim().isEmpty()) { + throw new IllegalArgumentException(label + " is required"); + } + return value; + } +} diff --git a/src/main/java/blue/bex/gas/BexGasLedger.java b/blue-bex-core/src/main/java/blue/bex/gas/BexGasLedger.java similarity index 80% rename from src/main/java/blue/bex/gas/BexGasLedger.java rename to blue-bex-core/src/main/java/blue/bex/gas/BexGasLedger.java index 0d1b3d4..d979124 100644 --- a/src/main/java/blue/bex/gas/BexGasLedger.java +++ b/blue-bex-core/src/main/java/blue/bex/gas/BexGasLedger.java @@ -23,14 +23,19 @@ public final class BexGasLedger { private final String manifestIdentity; public BexGasLedger(List trace) { - this(trace, + this(requireDefaultManifestTrace(trace), BexGasCounter.SCHEDULE_ID, BexGasCounter.MANIFEST_IDENTITY); } - BexGasLedger(List trace, - String scheduleId, - String manifestIdentity) { + /** + * Reconstructs a trace with an explicit schedule and manifest identity. + * Callers using non-default weights must use this constructor so evidence + * never falsely claims the default BEX manifest. + */ + public BexGasLedger(List trace, + String scheduleId, + String manifestIdentity) { Objects.requireNonNull(trace, "trace"); ArrayList copy = new ArrayList<>(trace.size()); EnumMap quantityTotals = @@ -122,6 +127,24 @@ public String manifestIdentity() { return manifestIdentity; } + private static List requireDefaultManifestTrace( + List trace) { + Objects.requireNonNull(trace, "trace"); + for (BexGasCharge charge : trace) { + Objects.requireNonNull(charge, "trace entry"); + BexGasCounter counter = charge.portableCounter(); + if (counter == null + || !BexGasCounter.NAMESPACE.equals(charge.namespace()) + || charge.weight() != counter.defaultWeight()) { + throw new IllegalArgumentException( + "Caller-supplied gas trace does not match the default " + + "BEX manifest; provide explicit schedule and " + + "manifest identities"); + } + } + return trace; + } + private static long addExact(long left, long right) { if (right > Long.MAX_VALUE - left) { throw new IllegalArgumentException("Gas total exceeds long range"); diff --git a/blue-bex-core/src/main/java/blue/bex/gas/BexGasLedgerCapability.java b/blue-bex-core/src/main/java/blue/bex/gas/BexGasLedgerCapability.java new file mode 100644 index 0000000..33a7f0a --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/gas/BexGasLedgerCapability.java @@ -0,0 +1,20 @@ +package blue.bex.gas; + +import java.util.Map; + +/** + * Narrow live-ledger capability consumed by pure BEX core. + */ +public interface BexGasLedgerCapability { + String namespace(); + long totalGas(); + long remainingGas(); + long effectiveBudget(); + Map counterWeights(); + + default void charge(String counter, long quantity) { + charge(counter, quantity, BexGasChargeContext.empty()); + } + + void charge(String counter, long quantity, BexGasChargeContext context); +} diff --git a/blue-bex-core/src/main/java/blue/bex/gas/BexGasLedgerLifecycle.java b/blue-bex-core/src/main/java/blue/bex/gas/BexGasLedgerLifecycle.java new file mode 100644 index 0000000..a3bba96 --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/gas/BexGasLedgerLifecycle.java @@ -0,0 +1,55 @@ +package blue.bex.gas; + +import java.util.Map; +import java.util.Objects; + +/** Host-neutral lifecycle for live BEX and intrinsic gas ledgers. */ +public interface BexGasLedgerLifecycle { + BexGasLedgerCapability open( + String namespace, + Map counterWeights); + + default BexSharedGasBudget openSharedBudget(long maximumGas) { + if (maximumGas < 0L) { + throw new IllegalArgumentException( + "Shared BEX gas budget must be non-negative"); + } + return null; + } + + default BexGasLedgerCapability open( + String namespace, + Map counterWeights, + BexSharedGasBudget sharedBudget) { + if (sharedBudget != null) { + throw new UnsupportedOperationException( + "This gas host cannot attach a shared runtime work budget"); + } + return open(namespace, counterWeights); + } + + void submit(BexGasLedgerCapability ledger); + + default boolean separatesRuntimeNamespaces() { + return true; + } + + void failedDeterministically(BexGasLedgerCapability ledger); + + void evidenceUnavailable(BexGasLedgerCapability ledger); + + default RuntimeException localGasLimitExceeded( + BexGasLimitExceededException exhaustion, + RuntimeException originalFailure) { + Objects.requireNonNull(exhaustion, "exhaustion"); + return Objects.requireNonNull(originalFailure, "originalFailure"); + } + + default void propagateGasExhaustion( + BexGasLedgerCapability ledger, + BexHostGasExhaustion exhaustion) { + Objects.requireNonNull(ledger, "ledger"); + throw Objects.requireNonNull( + exhaustion, "exhaustion").hostFailure(); + } +} diff --git a/src/main/java/blue/bex/gas/BexGasLimitExceededException.java b/blue-bex-core/src/main/java/blue/bex/gas/BexGasLimitExceededException.java similarity index 83% rename from src/main/java/blue/bex/gas/BexGasLimitExceededException.java rename to blue-bex-core/src/main/java/blue/bex/gas/BexGasLimitExceededException.java index 6afc0e8..cfcb574 100644 --- a/src/main/java/blue/bex/gas/BexGasLimitExceededException.java +++ b/blue-bex-core/src/main/java/blue/bex/gas/BexGasLimitExceededException.java @@ -1,7 +1,6 @@ package blue.bex.gas; import blue.bex.BexException; -import blue.language.processor.GasLimitExceededException; /** * Raised before work when the next named BEX charge cannot be admitted. @@ -16,7 +15,7 @@ public final class BexGasLimitExceededException extends BexException { private final long weight; private final long admittedGas; private final long effectiveBudget; - private final GasLimitExceededException hostGasLimitExceeded; + private final BexHostGasExhaustion hostGasExhaustion; BexGasLimitExceededException(BexGasCounter counter, long quantity, @@ -54,8 +53,7 @@ public final class BexGasLimitExceededException extends BexException { long weight, long admittedGas, long effectiveBudget, - GasLimitExceededException - hostGasLimitExceeded) { + BexHostGasExhaustion hostGasExhaustion) { this(BexGasCounter.NAMESPACE, counter, counter.canonicalName(), @@ -63,7 +61,7 @@ public final class BexGasLimitExceededException extends BexException { weight, admittedGas, effectiveBudget, - hostGasLimitExceeded); + hostGasExhaustion); } BexGasLimitExceededException(String namespace, @@ -72,8 +70,7 @@ public final class BexGasLimitExceededException extends BexException { long weight, long admittedGas, long effectiveBudget, - GasLimitExceededException - hostGasLimitExceeded) { + BexHostGasExhaustion hostGasExhaustion) { this(namespace, null, counterName, @@ -81,7 +78,7 @@ public final class BexGasLimitExceededException extends BexException { weight, admittedGas, effectiveBudget, - hostGasLimitExceeded); + hostGasExhaustion); } private BexGasLimitExceededException(String namespace, @@ -91,12 +88,12 @@ private BexGasLimitExceededException(String namespace, long weight, long admittedGas, long effectiveBudget, - GasLimitExceededException - hostGasLimitExceeded) { + BexHostGasExhaustion + hostGasExhaustion) { super("BEX gas exhausted before " + namespace + "." + counterName + " at " + admittedGas + " of " + effectiveBudget + " gas units", - hostGasLimitExceeded); + hostGasExhaustion); this.namespace = namespace; this.counter = counter; this.counterName = counterName; @@ -104,7 +101,7 @@ private BexGasLimitExceededException(String namespace, this.weight = weight; this.admittedGas = admittedGas; this.effectiveBudget = effectiveBudget; - this.hostGasLimitExceeded = hostGasLimitExceeded; + this.hostGasExhaustion = hostGasExhaustion; } public BexGasCounter counter() { @@ -140,7 +137,7 @@ public long effectiveBudget() { * {@code null} when the stricter BEX-local sub-limit rejected the charge * before the host ledger was touched. */ - public GasLimitExceededException hostGasLimitExceeded() { - return hostGasLimitExceeded; + public BexHostGasExhaustion hostGasExhaustion() { + return hostGasExhaustion; } } diff --git a/src/main/java/blue/bex/gas/BexGasManifest.java b/blue-bex-core/src/main/java/blue/bex/gas/BexGasManifest.java similarity index 100% rename from src/main/java/blue/bex/gas/BexGasManifest.java rename to blue-bex-core/src/main/java/blue/bex/gas/BexGasManifest.java diff --git a/blue-bex-core/src/main/java/blue/bex/gas/BexGasMeter.java b/blue-bex-core/src/main/java/blue/bex/gas/BexGasMeter.java new file mode 100644 index 0000000..13b8bfb --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/gas/BexGasMeter.java @@ -0,0 +1,500 @@ +package blue.bex.gas; + +import blue.bex.BexSourcePath; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +/** + * Deterministic, live-bounded BEX 2.0 named gas meter. + * + *

Every charge is checked and, when host-backed, admitted to the shared + * child ledger before the corresponding local trace entry is appended and + * before the caller performs its work. A charge which cannot fit is absent + * from both traces.

+ */ +public final class BexGasMeter { + /** Sentinel accepted by local-limit constructors for no local sub-limit. */ + public static final long NO_LOCAL_LIMIT = -1L; + + private final BexGasCounterCatalog catalog; + private final BexGasBudget budget; + private final BexGasHostSession hostSession; + private final BexGasTraceRecorder traceRecorder; + private final BexGasAdmission admission; + + /** + * Creates a standalone meter bounded by the supplied parent budget. + */ + public BexGasMeter(BexGasSchedule schedule, long parentRemainingGas) { + this(schedule, + requireBudget(parentRemainingGas, "parentRemainingGas"), + NO_LOCAL_LIMIT, + Collections.emptyMap(), + false, + false, + Collections.emptyMap()); + } + + /** + * Creates a standalone meter whose local BEX limit can only reduce the + * supplied parent budget. + */ + public BexGasMeter(BexGasSchedule schedule, + long parentRemainingGas, + long localLimit) { + this(schedule, + requireBudget(parentRemainingGas, "parentRemainingGas"), + requireLocalLimit(localLimit), + Collections.emptyMap(), + false, + false, + Collections.emptyMap()); + } + + /** + * Creates a standalone meter with registry-bound named child counters. + */ + public BexGasMeter(BexGasSchedule schedule, + long parentRemainingGas, + long localLimit, + Map registeredNamedWeights) { + this(schedule, + requireBudget(parentRemainingGas, "parentRemainingGas"), + requireLocalLimit(localLimit), + Collections.emptyMap(), + false, + false, + registeredNamedWeights); + } + + /** + * Creates a meter over a live parent-bounded host child ledger. + */ + public BexGasMeter(BexGasSchedule schedule, + BexGasLedgerCapability hostLedger) { + this(schedule, hostLedger, NO_LOCAL_LIMIT); + } + + /** + * Creates a meter over a live parent-bounded host child ledger. The local + * limit may only reduce the child ledger's initial remaining budget. + */ + public BexGasMeter(BexGasSchedule schedule, + BexGasLedgerCapability hostLedger, + long localLimit) { + this(schedule, + Objects.requireNonNull(hostLedger, "hostLedger").remainingGas(), + requireLocalLimit(localLimit), + singletonHostLedger(hostLedger), + true, + false, + Collections.emptyMap()); + } + + /** + * Creates a hosted meter with one physical child ledger per logical + * runtime namespace. The map must contain {@code bex}; registered + * intrinsic namespaces use their own unqualified counter catalogs. + */ + public BexGasMeter( + BexGasSchedule schedule, + Map hostLedgers, + long localLimit, + Map registeredNamedWeights) { + this(schedule, + parentBudget(hostLedgers), + requireLocalLimit(localLimit), + hostLedgers, + false, + false, + registeredNamedWeights); + } + + /** + * Creates a hosted meter whose configured local limit is enforced by one + * invocation-owned budget shared by every supplied host ledger. + * + *

The meter retains the configured limit for portable diagnostics but + * does not race the canonical host admission path with a duplicate local + * precheck. The host therefore records the exact rejected charge before + * any corresponding BEX or intrinsic work occurs.

+ * + * @param schedule exact BEX gas schedule + * @param hostLedgers one live physical ledger per logical namespace + * @param localLimit non-negative maximum enforced by the shared host + * budget + * @param registeredNamedWeights exact intrinsic counter registry + * @return live BEX meter using canonical host-side local admission + */ + public static BexGasMeter hostedWithSharedLocalLimit( + BexGasSchedule schedule, + Map hostLedgers, + long localLimit, + Map registeredNamedWeights) { + return new BexGasMeter( + schedule, + parentBudget(hostLedgers), + requireLocalLimit(localLimit), + hostLedgers, + false, + true, + registeredNamedWeights); + } + + private BexGasMeter(BexGasSchedule schedule, + long parentRemainingGas, + long localLimit, + Map hostLedgers, + boolean qualifiedHostCounters, + boolean hostEnforcesLocalLimit, + Map registeredNamedWeights) { + BexGasSchedule exactSchedule = + Objects.requireNonNull(schedule, "schedule"); + this.catalog = new BexGasCounterCatalog( + exactSchedule, registeredNamedWeights); + this.budget = new BexGasBudget(parentRemainingGas, localLimit); + this.hostSession = new BexGasHostSession( + hostLedgers, + qualifiedHostCounters, + hostEnforcesLocalLimit, + localLimit); + this.traceRecorder = new BexGasTraceRecorder(exactSchedule); + this.admission = new BexGasAdmission( + budget, hostSession, traceRecorder); + } + + public BexGasSchedule schedule() { + return catalog.schedule(); + } + + /** + * Produces the deterministic host-ledger key for a counter. Portable BEX + * counters remain unqualified; registry child counters are qualified by + * their exact namespace. + */ + public static String qualifiedCounterName(String namespace, + String counterName) { + return BexGasCounterCatalog.qualifiedName( + namespace, counterName); + } + + /** + * Builds a deterministic combined catalog for standalone inspection. + * Hosted intrinsic execution uses one physical ledger per namespace and + * never passes this combined map to a host. Registered map keys must + * already be produced by + * {@link #qualifiedCounterName(String, String)}. + */ + public static Map childLedgerWeights( + BexGasSchedule schedule, + Map registeredNamedWeights) { + return BexGasCounterCatalog.combinedWeights( + schedule, registeredNamedWeights); + } + + public Map registeredNamedWeights() { + return catalog.registeredWeights(); + } + + public Map childLedgerWeights() { + return catalog.combinedWeights(); + } + + /** + * Returns the exact parent budget observed when this child meter began. + */ + public long parentRemainingGas() { + return budget.parentRemainingGas(); + } + + /** + * Returns the configured local sub-limit, or {@link #NO_LOCAL_LIMIT}. + */ + public long localLimit() { + return budget.localLimit(); + } + + public long effectiveBudget() { + return budget.effectiveBudget(); + } + + public long totalGas() { + return traceRecorder.totalGas(); + } + + /** Compatibility alias for {@link #totalGas()}. */ + public long used() { + return totalGas(); + } + + public long remainingGas() { + return budget.remaining(totalGas()); + } + + /** Alias for {@link #remainingGas()}. */ + public long remaining() { + return remainingGas(); + } + + /** + * Returns an immutable snapshot of all successfully admitted charges. + */ + public List trace() { + return traceRecorder.snapshot(); + } + + /** + * Returns an immutable snapshot of the current admitted ledger. + */ + public BexGasLedger ledger() { + return traceRecorder.ledger(); + } + + public boolean hasHostLedger() { + return hostSession.isHosted(); + } + + public boolean hostLedgerSubmitted() { + return hostSession.submitted(); + } + + public boolean hostLedgerFinalized() { + return hostSession.finalized(); + } + + public void charge(BexGasCounter counter) { + charge(counter, 1L); + } + + public void charge(BexGasCounter counter, long quantity) { + BexGasCounter exactCounter = + Objects.requireNonNull(counter, "counter"); + charge(exactCounter, + quantity, + (String) null, + null, + exactCounter.canonicalName()); + } + + public void charge(BexGasCounter counter, + long quantity, + String reason) { + charge(counter, quantity, (String) null, null, reason); + } + + public void charge(BexGasCounter counter, + String sourcePath, + String operator, + String reason) { + charge(counter, 1L, sourcePath, operator, reason); + } + + public void charge(BexGasCounter counter, + BexSourcePath sourcePath, + String operator, + String reason) { + charge(counter, + 1L, + sourcePath != null ? sourcePath.toString() : null, + operator, + reason); + } + + public void charge(BexGasCounter counter, + long quantity, + BexSourcePath sourcePath, + String operator, + String reason) { + charge(counter, + quantity, + sourcePath != null ? sourcePath.toString() : null, + operator, + reason); + } + + public void charge(BexGasCounter counter, + long quantity, + String sourcePath, + String operator, + String reason) { + BexGasCounter exactCounter = + Objects.requireNonNull(counter, "counter"); + long weight = catalog.schedule().weight(exactCounter); + admission.admit( + BexGasCounter.NAMESPACE, + exactCounter.canonicalName(), + exactCounter, + quantity, + weight, + sourcePath, + operator, + reason); + } + + public void chargeNamed(String namespace, + String counterName, + long quantity) { + chargeNamed(namespace, + counterName, + quantity, + (String) null, + null, + qualifiedCounterName(namespace, counterName)); + } + + public void chargeNamed(String namespace, + String counterName, + long quantity, + String reason) { + chargeNamed(namespace, + counterName, + quantity, + (String) null, + null, + reason); + } + + public void chargeNamed(String namespace, + String counterName, + long quantity, + String sourcePath, + String operator, + String reason) { + BexGasCounterCatalog.Weight named = + catalog.weight(namespace, counterName); + admission.admit( + named.namespace, + named.counterName, + named.portableCounter, + quantity, + named.weight, + sourcePath, + operator, + reason); + } + + public void chargeNamed(String namespace, + String counterName, + long quantity, + long declaredWeight, + String sourcePath, + String operator, + String reason) { + BexGasCounterCatalog.Weight named = + catalog.weight(namespace, counterName); + if (declaredWeight != named.weight) { + throw new IllegalArgumentException( + "Registered gas weight mismatch for " + + qualifiedCounterName(namespace, counterName) + + ": expected " + named.weight + + " but was " + declaredWeight); + } + admission.admit( + named.namespace, + named.counterName, + named.portableCounter, + quantity, + named.weight, + sourcePath, + operator, + reason); + } + + public void chargeNamed(String namespace, + String counterName, + long quantity, + BexSourcePath sourcePath, + String operator, + String reason) { + chargeNamed(namespace, + counterName, + quantity, + sourcePath != null ? sourcePath.toString() : null, + operator, + reason); + } + + /** + * Submits a successfully completed wrapped host child ledger exactly once. + * The final state is set before invoking the callback, so a + * throwing callback cannot cause an accidental second merge attempt. + */ + public void submitHostLedger( + Consumer submitter) { + hostSession.submit(submitter); + } + + /** + * Finalizes the BEX side of a deterministic-failure callback. The host + * decides whether its contract merges immediately or leaves the ledger + * staged for enclosing-session finalization. + */ + public void failHostLedger( + Consumer failureHandler) { + hostSession.fail(failureHandler); + } + + /** + * Finalizes the BEX side of a transient-unavailability callback. + */ + public void unavailableHostLedger( + Consumer unavailableHandler) { + hostSession.unavailable(unavailableHandler); + } + + /** + * Hands the exact recorded host rejection back to its owner. + */ + public void propagateHostGasExhaustion( + BexHostGasExhaustion exhaustion, + Consumer prefixHandler, + BiConsumer exhaustionHandler) { + hostSession.propagateExhaustion( + exhaustion, prefixHandler, exhaustionHandler); + } + + private static long requireBudget(long value, String name) { + if (value < 0L) { + throw new IllegalArgumentException(name + " must be non-negative"); + } + return value; + } + + private static long requireLocalLimit(long value) { + if (value < NO_LOCAL_LIMIT) { + throw new IllegalArgumentException( + "localLimit must be non-negative or NO_LOCAL_LIMIT"); + } + return value; + } + + private static Map + singletonHostLedger(BexGasLedgerCapability hostLedger) { + LinkedHashMap singleton = + new LinkedHashMap<>(); + singleton.put( + BexGasCounter.NAMESPACE, + Objects.requireNonNull(hostLedger, "hostLedger")); + return singleton; + } + + private static long parentBudget( + Map hostLedgers) { + Objects.requireNonNull(hostLedgers, "hostLedgers"); + BexGasLedgerCapability bexLedger = + hostLedgers.get(BexGasCounter.NAMESPACE); + if (bexLedger == null) { + throw new IllegalArgumentException( + "Hosted BEX ledger map must contain logical namespace " + + BexGasCounter.NAMESPACE); + } + return bexLedger.remainingGas(); + } +} diff --git a/src/main/java/blue/bex/gas/BexGasSchedule.java b/blue-bex-core/src/main/java/blue/bex/gas/BexGasSchedule.java similarity index 100% rename from src/main/java/blue/bex/gas/BexGasSchedule.java rename to blue-bex-core/src/main/java/blue/bex/gas/BexGasSchedule.java diff --git a/blue-bex-core/src/main/java/blue/bex/gas/BexGasTraceRecorder.java b/blue-bex-core/src/main/java/blue/bex/gas/BexGasTraceRecorder.java new file mode 100644 index 0000000..58d577c --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/gas/BexGasTraceRecorder.java @@ -0,0 +1,53 @@ +package blue.bex.gas; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** Mutable per-run trace recorder with immutable snapshot views. */ +final class BexGasTraceRecorder { + private final BexGasSchedule schedule; + private final List trace = new ArrayList<>(); + private long totalGas; + + BexGasTraceRecorder(BexGasSchedule schedule) { + this.schedule = schedule; + } + + long totalGas() { + return totalGas; + } + + List snapshot() { + return Collections.unmodifiableList(new ArrayList<>(trace)); + } + + BexGasLedger ledger() { + return new BexGasLedger( + trace, + schedule.scheduleId(), + schedule.manifestIdentity()); + } + + void append( + String namespace, + String counterName, + long quantity, + long weight, + long gas, + String sourcePath, + String operator, + String reason) { + trace.add(new BexGasCharge( + trace.size(), + namespace, + counterName, + quantity, + weight, + gas, + sourcePath, + operator, + reason)); + totalGas += gas; + } +} diff --git a/blue-bex-core/src/main/java/blue/bex/gas/BexHostGasExhaustion.java b/blue-bex-core/src/main/java/blue/bex/gas/BexHostGasExhaustion.java new file mode 100644 index 0000000..d064d42 --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/gas/BexHostGasExhaustion.java @@ -0,0 +1,46 @@ +package blue.bex.gas; + +import java.util.Objects; + +/** + * Host-neutral rejected-charge evidence retaining the exact native host + * exception as an opaque capability. + */ +public final class BexHostGasExhaustion extends RuntimeException { + private static final long serialVersionUID = 1L; + + private final String namespace; + private final String counterName; + private final long quantity; + private final long weight; + private final long admittedGas; + private final long effectiveBudget; + private final RuntimeException hostFailure; + + public BexHostGasExhaustion( + String namespace, + String counterName, + long quantity, + long weight, + long admittedGas, + long effectiveBudget, + RuntimeException hostFailure) { + super("Host gas exhausted before " + namespace + "." + counterName, + Objects.requireNonNull(hostFailure, "hostFailure")); + this.namespace = Objects.requireNonNull(namespace, "namespace"); + this.counterName = Objects.requireNonNull(counterName, "counterName"); + this.quantity = quantity; + this.weight = weight; + this.admittedGas = admittedGas; + this.effectiveBudget = effectiveBudget; + this.hostFailure = hostFailure; + } + + public String namespace() { return namespace; } + public String counterName() { return counterName; } + public long quantity() { return quantity; } + public long weight() { return weight; } + public long admittedGas() { return admittedGas; } + public long effectiveBudget() { return effectiveBudget; } + public RuntimeException hostFailure() { return hostFailure; } +} diff --git a/blue-bex-core/src/main/java/blue/bex/gas/BexSharedGasBudget.java b/blue-bex-core/src/main/java/blue/bex/gas/BexSharedGasBudget.java new file mode 100644 index 0000000..16d1d9c --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/gas/BexSharedGasBudget.java @@ -0,0 +1,8 @@ +package blue.bex.gas; + +/** Invocation-owned local gas budget shared by hosted BEX ledgers. */ +public interface BexSharedGasBudget { + long maximumGas(); + long admittedGas(); + long remainingGas(); +} diff --git a/blue-bex-core/src/main/java/blue/bex/gas/package-info.java b/blue-bex-core/src/main/java/blue/bex/gas/package-info.java new file mode 100644 index 0000000..f25048f --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/gas/package-info.java @@ -0,0 +1,13 @@ +/** + * Closed BEX 2.0 gas vocabulary, immutable schedule/trace evidence, run-local + * meters, and host-neutral ledger/shared-budget capabilities. + * + *

Schedules, counters, charges, and completed ledgers are immutable and may + * be shared; meters and open host capabilities belong to one execution and are + * not generally thread-safe. Counter names, weights, charge contexts, and host + * capabilities are non-null, and quantities/limits must satisfy their documented + * ranges. Exhaustion fails before work: the rejected charge and all later work + * are absent. Local limits may reduce but never replenish a parent/shared + * budget, and totals are always derived from admitted named entries.

+ */ +package blue.bex.gas; diff --git a/src/main/java/blue/bex/output/BexAdmittedValue.java b/blue-bex-core/src/main/java/blue/bex/output/BexAdmittedValue.java similarity index 100% rename from src/main/java/blue/bex/output/BexAdmittedValue.java rename to blue-bex-core/src/main/java/blue/bex/output/BexAdmittedValue.java diff --git a/src/main/java/blue/bex/output/BexEstablishedIdentity.java b/blue-bex-core/src/main/java/blue/bex/output/BexEstablishedIdentity.java similarity index 100% rename from src/main/java/blue/bex/output/BexEstablishedIdentity.java rename to blue-bex-core/src/main/java/blue/bex/output/BexEstablishedIdentity.java diff --git a/blue-bex-core/src/main/java/blue/bex/output/BexFailurePolicy.java b/blue-bex-core/src/main/java/blue/bex/output/BexFailurePolicy.java new file mode 100644 index 0000000..65b0c8d --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/output/BexFailurePolicy.java @@ -0,0 +1,47 @@ +package blue.bex.output; + +import blue.bex.BexException; +import blue.bex.BexExecutionEvidenceUnavailableException; + +import java.util.Objects; + +/** Host-neutral failure policy consumed by the pure runtime and output code. */ +public interface BexFailurePolicy { + /** Pure-runtime policy with no host-specific exception dependency. */ + BexFailurePolicy STANDALONE = failure -> { + Throwable current = failure; + while (current != null) { + if (current instanceof BexExecutionEvidenceUnavailableException) { + return true; + } + Throwable cause = current.getCause(); + if (cause == current) { + break; + } + current = cause; + } + return false; + }; + + /** Whether this failure represents transient evidence unavailability. */ + boolean evidenceUnavailable(Throwable failure); + + /** Translates a failure at the outer hosting boundary. */ + default RuntimeException translate(RuntimeException failure) { + return Objects.requireNonNull(failure, "failure"); + } + + /** Preserves classified host failures and wraps implementation failures. */ + default RuntimeException preserveOrWrap( + String operation, + RuntimeException failure) { + RuntimeException exact = Objects.requireNonNull(failure, "failure"); + if (evidenceUnavailable(exact) || exact instanceof BexException) { + return exact; + } + return new BexException( + Objects.requireNonNull(operation, "operation") + + ": " + exact.getMessage(), + exact); + } +} diff --git a/src/main/java/blue/bex/output/BexOutputAdmission.java b/blue-bex-core/src/main/java/blue/bex/output/BexOutputAdmission.java similarity index 86% rename from src/main/java/blue/bex/output/BexOutputAdmission.java rename to blue-bex-core/src/main/java/blue/bex/output/BexOutputAdmission.java index a2c6f84..ec0bbe2 100644 --- a/src/main/java/blue/bex/output/BexOutputAdmission.java +++ b/blue-bex-core/src/main/java/blue/bex/output/BexOutputAdmission.java @@ -7,11 +7,6 @@ import blue.bex.value.BexValue; import blue.bex.value.BexValues; import blue.language.model.Node; -import blue.language.processor.ExecutionEvidenceUnavailableException; -import blue.language.processor.GasLimitExceededException; -import blue.language.processor.InvalidExecutionEvidenceException; -import blue.language.processor.PortableLimitExceededException; -import blue.language.processor.ProcessorFailureException; import blue.language.snapshot.FrozenNode; import blue.language.identity.BlueIds; @@ -25,16 +20,27 @@ public final class BexOutputAdmission { private final BexGasMeter gas; private final BexSemanticIdentityBoundary semanticIdentity; + private final BexFailurePolicy failureBoundary; private final Map admittedTransientValues = new IdentityHashMap<>(); private long semanticIdentityMergeCount; public BexOutputAdmission(BexGasMeter gas, BexSemanticIdentityBoundary semanticIdentity) { + this(gas, semanticIdentity, BexFailurePolicy.STANDALONE); + } + + public BexOutputAdmission( + BexGasMeter gas, + BexSemanticIdentityBoundary semanticIdentity, + BexFailurePolicy failureBoundary) { this.gas = Objects.requireNonNull(gas, "gas"); this.semanticIdentity = semanticIdentity != null ? semanticIdentity : BexSemanticIdentityBoundary.STANDALONE; + this.failureBoundary = failureBoundary != null + ? failureBoundary + : BexFailurePolicy.STANDALONE; } public BexAdmittedValue admit(BexValue value, BexOutputKind kind) { @@ -73,15 +79,10 @@ public BexAdmittedValue admit(BexValue value, BexOutputKind kind) { "semantic identity result"); } catch (BexException ex) { throw ex; - } catch (ExecutionEvidenceUnavailableException - | InvalidExecutionEvidenceException - | PortableLimitExceededException - | ProcessorFailureException - | GasLimitExceededException ex) { - throw ex; } catch (RuntimeException ex) { - throw new BexException("Blue output identity establishment failed: " - + ex.getMessage(), ex); + throw failureBoundary.preserveOrWrap( + "Blue output identity establishment failed", + ex); } String blueId = BlueIds.requireBlueIdOrCyclicMember( established.blueId(), diff --git a/src/main/java/blue/bex/output/BexOutputKind.java b/blue-bex-core/src/main/java/blue/bex/output/BexOutputKind.java similarity index 100% rename from src/main/java/blue/bex/output/BexOutputKind.java rename to blue-bex-core/src/main/java/blue/bex/output/BexOutputKind.java diff --git a/src/main/java/blue/bex/output/BexSemanticIdentityBoundary.java b/blue-bex-core/src/main/java/blue/bex/output/BexSemanticIdentityBoundary.java similarity index 100% rename from src/main/java/blue/bex/output/BexSemanticIdentityBoundary.java rename to blue-bex-core/src/main/java/blue/bex/output/BexSemanticIdentityBoundary.java diff --git a/blue-bex-core/src/main/java/blue/bex/output/package-info.java b/blue-bex-core/src/main/java/blue/bex/output/package-info.java new file mode 100644 index 0000000..b9edfc2 --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/output/package-info.java @@ -0,0 +1,13 @@ +/** + * Strict conversion and ordinary Blue identity establishment for values leaving + * the BEX runtime. + * + *

Admission objects belong to one execution; successfully admitted metadata + * is immutable and returns defensive mutable-Node copies where needed. Values, + * kinds, and host boundaries are non-null unless explicitly defaulted. Invalid + * runtime Blue content, missing identity evidence, or boundary failure fails the + * run atomically. The output charge is admitted before validation; exact values + * pass by identity, while transient values pay only for actual conversion and + * direct identity work.

+ */ +package blue.bex.output; diff --git a/blue-bex-core/src/main/java/blue/bex/package-info.java b/blue-bex-core/src/main/java/blue/bex/package-info.java new file mode 100644 index 0000000..6be9268 --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/package-info.java @@ -0,0 +1,12 @@ +/** + * Root BEX failures and source-location value objects. + * + *

Source paths are immutable and may be shared. Failure instances belong to + * one failed compile or execution and should not be reused as control state. + * Public inputs are non-null unless their member documentation explicitly says + * otherwise. BEX failures stop the current operation and preserve their cause; + * host adapters may translate only the classifications they own. Constructing + * diagnostics does not consume portable gas, but the admitted trace prefix of + * the failed work remains authoritative.

+ */ +package blue.bex; diff --git a/src/main/java/blue/bex/pointer/BexPointer.java b/blue-bex-core/src/main/java/blue/bex/pointer/BexPointer.java similarity index 100% rename from src/main/java/blue/bex/pointer/BexPointer.java rename to blue-bex-core/src/main/java/blue/bex/pointer/BexPointer.java diff --git a/src/main/java/blue/bex/pointer/BexPointerCache.java b/blue-bex-core/src/main/java/blue/bex/pointer/BexPointerCache.java similarity index 95% rename from src/main/java/blue/bex/pointer/BexPointerCache.java rename to blue-bex-core/src/main/java/blue/bex/pointer/BexPointerCache.java index 53c0af8..7670b2a 100644 --- a/src/main/java/blue/bex/pointer/BexPointerCache.java +++ b/blue-bex-core/src/main/java/blue/bex/pointer/BexPointerCache.java @@ -1,6 +1,6 @@ package blue.bex.pointer; -import blue.bex.result.BexMetrics; +import blue.bex.result.BexMetricsRecorder; import java.util.LinkedHashMap; import java.util.Map; @@ -26,7 +26,7 @@ protected boolean removeEldestEntry(Map.Entry eldest) { }; } - public synchronized BexPointer get(String pointer, BexMetrics metrics) { + public synchronized BexPointer get(String pointer, BexMetricsRecorder metrics) { BexPointer cached = pointers.get(pointer); if (cached != null) { if (metrics != null) { diff --git a/blue-bex-core/src/main/java/blue/bex/pointer/package-info.java b/blue-bex-core/src/main/java/blue/bex/pointer/package-info.java new file mode 100644 index 0000000..6535207 --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/pointer/package-info.java @@ -0,0 +1,12 @@ +/** + * Canonical JSON Pointer parsing and bounded pointer caching for BEX reads and + * updates. + * + *

Parsed pointers are immutable and shareable. The provided LRU cache + * serializes mutation and may be shared by an engine; run-local metrics are not + * shared. Pointer text is non-null where parsing is requested, and malformed or + * invalid dynamic pointers fail deterministically. Cache hits never change + * portable gas: runtime pointer-segment charges reflect semantic work, not cache + * state.

+ */ +package blue.bex.pointer; diff --git a/src/main/java/blue/bex/result/BexChangeset.java b/blue-bex-core/src/main/java/blue/bex/result/BexChangeset.java similarity index 91% rename from src/main/java/blue/bex/result/BexChangeset.java rename to blue-bex-core/src/main/java/blue/bex/result/BexChangeset.java index 4bf9c37..bb56e74 100644 --- a/src/main/java/blue/bex/result/BexChangeset.java +++ b/blue-bex-core/src/main/java/blue/bex/result/BexChangeset.java @@ -2,6 +2,7 @@ import blue.bex.value.BexValue; import blue.bex.value.BexValues; +import blue.bex.value.BexChangesetValueView; import java.util.ArrayList; import java.util.Collections; @@ -12,7 +13,7 @@ /** * Ordered BEX changeset. Entries are never coalesced or reordered. */ -public final class BexChangeset { +public final class BexChangeset implements BexChangesetValueView { private final List entries; public BexChangeset(List entries) { diff --git a/src/main/java/blue/bex/result/BexEvents.java b/blue-bex-core/src/main/java/blue/bex/result/BexEvents.java similarity index 92% rename from src/main/java/blue/bex/result/BexEvents.java rename to blue-bex-core/src/main/java/blue/bex/result/BexEvents.java index fd17f00..dc79491 100644 --- a/src/main/java/blue/bex/result/BexEvents.java +++ b/blue-bex-core/src/main/java/blue/bex/result/BexEvents.java @@ -3,6 +3,7 @@ import blue.bex.output.BexAdmittedValue; import blue.bex.value.BexValue; import blue.bex.value.BexValues; +import blue.bex.value.BexEventsValueView; import java.util.ArrayList; import java.util.Collections; @@ -11,7 +12,7 @@ /** * Ordered events computed by BEX. Emission is host-runtime behavior. */ -public final class BexEvents { +public final class BexEvents implements BexEventsValueView { private final List events; private final List admittedEvents; diff --git a/src/main/java/blue/bex/result/BexExecutionResult.java b/blue-bex-core/src/main/java/blue/bex/result/BexExecutionResult.java similarity index 84% rename from src/main/java/blue/bex/result/BexExecutionResult.java rename to blue-bex-core/src/main/java/blue/bex/result/BexExecutionResult.java index d943604..73dfa53 100644 --- a/src/main/java/blue/bex/result/BexExecutionResult.java +++ b/blue-bex-core/src/main/java/blue/bex/result/BexExecutionResult.java @@ -22,14 +22,14 @@ public final class BexExecutionResult { private final BexChangeset changeset; private final BexEvents events; private final BexGasLedger gasLedger; - private final BexMetrics metrics; + private final BexMetricsSnapshot metrics; private final BexAdmittedValue output; public BexExecutionResult(BexValue value, BexChangeset changeset, BexEvents events, List gasTrace, - BexMetrics metrics) { + BexMetricsSnapshot metrics) { this(value, changeset, events, @@ -42,7 +42,7 @@ public BexExecutionResult(BexValue value, BexChangeset changeset, BexEvents events, BexGasLedger gasLedger, - BexMetrics metrics) { + BexMetricsSnapshot metrics) { this(value, changeset, events, gasLedger, metrics, null); } @@ -50,13 +50,15 @@ public BexExecutionResult(BexValue value, BexChangeset changeset, BexEvents events, BexGasLedger gasLedger, - BexMetrics metrics, + BexMetricsSnapshot metrics, BexAdmittedValue output) { this.value = value; this.changeset = changeset; this.events = events; this.gasLedger = Objects.requireNonNull(gasLedger, "gasLedger"); - this.metrics = metrics != null ? metrics.copy() : new BexMetrics(); + this.metrics = metrics != null + ? metrics + : new BexMetricsRecorder().snapshot(); this.output = output; } @@ -106,7 +108,12 @@ public BexAdmittedValue output() { } public BexMetrics metrics() { - return metrics.copy(); + return BexMetrics.fromSnapshot(metrics); + } + + /** Returns the immutable metrics retained by this result. */ + public BexMetricsSnapshot metricsSnapshot() { + return metrics; } } diff --git a/blue-bex-core/src/main/java/blue/bex/result/BexMetrics.java b/blue-bex-core/src/main/java/blue/bex/result/BexMetrics.java new file mode 100644 index 0000000..c5a6374 --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/result/BexMetrics.java @@ -0,0 +1,60 @@ +package blue.bex.result; + +/** + * Immutable compatibility view of invocation metrics. + * + * @deprecated use {@link BexMetricsSnapshot}; mutable recording is an internal + * implementation concern. + */ +@Deprecated +public final class BexMetrics { + private final BexMetricsSnapshot snapshot; + + public BexMetrics() { + this(new BexMetricsRecorder().snapshot()); + } + + BexMetrics(BexMetricsSnapshot snapshot) { + this.snapshot = snapshot; + } + + public BexMetrics copy() { return new BexMetrics(snapshot); } + public BexMetricsSnapshot snapshot() { return snapshot; } + + /** Creates an independent compatibility recorder from a snapshot. */ + public static BexMetrics fromSnapshot(BexMetricsSnapshot snapshot) { + return new BexMetrics(snapshot); + } + + public long compiledExecutions() { return snapshot.compiledExecutions(); } + public long compileCacheHits() { return snapshot.compileCacheHits(); } + public long compileCacheMisses() { return snapshot.compileCacheMisses(); } + public long interpretedFallbacks() { return snapshot.interpretedFallbacks(); } + public long expressionEvaluations() { return snapshot.expressionEvaluations(); } + public long statementExecutions() { return snapshot.statementExecutions(); } + public long functionCalls() { return snapshot.functionCalls(); } + public long loopIterations() { return snapshot.loopIterations(); } + public long frozenDocumentReads() { return snapshot.frozenDocumentReads(); } + public long resolvedDocumentReads() { return snapshot.resolvedDocumentReads(); } + public long eventReads() { return snapshot.eventReads(); } + public long stepsReads() { return snapshot.stepsReads(); } + public long currentContractReads() { return snapshot.currentContractReads(); } + public long nodeMaterializations() { return snapshot.nodeMaterializations(); } + public long simpleMaterializations() { return snapshot.simpleMaterializations(); } + public long frozenOutputConversions() { return snapshot.frozenOutputConversions(); } + public long nodeOutputConversions() { return snapshot.nodeOutputConversions(); } + public long containsBexScans() { return snapshot.containsBexScans(); } + public long containsBexCacheHits() { return snapshot.containsBexCacheHits(); } + public long containsBexCacheMisses() { return snapshot.containsBexCacheMisses(); } + public long resultValueReads() { return snapshot.resultValueReads(); } + public long resultOverlayExactHits() { return snapshot.resultOverlayExactHits(); } + public long resultOverlayAncestorHits() { return snapshot.resultOverlayAncestorHits(); } + public long resultOverlayDocumentFallbacks() { return snapshot.resultOverlayDocumentFallbacks(); } + public long pointerParses() { return snapshot.pointerParses(); } + public long pointerCacheHits() { return snapshot.pointerCacheHits(); } + public long pointerCacheMisses() { return snapshot.pointerCacheMisses(); } + public long functionArgMapAllocations() { return snapshot.functionArgMapAllocations(); } + public long frozenWriterNodeFallbacks() { return snapshot.frozenWriterNodeFallbacks(); } + public long compileNanos() { return snapshot.compileNanos(); } + public long executeNanos() { return snapshot.executeNanos(); } +} diff --git a/blue-bex-core/src/main/java/blue/bex/result/BexMetricsRecorder.java b/blue-bex-core/src/main/java/blue/bex/result/BexMetricsRecorder.java new file mode 100644 index 0000000..67f4e13 --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/result/BexMetricsRecorder.java @@ -0,0 +1,138 @@ +package blue.bex.result; + +import blue.bex.value.BexValueMetrics; + +/** + * Invocation-owned mutable metrics state used only by BEX implementation + * code. Public API consumers receive {@link BexMetricsSnapshot} instances. + * + *

This type is public solely because the implementation spans cohesive Java + * packages; it is classified as internal implementation and is not a supported + * extension point.

+ */ +public final class BexMetricsRecorder implements BexValueMetrics { + static final int COMPILED_EXECUTIONS = 0; + static final int COMPILE_CACHE_HITS = 1; + static final int COMPILE_CACHE_MISSES = 2; + static final int INTERPRETED_FALLBACKS = 3; + static final int EXPRESSION_EVALUATIONS = 4; + static final int STATEMENT_EXECUTIONS = 5; + static final int FUNCTION_CALLS = 6; + static final int LOOP_ITERATIONS = 7; + static final int FROZEN_DOCUMENT_READS = 8; + static final int RESOLVED_DOCUMENT_READS = 9; + static final int EVENT_READS = 10; + static final int STEPS_READS = 11; + static final int CURRENT_CONTRACT_READS = 12; + static final int NODE_MATERIALIZATIONS = 13; + static final int SIMPLE_MATERIALIZATIONS = 14; + static final int FROZEN_OUTPUT_CONVERSIONS = 15; + static final int NODE_OUTPUT_CONVERSIONS = 16; + static final int CONTAINS_BEX_SCANS = 17; + static final int CONTAINS_BEX_CACHE_HITS = 18; + static final int CONTAINS_BEX_CACHE_MISSES = 19; + static final int RESULT_VALUE_READS = 20; + static final int RESULT_OVERLAY_EXACT_HITS = 21; + static final int RESULT_OVERLAY_ANCESTOR_HITS = 22; + static final int RESULT_OVERLAY_DOCUMENT_FALLBACKS = 23; + static final int POINTER_PARSES = 24; + static final int POINTER_CACHE_HITS = 25; + static final int POINTER_CACHE_MISSES = 26; + static final int FUNCTION_ARG_MAP_ALLOCATIONS = 27; + static final int FROZEN_WRITER_NODE_FALLBACKS = 28; + static final int COMPILE_NANOS = 29; + static final int EXECUTE_NANOS = 30; + private static final int SIZE = 31; + + private final long[] values; + + public BexMetricsRecorder() { + this.values = new long[SIZE]; + } + + BexMetricsRecorder(BexMetricsSnapshot snapshot) { + this.values = snapshot.copyValues(); + } + + void increment(int counter) { + values[counter]++; + } + + void addNonNegative(int counter, long amount) { + values[counter] += Math.max(0L, amount); + } + + long value(int counter) { + return values[counter]; + } + + public BexMetricsSnapshot snapshot() { + return new BexMetricsSnapshot(values); + } + + public void incrementCompiledExecutions() { increment(COMPILED_EXECUTIONS); } + public void incrementCompileCacheHits() { increment(COMPILE_CACHE_HITS); } + public void incrementCompileCacheMisses() { increment(COMPILE_CACHE_MISSES); } + public void incrementInterpretedFallbacks() { increment(INTERPRETED_FALLBACKS); } + public void incrementExpressionEvaluations() { increment(EXPRESSION_EVALUATIONS); } + public void incrementStatementExecutions() { increment(STATEMENT_EXECUTIONS); } + public void incrementFunctionCalls() { increment(FUNCTION_CALLS); } + public void incrementLoopIterations() { increment(LOOP_ITERATIONS); } + public void incrementFrozenDocumentReads() { increment(FROZEN_DOCUMENT_READS); } + public void incrementResolvedDocumentReads() { increment(RESOLVED_DOCUMENT_READS); } + public void incrementEventReads() { increment(EVENT_READS); } + public void incrementStepsReads() { increment(STEPS_READS); } + public void incrementCurrentContractReads() { increment(CURRENT_CONTRACT_READS); } + public void incrementNodeMaterializations() { increment(NODE_MATERIALIZATIONS); } + public void incrementSimpleMaterializations() { increment(SIMPLE_MATERIALIZATIONS); } + @Override + public void incrementFrozenOutputConversions() { increment(FROZEN_OUTPUT_CONVERSIONS); } + public void incrementNodeOutputConversions() { increment(NODE_OUTPUT_CONVERSIONS); } + public void incrementContainsBexScans() { increment(CONTAINS_BEX_SCANS); } + public void incrementContainsBexCacheHits() { increment(CONTAINS_BEX_CACHE_HITS); } + public void incrementContainsBexCacheMisses() { increment(CONTAINS_BEX_CACHE_MISSES); } + public void incrementResultValueReads() { increment(RESULT_VALUE_READS); } + public void incrementResultOverlayExactHits() { increment(RESULT_OVERLAY_EXACT_HITS); } + public void incrementResultOverlayAncestorHits() { increment(RESULT_OVERLAY_ANCESTOR_HITS); } + public void incrementResultOverlayDocumentFallbacks() { increment(RESULT_OVERLAY_DOCUMENT_FALLBACKS); } + public void incrementPointerParses() { increment(POINTER_PARSES); } + public void incrementPointerCacheHits() { increment(POINTER_CACHE_HITS); } + public void incrementPointerCacheMisses() { increment(POINTER_CACHE_MISSES); } + public void incrementFunctionArgMapAllocations() { increment(FUNCTION_ARG_MAP_ALLOCATIONS); } + @Override + public void incrementFrozenWriterNodeFallbacks() { increment(FROZEN_WRITER_NODE_FALLBACKS); } + public void addCompileNanos(long nanos) { addNonNegative(COMPILE_NANOS, nanos); } + public void addExecuteNanos(long nanos) { addNonNegative(EXECUTE_NANOS, nanos); } + + public long compiledExecutions() { return value(COMPILED_EXECUTIONS); } + public long compileCacheHits() { return value(COMPILE_CACHE_HITS); } + public long compileCacheMisses() { return value(COMPILE_CACHE_MISSES); } + public long interpretedFallbacks() { return value(INTERPRETED_FALLBACKS); } + public long expressionEvaluations() { return value(EXPRESSION_EVALUATIONS); } + public long statementExecutions() { return value(STATEMENT_EXECUTIONS); } + public long functionCalls() { return value(FUNCTION_CALLS); } + public long loopIterations() { return value(LOOP_ITERATIONS); } + public long frozenDocumentReads() { return value(FROZEN_DOCUMENT_READS); } + public long resolvedDocumentReads() { return value(RESOLVED_DOCUMENT_READS); } + public long eventReads() { return value(EVENT_READS); } + public long stepsReads() { return value(STEPS_READS); } + public long currentContractReads() { return value(CURRENT_CONTRACT_READS); } + public long nodeMaterializations() { return value(NODE_MATERIALIZATIONS); } + public long simpleMaterializations() { return value(SIMPLE_MATERIALIZATIONS); } + public long frozenOutputConversions() { return value(FROZEN_OUTPUT_CONVERSIONS); } + public long nodeOutputConversions() { return value(NODE_OUTPUT_CONVERSIONS); } + public long containsBexScans() { return value(CONTAINS_BEX_SCANS); } + public long containsBexCacheHits() { return value(CONTAINS_BEX_CACHE_HITS); } + public long containsBexCacheMisses() { return value(CONTAINS_BEX_CACHE_MISSES); } + public long resultValueReads() { return value(RESULT_VALUE_READS); } + public long resultOverlayExactHits() { return value(RESULT_OVERLAY_EXACT_HITS); } + public long resultOverlayAncestorHits() { return value(RESULT_OVERLAY_ANCESTOR_HITS); } + public long resultOverlayDocumentFallbacks() { return value(RESULT_OVERLAY_DOCUMENT_FALLBACKS); } + public long pointerParses() { return value(POINTER_PARSES); } + public long pointerCacheHits() { return value(POINTER_CACHE_HITS); } + public long pointerCacheMisses() { return value(POINTER_CACHE_MISSES); } + public long functionArgMapAllocations() { return value(FUNCTION_ARG_MAP_ALLOCATIONS); } + public long frozenWriterNodeFallbacks() { return value(FROZEN_WRITER_NODE_FALLBACKS); } + public long compileNanos() { return value(COMPILE_NANOS); } + public long executeNanos() { return value(EXECUTE_NANOS); } +} diff --git a/blue-bex-core/src/main/java/blue/bex/result/BexMetricsSnapshot.java b/blue-bex-core/src/main/java/blue/bex/result/BexMetricsSnapshot.java new file mode 100644 index 0000000..4a85160 --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/result/BexMetricsSnapshot.java @@ -0,0 +1,50 @@ +package blue.bex.result; + +/** Immutable metrics captured at a compile or execution boundary. */ +public final class BexMetricsSnapshot { + private final long[] values; + + BexMetricsSnapshot(long[] values) { + this.values = values.clone(); + } + + long[] copyValues() { + return values.clone(); + } + + private long value(int counter) { + return values[counter]; + } + + public long compiledExecutions() { return value(BexMetricsRecorder.COMPILED_EXECUTIONS); } + public long compileCacheHits() { return value(BexMetricsRecorder.COMPILE_CACHE_HITS); } + public long compileCacheMisses() { return value(BexMetricsRecorder.COMPILE_CACHE_MISSES); } + public long interpretedFallbacks() { return value(BexMetricsRecorder.INTERPRETED_FALLBACKS); } + public long expressionEvaluations() { return value(BexMetricsRecorder.EXPRESSION_EVALUATIONS); } + public long statementExecutions() { return value(BexMetricsRecorder.STATEMENT_EXECUTIONS); } + public long functionCalls() { return value(BexMetricsRecorder.FUNCTION_CALLS); } + public long loopIterations() { return value(BexMetricsRecorder.LOOP_ITERATIONS); } + public long frozenDocumentReads() { return value(BexMetricsRecorder.FROZEN_DOCUMENT_READS); } + public long resolvedDocumentReads() { return value(BexMetricsRecorder.RESOLVED_DOCUMENT_READS); } + public long eventReads() { return value(BexMetricsRecorder.EVENT_READS); } + public long stepsReads() { return value(BexMetricsRecorder.STEPS_READS); } + public long currentContractReads() { return value(BexMetricsRecorder.CURRENT_CONTRACT_READS); } + public long nodeMaterializations() { return value(BexMetricsRecorder.NODE_MATERIALIZATIONS); } + public long simpleMaterializations() { return value(BexMetricsRecorder.SIMPLE_MATERIALIZATIONS); } + public long frozenOutputConversions() { return value(BexMetricsRecorder.FROZEN_OUTPUT_CONVERSIONS); } + public long nodeOutputConversions() { return value(BexMetricsRecorder.NODE_OUTPUT_CONVERSIONS); } + public long containsBexScans() { return value(BexMetricsRecorder.CONTAINS_BEX_SCANS); } + public long containsBexCacheHits() { return value(BexMetricsRecorder.CONTAINS_BEX_CACHE_HITS); } + public long containsBexCacheMisses() { return value(BexMetricsRecorder.CONTAINS_BEX_CACHE_MISSES); } + public long resultValueReads() { return value(BexMetricsRecorder.RESULT_VALUE_READS); } + public long resultOverlayExactHits() { return value(BexMetricsRecorder.RESULT_OVERLAY_EXACT_HITS); } + public long resultOverlayAncestorHits() { return value(BexMetricsRecorder.RESULT_OVERLAY_ANCESTOR_HITS); } + public long resultOverlayDocumentFallbacks() { return value(BexMetricsRecorder.RESULT_OVERLAY_DOCUMENT_FALLBACKS); } + public long pointerParses() { return value(BexMetricsRecorder.POINTER_PARSES); } + public long pointerCacheHits() { return value(BexMetricsRecorder.POINTER_CACHE_HITS); } + public long pointerCacheMisses() { return value(BexMetricsRecorder.POINTER_CACHE_MISSES); } + public long functionArgMapAllocations() { return value(BexMetricsRecorder.FUNCTION_ARG_MAP_ALLOCATIONS); } + public long frozenWriterNodeFallbacks() { return value(BexMetricsRecorder.FROZEN_WRITER_NODE_FALLBACKS); } + public long compileNanos() { return value(BexMetricsRecorder.COMPILE_NANOS); } + public long executeNanos() { return value(BexMetricsRecorder.EXECUTE_NANOS); } +} diff --git a/src/main/java/blue/bex/result/BexPatchEntry.java b/blue-bex-core/src/main/java/blue/bex/result/BexPatchEntry.java similarity index 95% rename from src/main/java/blue/bex/result/BexPatchEntry.java rename to blue-bex-core/src/main/java/blue/bex/result/BexPatchEntry.java index 6f28576..096734c 100644 --- a/src/main/java/blue/bex/result/BexPatchEntry.java +++ b/blue-bex-core/src/main/java/blue/bex/result/BexPatchEntry.java @@ -4,6 +4,7 @@ import blue.bex.output.BexAdmittedValue; import blue.bex.value.BexValue; import blue.bex.value.BexValues; +import blue.bex.value.BexPatchValueView; import blue.language.model.wire.JsonPointer; import java.util.Collections; @@ -13,7 +14,7 @@ /** * One ordered JSON patch entry produced by BEX. */ -public final class BexPatchEntry { +public final class BexPatchEntry implements BexPatchValueView { private final String op; private final String authoredPath; private final String absolutePath; diff --git a/src/main/java/blue/bex/result/BexResultOverlay.java b/blue-bex-core/src/main/java/blue/bex/result/BexResultOverlay.java similarity index 92% rename from src/main/java/blue/bex/result/BexResultOverlay.java rename to blue-bex-core/src/main/java/blue/bex/result/BexResultOverlay.java index 3add772..3c337f0 100644 --- a/src/main/java/blue/bex/result/BexResultOverlay.java +++ b/blue-bex-core/src/main/java/blue/bex/result/BexResultOverlay.java @@ -1,6 +1,6 @@ package blue.bex.result; -import blue.bex.api.BexDocumentView; +import blue.bex.spi.BexDocumentAccess; import blue.bex.value.BexValue; import blue.bex.value.BexValues; import blue.language.model.wire.JsonPointer; @@ -13,18 +13,18 @@ * Ordered overlay materializing accumulated patch effects for $resultValue reads. */ public final class BexResultOverlay { - private final BexDocumentView document; + private final BexDocumentAccess document; private final List entries = new ArrayList<>(); - private final BexMetrics metrics; + private final BexMetricsRecorder metrics; private final BlueLanguage blue; - public BexResultOverlay(BexDocumentView document, BexMetrics metrics) { + public BexResultOverlay(BexDocumentAccess document, BexMetricsRecorder metrics) { this(document, metrics, null); } public BexResultOverlay( - BexDocumentView document, - BexMetrics metrics, + BexDocumentAccess document, + BexMetricsRecorder metrics, BlueLanguage blue) { this.document = document; this.metrics = metrics; diff --git a/blue-bex-core/src/main/java/blue/bex/result/package-info.java b/blue-bex-core/src/main/java/blue/bex/result/package-info.java new file mode 100644 index 0000000..3881909 --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/result/package-info.java @@ -0,0 +1,13 @@ +/** + * Returned values, ordered changesets/events, overlays, gas evidence, and + * diagnostic execution metrics. + * + *

A result belongs to one completed run and exposes immutable collections or + * defensive copies. Mutable metric accumulators are run-owned and are not + * shared; metric snapshots may be read independently. Required constructor + * evidence is non-null, while explicitly optional admitted-output metadata may + * be absent. Result construction does not hide failures or commit effects. Gas + * totals are derived from the canonical ledger; metrics and cache state never + * alter portable gas.

+ */ +package blue.bex.result; diff --git a/src/main/java/blue/bex/runtime/BexExecutionAccumulator.java b/blue-bex-core/src/main/java/blue/bex/runtime/BexExecutionAccumulator.java similarity index 100% rename from src/main/java/blue/bex/runtime/BexExecutionAccumulator.java rename to blue-bex-core/src/main/java/blue/bex/runtime/BexExecutionAccumulator.java diff --git a/blue-bex-core/src/main/java/blue/bex/runtime/BexRuntime.java b/blue-bex-core/src/main/java/blue/bex/runtime/BexRuntime.java new file mode 100644 index 0000000..c402397 --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/runtime/BexRuntime.java @@ -0,0 +1,281 @@ +package blue.bex.runtime; + +import blue.bex.compile.BexCompiledProgram; +import blue.bex.compile.BexCompiledProgramRuntimeAccess; +import blue.bex.compile.BexExecutionMachine; +import blue.bex.gas.BexGasCounter; +import blue.bex.gas.BexGasMeter; +import blue.bex.gas.BexGasSchedule; +import blue.bex.output.BexAdmittedValue; +import blue.bex.output.BexOutputAdmission; +import blue.bex.output.BexOutputKind; +import blue.bex.pointer.BexPointerCache; +import blue.bex.result.BexChangeset; +import blue.bex.result.BexExecutionResult; +import blue.bex.result.BexMetricsRecorder; +import blue.bex.result.BexPatchEntry; +import blue.bex.result.BexResultOverlay; +import blue.bex.type.BexBlueTypeMatcher; +import blue.bex.value.BexValue; +import blue.bex.value.BexValues; +import blue.language.runtime.BlueLanguage; +import blue.language.model.wire.JsonPointer; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Runtime for one compiled BEX execution. + */ +public final class BexRuntime implements BexExecutionMachine { + private final BexCompiledProgram program; + private final BexRuntimeContext context; + private final BexGasMeter gas; + private final BexMetricsRecorder metrics; + private final BexPointerCache pointerCache; + private final BexExecutionAccumulator accumulator; + private final BexBlueTypeMatcher typeMatcher; + private final BlueLanguage blue; + private final BexRuntimeIntrinsics intrinsics; + private final BexOutputAdmission outputAdmission; + private final BexRuntimeGasSession gasSession; + private final BexResultOverlay rollbackOverlay; + + public BexRuntime(BexCompiledProgram program, + BexRuntimeContext context, + BlueLanguage blue, + BexGasSchedule gasSchedule, + BexMetricsRecorder metrics, + BexPointerCache pointerCache) { + this(program, context, blue, gasSchedule, metrics, pointerCache, + BexRuntimeIntrinsics.EMPTY); + } + + public BexRuntime(BexCompiledProgram program, + BexRuntimeContext context, + BlueLanguage blue, + BexGasSchedule gasSchedule, + BexMetricsRecorder metrics, + BexPointerCache pointerCache, + BexRuntimeIntrinsics intrinsics) { + this.program = program; + this.context = context; + this.blue = blue; + this.intrinsics = intrinsics != null + ? intrinsics + : BexRuntimeIntrinsics.EMPTY; + this.gasSession = BexRuntimeGasSession.open( + context, + gasSchedule, + this.intrinsics, + program.requiredIntrinsicBlueIds()); + this.gas = gasSession.meter(); + this.metrics = metrics; + this.pointerCache = pointerCache; + this.outputAdmission = new BexOutputAdmission( + gas, + context.semanticIdentityBoundary(), + context.failureBoundary()); + BexResultOverlay activeOverlay = + new BexResultOverlay( + context.document(), metrics, blue); + this.accumulator = new BexExecutionAccumulator( + activeOverlay, + outputAdmission); + this.rollbackOverlay = new BexResultOverlay( + context.document(), metrics, blue); + this.typeMatcher = new BexBlueTypeMatcher(blue); + } + + public BexExecutionResult execute() { + try { + BexValue value = BexCompiledProgramRuntimeAccess.execute( + program, this); + BexAdmittedValue output = + outputAdmission.admit(value, BexOutputKind.ROOT_RESULT); + BexExecutionResult result = new BexExecutionResult( + value, + accumulator.changeset(), + accumulator.events(), + gas.ledger(), + metrics.snapshot(), + output); + gasSession.completeSuccessfully(); + return result; + } catch (RuntimeException | Error ex) { + accumulator.discard(rollbackOverlay); + gasSession.completeAfterFailure(ex); + if (ex instanceof RuntimeException) { + throw context.failureBoundary().translate( + (RuntimeException) ex); + } + throw ex; + } + } + + public BexCompiledProgram program() { return program; } + public BexRuntimeContext context() { return context; } + public BexGasMeter gas() { return gas; } + public BexMetricsRecorder metrics() { return metrics; } + public BexPointerCache pointerCache() { return pointerCache; } + public BexExecutionAccumulator accumulator() { return accumulator; } + public BexBlueTypeMatcher typeMatcher() { return typeMatcher; } + public BexRuntimeIntrinsics intrinsics() { return intrinsics; } + public BexOutputAdmission outputAdmission() { return outputAdmission; } + + public BexValue readDocument(String absolutePointer, List precompiledSegments, boolean resolved) { + gas.charge(BexGasCounter.DOCUMENT_READ); + if (resolved) { + metrics.incrementResolvedDocumentReads(); + } else { + metrics.incrementFrozenDocumentReads(); + } + + /* + * Traverse from the host's exact root so every intermediate reference + * can be materialized lazily through Blue's verified provider + * boundary. A final pure or cyclic-set reference stays opaque when the + * program only carries it or asks for its established identity. + */ + return readValuePointer(documentAt("/", resolved), + precompiledSegments); + } + + private BexValue documentAt(String absolutePointer, boolean resolved) { + return resolved + ? context.document().resolvedAt(absolutePointer) + : context.document().canonicalAt(absolutePointer); + } + + public BexValue readEvent(List precompiledSegments) { + gas.charge(BexGasCounter.EVENT_READ); + metrics.incrementEventReads(); + return readValuePointer(context.event(), precompiledSegments); + } + + public BexValue readProcessingEvent(List precompiledSegments) { + gas.charge(BexGasCounter.PROCESSING_EVENT_READ); + return readValuePointer(context.processingEvent(), precompiledSegments); + } + + public BexValue readCurrentContract(List precompiledSegments) { + gas.charge(BexGasCounter.CURRENT_CONTRACT_READ); + metrics.incrementCurrentContractReads(); + return readValuePointer(context.currentContract(), precompiledSegments); + } + + public BexValue readBinding(String name, List pathSegments) { + gas.charge(BexGasCounter.BINDING_READ); + if (name == null || name.isEmpty()) { + return BexValues.undefined(); + } + return readValuePointer(context.binding(name), pathSegments); + } + + public BexValue readSteps(String step, List pathSegments) { + gas.charge(BexGasCounter.STEPS_READ); + metrics.incrementStepsReads(); + return readValuePointer(context.steps().step(step), pathSegments); + } + + public BexValue readResultValue(String absolutePointer, List segments) { + gas.charge(BexGasCounter.RESULT_VALUE_READ); + metrics.incrementResultValueReads(); + return readValuePointer(accumulator.overlay().rootValue(), segments); + } + + public BexValue defaultResultValue() { + Map result = new LinkedHashMap<>(); + BexChangeset changeset = accumulator.changeset(); + result.put("changeset", changeset.asValue()); + result.put("events", accumulator.events().asValue()); + return BexValues.map(result); + } + + public BexValue invokeIntrinsic(String blueId, BexValue type, Map fields) { + return intrinsics.invoke( + blueId, type, fields, gas, outputAdmission); + } + + public BexValue nodeBlueId(BexValue value) { + gas.charge(BexGasCounter.NODE_IDENTITY_REQUESTED); + if (value == null || value.isUndefined()) { + throw new blue.bex.BexException( + "$nodeBlueId operand must not be undefined"); + } + if (value.isExact()) { + return BexValues.scalar(value.exactBlueId()); + } + return BexValues.scalar(outputAdmission + .admit(value, BexOutputKind.NODE_IDENTITY) + .nodeBlueId()); + } + + @Override + public boolean matchesType( + BexValue value, + blue.language.snapshot.FrozenNode pattern, + blue.bex.BexSourcePath sourcePath) { + return typeMatcher.matches(value, pattern, gas, sourcePath); + } + + public String resolvePointer(String authoredPointer) { + return context.document().resolvePointer(authoredPointer); + } + + public List parseDynamicPointer(String pointer) { + return pointerCache.get(pointer, metrics).segments(); + } + + /** + * Traverses one semantic value pointer with canonical per-segment read + * ownership. The charge is admitted before examining each next member. + */ + public BexValue readValuePointer(BexValue root, List segments) { + BexValue current = BexValues.referenceBacked( + root != null ? root : BexValues.undefined(), + blue); + if (segments == null) { + return current; + } + for (String segment : segments) { + if (current.isUndefined()) { + return current; + } + gas.charge(BexGasCounter.POINTER_SEGMENT_READ); + if (current.isList()) { + gas.charge(BexGasCounter.LIST_ITEM_READ); + } else if (current.isObject()) { + gas.charge(BexGasCounter.OBJECT_MEMBER_READ); + } + current = current.get(segment); + } + return current; + } + + public String canonicalPointer(String pointer) { + return JsonPointer.canonicalize(pointer); + } + + @Override + public void appendChange(BexPatchEntry entry) { + accumulator.appendChange(entry); + } + + @Override + public void appendEvent(BexValue event) { + accumulator.appendEvent(event); + } + + @Override + public BexValue changesetValue() { + return accumulator.changeset().asValue(); + } + + @Override + public BexValue eventsValue() { + return accumulator.events().asValue(); + } + +} diff --git a/blue-bex-core/src/main/java/blue/bex/runtime/BexRuntimeContext.java b/blue-bex-core/src/main/java/blue/bex/runtime/BexRuntimeContext.java new file mode 100644 index 0000000..74d9efd --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/runtime/BexRuntimeContext.java @@ -0,0 +1,23 @@ +package blue.bex.runtime; + +import blue.bex.gas.BexGasLedgerLifecycle; +import blue.bex.output.BexFailurePolicy; +import blue.bex.output.BexSemanticIdentityBoundary; +import blue.bex.spi.BexDocumentAccess; +import blue.bex.value.BexValue; + +/** Invocation context consumed by the pure BEX runtime. */ +public interface BexRuntimeContext { + BexDocumentAccess document(); + BexValue event(); + BexValue processingEvent(); + BexValue currentContract(); + BexStepResultView steps(); + BexValue binding(String name); + String currentScopePath(); + long gasLimit(); + long parentRemainingGas(); + BexGasLedgerLifecycle gasLedgerHost(); + BexSemanticIdentityBoundary semanticIdentityBoundary(); + BexFailurePolicy failureBoundary(); +} diff --git a/blue-bex-core/src/main/java/blue/bex/runtime/BexRuntimeGasSession.java b/blue-bex-core/src/main/java/blue/bex/runtime/BexRuntimeGasSession.java new file mode 100644 index 0000000..2769800 --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/runtime/BexRuntimeGasSession.java @@ -0,0 +1,272 @@ +package blue.bex.runtime; + +import blue.bex.gas.BexGasCounter; +import blue.bex.gas.BexGasLedgerCapability; +import blue.bex.gas.BexGasLedgerLifecycle; +import blue.bex.gas.BexGasLimitExceededException; +import blue.bex.gas.BexGasMeter; +import blue.bex.gas.BexGasSchedule; +import blue.bex.gas.BexHostGasExhaustion; +import blue.bex.gas.BexSharedGasBudget; +import blue.bex.output.BexFailurePolicy; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** Owns construction and exactly-once finalization of one runtime gas set. */ +final class BexRuntimeGasSession { + private final BexRuntimeContext context; + private final BexGasLedgerLifecycle host; + private final BexGasMeter meter; + + private BexRuntimeGasSession( + BexRuntimeContext context, + BexGasLedgerLifecycle host, + BexGasMeter meter) { + this.context = context; + this.host = host; + this.meter = meter; + } + + static BexRuntimeGasSession open( + BexRuntimeContext context, + BexGasSchedule schedule, + BexRuntimeIntrinsics intrinsics, + Set requiredIntrinsicBlueIds) { + BexGasLedgerLifecycle host = context.gasLedgerHost(); + return new BexRuntimeGasSession( + context, + host, + openMeter( + context, + schedule, + host, + intrinsics, + requiredIntrinsicBlueIds)); + } + + BexGasMeter meter() { + return meter; + } + + void completeSuccessfully() { + if (host != null && !meter.hostLedgerFinalized()) { + meter.submitHostLedger(host::submit); + } + } + + void completeAfterFailure(Throwable primaryFailure) { + if (host == null || meter.hostLedgerFinalized()) { + return; + } + if (evidenceUnavailableWins( + primaryFailure, context.failureBoundary())) { + notifyFailureLifecycle( + primaryFailure, + () -> meter.unavailableHostLedger( + host::evidenceUnavailable)); + return; + } + + BexHostGasExhaustion hostExhaustion = findCause( + primaryFailure, BexHostGasExhaustion.class); + if (hostExhaustion != null) { + try { + meter.propagateHostGasExhaustion( + hostExhaustion, + host::failedDeterministically, + host::propagateGasExhaustion); + } catch (RuntimeException canonical) { + if (canonical == hostExhaustion.hostFailure()) { + throw canonical; + } + addSuppressed(primaryFailure, canonical); + } catch (Error lifecycleFailure) { + addSuppressed(primaryFailure, lifecycleFailure); + } + return; + } + + BexGasLimitExceededException localExhaustion = findCause( + primaryFailure, BexGasLimitExceededException.class); + if (localExhaustion != null) { + notifyFailureLifecycle( + primaryFailure, + () -> meter.failHostLedger( + host::failedDeterministically)); + if (primaryFailure instanceof RuntimeException) { + throw Objects.requireNonNull( + host.localGasLimitExceeded( + localExhaustion, + (RuntimeException) primaryFailure), + "local gas-limit mapping"); + } + return; + } + notifyFailureLifecycle( + primaryFailure, + () -> meter.failHostLedger( + host::failedDeterministically)); + } + + private static BexGasMeter openMeter( + BexRuntimeContext context, + BexGasSchedule schedule, + BexGasLedgerLifecycle host, + BexRuntimeIntrinsics intrinsics, + Set requiredIntrinsicBlueIds) { + Map registered = + intrinsics.registeredNamedWeights( + requiredIntrinsicBlueIds); + Map> namespaceWeights = + intrinsics.registeredNamespaceWeights( + requiredIntrinsicBlueIds); + if (host == null) { + return new BexGasMeter( + schedule, + context.parentRemainingGas(), + context.gasLimit(), + registered); + } + if (!host.separatesRuntimeNamespaces() + && !namespaceWeights.isEmpty()) { + throw new IllegalArgumentException( + "A hosted intrinsic registry requires separate runtime namespaces"); + } + LinkedHashMap children = + new LinkedHashMap<>(); + BexSharedGasBudget sharedBudget = null; + try { + if (context.gasLimit() != BexGasMeter.NO_LOCAL_LIMIT) { + sharedBudget = host.openSharedBudget(context.gasLimit()); + if (sharedBudget != null + && sharedBudget.maximumGas() != context.gasLimit()) { + throw new IllegalStateException( + "Gas host returned a shared budget with maximum " + + sharedBudget.maximumGas() + + " instead of " + context.gasLimit()); + } + } + children.put( + BexGasCounter.NAMESPACE, + requireOpenedLedger( + openHostLedger( + host, + BexGasCounter.NAMESPACE, + schedule.counterWeights(), + sharedBudget), + BexGasCounter.NAMESPACE)); + for (Map.Entry> intrinsic + : namespaceWeights.entrySet()) { + children.put( + intrinsic.getKey(), + requireOpenedLedger( + openHostLedger( + host, + intrinsic.getKey(), + intrinsic.getValue(), + sharedBudget), + intrinsic.getKey())); + } + return sharedBudget == null + ? new BexGasMeter( + schedule, + children, + context.gasLimit(), + registered) + : BexGasMeter.hostedWithSharedLocalLimit( + schedule, + children, + context.gasLimit(), + registered); + } catch (RuntimeException | Error openingFailure) { + finishOpenedAfterConstructionFailure( + host, + children, + openingFailure, + context.failureBoundary()); + throw openingFailure; + } + } + + private static BexGasLedgerCapability openHostLedger( + BexGasLedgerLifecycle host, + String namespace, + Map counterWeights, + BexSharedGasBudget sharedBudget) { + return sharedBudget == null + ? host.open(namespace, counterWeights) + : host.open(namespace, counterWeights, sharedBudget); + } + + private static BexGasLedgerCapability requireOpenedLedger( + BexGasLedgerCapability ledger, + String namespace) { + if (ledger == null) { + throw new IllegalStateException( + "Gas host returned no child ledger for " + namespace); + } + return ledger; + } + + private static void finishOpenedAfterConstructionFailure( + BexGasLedgerLifecycle host, + Map opened, + Throwable openingFailure, + BexFailurePolicy failureBoundary) { + boolean unavailable = evidenceUnavailableWins( + openingFailure, failureBoundary); + for (BexGasLedgerCapability ledger : opened.values()) { + try { + if (unavailable) { + host.evidenceUnavailable(ledger); + } else { + host.failedDeterministically(ledger); + } + } catch (RuntimeException | Error lifecycleFailure) { + addSuppressed(openingFailure, lifecycleFailure); + } + } + } + + private static void notifyFailureLifecycle( + Throwable primaryFailure, + Runnable lifecycle) { + try { + lifecycle.run(); + } catch (RuntimeException | Error lifecycleFailure) { + addSuppressed(primaryFailure, lifecycleFailure); + } + } + + private static void addSuppressed( + Throwable primaryFailure, + Throwable lifecycleFailure) { + if (primaryFailure != lifecycleFailure) { + primaryFailure.addSuppressed(lifecycleFailure); + } + } + + private static boolean evidenceUnavailableWins( + Throwable failure, + BexFailurePolicy failureBoundary) { + return Objects.requireNonNull( + failureBoundary, "failureBoundary") + .evidenceUnavailable(failure); + } + + private static T findCause( + Throwable failure, + Class type) { + Throwable current = failure; + while (current != null) { + if (type.isInstance(current)) { + return type.cast(current); + } + current = current.getCause(); + } + return null; + } +} diff --git a/blue-bex-core/src/main/java/blue/bex/runtime/BexRuntimeIntrinsics.java b/blue-bex-core/src/main/java/blue/bex/runtime/BexRuntimeIntrinsics.java new file mode 100644 index 0000000..9cf6b60 --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/runtime/BexRuntimeIntrinsics.java @@ -0,0 +1,49 @@ +package blue.bex.runtime; + +import blue.bex.BexException; +import blue.bex.gas.BexGasMeter; +import blue.bex.output.BexOutputAdmission; +import blue.bex.value.BexValue; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +/** Runtime-only intrinsic catalog and invocation boundary. */ +public interface BexRuntimeIntrinsics { + BexRuntimeIntrinsics EMPTY = new BexRuntimeIntrinsics() { + @Override + public Map registeredNamedWeights( + Set requiredBlueIds) { + return Collections.emptyMap(); + } + + @Override + public Map> registeredNamespaceWeights( + Set requiredBlueIds) { + return Collections.emptyMap(); + } + + @Override + public BexValue invoke( + String blueId, + BexValue type, + Map fields, + BexGasMeter gas, + BexOutputAdmission outputAdmission) { + throw new BexException("Unsupported intrinsic BlueId: " + blueId); + } + }; + + Map registeredNamedWeights(Set requiredBlueIds); + + Map> registeredNamespaceWeights( + Set requiredBlueIds); + + BexValue invoke( + String blueId, + BexValue type, + Map fields, + BexGasMeter gas, + BexOutputAdmission outputAdmission); +} diff --git a/blue-bex-core/src/main/java/blue/bex/runtime/BexStepResultView.java b/blue-bex-core/src/main/java/blue/bex/runtime/BexStepResultView.java new file mode 100644 index 0000000..dd81f94 --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/runtime/BexStepResultView.java @@ -0,0 +1,9 @@ +package blue.bex.runtime; + +import blue.bex.value.BexValue; + +/** Read-only access to prior workflow step results. */ +public interface BexStepResultView { + BexValue step(String name); + BexValue asValue(); +} diff --git a/blue-bex-core/src/main/java/blue/bex/runtime/package-info.java b/blue-bex-core/src/main/java/blue/bex/runtime/package-info.java new file mode 100644 index 0000000..090b2fc --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/runtime/package-info.java @@ -0,0 +1,15 @@ +/** + * Run-local execution orchestration, host context, gas sessions, and buffered + * effect accumulation. + * + *

Frames, variables, accumulators, metrics, and runtime instances belong to + * one execution and are not shareable. Compiler-owned frames, control flow, + * and instruction interfaces live in {@code blue.bex.compile}; this package + * consumes their opaque immutable program handle and host context values. + * Required runtime inputs are non-null; language-level + * absence is represented by an explicit BEX undefined value. Runtime failure or + * exhaustion stops later work and leaves buffered effects uncommitted. Every + * instruction admits its named gas charge before performing the associated + * logical work.

+ */ +package blue.bex.runtime; diff --git a/blue-bex-core/src/main/java/blue/bex/spi/BexDocumentAccess.java b/blue-bex-core/src/main/java/blue/bex/spi/BexDocumentAccess.java new file mode 100644 index 0000000..23b5e61 --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/spi/BexDocumentAccess.java @@ -0,0 +1,11 @@ +package blue.bex.spi; + +import blue.bex.value.BexValue; + +/** Read-only canonical/resolved document access used below the public API. */ +public interface BexDocumentAccess { + String resolvePointer(String authoredPointer); + BexValue canonicalAt(String absolutePointer); + BexValue resolvedAt(String absolutePointer); + String currentScopePath(); +} diff --git a/blue-bex-core/src/main/java/blue/bex/spi/package-info.java b/blue-bex-core/src/main/java/blue/bex/spi/package-info.java new file mode 100644 index 0000000..472b04a --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/spi/package-info.java @@ -0,0 +1,10 @@ +/** + * Host-neutral, read-only ports used below the public BEX composition API. + * + *

Implementations are invocation-owned unless their type explicitly states + * that they are immutable and shareable. Inputs are non-null, evidence failures + * remain fail-closed, and a port must not perform work after rejecting a gas + * charge. These interfaces carry no Contracts processor types; hosted failure, + * provenance, and lifecycle translation belongs to {@code blue.bex.contracts}.

+ */ +package blue.bex.spi; diff --git a/blue-bex-core/src/main/java/blue/bex/type/BexBlueTypeMatcher.java b/blue-bex-core/src/main/java/blue/bex/type/BexBlueTypeMatcher.java new file mode 100644 index 0000000..56dfddd --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/type/BexBlueTypeMatcher.java @@ -0,0 +1,26 @@ +package blue.bex.type; + +import blue.bex.BexSourcePath; +import blue.bex.gas.BexGasMeter; +import blue.bex.value.BexValue; +import blue.language.runtime.BlueLanguage; +import blue.language.snapshot.FrozenNode; + +/** + * Compatibility facade for the cohesive {@link BexTypeMatcher} boundary. + */ +public final class BexBlueTypeMatcher { + private final BexTypeMatcher delegate; + + public BexBlueTypeMatcher(BlueLanguage blue) { + this.delegate = new BexTypeMatcher(blue); + } + + public boolean matches( + BexValue value, + FrozenNode pattern, + BexGasMeter gas, + BexSourcePath sourcePath) { + return delegate.matches(value, pattern, gas, sourcePath); + } +} diff --git a/blue-bex-core/src/main/java/blue/bex/type/BexFrozenTypeMatcher.java b/blue-bex-core/src/main/java/blue/bex/type/BexFrozenTypeMatcher.java new file mode 100644 index 0000000..332bd09 --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/type/BexFrozenTypeMatcher.java @@ -0,0 +1,159 @@ +package blue.bex.type; + +import blue.language.matching.FrozenTypeMatcher; +import blue.language.snapshot.FrozenNode; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** Recursive matching of already-frozen candidate evidence. */ +final class BexFrozenTypeMatcher { + private final FrozenTypeMatcher matcher; + private final BexPatternValidator patternValidator; + + BexFrozenTypeMatcher( + FrozenTypeMatcher matcher, + BexPatternValidator patternValidator) { + this.matcher = Objects.requireNonNull(matcher, "matcher"); + this.patternValidator = Objects.requireNonNull( + patternValidator, "patternValidator"); + } + + boolean matches( + FrozenNode candidate, + FrozenNode pattern, + BexTypeMatchWorkRecorder work) { + work.comparisonNode(); + return matchesAfterAdmission(candidate, pattern, work); + } + + boolean matchesAfterAdmission( + FrozenNode candidate, + FrozenNode pattern, + BexTypeMatchWorkRecorder work) { + if (pattern.isEmptyNode()) { + return true; + } + if (pattern.isReferenceOnly()) { + return matcher.matchesType(candidate, pattern); + } + if (!work.scalarMatches(candidate.getValue(), pattern.getValue())) { + return false; + } + if (!matchesItemType(candidate, pattern.getItemType(), work) + || !matchesKeyType(candidate, pattern.getKeyType(), work) + || !matchesValueType(candidate, pattern.getValueType(), work) + || !matchesItems(candidate, pattern.getItems(), work) + || !matchesProperties( + candidate, pattern.getProperties(), work)) { + return false; + } + return matcher.matchesType(candidate, pattern); + } + + private boolean matchesItemType( + FrozenNode candidate, + FrozenNode targetItemType, + BexTypeMatchWorkRecorder work) { + if (targetItemType == null || candidate.getItems() == null) { + return true; + } + for (FrozenNode item : candidate.getItems()) { + if (!matches(item, targetItemType, work)) { + return false; + } + } + return true; + } + + private boolean matchesKeyType( + FrozenNode candidate, + FrozenNode targetKeyType, + BexTypeMatchWorkRecorder work) { + if (targetKeyType == null || candidate.getProperties() == null) { + return true; + } + for (String key : candidate.getProperties().keySet()) { + work.comparisonNode(); + if (!patternValidator.keyMatchesType(key, targetKeyType)) { + return false; + } + } + return true; + } + + private boolean matchesValueType( + FrozenNode candidate, + FrozenNode targetValueType, + BexTypeMatchWorkRecorder work) { + if (targetValueType == null || candidate.getProperties() == null) { + return true; + } + for (String key : candidate.getProperties().keySet()) { + if (!matches( + candidate.getProperties().get(key), + targetValueType, + work)) { + return false; + } + } + return true; + } + + private boolean matchesItems( + FrozenNode candidate, + List targetItems, + BexTypeMatchWorkRecorder work) { + if (targetItems == null) { + return true; + } + List candidateItems = candidate.getItems() != null + ? candidate.getItems() + : Collections.emptyList(); + for (int index = 0; index < targetItems.size(); index++) { + FrozenNode targetItem = targetItems.get(index); + if (index < candidateItems.size()) { + if (!matches(candidateItems.get(index), targetItem, work)) { + return false; + } + } else if (patternValidator.requiresPresence(targetItem)) { + return false; + } + } + return true; + } + + private boolean matchesProperties( + FrozenNode candidate, + Map targetProperties, + BexTypeMatchWorkRecorder work) { + if (targetProperties == null) { + return true; + } + Map candidateProperties = + candidate.getProperties() != null + ? candidate.getProperties() + : Collections.emptyMap(); + for (Map.Entry candidateEntry + : candidateProperties.entrySet()) { + FrozenNode targetProperty = + targetProperties.get(candidateEntry.getKey()); + if (targetProperty != null + && !matches( + candidateEntry.getValue(), targetProperty, work)) { + return false; + } + } + for (Map.Entry targetEntry + : targetProperties.entrySet()) { + if (!candidateProperties.containsKey(targetEntry.getKey()) + && patternValidator.requiresPresence( + targetEntry.getValue())) { + return false; + } + } + return true; + } +} diff --git a/blue-bex-core/src/main/java/blue/bex/type/BexPatternValidator.java b/blue-bex-core/src/main/java/blue/bex/type/BexPatternValidator.java new file mode 100644 index 0000000..83028e1 --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/type/BexPatternValidator.java @@ -0,0 +1,272 @@ +package blue.bex.type; + +import blue.bex.value.BexValue; +import blue.language.model.Schema; +import blue.language.registry.BlueCoreTypeRegistry; +import blue.language.snapshot.FrozenNode; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** Closed shape and required-presence rules used by BEX type matching. */ +public final class BexPatternValidator { + private static final String TEXT_TYPE_BLUE_ID = + BlueCoreTypeRegistry.INSTANCE.blueId("Text"); + private static final String INTEGER_TYPE_BLUE_ID = + BlueCoreTypeRegistry.INSTANCE.blueId("Integer"); + private static final String DOUBLE_TYPE_BLUE_ID = + BlueCoreTypeRegistry.INSTANCE.blueId("Double"); + private static final String BOOLEAN_TYPE_BLUE_ID = + BlueCoreTypeRegistry.INSTANCE.blueId("Boolean"); + + public boolean requiresPresence(FrozenNode target) { + Schema schema = target.getSchema(); + if (schema != null && Boolean.TRUE.equals( + schema.getRequiredValue())) { + return true; + } + return hasValueInNestedStructure(target); + } + + public boolean keyMatchesType( + String key, + FrozenNode targetKeyType) { + String identity = targetKeyType.getReferenceBlueId(); + if (identity == null && targetKeyType.getType() != null) { + identity = targetKeyType.getType().getReferenceBlueId(); + } + if (TEXT_TYPE_BLUE_ID.equals(identity)) { + return true; + } + if (INTEGER_TYPE_BLUE_ID.equals(identity)) { + try { + new BigInteger(key); + return true; + } catch (NumberFormatException invalidInteger) { + return false; + } + } + if (DOUBLE_TYPE_BLUE_ID.equals(identity)) { + try { + return Double.isFinite(Double.parseDouble(key)); + } catch (NumberFormatException invalidDouble) { + return false; + } + } + if (BOOLEAN_TYPE_BLUE_ID.equals(identity)) { + return "true".equalsIgnoreCase(key) + || "false".equalsIgnoreCase(key); + } + return false; + } + + private boolean hasValueInNestedStructure(FrozenNode node) { + if (node.isReferenceOnly() || node.getValue() != null) { + return true; + } + if (node.getItems() != null) { + for (FrozenNode item : node.getItems()) { + if (hasValueInNestedStructure(item)) { + return true; + } + } + } + if (node.getProperties() != null) { + for (FrozenNode property : node.getProperties().values()) { + if (hasValueInNestedStructure(property)) { + return true; + } + } + } + return false; + } + + enum CandidatePosition { + ROOT, + OBJECT_MEMBER, + LIST_ITEM + } + + /** Current-node-only view; descendants remain lazy BEX cursors. */ + static final class CandidateView { + final BexValue source; + final Object scalar; + final BexValue items; + final List propertyKeys; + final boolean materializeCurrent; + final boolean valid; + + private CandidateView( + BexValue source, + Object scalar, + BexValue items, + List propertyKeys, + boolean materializeCurrent, + boolean valid) { + this.source = source; + this.scalar = scalar; + this.items = items; + this.propertyKeys = propertyKeys; + this.materializeCurrent = materializeCurrent; + this.valid = valid; + } + + static CandidateView from( + BexValue source, + CandidatePosition position) { + if (source == null || source.isUndefined()) { + return invalid(source); + } + if (source.isNull()) { + return empty(source); + } + if (source.isScalar()) { + return new CandidateView( + source, + source.toSimple(), + null, + Collections.emptyList(), + false, + true); + } + if (source.isList()) { + return new CandidateView( + source, + null, + source, + Collections.emptyList(), + false, + true); + } + if (!source.isObject()) { + return invalid(source); + } + + List keys = source.keys(); + if (isEmptyPlaceholder(source, keys)) { + return position == CandidatePosition.LIST_ITEM + ? empty(source) + : invalid(source); + } + + boolean hasBlueId = false; + boolean hasValue = false; + boolean hasItems = false; + int retainedFields = 0; + BexValue items = null; + ArrayList properties = new ArrayList<>(); + for (String key : keys) { + retainedFields++; + if (isForbiddenField(key)) { + return invalid(source); + } + if ("blueId".equals(key)) { + hasBlueId = true; + } else if ("value".equals(key)) { + BexValue child = source.get(key); + hasValue = true; + if (child == null || child.isUndefined() + || child.isNull() || !child.isScalar()) { + return invalid(source); + } + } else if ("items".equals(key)) { + BexValue child = source.get(key); + hasItems = true; + if (child == null || child.isUndefined() + || !child.isList()) { + return invalid(source); + } + items = child; + } else if (isOrdinaryProperty(key)) { + properties.add(key); + } + } + + if (hasBlueId) { + return retainedFields == 1 + ? materialized(source) + : invalid(source); + } + int payloadKinds = (hasValue ? 1 : 0) + + (hasItems ? 1 : 0) + + (!properties.isEmpty() ? 1 : 0); + if (payloadKinds > 1) { + return invalid(source); + } + if (hasValue) { + return materialized(source); + } + return new CandidateView( + source, + null, + items, + Collections.unmodifiableList(properties), + false, + true); + } + + boolean hasProperty(String key) { + return isOrdinaryProperty(key) + && propertyKeys.contains(key); + } + + private static CandidateView empty(BexValue source) { + return new CandidateView( + source, null, null, + Collections.emptyList(), false, true); + } + + private static CandidateView materialized(BexValue source) { + return new CandidateView( + source, null, null, + Collections.emptyList(), true, true); + } + + private static CandidateView invalid(BexValue source) { + return new CandidateView( + source, null, null, + Collections.emptyList(), false, false); + } + + private static boolean isEmptyPlaceholder( + BexValue source, + List keys) { + if (keys.size() != 1 || !"$empty".equals(keys.get(0))) { + return false; + } + BexValue marker = source.get("$empty"); + return marker != null && marker.isScalar() + && Boolean.TRUE.equals(marker.toSimple()); + } + } + + private static boolean isOrdinaryProperty(String key) { + return !"name".equals(key) + && !"description".equals(key) + && !"type".equals(key) + && !"itemType".equals(key) + && !"keyType".equals(key) + && !"valueType".equals(key) + && !"mergePolicy".equals(key) + && !"value".equals(key) + && !"items".equals(key) + && !"blueId".equals(key) + && !"contracts".equals(key) + && !"schema".equals(key) + && !isForbiddenField(key); + } + + private static boolean isForbiddenField(String key) { + return "properties".equals(key) + || "constraints".equals(key) + || "allowMultiple".equals(key) + || "options".equals(key) + || "blue".equals(key) + || "$previous".equals(key) + || "$pos".equals(key) + || "$replace".equals(key) + || "$empty".equals(key); + } +} diff --git a/blue-bex-core/src/main/java/blue/bex/type/BexTypeMatchWorkRecorder.java b/blue-bex-core/src/main/java/blue/bex/type/BexTypeMatchWorkRecorder.java new file mode 100644 index 0000000..4d0eb87 --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/type/BexTypeMatchWorkRecorder.java @@ -0,0 +1,122 @@ +package blue.bex.type; + +import blue.bex.BexSourcePath; +import blue.bex.gas.BexGasCounter; +import blue.bex.gas.BexGasMeter; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Objects; + +/** Records the closed, cache-independent gas model for Blue type matching. */ +public final class BexTypeMatchWorkRecorder { + private static final int TEXT_BLOCK_CODE_POINTS = 64; + + private final BexGasMeter gas; + private final BexSourcePath sourcePath; + + public BexTypeMatchWorkRecorder( + BexGasMeter gas, + BexSourcePath sourcePath) { + this.gas = Objects.requireNonNull(gas, "gas"); + this.sourcePath = sourcePath; + } + + public void comparisonNode() { + charge(BexGasCounter.COMPARISON_NODE_VISITED, 1L); + } + + public boolean scalarMatches(Object candidate, Object target) { + if (target == null) { + return true; + } + if (candidate == null) { + return false; + } + if (candidate instanceof Number && target instanceof Number) { + integerLimbs( + integerLimbs(unscaled(candidate)) + + integerLimbs(unscaled(target)) + + (candidate instanceof BigDecimal + || target instanceof BigDecimal ? 1L : 0L)); + return number(candidate).compareTo(number(target)) == 0; + } + if (candidate instanceof String && target instanceof String) { + return compareText((String) candidate, (String) target) == 0; + } + return candidate.equals(target); + } + + /** + * Canonical comparison with each pair of 64-code-point blocks admitted + * before either block is inspected. + */ + public int compareText(String left, String right) { + if (left == right) { + return 0; + } + if (left == null) { + return -1; + } + if (right == null) { + return 1; + } + int leftOffset = 0; + int rightOffset = 0; + while (leftOffset < left.length() + && rightOffset < right.length()) { + charge(BexGasCounter.TEXT_BLOCK_EXAMINED, 2L); + int inBlock = 0; + while (inBlock < TEXT_BLOCK_CODE_POINTS + && leftOffset < left.length() + && rightOffset < right.length()) { + int leftCodePoint = left.codePointAt(leftOffset); + int rightCodePoint = right.codePointAt(rightOffset); + leftOffset += Character.charCount(leftCodePoint); + rightOffset += Character.charCount(rightCodePoint); + if (leftCodePoint != rightCodePoint) { + return Integer.compare(leftCodePoint, rightCodePoint); + } + inBlock++; + } + } + return Integer.compare( + left.length() - leftOffset, + right.length() - rightOffset); + } + + private void integerLimbs(long quantity) { + charge(BexGasCounter.INTEGER_LIMB_OPERATION, quantity); + } + + private void charge(BexGasCounter counter, long quantity) { + if (quantity <= 0L) { + return; + } + gas.charge( + counter, + quantity, + sourcePath, + sourcePath != null ? sourcePath.operator() : null, + counter.canonicalName()); + } + + private static BigDecimal number(Object value) { + if (value instanceof BigDecimal) { + return (BigDecimal) value; + } + if (value instanceof BigInteger) { + return new BigDecimal((BigInteger) value); + } + return new BigDecimal(value.toString()); + } + + private static BigInteger unscaled(Object value) { + return number(value).unscaledValue(); + } + + private static long integerLimbs(BigInteger value) { + int bits = value.abs().bitLength(); + return Math.max(1L, (bits + 31L) / 32L); + } +} diff --git a/blue-bex-core/src/main/java/blue/bex/type/BexTypeMatcher.java b/blue-bex-core/src/main/java/blue/bex/type/BexTypeMatcher.java new file mode 100644 index 0000000..5181def --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/type/BexTypeMatcher.java @@ -0,0 +1,390 @@ +package blue.bex.type; + +import blue.bex.BexExecutionEvidenceUnavailableException; +import blue.bex.BexInvalidExecutionEvidenceException; +import blue.bex.BexSourcePath; +import blue.bex.gas.BexGasLimitExceededException; +import blue.bex.gas.BexGasMeter; +import blue.bex.value.BexBlueNodeWriter; +import blue.bex.value.BexValue; +import blue.bex.value.BexValues; +import blue.language.model.Node; +import blue.language.runtime.BlueLanguage; +import blue.language.snapshot.FrozenNode; +import blue.language.matching.FrozenTypeMatcher; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import blue.bex.type.BexPatternValidator.CandidatePosition; +import blue.bex.type.BexPatternValidator.CandidateView; + +/** + * BEX boundary adapter for Blue's node/type matcher. + */ +public final class BexTypeMatcher { + private final BlueLanguage blue; + private final FrozenTypeMatcher matcher; + private final BexPatternValidator patternValidator = + new BexPatternValidator(); + private final BexFrozenTypeMatcher frozenMatcher; + + public BexTypeMatcher(BlueLanguage blue) { + this.blue = blue != null + ? blue : BlueLanguage.builder().build(); + this.matcher = FrozenTypeMatcher.withVerifiedReferenceMaterializer( + reference -> FrozenNode.fromResolvedNode( + BexValues.referenceBacked( + BexValues.frozen(reference), + this.blue) + .toNode())); + this.frozenMatcher = new BexFrozenTypeMatcher( + matcher, patternValidator); + } + + /** + * Matches a BEX value at the Blue Language boundary while recording the + * canonical BEX comparison work. + * + *

The metering walk deliberately does not depend on + * {@link FrozenTypeMatcher}'s caches. Every semantic occurrence that the + * pattern compares is admitted before that recursive match is performed, + * so warm and cold executions have the same BEX trace.

+ */ + public boolean matches(BexValue value, + FrozenNode pattern, + BexGasMeter gas, + BexSourcePath sourcePath) { + if (value == null || value.isUndefined()) { + return false; + } + if (pattern == null) { + return true; + } + BexTypeMatchWorkRecorder matchGas = + new BexTypeMatchWorkRecorder(gas, sourcePath); + return matchesMetered( + value, + pattern, + matchGas, + CandidatePosition.ROOT); + } + + private static RuntimeException classifiedBoundaryFailure( + RuntimeException failure) { + Throwable current = failure; + while (current != null) { + if (current + instanceof BexExecutionEvidenceUnavailableException + || current + instanceof BexInvalidExecutionEvidenceException + || current + instanceof BexGasLimitExceededException) { + return (RuntimeException) current; + } + Throwable cause = current.getCause(); + if (cause == current) { + break; + } + current = cause; + } + /* + * An unexpected cursor/provider failure is still an execution + * failure. It must never be converted into a semantic non-match. + */ + return failure; + } + + /** + * Walks a candidate cursor lazily. The current occurrence is admitted + * before any semantic cursor access, and a descendant is not converted or + * materialized until its own recursive admission succeeds. + */ + private boolean matchesMetered( + BexValue candidate, + FrozenNode pattern, + BexTypeMatchWorkRecorder gas, + CandidatePosition position) { + gas.comparisonNode(); + return matchesAfterAdmission( + candidate, pattern, gas, position); + } + + private boolean matchesAfterAdmission( + BexValue candidate, + FrozenNode pattern, + BexTypeMatchWorkRecorder gas, + CandidatePosition position) { + if (pattern.isEmptyNode()) { + return true; + } + if (candidate == null + || candidate.isUndefined()) { + return !patternValidator.requiresPresence(pattern); + } + + /* + * Reference-only matching has no nested BEX occurrences. Blue may + * therefore materialize the admitted current occurrence directly. + */ + if (pattern.isReferenceOnly()) { + if (candidate.isExact() + && pattern.getReferenceBlueId().equals( + candidate.exactBlueId())) { + return true; + } + /* + * A transient candidate must still be a valid local Blue shape + * before its identity can be compared. Invalid local content is + * a semantic non-match; provider and cursor failures raised while + * inspecting the admitted occurrence continue to propagate. + */ + if (!candidate.isExact()) { + CandidateView referenceView; + try { + referenceView = CandidateView.from( + candidate, position); + } catch (RuntimeException viewFailure) { + throw classifiedBoundaryFailure( + viewFailure); + } + if (!referenceView.valid) { + return false; + } + } + return matchesAuthoritatively( + candidate, pattern, position); + } + + CandidateView view; + try { + view = CandidateView.from( + candidate, position); + } catch (RuntimeException viewFailure) { + throw classifiedBoundaryFailure(viewFailure); + } + if (!view.valid) { + return false; + } + if (view.materializeCurrent) { + FrozenNode materialized = freezeCandidate( + candidate, position); + return materialized != null + && frozenMatcher.matchesAfterAdmission( + materialized, pattern, gas); + } + + if (!meterScalarComparison( + view.scalar, pattern.getValue(), gas)) { + return false; + } + if (!meterItemType( + view, pattern.getItemType(), gas)) { + return false; + } + if (!meterKeyType( + view, pattern.getKeyType(), gas)) { + return false; + } + if (!meterValueType( + view, pattern.getValueType(), gas)) { + return false; + } + if (!meterItems( + view, pattern.getItems(), gas)) { + return false; + } + if (!meterProperties( + view, pattern.getProperties(), gas)) { + return false; + } + return matchesAuthoritatively( + candidate, pattern, position); + } + + private boolean matchesAuthoritatively( + BexValue candidate, + FrozenNode pattern, + CandidatePosition position) { + FrozenNode frozen = freezeCandidate( + candidate, position); + return frozen != null + && matcher.matchesType(frozen, pattern); + } + + private FrozenNode freezeCandidate( + BexValue candidate, + CandidatePosition position) { + try { + if (candidate.isExact()) { + return FrozenNode.fromResolvedNode( + candidate.toNode()); + } + if (position == CandidatePosition.ROOT) { + return FrozenNode.fromResolvedNode( + BexBlueNodeWriter.toSemanticNode( + candidate)); + } + if (position == CandidatePosition.LIST_ITEM) { + BexValue wrapper = BexValues.list( + Collections.singletonList(candidate)); + FrozenNode frozenWrapper = + FrozenNode.fromResolvedNode( + BexBlueNodeWriter + .toSemanticNode(wrapper)); + return frozenWrapper.getItems().get(0); + } + Map member = + Collections.singletonMap( + "_bexCandidate", candidate); + FrozenNode frozenWrapper = + FrozenNode.fromResolvedNode( + BexBlueNodeWriter.toSemanticNode( + BexValues.map(member))); + return frozenWrapper.getProperties().get( + "_bexCandidate"); + } catch (RuntimeException conversionFailure) { + throw classifiedBoundaryFailure( + conversionFailure); + } + } + + private boolean meterItemType( + CandidateView candidate, + FrozenNode targetItemType, + BexTypeMatchWorkRecorder gas) { + if (targetItemType == null + || candidate.items == null) { + return true; + } + for (int index = 0; + index < candidate.items.size(); + index++) { + gas.comparisonNode(); + BexValue item = candidate.items.get( + String.valueOf(index)); + if (!matchesAfterAdmission( + item, + targetItemType, + gas, + CandidatePosition.LIST_ITEM)) { + return false; + } + } + return true; + } + + private boolean meterKeyType( + CandidateView candidate, + FrozenNode targetKeyType, + BexTypeMatchWorkRecorder gas) { + if (targetKeyType == null) { + return true; + } + for (String key : candidate.propertyKeys) { + gas.comparisonNode(); + if (!patternValidator.keyMatchesType(key, targetKeyType)) { + return false; + } + } + return true; + } + + private boolean meterValueType( + CandidateView candidate, + FrozenNode targetValueType, + BexTypeMatchWorkRecorder gas) { + if (targetValueType == null) { + return true; + } + for (String key : candidate.propertyKeys) { + gas.comparisonNode(); + BexValue property = + candidate.source.get(key); + if (!matchesAfterAdmission( + property, + targetValueType, + gas, + CandidatePosition.OBJECT_MEMBER)) { + return false; + } + } + return true; + } + + private boolean meterItems( + CandidateView candidate, + List targetItems, + BexTypeMatchWorkRecorder gas) { + if (targetItems == null) { + return true; + } + int candidateSize = candidate.items != null + ? candidate.items.size() + : 0; + for (int index = 0; + index < targetItems.size(); + index++) { + FrozenNode targetItem = + targetItems.get(index); + if (index < candidateSize) { + gas.comparisonNode(); + BexValue item = candidate.items.get( + String.valueOf(index)); + if (!matchesAfterAdmission( + item, + targetItem, + gas, + CandidatePosition.LIST_ITEM)) { + return false; + } + } else if (patternValidator.requiresPresence(targetItem)) { + return false; + } + } + return true; + } + + private boolean meterProperties( + CandidateView candidate, + Map targetProperties, + BexTypeMatchWorkRecorder gas) { + if (targetProperties == null) { + return true; + } + for (String key : candidate.propertyKeys) { + FrozenNode targetProperty = + targetProperties.get(key); + if (targetProperty != null) { + gas.comparisonNode(); + BexValue candidateProperty = + candidate.source.get(key); + if (!matchesAfterAdmission( + candidateProperty, + targetProperty, + gas, + CandidatePosition.OBJECT_MEMBER)) { + return false; + } + } + } + for (Map.Entry targetEntry + : targetProperties.entrySet()) { + if (!candidate.hasProperty( + targetEntry.getKey()) + && patternValidator.requiresPresence( + targetEntry.getValue())) { + return false; + } + } + return true; + } + + private boolean meterScalarComparison( + Object candidate, + Object target, + BexTypeMatchWorkRecorder gas) { + return gas.scalarMatches(candidate, target); + } +} diff --git a/blue-bex-core/src/main/java/blue/bex/type/package-info.java b/blue-bex-core/src/main/java/blue/bex/type/package-info.java new file mode 100644 index 0000000..f987261 --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/type/package-info.java @@ -0,0 +1,11 @@ +/** + * Focused adapter from BEX values to Blue Language type and shape matching. + * + *

A matcher boundary uses immutable patterns and run-owned gas/evidence + * state; callers should not assume an instance is safe for concurrent mutable + * sessions. Required matcher and gas inputs are non-null, while documented + * absent candidates/patterns follow BEX matching rules. Provider or validation + * failures propagate rather than becoming a non-match. Comparison occurrences + * are charged before semantic access, and warm caches receive no gas credit.

+ */ +package blue.bex.type; diff --git a/src/main/java/blue/bex/value/AbstractBexValue.java b/blue-bex-core/src/main/java/blue/bex/value/AbstractBexValue.java similarity index 100% rename from src/main/java/blue/bex/value/AbstractBexValue.java rename to blue-bex-core/src/main/java/blue/bex/value/AbstractBexValue.java diff --git a/src/main/java/blue/bex/value/AdmittedExactBexValue.java b/blue-bex-core/src/main/java/blue/bex/value/AdmittedExactBexValue.java similarity index 100% rename from src/main/java/blue/bex/value/AdmittedExactBexValue.java rename to blue-bex-core/src/main/java/blue/bex/value/AdmittedExactBexValue.java diff --git a/src/main/java/blue/bex/value/BexBlueNodeWriter.java b/blue-bex-core/src/main/java/blue/bex/value/BexBlueNodeWriter.java similarity index 100% rename from src/main/java/blue/bex/value/BexBlueNodeWriter.java rename to blue-bex-core/src/main/java/blue/bex/value/BexBlueNodeWriter.java diff --git a/src/main/java/blue/bex/value/BexBlueValueImporter.java b/blue-bex-core/src/main/java/blue/bex/value/BexBlueValueImporter.java similarity index 100% rename from src/main/java/blue/bex/value/BexBlueValueImporter.java rename to blue-bex-core/src/main/java/blue/bex/value/BexBlueValueImporter.java diff --git a/blue-bex-core/src/main/java/blue/bex/value/BexChangesetValueView.java b/blue-bex-core/src/main/java/blue/bex/value/BexChangesetValueView.java new file mode 100644 index 0000000..992bcda --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/value/BexChangesetValueView.java @@ -0,0 +1,8 @@ +package blue.bex.value; + +import java.util.List; + +/** Read-only ordered changeset projection consumed by the value layer. */ +public interface BexChangesetValueView { + List entries(); +} diff --git a/src/main/java/blue/bex/value/BexEquality.java b/blue-bex-core/src/main/java/blue/bex/value/BexEquality.java similarity index 100% rename from src/main/java/blue/bex/value/BexEquality.java rename to blue-bex-core/src/main/java/blue/bex/value/BexEquality.java diff --git a/blue-bex-core/src/main/java/blue/bex/value/BexEventsValueView.java b/blue-bex-core/src/main/java/blue/bex/value/BexEventsValueView.java new file mode 100644 index 0000000..22e7150 --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/value/BexEventsValueView.java @@ -0,0 +1,8 @@ +package blue.bex.value; + +import java.util.List; + +/** Read-only event projection consumed by the value layer. */ +public interface BexEventsValueView { + List events(); +} diff --git a/src/main/java/blue/bex/value/BexFrozenWriter.java b/blue-bex-core/src/main/java/blue/bex/value/BexFrozenWriter.java similarity index 82% rename from src/main/java/blue/bex/value/BexFrozenWriter.java rename to blue-bex-core/src/main/java/blue/bex/value/BexFrozenWriter.java index 0befd5a..97ac29e 100644 --- a/src/main/java/blue/bex/value/BexFrozenWriter.java +++ b/blue-bex-core/src/main/java/blue/bex/value/BexFrozenWriter.java @@ -1,15 +1,14 @@ package blue.bex.value; -import blue.bex.result.BexMetrics; import blue.language.snapshot.FrozenNode; /** * Immutable projection of the single strict Blue output conversion path. */ public final class BexFrozenWriter { - private final BexMetrics metrics; + private final BexValueMetrics metrics; - private BexFrozenWriter(BexMetrics metrics) { + private BexFrozenWriter(BexValueMetrics metrics) { this.metrics = metrics; } @@ -17,7 +16,9 @@ public static FrozenNode toFrozen(BexValue value) { return toFrozen(value, null); } - public static FrozenNode toFrozen(BexValue value, BexMetrics metrics) { + public static FrozenNode toFrozen( + BexValue value, + BexValueMetrics metrics) { return new BexFrozenWriter(metrics).toFrozenValue(value); } diff --git a/src/main/java/blue/bex/value/BexNodeWriter.java b/blue-bex-core/src/main/java/blue/bex/value/BexNodeWriter.java similarity index 100% rename from src/main/java/blue/bex/value/BexNodeWriter.java rename to blue-bex-core/src/main/java/blue/bex/value/BexNodeWriter.java diff --git a/blue-bex-core/src/main/java/blue/bex/value/BexPatchValueView.java b/blue-bex-core/src/main/java/blue/bex/value/BexPatchValueView.java new file mode 100644 index 0000000..325e033 --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/value/BexPatchValueView.java @@ -0,0 +1,8 @@ +package blue.bex.value; + +/** Read-only patch-entry projection consumed by the value layer. */ +public interface BexPatchValueView { + String op(); + String absolutePath(); + BexValue val(); +} diff --git a/src/main/java/blue/bex/value/BexSimpleWriter.java b/blue-bex-core/src/main/java/blue/bex/value/BexSimpleWriter.java similarity index 100% rename from src/main/java/blue/bex/value/BexSimpleWriter.java rename to blue-bex-core/src/main/java/blue/bex/value/BexSimpleWriter.java diff --git a/src/main/java/blue/bex/value/BexTruthiness.java b/blue-bex-core/src/main/java/blue/bex/value/BexTruthiness.java similarity index 100% rename from src/main/java/blue/bex/value/BexTruthiness.java rename to blue-bex-core/src/main/java/blue/bex/value/BexTruthiness.java diff --git a/src/main/java/blue/bex/value/BexUnicodeOrder.java b/blue-bex-core/src/main/java/blue/bex/value/BexUnicodeOrder.java similarity index 100% rename from src/main/java/blue/bex/value/BexUnicodeOrder.java rename to blue-bex-core/src/main/java/blue/bex/value/BexUnicodeOrder.java diff --git a/src/main/java/blue/bex/value/BexValue.java b/blue-bex-core/src/main/java/blue/bex/value/BexValue.java similarity index 91% rename from src/main/java/blue/bex/value/BexValue.java rename to blue-bex-core/src/main/java/blue/bex/value/BexValue.java index 88b9e57..fc8c811 100644 --- a/src/main/java/blue/bex/value/BexValue.java +++ b/blue-bex-core/src/main/java/blue/bex/value/BexValue.java @@ -14,6 +14,11 @@ * {@link Node} or simple Java object is an explicit boundary operation.

*/ public interface BexValue { + /** Returns the closed semantic kind, independent of exactness. */ + default BexValueKind semanticKind() { + return BexValues.semanticKind(this); + } + /** * Whether this value is an already established Blue node. * diff --git a/blue-bex-core/src/main/java/blue/bex/value/BexValueKind.java b/blue-bex-core/src/main/java/blue/bex/value/BexValueKind.java new file mode 100644 index 0000000..4fa8cad --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/value/BexValueKind.java @@ -0,0 +1,24 @@ +package blue.bex.value; + +/** Closed semantic shape/kind model, independent of exactness provenance. */ +public enum BexValueKind { + UNDEFINED("undefined"), + NULL("null"), + BOOLEAN("boolean"), + INTEGER("integer"), + DECIMAL("double"), + TEXT("text"), + OBJECT("object"), + LIST("list"); + + private final String operatorName; + + BexValueKind(String operatorName) { + this.operatorName = operatorName; + } + + /** Existing BEX {@code $kind} spelling; decimal remains {@code double}. */ + public String operatorName() { + return operatorName; + } +} diff --git a/blue-bex-core/src/main/java/blue/bex/value/BexValueMetrics.java b/blue-bex-core/src/main/java/blue/bex/value/BexValueMetrics.java new file mode 100644 index 0000000..8f5b32a --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/value/BexValueMetrics.java @@ -0,0 +1,7 @@ +package blue.bex.value; + +/** Narrow conversion metrics port used by value writers. */ +public interface BexValueMetrics { + void incrementFrozenOutputConversions(); + void incrementFrozenWriterNodeFallbacks(); +} diff --git a/src/main/java/blue/bex/value/BexValues.java b/blue-bex-core/src/main/java/blue/bex/value/BexValues.java similarity index 94% rename from src/main/java/blue/bex/value/BexValues.java rename to blue-bex-core/src/main/java/blue/bex/value/BexValues.java index 43739a9..4fd09ac 100644 --- a/src/main/java/blue/bex/value/BexValues.java +++ b/blue-bex-core/src/main/java/blue/bex/value/BexValues.java @@ -6,8 +6,8 @@ import blue.language.api.BlueOperationResult; import blue.language.model.Node; import blue.language.model.Schema; -import blue.language.processor.ExecutionEvidenceUnavailableException; -import blue.language.processor.InvalidExecutionEvidenceException; +import blue.bex.BexExecutionEvidenceUnavailableException; +import blue.bex.BexInvalidExecutionEvidenceException; import blue.language.snapshot.FrozenNode; import blue.language.merge.ResolvedSnapshot; import blue.language.model.wire.JsonPointer; @@ -180,7 +180,7 @@ private static ResolvedSnapshot loadReference( new Node().blueId(blueId), BlueOperationLimits.demandedPath("")); if (result.outcome() == BlueOperationOutcome.INVALID) { - throw new InvalidExecutionEvidenceException( + throw new BexInvalidExecutionEvidenceException( result.reason().orElse( "Invalid exact reference evidence for " + blueId)); } @@ -188,7 +188,7 @@ private static ResolvedSnapshot loadReference( || !result.value().isPresent()) { java.util.Set outstanding = result.outstandingBlueIds(); - throw new ExecutionEvidenceUnavailableException( + throw new BexExecutionEvidenceUnavailableException( result.reason().orElse( "Exact reference evidence is unavailable for " + blueId), @@ -310,38 +310,43 @@ public static boolean equal(BexValue left, BexValue right) { } public static String kind(BexValue value) { + return semanticKind(value).operatorName(); + } + + /** Returns the closed semantic kind without exposing exactness. */ + public static BexValueKind semanticKind(BexValue value) { if (value == null || value.isUndefined()) { - return "undefined"; + return BexValueKind.UNDEFINED; } if (value.isNull()) { - return "null"; + return BexValueKind.NULL; } if (value.isList()) { - return "list"; + return BexValueKind.LIST; } if (value.isObject()) { - return "object"; + return BexValueKind.OBJECT; } if (!value.isScalar()) { - return "undefined"; + return BexValueKind.UNDEFINED; } Object raw = rawScalar(value); if (raw instanceof Boolean) { - return "boolean"; + return BexValueKind.BOOLEAN; } if (raw instanceof BigInteger || raw instanceof Integer || raw instanceof Long || raw instanceof Short || raw instanceof Byte) { - return "integer"; + return BexValueKind.INTEGER; } if (raw instanceof BigDecimal || raw instanceof Float || raw instanceof Double) { - return "double"; + return BexValueKind.DECIMAL; } - return "text"; + return BexValueKind.TEXT; } static Object scalarSimple(Object value) { diff --git a/src/main/java/blue/bex/value/ChangesetBexValue.java b/blue-bex-core/src/main/java/blue/bex/value/ChangesetBexValue.java similarity index 87% rename from src/main/java/blue/bex/value/ChangesetBexValue.java rename to blue-bex-core/src/main/java/blue/bex/value/ChangesetBexValue.java index ef95ef5..6f7c537 100644 --- a/src/main/java/blue/bex/value/ChangesetBexValue.java +++ b/blue-bex-core/src/main/java/blue/bex/value/ChangesetBexValue.java @@ -1,14 +1,12 @@ package blue.bex.value; -import blue.bex.result.BexChangeset; - /** * BEX list view over an ordered changeset. */ public final class ChangesetBexValue extends AbstractBexValue { - private final BexChangeset changeset; + private final BexChangesetValueView changeset; - public ChangesetBexValue(BexChangeset changeset) { + public ChangesetBexValue(BexChangesetValueView changeset) { this.changeset = changeset; } diff --git a/src/main/java/blue/bex/value/EventsBexValue.java b/blue-bex-core/src/main/java/blue/bex/value/EventsBexValue.java similarity index 88% rename from src/main/java/blue/bex/value/EventsBexValue.java rename to blue-bex-core/src/main/java/blue/bex/value/EventsBexValue.java index 28cf0d5..d3468ba 100644 --- a/src/main/java/blue/bex/value/EventsBexValue.java +++ b/blue-bex-core/src/main/java/blue/bex/value/EventsBexValue.java @@ -1,14 +1,12 @@ package blue.bex.value; -import blue.bex.result.BexEvents; - /** * BEX list view over computed events. */ public final class EventsBexValue extends AbstractBexValue { - private final BexEvents events; + private final BexEventsValueView events; - public EventsBexValue(BexEvents events) { + public EventsBexValue(BexEventsValueView events) { this.events = events; } diff --git a/src/main/java/blue/bex/value/FrozenNodeBexValue.java b/blue-bex-core/src/main/java/blue/bex/value/FrozenNodeBexValue.java similarity index 100% rename from src/main/java/blue/bex/value/FrozenNodeBexValue.java rename to blue-bex-core/src/main/java/blue/bex/value/FrozenNodeBexValue.java diff --git a/src/main/java/blue/bex/value/ListBexValue.java b/blue-bex-core/src/main/java/blue/bex/value/ListBexValue.java similarity index 100% rename from src/main/java/blue/bex/value/ListBexValue.java rename to blue-bex-core/src/main/java/blue/bex/value/ListBexValue.java diff --git a/src/main/java/blue/bex/value/MapBexValue.java b/blue-bex-core/src/main/java/blue/bex/value/MapBexValue.java similarity index 100% rename from src/main/java/blue/bex/value/MapBexValue.java rename to blue-bex-core/src/main/java/blue/bex/value/MapBexValue.java diff --git a/src/main/java/blue/bex/value/NodeBexValue.java b/blue-bex-core/src/main/java/blue/bex/value/NodeBexValue.java similarity index 100% rename from src/main/java/blue/bex/value/NodeBexValue.java rename to blue-bex-core/src/main/java/blue/bex/value/NodeBexValue.java diff --git a/src/main/java/blue/bex/value/NullBexValue.java b/blue-bex-core/src/main/java/blue/bex/value/NullBexValue.java similarity index 100% rename from src/main/java/blue/bex/value/NullBexValue.java rename to blue-bex-core/src/main/java/blue/bex/value/NullBexValue.java diff --git a/src/main/java/blue/bex/value/OverlayListBexValue.java b/blue-bex-core/src/main/java/blue/bex/value/OverlayListBexValue.java similarity index 100% rename from src/main/java/blue/bex/value/OverlayListBexValue.java rename to blue-bex-core/src/main/java/blue/bex/value/OverlayListBexValue.java diff --git a/src/main/java/blue/bex/value/OverlayMapBexValue.java b/blue-bex-core/src/main/java/blue/bex/value/OverlayMapBexValue.java similarity index 100% rename from src/main/java/blue/bex/value/OverlayMapBexValue.java rename to blue-bex-core/src/main/java/blue/bex/value/OverlayMapBexValue.java diff --git a/src/main/java/blue/bex/value/PatchEntryBexValue.java b/blue-bex-core/src/main/java/blue/bex/value/PatchEntryBexValue.java similarity index 90% rename from src/main/java/blue/bex/value/PatchEntryBexValue.java rename to blue-bex-core/src/main/java/blue/bex/value/PatchEntryBexValue.java index 5968205..84ba737 100644 --- a/src/main/java/blue/bex/value/PatchEntryBexValue.java +++ b/blue-bex-core/src/main/java/blue/bex/value/PatchEntryBexValue.java @@ -1,7 +1,5 @@ package blue.bex.value; -import blue.bex.result.BexPatchEntry; - import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -10,9 +8,9 @@ * BEX object view over one patch entry. */ public final class PatchEntryBexValue extends AbstractBexValue { - private final BexPatchEntry entry; + private final BexPatchValueView entry; - public PatchEntryBexValue(BexPatchEntry entry) { + public PatchEntryBexValue(BexPatchValueView entry) { this.entry = entry; } diff --git a/src/main/java/blue/bex/value/PointerSetBexValue.java b/blue-bex-core/src/main/java/blue/bex/value/PointerSetBexValue.java similarity index 100% rename from src/main/java/blue/bex/value/PointerSetBexValue.java rename to blue-bex-core/src/main/java/blue/bex/value/PointerSetBexValue.java diff --git a/src/main/java/blue/bex/value/ScalarBexValue.java b/blue-bex-core/src/main/java/blue/bex/value/ScalarBexValue.java similarity index 100% rename from src/main/java/blue/bex/value/ScalarBexValue.java rename to blue-bex-core/src/main/java/blue/bex/value/ScalarBexValue.java diff --git a/src/main/java/blue/bex/value/UndefinedBexValue.java b/blue-bex-core/src/main/java/blue/bex/value/UndefinedBexValue.java similarity index 100% rename from src/main/java/blue/bex/value/UndefinedBexValue.java rename to blue-bex-core/src/main/java/blue/bex/value/UndefinedBexValue.java diff --git a/blue-bex-core/src/main/java/blue/bex/value/package-info.java b/blue-bex-core/src/main/java/blue/bex/value/package-info.java new file mode 100644 index 0000000..c6ad60d --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/value/package-info.java @@ -0,0 +1,13 @@ +/** + * Representation-blind BEX values, exact Blue cursors, transient collections, + * overlays, conversion helpers, equality, truthiness, and deterministic order. + * + *

Immutable exact/scalar values may be shared; overlays, lazy materializers, + * and writers are owned by the execution that creates them unless documented + * otherwise. Java null is accepted only by factories that explicitly map it to + * BEX null/undefined. Invalid conversion or unavailable/invalid reference + * evidence fails instead of fabricating a value. Merely carrying an exact value + * has no recursive gas cost; callers charge actual reads, traversal, + * construction, comparison, and output work before invoking it.

+ */ +package blue.bex.value; diff --git a/src/main/resources/blue/bex/gas/blue-bex-gas-2.0.yaml b/blue-bex-core/src/main/resources/blue/bex/gas/blue-bex-gas-2.0.yaml similarity index 100% rename from src/main/resources/blue/bex/gas/blue-bex-gas-2.0.yaml rename to blue-bex-core/src/main/resources/blue/bex/gas/blue-bex-gas-2.0.yaml diff --git a/blue-bex-java/build.gradle.kts b/blue-bex-java/build.gradle.kts new file mode 100644 index 0000000..fd384a2 --- /dev/null +++ b/blue-bex-java/build.gradle.kts @@ -0,0 +1,22 @@ +plugins { + id("blue.bex.java8-library") + id("blue.bex.language-dependencies") + id("blue.bex.reproducible-archives") + id("blue.bex.publication") +} + +description = "One-coordinate aggregate for Blue BEX core and Contracts hosting" + +base { + archivesName.set("blue-bex-java") +} + +dependencies { + api(project(":blue-bex-core")) + api(project(":blue-bex-contracts")) +} + +tasks.named( + "java8BytecodeCheck") { + allowEmpty.set(true) +} diff --git a/blue-bex-java/src/main/java/.gitkeep b/blue-bex-java/src/main/java/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/blue-bex-java/src/main/java/.gitkeep @@ -0,0 +1 @@ + diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts new file mode 100644 index 0000000..9eeca6c --- /dev/null +++ b/build-logic/build.gradle.kts @@ -0,0 +1,81 @@ +plugins { + `java-gradle-plugin` +} + +group = "blue.bex.buildlogic" + +repositories { + gradlePluginPortal() + mavenCentral() +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } +} + +dependencies { + implementation("me.champeau.jmh:me.champeau.jmh.gradle.plugin:0.7.3") + implementation("org.jreleaser:org.jreleaser.gradle.plugin:1.24.0") +} + +gradlePlugin { + plugins { + register("java8Library") { + id = "blue.bex.java8-library" + implementationClass = + "blue.bex.buildlogic.Java8LibraryConventionsPlugin" + } + register("languageDependencies") { + id = "blue.bex.language-dependencies" + implementationClass = + "blue.bex.buildlogic.LanguageDependencyModePlugin" + } + register("conformance") { + id = "blue.bex.conformance" + implementationClass = + "blue.bex.buildlogic.ConformanceConventionsPlugin" + } + register("apiEvidence") { + id = "blue.bex.api-evidence" + implementationClass = + "blue.bex.buildlogic.ApiEvidencePlugin" + } + register("reproducibleArchives") { + id = "blue.bex.reproducible-archives" + implementationClass = + "blue.bex.buildlogic.ReproducibleArchivesPlugin" + } + register("releaseEvidence") { + id = "blue.bex.release-evidence" + implementationClass = + "blue.bex.buildlogic.ReleaseEvidencePlugin" + } + register("architecture") { + id = "blue.bex.architecture" + implementationClass = + "blue.bex.buildlogic.ArchitectureVerificationPlugin" + } + register("jmh") { + id = "blue.bex.jmh" + implementationClass = + "blue.bex.buildlogic.JmhConventionsPlugin" + } + register("publication") { + id = "blue.bex.publication" + implementationClass = + "blue.bex.buildlogic.PublicationConventionsPlugin" + } + register("rootOrchestration") { + id = "blue.bex.root-orchestration" + implementationClass = + "blue.bex.buildlogic.RootOrchestrationPlugin" + } + } +} + +tasks.withType().configureEach { + options.encoding = "UTF-8" + options.release.set(17) +} diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts new file mode 100644 index 0000000..892b96d --- /dev/null +++ b/build-logic/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "blue-bex-build-logic" diff --git a/build-logic/src/main/java/blue/bex/buildlogic/ApiEvidencePlugin.java b/build-logic/src/main/java/blue/bex/buildlogic/ApiEvidencePlugin.java new file mode 100644 index 0000000..929348c --- /dev/null +++ b/build-logic/src/main/java/blue/bex/buildlogic/ApiEvidencePlugin.java @@ -0,0 +1,49 @@ +package blue.bex.buildlogic; + +import blue.bex.buildlogic.tasks.GenerateApiClassificationTask; +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.tasks.TaskProvider; + +/** Provides stable lifecycle names for API descriptors and migration ledgers. */ +public final class ApiEvidencePlugin implements Plugin { + @Override + public void apply(Project project) { + TaskProvider classification = + project.getTasks().register( + "generateApiClassification", + GenerateApiClassificationTask.class, + task -> { + task.setGroup("verification"); + task.setDescription( + "Classifies every same-run public BEX descriptor."); + task.getManifestFile().set( + project.getLayout().getBuildDirectory().file( + "reports/bex-release/public-api.txt")); + task.getOutputFile().set( + project.getLayout().getBuildDirectory().file( + "reports/bex-release/" + + "public-api-classification.json")); + }); + TaskProvider evidence = + project.getTasks().register("bexApiEvidence", task -> { + task.setGroup("verification"); + task.setDescription( + "Generates and verifies the BEX API descriptor, " + + "classification, and migration ledger."); + task.dependsOn(classification); + }); + project.afterEvaluate(ignored -> { + if (project.getTasks().findByName("binaryApiCheck") != null) { + evidence.configure(owner -> owner.dependsOn("binaryApiCheck")); + } + if (project.getTasks().findByName("generateBinaryApiManifest") + != null) { + evidence.configure(owner -> + owner.dependsOn("generateBinaryApiManifest")); + classification.configure(owner -> + owner.dependsOn("generateBinaryApiManifest")); + } + }); + } +} diff --git a/build-logic/src/main/java/blue/bex/buildlogic/ArchitectureVerificationPlugin.java b/build-logic/src/main/java/blue/bex/buildlogic/ArchitectureVerificationPlugin.java new file mode 100644 index 0000000..da93064 --- /dev/null +++ b/build-logic/src/main/java/blue/bex/buildlogic/ArchitectureVerificationPlugin.java @@ -0,0 +1,55 @@ +package blue.bex.buildlogic; + +import blue.bex.buildlogic.tasks.VerifyBexArchitectureTask; +import java.util.Arrays; +import org.gradle.api.Plugin; +import org.gradle.api.Project; + +/** Registers the fail-closed architecture and cohesion gate. */ +public final class ArchitectureVerificationPlugin implements Plugin { + @Override + public void apply(Project project) { + project.getTasks().register( + "verifyBexArchitecture", + VerifyBexArchitectureTask.class, + task -> { + task.setGroup("verification"); + task.getRepositoryDirectory().set( + project.getRootProject().getLayout() + .getProjectDirectory()); + task.getArchitectureInputs().from( + project.getRootProject().fileTree( + project.getRootProject().getProjectDir(), spec -> { + spec.include( + "blue-bex-core/src/main/java/**/*.java", + "blue-bex-contracts/src/main/java/**/*.java", + "blue-bex-conformance/src/main/java/**/*.java", + "blue-bex-java/src/main/java/**/*.java", + "examples/src/main/java/**/*.java", + "blue-bex-core/build.gradle.kts", + "blue-bex-contracts/build.gradle.kts", + "blue-bex-conformance/build.gradle.kts", + "blue-bex-java/build.gradle.kts", + "examples/build.gradle.kts", + "build.gradle.kts"); + })); + task.getModuleNames().set(Arrays.asList( + "blue-bex-core", + "blue-bex-contracts", + "blue-bex-conformance", + "blue-bex-java", + "examples")); + task.getModuleEdges().set(Arrays.asList( + "blue-bex-contracts->blue-bex-core", + "blue-bex-conformance->blue-bex-core", + "blue-bex-conformance->blue-bex-contracts", + "blue-bex-conformance->blue-bex-java", + "blue-bex-java->blue-bex-core", + "blue-bex-java->blue-bex-contracts", + "examples->blue-bex-java")); + task.getOutputFile().set( + project.getLayout().getBuildDirectory().file( + "reports/bex-modernization/architecture.json")); + }); + } +} diff --git a/build-logic/src/main/java/blue/bex/buildlogic/ConformanceConventionsPlugin.java b/build-logic/src/main/java/blue/bex/buildlogic/ConformanceConventionsPlugin.java new file mode 100644 index 0000000..7dce1d0 --- /dev/null +++ b/build-logic/src/main/java/blue/bex/buildlogic/ConformanceConventionsPlugin.java @@ -0,0 +1,25 @@ +package blue.bex.buildlogic; + +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.tasks.TaskProvider; +import org.gradle.api.tasks.testing.Test; + +/** Owns the complete, non-skipping BEX conformance lifecycle. */ +public final class ConformanceConventionsPlugin implements Plugin { + @Override + public void apply(Project project) { + project.getPluginManager().apply(Java8LibraryConventionsPlugin.class); + TaskProvider test = project.getTasks().named("test", Test.class); + test.configure(task -> { + task.setFailFast(false); + task.getOutputs().upToDateWhen(ignored -> false); + }); + project.getTasks().register("bexConformance", task -> { + task.setGroup("verification"); + task.setDescription( + "Runs the complete BEX semantic, fixture, gas, and hosted suite."); + task.dependsOn(test); + }); + } +} diff --git a/build-logic/src/main/java/blue/bex/buildlogic/Java8LibraryConventionsPlugin.java b/build-logic/src/main/java/blue/bex/buildlogic/Java8LibraryConventionsPlugin.java new file mode 100644 index 0000000..2e08baf --- /dev/null +++ b/build-logic/src/main/java/blue/bex/buildlogic/Java8LibraryConventionsPlugin.java @@ -0,0 +1,91 @@ +package blue.bex.buildlogic; + +import blue.bex.buildlogic.tasks.VerifyJava8BytecodeTask; +import blue.bex.buildlogic.tasks.VerifyLegacyLanguageImportsTask; +import java.nio.charset.StandardCharsets; +import org.gradle.api.JavaVersion; +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.plugins.JavaLibraryPlugin; +import org.gradle.api.plugins.JavaPluginExtension; +import org.gradle.jvm.toolchain.JavaLanguageVersion; +import org.gradle.jvm.toolchain.JavaToolchainService; +import org.gradle.api.tasks.compile.JavaCompile; +import org.gradle.api.tasks.javadoc.Javadoc; +import org.gradle.api.tasks.testing.Test; +import org.gradle.api.tasks.TaskProvider; +import org.gradle.external.javadoc.StandardJavadocDocletOptions; + +/** Shared Java 8, testing, and documentation policy for every BEX module. */ +public final class Java8LibraryConventionsPlugin implements Plugin { + @Override + public void apply(Project project) { + project.getPluginManager().apply(JavaLibraryPlugin.class); + + JavaPluginExtension java = + project.getExtensions().getByType(JavaPluginExtension.class); + JavaToolchainService toolchains = + project.getExtensions().getByType(JavaToolchainService.class); + java.setSourceCompatibility(JavaVersion.VERSION_1_8); + java.setTargetCompatibility(JavaVersion.VERSION_1_8); + java.withSourcesJar(); + java.withJavadocJar(); + + project.getTasks().withType(JavaCompile.class).configureEach(task -> { + task.getOptions().setEncoding(StandardCharsets.UTF_8.name()); + task.getOptions().getRelease().set(8); + }); + project.getTasks().withType(Javadoc.class).configureEach(task -> { + task.getJavadocTool().set(toolchains.javadocToolFor(spec -> + spec.getLanguageVersion().set(JavaLanguageVersion.of(8)))); + StandardJavadocDocletOptions options = + (StandardJavadocDocletOptions) task.getOptions(); + options.setEncoding(StandardCharsets.UTF_8.name()); + options.setCharSet(StandardCharsets.UTF_8.name()); + options.addBooleanOption("notimestamp", true); + }); + project.getTasks().withType(Test.class).configureEach(task -> { + task.useJUnitPlatform(); + task.getTestLogging().events("failed", "skipped"); + }); + + project.getDependencies().add( + "testImplementation", + project.getDependencies().platform( + "org.junit:junit-bom:5.10.2")); + project.getDependencies().add( + "testImplementation", "org.junit.jupiter:junit-jupiter"); + project.getDependencies().add( + "testRuntimeOnly", + "org.junit.platform:junit-platform-launcher"); + + TaskProvider bytecode = + project.getTasks().register( + "java8BytecodeCheck", + VerifyJava8BytecodeTask.class, + task -> { + task.setGroup("verification"); + task.setDescription( + "Verifies all production classes use Java 8 bytecode."); + task.getAllowEmpty().convention(false); + task.dependsOn("classes"); + task.getClassDirectories().from( + java.getSourceSets().getByName("main") + .getOutput().getClassesDirs()); + }); + TaskProvider imports = + project.getTasks().register( + "verifyLegacyLanguageImports", + VerifyLegacyLanguageImportsTask.class, + task -> { + task.setGroup("verification"); + task.setDescription( + "Rejects imports removed by modular Blue Language."); + task.getSourceDirectories().from( + java.getSourceSets().getByName("main") + .getAllJava().getSourceDirectories()); + }); + project.getTasks().named("check", task -> task.dependsOn( + bytecode, imports)); + } +} diff --git a/build-logic/src/main/java/blue/bex/buildlogic/JmhConventionsPlugin.java b/build-logic/src/main/java/blue/bex/buildlogic/JmhConventionsPlugin.java new file mode 100644 index 0000000..59af32d --- /dev/null +++ b/build-logic/src/main/java/blue/bex/buildlogic/JmhConventionsPlugin.java @@ -0,0 +1,116 @@ +package blue.bex.buildlogic; + +import blue.bex.buildlogic.tasks.GenerateBenchmarkEnvironmentTask; +import java.util.Arrays; +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.plugins.JavaPluginExtension; +import org.gradle.api.tasks.JavaExec; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.TaskProvider; + +/** + * Adds a dedicated JMH source set and both serious and bounded evidence runs. + * + *

The convention deliberately avoids reflection and an opaque third-party + * Gradle plugin. The benchmark compiler and runner therefore have the same + * typed, reviewable dependency graph as the rest of the build. + */ +public final class JmhConventionsPlugin implements Plugin { + private static final String JMH_VERSION = "1.37"; + private static final int SERIOUS_FORKS = 2; + private static final int SERIOUS_WARMUPS = 3; + private static final int SERIOUS_MEASUREMENTS = 5; + private static final String SERIOUS_DURATION = "250ms"; + + @Override + public void apply(Project project) { + project.getPluginManager().apply(Java8LibraryConventionsPlugin.class); + JavaPluginExtension java = + project.getExtensions().getByType(JavaPluginExtension.class); + SourceSet main = java.getSourceSets().getByName(SourceSet.MAIN_SOURCE_SET_NAME); + SourceSet test = java.getSourceSets().getByName(SourceSet.TEST_SOURCE_SET_NAME); + SourceSet jmh = java.getSourceSets().create("jmh", sourceSet -> { + sourceSet.setCompileClasspath( + sourceSet.getCompileClasspath() + .plus(main.getOutput()) + .plus(test.getOutput())); + sourceSet.setRuntimeClasspath( + sourceSet.getRuntimeClasspath() + .plus(sourceSet.getOutput()) + .plus(main.getOutput()) + .plus(test.getOutput())); + }); + + project.getConfigurations().getByName(jmh.getImplementationConfigurationName()) + .extendsFrom(project.getConfigurations().getByName( + test.getImplementationConfigurationName())); + project.getDependencies().add( + jmh.getImplementationConfigurationName(), + "org.openjdk.jmh:jmh-core:" + JMH_VERSION); + project.getDependencies().add( + jmh.getAnnotationProcessorConfigurationName(), + "org.openjdk.jmh:jmh-generator-annprocess:" + JMH_VERSION); + + TaskProvider environment = + project.getTasks().register( + "jmhEnvironment", + GenerateBenchmarkEnvironmentTask.class, + task -> { + task.setGroup("benchmark"); + task.getForks().set(SERIOUS_FORKS); + task.getWarmups().set(SERIOUS_WARMUPS); + task.getMeasurements().set(SERIOUS_MEASUREMENTS); + task.getDuration().set(SERIOUS_DURATION); + task.getOutputFile().set( + project.getLayout().getBuildDirectory().file( + "reports/jmh/environment.json")); + }); + TaskProvider campaign = project.getTasks().register( + "jmh", JavaExec.class, task -> { + task.setGroup("benchmark"); + task.setDescription( + "Runs the serious BEX JMH campaign with allocation " + + "profiling and writes JSON evidence."); + task.dependsOn(jmh.getClassesTaskName(), environment); + task.setClasspath(jmh.getRuntimeClasspath()); + task.getMainClass().set("org.openjdk.jmh.Main"); + task.doFirst(ignored -> { + project.getLayout().getBuildDirectory().dir( + "reports/jmh").get().getAsFile().mkdirs(); + task.setArgs(Arrays.asList( + "-f", String.valueOf(SERIOUS_FORKS), + "-wi", String.valueOf(SERIOUS_WARMUPS), + "-i", String.valueOf(SERIOUS_MEASUREMENTS), + "-w", SERIOUS_DURATION, + "-r", SERIOUS_DURATION, + "-prof", "gc", "-foe", "true", + "-rf", "json", "-rff", + project.getLayout().getBuildDirectory().file( + "reports/jmh/results.json") + .get().getAsFile().getAbsolutePath())); + }); + }); + + project.getTasks().register("jmhSmoke", JavaExec.class, task -> { + task.setGroup("verification"); + task.setDescription( + "Runs every BEX benchmark with bounded iterations as same-run " + + "correctness and gas-identity evidence."); + task.dependsOn(jmh.getClassesTaskName()); + task.setClasspath(jmh.getRuntimeClasspath()); + task.getMainClass().set("org.openjdk.jmh.Main"); + task.doFirst(ignored -> { + project.getLayout().getBuildDirectory().dir( + "reports/jmh").get().getAsFile().mkdirs(); + task.setArgs(Arrays.asList( + "-f", "1", "-wi", "1", "-i", "1", + "-w", "100ms", "-r", "100ms", "-foe", "true", + "-rf", "json", "-rff", + project.getLayout().getBuildDirectory().file( + "reports/jmh/smoke-results.json") + .get().getAsFile().getAbsolutePath())); + }); + }); + } +} diff --git a/build-logic/src/main/java/blue/bex/buildlogic/LanguageDependencyModeExtension.java b/build-logic/src/main/java/blue/bex/buildlogic/LanguageDependencyModeExtension.java new file mode 100644 index 0000000..a8e4e7d --- /dev/null +++ b/build-logic/src/main/java/blue/bex/buildlogic/LanguageDependencyModeExtension.java @@ -0,0 +1,20 @@ +package blue.bex.buildlogic; + +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.provider.Property; + +/** Typed coordinates and dependency mode shared by BEX module builds. */ +public abstract class LanguageDependencyModeExtension { + public LanguageDependencyModeExtension(ObjectFactory objects) { + getVersion().convention("3.1.0-rc.19"); + getCompositePropertyName().convention("blueLanguageCompositePath"); + } + + public abstract Property getVersion(); + + public abstract Property getCompositePropertyName(); + + public String coordinate(String artifact) { + return "blue.language:" + artifact + ":" + getVersion().get(); + } +} diff --git a/build-logic/src/main/java/blue/bex/buildlogic/LanguageDependencyModePlugin.java b/build-logic/src/main/java/blue/bex/buildlogic/LanguageDependencyModePlugin.java new file mode 100644 index 0000000..a0aec80 --- /dev/null +++ b/build-logic/src/main/java/blue/bex/buildlogic/LanguageDependencyModePlugin.java @@ -0,0 +1,109 @@ +package blue.bex.buildlogic; + +import blue.bex.buildlogic.tasks.GenerateDependencyEvidenceTask; +import java.io.File; +import org.gradle.api.GradleException; +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.artifacts.Configuration; + +/** Establishes explicit local-composite versus published Language policy. */ +public final class LanguageDependencyModePlugin implements Plugin { + public static final String EXTENSION = "bexLanguage"; + + @Override + public void apply(Project project) { + LanguageDependencyModeExtension extension = + project.getExtensions().create( + EXTENSION, + LanguageDependencyModeExtension.class); + project.getRepositories().mavenCentral(); + + project.getPluginManager().withPlugin("java", ignored -> + project.getTasks().register( + "writeLanguageDependencyEvidence", + GenerateDependencyEvidenceTask.class, + task -> { + task.setGroup("verification"); + task.setDescription( + "Records exact resolved Language/module JAR hashes."); + String property = + extension.getCompositePropertyName().get(); + String composite = (String) project.findProperty(property); + boolean local = composite != null + && !composite.trim().isEmpty(); + task.getMode().set(local + ? "local-composite" : "standalone-published"); + task.getModuleName().set(project.getName()); + task.getDeclaredLanguageVersion().set( + extension.getVersion()); + File groupCache = new File( + project.getGradle().getGradleUserHomeDir(), + "caches/modules-2/files-2.1/blue.language"); + String version = extension.getVersion().get(); + String[] focusedModules = { + "blue-language-model", + "blue-language-core", + "blue-language-mapping", + "blue-contracts-core", + "blue-language-java" + }; + boolean initiallyAbsent = true; + for (String module : focusedModules) { + if (new File(new File(groupCache, module), version) + .exists()) { + initiallyAbsent = false; + } + } + task.getExactVersionCacheInitiallyAbsent().set( + initiallyAbsent); + task.getBexCheckout().set( + project.getRootProject().getLayout() + .getProjectDirectory()); + if (local) { + task.getLanguageCheckout().set( + project.getLayout().dir( + project.provider(() -> + project.file(composite.trim())))); + } + Configuration runtime = project.getConfigurations() + .getByName("runtimeClasspath"); + task.getArtifacts().from(runtime); + task.getResolvedComponents().set(project.provider(() -> + runtime.getIncoming().getResolutionResult() + .getAllComponents().stream() + .map(component -> component.getId() + .getDisplayName()) + .sorted() + .collect(java.util.stream.Collectors + .toList()))); + task.getOutputFile().set( + project.getLayout().getBuildDirectory().file( + "reports/dependencies/language.json")); + task.getOutputs().upToDateWhen(element -> false); + })); + + project.getTasks().register("verifyLanguageDependencyMode", task -> { + task.setGroup("verification"); + task.setDescription( + "Validates the explicit local-composite or published " + + "Blue Language dependency mode."); + task.doLast(ignored -> { + String property = extension.getCompositePropertyName().get(); + String value = (String) project.findProperty(property); + if (value != null && !value.trim().isEmpty()) { + File checkout = project.file(value.trim()); + if (!checkout.isDirectory()) { + throw new GradleException( + property + " is not a directory: " + checkout); + } + } + if (project.getRepositories().stream().anyMatch(repository -> + repository.getName().equalsIgnoreCase("MavenLocal"))) { + throw new GradleException( + "mavenLocal() is forbidden for BEX Language resolution"); + } + }); + }); + } +} diff --git a/build-logic/src/main/java/blue/bex/buildlogic/PublicationConventionsPlugin.java b/build-logic/src/main/java/blue/bex/buildlogic/PublicationConventionsPlugin.java new file mode 100644 index 0000000..2512df3 --- /dev/null +++ b/build-logic/src/main/java/blue/bex/buildlogic/PublicationConventionsPlugin.java @@ -0,0 +1,71 @@ +package blue.bex.buildlogic; + +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.publish.PublishingExtension; +import org.gradle.api.publish.maven.MavenPublication; +import org.gradle.api.publish.maven.tasks.AbstractPublishToMaven; + +/** Configures module Maven publications without introducing local repositories. */ +public final class PublicationConventionsPlugin implements Plugin { + @Override + public void apply(Project project) { + project.getPluginManager().apply("maven-publish"); + project.getPluginManager().apply(ReproducibleArchivesPlugin.class); + project.getPluginManager().withPlugin("java", ignored -> { + PublishingExtension publishing = + project.getExtensions().getByType( + PublishingExtension.class); + if (publishing.getPublications().findByName("mavenJava") + == null) { + publishing.getPublications().create( + "mavenJava", MavenPublication.class, + publication -> { + publication.from( + project.getComponents().getByName("java")); + publication.getPom().getName().set( + "Blue BEX " + project.getName()); + publication.getPom().getDescription().set( + project.provider(() -> + project.getDescription())); + publication.getPom().getUrl().set( + "https://timeline.blue"); + publication.getPom().licenses(licenses -> + licenses.license(license -> { + license.getName().set("MIT License"); + license.getUrl().set( + "https://github.com/" + + "bluecontract/blue-bex-java/" + + "blob/main/LICENSE"); + })); + publication.getPom().developers(developers -> + developers.developer(developer -> { + developer.getName().set("Blue"); + developer.getEmail().set( + "devsupport@timeline.blue"); + })); + publication.getPom().scm(scm -> { + scm.getUrl().set("https://github.com/" + + "bluecontract/blue-bex-java.git"); + scm.getConnection().set("scm:git:" + + "git@github.com:bluecontract/" + + "blue-bex-java.git"); + scm.getDeveloperConnection().set( + "scm:git:ssh://git@github.com/" + + "bluecontract/" + + "blue-bex-java.git"); + }); + }); + } + publishing.getRepositories().maven(repository -> { + repository.setName("staging"); + repository.setUrl(project.getRootProject().getLayout() + .getBuildDirectory().dir("staging-deploy")); + }); + }); + project.getTasks().withType(AbstractPublishToMaven.class) + .configureEach(task -> task.dependsOn( + project.getRootProject().getTasks() + .named("bexReleaseVerify"))); + } +} diff --git a/build-logic/src/main/java/blue/bex/buildlogic/ReleaseEvidencePlugin.java b/build-logic/src/main/java/blue/bex/buildlogic/ReleaseEvidencePlugin.java new file mode 100644 index 0000000..31bc90b --- /dev/null +++ b/build-logic/src/main/java/blue/bex/buildlogic/ReleaseEvidencePlugin.java @@ -0,0 +1,90 @@ +package blue.bex.buildlogic; + +import blue.bex.buildlogic.tasks.GenerateSourceFingerprintTask; +import blue.bex.buildlogic.tasks.VerifyPublishedLanguageTask; +import java.io.File; +import java.util.Arrays; +import java.util.Collections; +import java.util.regex.Pattern; +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.tasks.TaskProvider; + +/** Typed source and published-dependency evidence used by release gates. */ +public final class ReleaseEvidencePlugin implements Plugin { + @Override + public void apply(Project project) { + TaskProvider source = + project.getTasks().register( + "generateBexSourceFingerprint", + GenerateSourceFingerprintTask.class, + task -> { + task.setGroup("verification"); + task.getRepositoryDirectory().set( + project.getRootProject().getLayout() + .getProjectDirectory()); + task.getOutputFile().set( + project.getLayout().getBuildDirectory().file( + "reports/bex-modernization/source.json")); + task.getOutputs().upToDateWhen(ignored -> false); + }); + TaskProvider published = + project.getTasks().register( + "bexPublishedLanguageVerification", + VerifyPublishedLanguageTask.class, + task -> { + task.setGroup("verification"); + task.getRequired().convention(false); + task.getCoordinate().convention( + project.getProviders().gradleProperty( + "bexPublishedLanguageCoordinate")); + task.getArtifactSha256().convention( + project.getProviders().gradleProperty( + "bexPublishedLanguageSha256")); + task.getInspectionFile().set( + project.getLayout().getProjectDirectory().file( + "src/test/resources/hosted-release/" + + "published-api-inspection." + + "properties")); + task.getArtifacts().from(project.getProviders() + .gradleProperty( + "bexPublishedLanguageArtifacts") + .orElse("") + .map(value -> Arrays.asList(value.split( + Pattern.quote(File.pathSeparator)))) + .map(values -> values.size() == 1 + && values.get(0).trim().isEmpty() + ? Collections.emptyList() : values)); + task.getArtifacts().from(project.fileTree( + project.getLayout().getBuildDirectory().dir( + "reports/bex-release/inputs/" + + "published-artifacts"), + spec -> spec.include("*.jar"))); + Object differential = project.findProperty( + "bexLocalPublishedDifferential"); + if (differential != null + && !differential.toString().trim().isEmpty()) { + task.getDifferentialReport().fileValue( + project.file(differential.toString())); + } else { + File retained = project.getLayout() + .getBuildDirectory().file( + "reports/bex-release/inputs/" + + "local-published-" + + "differential.json") + .get().getAsFile(); + if (retained.isFile()) { + task.getDifferentialReport().fileValue(retained); + } + } + task.getOutputFile().set( + project.getLayout().getBuildDirectory().file( + "reports/bex-modernization/" + + "published-language.json")); + }); + project.getTasks().register("bexReleaseEvidence", task -> { + task.setGroup("verification"); + task.dependsOn(source, published); + }); + } +} diff --git a/build-logic/src/main/java/blue/bex/buildlogic/ReproducibleArchivesPlugin.java b/build-logic/src/main/java/blue/bex/buildlogic/ReproducibleArchivesPlugin.java new file mode 100644 index 0000000..40a75f6 --- /dev/null +++ b/build-logic/src/main/java/blue/bex/buildlogic/ReproducibleArchivesPlugin.java @@ -0,0 +1,108 @@ +package blue.bex.buildlogic; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import org.gradle.api.GradleException; +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.plugins.JavaPluginExtension; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.TaskProvider; +import org.gradle.api.tasks.bundling.AbstractArchiveTask; +import org.gradle.api.tasks.bundling.Jar; +import org.gradle.api.tasks.javadoc.Javadoc; + +/** Applies deterministic timestamp and entry-order policy to BEX archives. */ +public final class ReproducibleArchivesPlugin implements Plugin { + @Override + public void apply(Project project) { + project.getTasks().withType(AbstractArchiveTask.class) + .configureEach(task -> { + task.setPreserveFileTimestamps(false); + task.setReproducibleFileOrder(true); + }); + project.getPluginManager().withPlugin("java", ignored -> + configureReplicaVerification(project)); + } + + private static void configureReplicaVerification(Project project) { + JavaPluginExtension java = + project.getExtensions().getByType(JavaPluginExtension.class); + SourceSet main = java.getSourceSets().getByName(SourceSet.MAIN_SOURCE_SET_NAME); + TaskProvider primary = project.getTasks().named("jar", Jar.class); + TaskProvider replica = project.getTasks().register( + "replicaJar", Jar.class, task -> { + task.setGroup("verification"); + task.from(main.getOutput()); + task.getDestinationDirectory().set( + project.getLayout().getBuildDirectory().dir( + "reproducibility/main")); + task.getArchiveFileName().set( + primary.flatMap(Jar::getArchiveFileName)); + task.doFirst(unused -> task.getManifest().from( + primary.get().getManifest())); + }); + TaskProvider sources = + project.getTasks().named("sourcesJar", Jar.class); + TaskProvider sourcesReplica = project.getTasks().register( + "replicaSourcesJar", Jar.class, task -> { + task.setGroup("verification"); + task.from(main.getAllSource()); + task.getDestinationDirectory().set( + project.getLayout().getBuildDirectory().dir( + "reproducibility/sources")); + task.getArchiveFileName().set( + sources.flatMap(Jar::getArchiveFileName)); + task.doFirst(unused -> task.getManifest().from( + sources.get().getManifest())); + }); + TaskProvider javadoc = + project.getTasks().named("javadoc", Javadoc.class); + TaskProvider javadocPrimary = + project.getTasks().named("javadocJar", Jar.class); + TaskProvider javadocReplica = project.getTasks().register( + "replicaJavadocJar", Jar.class, task -> { + task.setGroup("verification"); + task.dependsOn(javadoc); + task.from(javadoc.map(Javadoc::getDestinationDir)); + task.getDestinationDirectory().set( + project.getLayout().getBuildDirectory().dir( + "reproducibility/javadoc")); + task.getArchiveFileName().set( + javadocPrimary.flatMap(Jar::getArchiveFileName)); + task.doFirst(unused -> task.getManifest().from( + javadocPrimary.get().getManifest())); + }); + project.getTasks().register("verifyReproducibleArchives", task -> { + task.setGroup("verification"); + task.setDescription( + "Builds independent module, sources, and Javadoc JAR " + + "replicas and compares bytes."); + task.dependsOn(primary, replica, sources, sourcesReplica, + javadocPrimary, javadocReplica); + task.doLast(unused -> { + compare( + primary.get().getArchiveFile().get().getAsFile(), + replica.get().getArchiveFile().get().getAsFile()); + compare( + sources.get().getArchiveFile().get().getAsFile(), + sourcesReplica.get().getArchiveFile().get().getAsFile()); + compare( + javadocPrimary.get().getArchiveFile().get().getAsFile(), + javadocReplica.get().getArchiveFile().get().getAsFile()); + }); + }); + } + + private static void compare(File first, File second) { + try { + if (Files.mismatch(first.toPath(), second.toPath()) != -1L) { + throw new GradleException( + "Archive replica differs: " + first + " and " + second); + } + } catch (IOException exception) { + throw new GradleException("Cannot compare archive replicas", exception); + } + } +} diff --git a/build-logic/src/main/java/blue/bex/buildlogic/RootOrchestrationPlugin.java b/build-logic/src/main/java/blue/bex/buildlogic/RootOrchestrationPlugin.java new file mode 100644 index 0000000..d8ef9ae --- /dev/null +++ b/build-logic/src/main/java/blue/bex/buildlogic/RootOrchestrationPlugin.java @@ -0,0 +1,437 @@ +package blue.bex.buildlogic; + +import blue.bex.buildlogic.tasks.GenerateModernizationReportTask; +import blue.bex.buildlogic.tasks.GenerateReleaseReportTask; +import blue.bex.buildlogic.tasks.GenerateWorkingReportTask; +import blue.bex.buildlogic.tasks.VerifyPublishedLanguageTask; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.Arrays; +import org.gradle.api.GradleException; +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.Task; +import org.gradle.api.tasks.TaskProvider; +import org.gradle.api.tasks.Copy; +import org.gradle.api.tasks.bundling.Zip; + +/** Thin root lifecycle wiring; implementation remains in focused plugins/tasks. */ +public final class RootOrchestrationPlugin implements Plugin { + @Override + public void apply(Project project) { + if (project != project.getRootProject()) { + throw new IllegalStateException( + "blue.bex.root-orchestration applies only to the root"); + } + project.getPluginManager().apply("base"); + project.getPluginManager().apply(ArchitectureVerificationPlugin.class); + project.getPluginManager().apply(ReleaseEvidencePlugin.class); + + TaskProvider check = lifecycle(project, "bexCheck", + "Runs module tests and architecture/API checks."); + TaskProvider conformance = lifecycle(project, "bexConformance", + "Runs all normative BEX conformance evidence."); + TaskProvider local = lifecycle( + project, "bexLocalLanguageVerification", + "Verifies every module against the explicit local Language composite."); + TaskProvider compatibility = lifecycle( + project, "bexCompatibilityCheck", + "Verifies API, bytecode, dependency, and semantic compatibility."); + TaskProvider reproducibility = lifecycle( + project, "bexReproducibilityCheck", + "Verifies deterministic BEX-owned module and aggregate archives."); + TaskProvider working = lifecycle( + project, "bexWorkingVerification", + "Runs the mandatory local-composite working gate."); + TaskProvider modern = lifecycle( + project, "bexModernizationVerification", + "Runs the complete architecture, documentation, property, and " + + "serious benchmark gate."); + TaskProvider release = lifecycle( + project, "bexReleaseVerify", + "Runs the strict published/local public-release gate."); + TaskProvider publishedLanguage = + project.getTasks().named( + "bexPublishedLanguageVerification", + VerifyPublishedLanguageTask.class); + TaskProvider sourceArchive = sourceArchive( + project, "sourceReleaseArchive", "distributions"); + TaskProvider sourceArchiveReplica = sourceArchive( + project, "replicaSourceReleaseArchive", + "reproducibility/source-release"); + TaskProvider verifySourceArchive = project.getTasks().register( + "verifySourceReleaseArchiveReproducibility", task -> { + task.setGroup("verification"); + task.dependsOn(sourceArchive, sourceArchiveReplica); + task.doLast(unused -> { + try { + long mismatch = Files.mismatch( + sourceArchive.get().getArchiveFile().get() + .getAsFile().toPath(), + sourceArchiveReplica.get().getArchiveFile().get() + .getAsFile().toPath()); + if (mismatch != -1L) { + throw new GradleException( + "Source release archive differs at byte " + + mismatch); + } + } catch (IOException exception) { + throw new GradleException( + "Cannot compare source release archives", + exception); + } + }); + }); + TaskProvider modernization = + project.getTasks().register( + "generateBexModernizationReport", + GenerateModernizationReportTask.class, + task -> { + task.setGroup("verification"); + task.setDescription( + "Aggregates same-run architecture, conformance, " + + "API, benchmark, and artifact evidence."); + task.getFailOnIncomplete().set(true); + task.getTestResultsDirectory().set( + project.getLayout().getProjectDirectory().dir( + "blue-bex-conformance/build/" + + "test-results/test")); + task.getEvidenceFiles().from( + project.getLayout().getBuildDirectory().file( + "reports/bex-modernization/architecture.json"), + project.getLayout().getProjectDirectory().file( + "blue-bex-conformance/build/reports/" + + "bex-conformance/report.json"), + project.getLayout().getProjectDirectory().file( + "blue-bex-conformance/build/reports/" + + "jmh/results.json"), + project.getLayout().getProjectDirectory().file( + "blue-bex-conformance/build/reports/" + + "jmh/environment.json"), + project.getLayout().getProjectDirectory().file( + "docs/public-api-classification.json"), + project.getLayout().getProjectDirectory().file( + "docs/latest-language-api-migration.json"), + project.getLayout().getBuildDirectory().file( + "reports/bex-modernization/" + + "published-language.json"), + project.getLayout().getBuildDirectory().file( + "reports/latest-language-migration/" + + "final.json"), + project.fileTree(project.getProjectDir(), spec -> + spec.include("docs/*.md"))); + task.getSourceFiles().from( + project.fileTree(project.getProjectDir(), spec -> + spec.include( + "blue-bex-core/src/main/" + + "java/**/*.java", + "blue-bex-contracts/src/main/" + + "java/**/*.java"))); + task.getArtifacts().from( + project.fileTree(project.getProjectDir(), spec -> { + spec.include("blue-bex-core/build/libs/*.jar"); + spec.include("blue-bex-contracts/build/libs/*.jar"); + spec.include("blue-bex-java/build/libs/*.jar"); + spec.include("build/distributions/" + + "*-source-release.zip"); + })); + task.getJsonOutputFile().set( + project.getLayout().getBuildDirectory().file( + "reports/bex-modernization/final.json")); + task.getMarkdownOutputFile().set( + project.getLayout().getBuildDirectory().file( + "reports/bex-modernization/final.md")); + }); + TaskProvider baselineReceipt = project.getTasks().register( + "writeLatestLanguageBaselineReport", Copy.class, task -> { + task.setGroup("verification"); + task.from(project.getLayout().getProjectDirectory().file( + "gradle/verification/latest-language-baseline.json")); + task.into(project.getLayout().getBuildDirectory().dir( + "reports/latest-language-migration")); + task.rename(ignored -> "baseline.json"); + }); + TaskProvider workingReport = + project.getTasks().register( + "generateBexWorkingReport", + GenerateWorkingReportTask.class, + task -> { + task.setGroup("verification"); + task.setDescription( + "Writes the exact local-composite BEX " + + "working-readiness receipt."); + task.getTestResultsDirectory().set( + project.getLayout().getProjectDirectory().dir( + "blue-bex-conformance/build/" + + "test-results/test")); + task.getBexRepository().set( + project.getLayout().getProjectDirectory()); + task.getLanguageRepositoryPath().set( + project.getProviders().gradleProperty( + "blueLanguageCompositePath") + .orElse("")); + task.getExpectedLanguageCommit().set( + "9a607e584ff5dd973684d35d71eb4022d946b760"); + task.getVerifiedImplementationCommit().set( + "63a9ed6a1a66d47119a80d16ed2ab0beda0d2453"); + task.getAllowedLanguageDeltaPaths().set(Arrays.asList( + "LICENSE", + "docs/collection-paths-and-cohesion-" + + "migration-report.md", + "reports/modernization/" + + "phase-collection-paths-final.json")); + task.getLocalCompositeCommand().set( + "./gradlew --no-daemon clean " + + "bexWorkingVerification " + + "-PblueLanguageCompositePath=" + + project.getProviders().gradleProperty( + "blueLanguageCompositePath") + .orElse("").get()); + task.getFailOnIncomplete().set(true); + task.getOutputFile().set( + project.getLayout().getBuildDirectory().file( + "reports/latest-language-migration/" + + "final.json")); + task.getOutputs().upToDateWhen(ignored -> false); + }); + TaskProvider releaseReport = + project.getTasks().register( + "generateBexReleaseReport", + GenerateReleaseReportTask.class, + task -> { + task.setGroup("verification"); + task.setDescription( + "Writes and enforces the strict public-release " + + "decision."); + task.getModernizationReport().set( + project.getLayout().getBuildDirectory().file( + "reports/bex-modernization/final.json")); + task.getPublishedLanguageReport().set( + project.getLayout().getBuildDirectory().file( + "reports/bex-modernization/" + + "published-language.json")); + task.getRepositoryDirectory().set( + project.getLayout().getProjectDirectory()); + task.getExpectedReleaseTag().set(project.provider( + () -> "v" + project.getVersion())); + Object independent = project.findProperty( + "bexIndependentCleanBuildReport"); + if (independent != null + && !independent.toString().trim().isEmpty()) { + task.getIndependentCleanBuildReport().fileValue( + project.file(independent.toString())); + } else { + File retained = project.getLayout() + .getBuildDirectory().file( + "reports/bex-release/inputs/" + + "independent-clean-" + + "builds.json") + .get().getAsFile(); + if (retained.isFile()) { + task.getIndependentCleanBuildReport() + .fileValue(retained); + } + } + Object differential = project.findProperty( + "bexLocalPublishedDifferential"); + if (differential != null + && !differential.toString().trim().isEmpty()) { + task.getDifferentialReport().fileValue( + project.file(differential.toString())); + } else { + File retained = project.getLayout() + .getBuildDirectory().file( + "reports/bex-release/inputs/" + + "local-published-" + + "differential.json") + .get().getAsFile(); + if (retained.isFile()) { + task.getDifferentialReport().fileValue(retained); + } + } + task.getJsonOutputFile().set( + project.getLayout().getBuildDirectory().file( + "reports/bex-release/final.json")); + task.getMarkdownOutputFile().set( + project.getLayout().getBuildDirectory().file( + "reports/bex-release/final.md")); + task.getOutputs().upToDateWhen(ignored -> false); + }); + + project.getGradle().projectsEvaluated(ignored -> { + Project core = project.project(":blue-bex-core"); + Project contracts = project.project(":blue-bex-contracts"); + Project suite = project.project(":blue-bex-conformance"); + Project aggregate = project.project(":blue-bex-java"); + Project examples = project.project(":examples"); + + local.configure(task -> task.doFirst(unused -> { + Object configured = project.findProperty( + "blueLanguageCompositePath"); + if (configured == null + || configured.toString().trim().isEmpty()) { + throw new GradleException( + "bexLocalLanguageVerification requires " + + "-PblueLanguageCompositePath="); + } + File checkout = project.file(configured.toString().trim()); + if (!checkout.isDirectory()) { + throw new GradleException( + "Blue Language composite is not a directory: " + + checkout); + } + })); + + project.getTasks().named("check").configure(task -> + task.dependsOn(check)); + project.getTasks().named("assemble").configure(task -> + task.dependsOn( + core.getTasks().named("assemble"), + contracts.getTasks().named("assemble"), + aggregate.getTasks().named("assemble"), + sourceArchive)); + + check.configure(task -> task.dependsOn( + core.getTasks().named("check"), + contracts.getTasks().named("check"), + suite.getTasks().named("check"), + aggregate.getTasks().named("check"), + examples.getTasks().named("check"), + project.getTasks().named("verifyBexArchitecture"))); + conformance.configure(task -> task.dependsOn( + suite.getTasks().named("bexConformance"))); + local.configure(task -> task.dependsOn(Arrays.asList( + core.getTasks().named("verifyLanguageDependencyMode"), + contracts.getTasks().named("verifyLanguageDependencyMode"), + suite.getTasks().named("verifyLanguageDependencyMode"), + aggregate.getTasks().named("verifyLanguageDependencyMode"), + examples.getTasks().named("verifyLanguageDependencyMode"), + core.getTasks().named("writeLanguageDependencyEvidence"), + contracts.getTasks().named("writeLanguageDependencyEvidence"), + suite.getTasks().named("writeLanguageDependencyEvidence"), + aggregate.getTasks().named("writeLanguageDependencyEvidence"), + examples.getTasks().named("writeLanguageDependencyEvidence")))); + compatibility.configure(task -> task.dependsOn( + check, conformance, + suite.getTasks().named("bexApiEvidence"))); + reproducibility.configure(task -> task.dependsOn( + core.getTasks().named("verifyReproducibleArchives"), + contracts.getTasks().named("verifyReproducibleArchives"), + aggregate.getTasks().named("verifyReproducibleArchives"), + verifySourceArchive)); + modernization.configure(task -> task.dependsOn( + working, + publishedLanguage, + project.getTasks().named("verifyBexArchitecture"), + suite.getTasks().named("writeBexConformanceReport"), + suite.getTasks().named("jmh"), + suite.getTasks().named("bexApiEvidence"), + core.getTasks().named("assemble"), + contracts.getTasks().named("assemble"), + aggregate.getTasks().named("assemble"), + sourceArchive)); + workingReport.configure(task -> { + task.dependsOn( + local, compatibility, reproducibility, + suite.getTasks().named("jmhSmoke"), + project.getTasks().named("verifyBexArchitecture"), + core.getTasks().named("assemble"), + contracts.getTasks().named("assemble"), + aggregate.getTasks().named("assemble"), + sourceArchive, + project.getTasks().named("generateBexSourceFingerprint"), + baselineReceipt); + task.getEvidenceFiles().from( + project.getLayout().getBuildDirectory().file( + "reports/bex-modernization/architecture.json"), + suite.getLayout().getBuildDirectory().file( + "reports/bex-conformance/report.json"), + suite.getLayout().getBuildDirectory().file( + "reports/jmh/smoke-results.json"), + suite.getLayout().getBuildDirectory().file( + "reports/bex-release/public-api.txt"), + suite.getLayout().getBuildDirectory().file( + "reports/bex-release/" + + "public-api-classification.json"), + project.getLayout().getProjectDirectory().file( + "src/test/resources/hosted-release/" + + "required-public-api.txt"), + project.getLayout().getProjectDirectory().file( + "docs/latest-language-api-migration.json"), + project.getLayout().getProjectDirectory().file( + "docs/public-api-classification.json"), + project.getLayout().getProjectDirectory().file( + "gradle/verification/" + + "latest-language-baseline.json"), + project.getLayout().getBuildDirectory().file( + "reports/bex-modernization/source.json"), + core.getLayout().getBuildDirectory().file( + "reports/dependencies/language.json"), + contracts.getLayout().getBuildDirectory().file( + "reports/dependencies/language.json"), + suite.getLayout().getBuildDirectory().file( + "reports/dependencies/language.json"), + aggregate.getLayout().getBuildDirectory().file( + "reports/dependencies/language.json"), + examples.getLayout().getBuildDirectory().file( + "reports/dependencies/language.json")); + task.getPrimaryArtifacts().from(project.fileTree( + project.getProjectDir(), spec -> spec.include( + "blue-bex-core/build/libs/*.jar", + "blue-bex-contracts/build/libs/*.jar", + "blue-bex-java/build/libs/*.jar", + "build/distributions/*-source-release.zip"))); + task.getReplicaArtifacts().from(project.fileTree( + project.getProjectDir(), spec -> spec.include( + "blue-bex-core/build/reproducibility/**/*.jar", + "blue-bex-contracts/build/reproducibility/**/*.jar", + "blue-bex-java/build/reproducibility/**/*.jar", + "build/reproducibility/source-release/" + + "*-source-release.zip"))); + task.getProductionSources().from(project.fileTree( + project.getProjectDir(), spec -> spec.include( + "blue-bex-core/src/main/java/**/*.java", + "blue-bex-contracts/src/main/java/**/*.java"))); + }); + working.configure(task -> task.dependsOn( + local, compatibility, reproducibility, + workingReport, + project.getTasks().named("generateBexSourceFingerprint"))); + modern.configure(task -> task.dependsOn(working, modernization)); + releaseReport.configure(task -> task.dependsOn( + modern, publishedLanguage)); + release.configure(task -> task.dependsOn(releaseReport)); + }); + } + + private static TaskProvider sourceArchive( + Project project, String taskName, String destination) { + return project.getTasks().register(taskName, Zip.class, task -> { + task.setGroup("distribution"); + task.setDescription("Creates the reproducible BEX source release."); + task.setPreserveFileTimestamps(false); + task.setReproducibleFileOrder(true); + task.getArchiveBaseName().set("blue-bex-java"); + task.getArchiveVersion().set(project.provider( + () -> String.valueOf(project.getVersion()))); + task.getArchiveClassifier().set("source-release"); + task.getDestinationDirectory().set( + project.getLayout().getBuildDirectory().dir(destination)); + task.into(project.provider(() -> "blue-bex-java-" + + project.getVersion()), copy -> copy.from( + project.getRootProject().getProjectDir(), spec -> + spec.exclude( + ".git/**", ".gradle/**", "**/build/**", + "**/.DS_Store", "*.zip", "work-status.txt"))); + }); + } + + private static TaskProvider lifecycle( + Project project, String name, String description) { + return project.getTasks().register(name, task -> { + task.setGroup("verification"); + task.setDescription(description); + }); + } +} diff --git a/build-logic/src/main/java/blue/bex/buildlogic/package-info.java b/build-logic/src/main/java/blue/bex/buildlogic/package-info.java new file mode 100644 index 0000000..0f7a636 --- /dev/null +++ b/build-logic/src/main/java/blue/bex/buildlogic/package-info.java @@ -0,0 +1,12 @@ +/** + * Typed Gradle convention plugins for BEX module, dependency-mode, Java 8, + * conformance, architecture, reproducibility, benchmark, publication, and + * release-evidence configuration. + * + *

Plugin instances and extensions are owned by their Gradle project and must + * not be used as application runtime state. Required Gradle properties are + * non-null and missing or contradictory release inputs fail the build closed. + * Build plugins do not participate in portable BEX execution and therefore have + * no BEX gas effects; they verify rather than manufacture runtime evidence.

+ */ +package blue.bex.buildlogic; diff --git a/build-logic/src/main/java/blue/bex/buildlogic/tasks/GenerateApiClassificationTask.java b/build-logic/src/main/java/blue/bex/buildlogic/tasks/GenerateApiClassificationTask.java new file mode 100644 index 0000000..f68e34b --- /dev/null +++ b/build-logic/src/main/java/blue/bex/buildlogic/tasks/GenerateApiClassificationTask.java @@ -0,0 +1,189 @@ +package blue.bex.buildlogic.tasks; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.gradle.api.DefaultTask; +import org.gradle.api.GradleException; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.tasks.InputFile; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.PathSensitive; +import org.gradle.api.tasks.PathSensitivity; +import org.gradle.api.tasks.TaskAction; + +/** Classifies every public BEX binary type from the same generated manifest. */ +public abstract class GenerateApiClassificationTask extends DefaultTask { + private static final Pattern TYPE = Pattern.compile( + "^class .*?\\s(blue\\.bex\\.[^\\s]+)(?:\\s|$)"); + + @InputFile + @PathSensitive(PathSensitivity.NONE) + public abstract RegularFileProperty getManifestFile(); + + @OutputFile + public abstract RegularFileProperty getOutputFile(); + + @TaskAction + public void generate() { + try { + File manifest = getManifestFile().get().getAsFile(); + List lines = Files.readAllLines( + manifest.toPath(), StandardCharsets.UTF_8); + Map> categories = new LinkedHashMap<>(); + categories.put("stable API", new ArrayList<>()); + categories.put("host SPI", new ArrayList<>()); + categories.put("intrinsic SPI", new ArrayList<>()); + categories.put("internal implementation", new ArrayList<>()); + categories.put("conformance-only", new ArrayList<>()); + int descriptors = 0; + for (String line : lines) { + if (!line.startsWith("schema=")) { + descriptors++; + } + Matcher matcher = TYPE.matcher(line); + if (matcher.find()) { + String type = matcher.group(1); + categories.get(classification(type)).add(type); + } + } + int typeCount = 0; + for (List types : categories.values()) { + Collections.sort(types); + typeCount += types.size(); + } + StringBuilder json = new StringBuilder(); + json.append("{\n") + .append(" \"schema\": \"blue-bex-public-api-classification/2.0\",\n") + .append(" \"inventory\": {\n") + .append(" \"path\": \"src/test/resources/hosted-release/required-public-api.txt\",\n") + .append(" \"sha256\": \"").append(sha256(manifest)) + .append("\",\n") + .append(" \"manifestSchema\": \"blue-bex-binary-api-manifest/1.0\",\n") + .append(" \"publicTypeCount\": ").append(typeCount) + .append(",\n") + .append(" \"publicDescriptorCount\": ") + .append(descriptors).append("\n },\n") + .append(" \"classifications\": {\n"); + int index = 0; + for (Map.Entry> entry : categories.entrySet()) { + json.append(" ").append(quote(entry.getKey())).append(": ") + .append(array(entry.getValue())); + if (++index < categories.size()) { + json.append(','); + } + json.append('\n'); + } + json.append(" },\n") + .append(" \"notes\": [\n") + .append(" \"Every public/protected binary type is classified exactly once from same-run manifest evidence.\",\n") + .append(" \"Public visibility does not promote an internal implementation type to stable API.\",\n") + .append(" \"Conformance-only types are absent from runtime module JARs.\"\n") + .append(" ]\n}\n"); + File output = getOutputFile().get().getAsFile(); + output.getParentFile().mkdirs(); + Files.write(output.toPath(), + json.toString().getBytes(StandardCharsets.UTF_8)); + } catch (IOException | NoSuchAlgorithmException exception) { + throw new GradleException("Cannot classify BEX public API", exception); + } + } + + private static String classification(String type) { + if (type.contains(".conformance.")) { + return "conformance-only"; + } + if (type.contains("BexIntrinsic") + || type.endsWith("BexTypeBlueIdResolver") + || type.endsWith("BexRuntimeIntrinsics")) { + return "intrinsic SPI"; + } + if (type.contains(".contracts.") || type.contains(".spi.") + || type.contains("Boundary") || type.contains("Policy") + || type.contains("DocumentView") + || type.contains("GasLedgerHost") + || type.contains("GasLedgerCapability") + || type.contains("GasLedgerLifecycle") + || type.contains("SharedGasBudget") + || type.contains("HostGas") + || type.endsWith("BexRuntimeContext") + || type.endsWith("BexStepResultView") + || type.startsWith("blue.bex.output.")) { + return "host SPI"; + } + if (type.startsWith("blue.bex.api.") + || type.equals("blue.bex.BexException") + || type.equals("blue.bex.BexSourcePath") + || stableResult(type) + || stableGas(type) || stableValue(type) + || stableCompile(type)) { + return "stable API"; + } + return "internal implementation"; + } + + private static boolean stableGas(String type) { + return type.matches("blue\\.bex\\.gas\\.BexGas(Charge|Counter|Ledger|LimitExceededException|Manifest|Meter|Schedule)(\\$.*)?"); + } + + private static boolean stableResult(String type) { + return type.matches("blue\\.bex\\.result\\.Bex(Changeset|Events|ExecutionResult|Metrics|MetricsSnapshot|PatchEntry)(\\$.*)?"); + } + + private static boolean stableValue(String type) { + return type.matches("blue\\.bex\\.value\\.Bex(Value|Values|ValueKind|UnicodeOrder)(\\$.*)?"); + } + + private static boolean stableCompile(String type) { + return type.matches("blue\\.bex\\.compile\\.Bex(CompilationInput|CompiledProgram|CompiledProgramCache|CompiledProgramKey|Compiler)(\\$.*)?") + || type.equals("blue.bex.compile.LruBexCompiledProgramCache"); + } + + private static String array(List values) { + if (values.isEmpty()) { + return "[]"; + } + StringBuilder result = new StringBuilder("[\n"); + for (int index = 0; index < values.size(); index++) { + result.append(" ").append(quote(values.get(index))); + if (index + 1 < values.size()) { + result.append(','); + } + result.append('\n'); + } + return result.append(" ]").toString(); + } + + private static String sha256(File file) + throws IOException, NoSuchAlgorithmException { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + try (FileInputStream input = new FileInputStream(file)) { + byte[] buffer = new byte[8192]; + int read; + while ((read = input.read(buffer)) >= 0) { + digest.update(buffer, 0, read); + } + } + StringBuilder value = new StringBuilder(); + for (byte item : digest.digest()) { + value.append(String.format("%02x", item)); + } + return value.toString(); + } + + private static String quote(String value) { + return "\"" + value.replace("\\", "\\\\") + .replace("\"", "\\\"") + "\""; + } +} diff --git a/build-logic/src/main/java/blue/bex/buildlogic/tasks/GenerateBenchmarkEnvironmentTask.java b/build-logic/src/main/java/blue/bex/buildlogic/tasks/GenerateBenchmarkEnvironmentTask.java new file mode 100644 index 0000000..905904e --- /dev/null +++ b/build-logic/src/main/java/blue/bex/buildlogic/tasks/GenerateBenchmarkEnvironmentTask.java @@ -0,0 +1,60 @@ +package blue.bex.buildlogic.tasks; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import org.gradle.api.DefaultTask; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.TaskAction; + +/** Records the host and exact serious-campaign controls used by JMH. */ +public abstract class GenerateBenchmarkEnvironmentTask extends DefaultTask { + @Input + public abstract Property getForks(); + + @Input + public abstract Property getWarmups(); + + @Input + public abstract Property getMeasurements(); + + @Input + public abstract Property getDuration(); + + @OutputFile + public abstract RegularFileProperty getOutputFile(); + + @TaskAction + public void write() throws Exception { + String json = "{\n" + + " \"schema\": \"blue-bex-jmh-environment/1.0\",\n" + + " \"os\": {\"name\":" + + quote(System.getProperty("os.name")) + ",\"version\":" + + quote(System.getProperty("os.version")) + ",\"arch\":" + + quote(System.getProperty("os.arch")) + "},\n" + + " \"jvm\": {\"version\":" + + quote(System.getProperty("java.version")) + ",\"vendor\":" + + quote(System.getProperty("java.vendor")) + ",\"vmName\":" + + quote(System.getProperty("java.vm.name")) + "},\n" + + " \"cpu\": {\"availableProcessors\":" + + Runtime.getRuntime().availableProcessors() + "},\n" + + " \"campaign\": {\"forks\":" + getForks().get() + + ",\"warmups\":" + getWarmups().get() + + ",\"measurements\":" + getMeasurements().get() + + ",\"iterationDuration\":" + quote(getDuration().get()) + + ",\"profilers\":[\"gc\"]," + + "\"resultFormat\":\"json\"}\n" + + "}\n"; + File output = getOutputFile().get().getAsFile(); + output.getParentFile().mkdirs(); + Files.write(output.toPath(), json.getBytes(StandardCharsets.UTF_8)); + } + + private static String quote(String value) { + return "\"" + value.replace("\\", "\\\\") + .replace("\"", "\\\"") + "\""; + } +} diff --git a/build-logic/src/main/java/blue/bex/buildlogic/tasks/GenerateDependencyEvidenceTask.java b/build-logic/src/main/java/blue/bex/buildlogic/tasks/GenerateDependencyEvidenceTask.java new file mode 100644 index 0000000..c39d662 --- /dev/null +++ b/build-logic/src/main/java/blue/bex/buildlogic/tasks/GenerateDependencyEvidenceTask.java @@ -0,0 +1,179 @@ +package blue.bex.buildlogic.tasks; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import org.gradle.api.DefaultTask; +import org.gradle.api.GradleException; +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.file.DirectoryProperty; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.provider.ListProperty; +import org.gradle.api.tasks.Classpath; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Internal; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.TaskAction; + +/** Emits exact, BEX-owned dependency provenance for one runtime module. */ +public abstract class GenerateDependencyEvidenceTask extends DefaultTask { + @Classpath + public abstract ConfigurableFileCollection getArtifacts(); + + @Input + public abstract Property getMode(); + + @Input + public abstract Property getModuleName(); + + @Input + public abstract Property getDeclaredLanguageVersion(); + + @Input + public abstract ListProperty getResolvedComponents(); + + @Input + public abstract Property getExactVersionCacheInitiallyAbsent(); + + @Internal + public abstract DirectoryProperty getLanguageCheckout(); + + @Internal + public abstract DirectoryProperty getBexCheckout(); + + @OutputFile + public abstract RegularFileProperty getOutputFile(); + + @TaskAction + public void generate() { + try { + List artifacts = new ArrayList<>(getArtifacts().getFiles()); + artifacts.sort(Comparator.comparing(File::getName) + .thenComparing(File::getAbsolutePath)); + List artifactJson = new ArrayList<>(); + for (File artifact : artifacts) { + if (!artifact.isFile() || !artifact.getName().endsWith(".jar")) { + continue; + } + artifactJson.add(" {\"name\":" + quote(artifact.getName()) + + ",\"path\":" + quote(unix(artifact)) + + ",\"bytes\":" + artifact.length() + + ",\"sha256\":" + quote(sha256(artifact)) + "}"); + } + String languageHead = ""; + String languageStatus = "not-applicable"; + if (getLanguageCheckout().isPresent()) { + File checkout = getLanguageCheckout().get().getAsFile(); + languageHead = gitText(checkout, "rev-parse", "HEAD").trim(); + languageStatus = gitBytes( + checkout, "status", "--porcelain", "-z").length == 0 + ? "clean" : "dirty"; + } + File bex = getBexCheckout().get().getAsFile(); + String bexHead = gitText(bex, "rev-parse", "HEAD").trim(); + boolean resolved = !artifactJson.isEmpty() + && !getResolvedComponents().get().isEmpty() + && !"dirty".equals(languageStatus); + String json = "{\n" + + " \"schema\": \"blue-bex-dependency-evidence/1.0\",\n" + + " \"status\": " + + quote(resolved ? "passed" : "failed") + ",\n" + + " \"module\": " + quote(getModuleName().get()) + ",\n" + + " \"mode\": " + quote(getMode().get()) + ",\n" + + " \"declaredLanguageVersion\": " + + quote(getDeclaredLanguageVersion().get()) + ",\n" + + " \"bexCommit\": " + quote(bexHead) + ",\n" + + " \"languageCommit\": " + quote(languageHead) + ",\n" + + " \"languageCheckoutState\": " + + quote(languageStatus) + ",\n" + + " \"exactVersionCacheInitiallyAbsent\": " + + getExactVersionCacheInitiallyAbsent().get() + ",\n" + + " \"resolvedComponents\": " + + jsonArray(getResolvedComponents().get()) + ",\n" + + " \"artifacts\": [\n" + + String.join(",\n", artifactJson) + "\n ]\n" + + "}\n"; + File output = getOutputFile().get().getAsFile(); + output.getParentFile().mkdirs(); + Files.write(output.toPath(), json.getBytes(StandardCharsets.UTF_8)); + } catch (InterruptedException exception) { + Thread.currentThread().interrupt(); + throw new GradleException("Cannot generate dependency evidence", exception); + } catch (IOException | NoSuchAlgorithmException exception) { + throw new GradleException("Cannot generate dependency evidence", exception); + } + } + + private static String sha256(File file) + throws IOException, NoSuchAlgorithmException { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + try (FileInputStream input = new FileInputStream(file)) { + byte[] buffer = new byte[8192]; + int read; + while ((read = input.read(buffer)) >= 0) { + digest.update(buffer, 0, read); + } + } + return hex(digest.digest()); + } + + private static String gitText(File directory, String... arguments) + throws IOException, InterruptedException { + return new String(gitBytes(directory, arguments), StandardCharsets.UTF_8); + } + + private static byte[] gitBytes(File directory, String... arguments) + throws IOException, InterruptedException { + List command = new ArrayList<>(); + command.add("git"); + command.addAll(Arrays.asList(arguments)); + Process process = new ProcessBuilder(command) + .directory(directory).redirectErrorStream(true).start(); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + byte[] buffer = new byte[8192]; + int read; + while ((read = process.getInputStream().read(buffer)) >= 0) { + output.write(buffer, 0, read); + } + if (process.waitFor() != 0) { + throw new IOException(new String( + output.toByteArray(), StandardCharsets.UTF_8)); + } + return output.toByteArray(); + } + + private static String hex(byte[] bytes) { + StringBuilder value = new StringBuilder(bytes.length * 2); + for (byte item : bytes) { + value.append(String.format("%02x", item)); + } + return value.toString(); + } + + private static String quote(String value) { + return "\"" + value.replace("\\", "\\\\") + .replace("\"", "\\\"") + "\""; + } + + private static String unix(File file) { + return file.getAbsolutePath().replace(File.separatorChar, '/'); + } + + private static String jsonArray(List values) { + List result = new ArrayList<>(); + for (String value : values) { + result.add(quote(value)); + } + return "[" + String.join(",", result) + "]"; + } +} diff --git a/build-logic/src/main/java/blue/bex/buildlogic/tasks/GenerateModernizationReportTask.java b/build-logic/src/main/java/blue/bex/buildlogic/tasks/GenerateModernizationReportTask.java new file mode 100644 index 0000000..b3cae00 --- /dev/null +++ b/build-logic/src/main/java/blue/bex/buildlogic/tasks/GenerateModernizationReportTask.java @@ -0,0 +1,522 @@ +package blue.bex.buildlogic.tasks; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.xml.parsers.DocumentBuilderFactory; +import org.gradle.api.DefaultTask; +import org.gradle.api.GradleException; +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.file.DirectoryProperty; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.InputDirectory; +import org.gradle.api.tasks.InputFiles; +import org.gradle.api.tasks.Optional; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.PathSensitive; +import org.gradle.api.tasks.PathSensitivity; +import org.gradle.api.tasks.TaskAction; +import org.w3c.dom.Element; + +/** Aggregates same-run modernization evidence without inventing pass counts. */ +public abstract class GenerateModernizationReportTask extends DefaultTask { + @InputDirectory + public abstract DirectoryProperty getTestResultsDirectory(); + + @InputFiles + @PathSensitive(PathSensitivity.RELATIVE) + public abstract ConfigurableFileCollection getEvidenceFiles(); + + @InputFiles + @PathSensitive(PathSensitivity.NAME_ONLY) + public abstract ConfigurableFileCollection getArtifacts(); + + @InputFiles + @PathSensitive(PathSensitivity.RELATIVE) + public abstract ConfigurableFileCollection getSourceFiles(); + + @Input + public abstract Property getFailOnIncomplete(); + + @OutputFile + public abstract RegularFileProperty getJsonOutputFile(); + + @OutputFile + public abstract RegularFileProperty getMarkdownOutputFile(); + + @TaskAction + public void generate() { + try { + TestTotals tests = readTests(getTestResultsDirectory().get().getAsFile()); + List evidence = describe(getEvidenceFiles()); + List artifacts = describe(getArtifacts()); + String conformance = textFor( + evidence, "/bex-conformance/report.json"); + ConformanceTotals totals = conformanceTotals(conformance); + String architecture = textFor(evidence, "architecture"); + String published = textFor(evidence, "published-language"); + String jmh = textFor(evidence, "jmh/results"); + String jmhEnvironment = textFor(evidence, "jmh/environment"); + String working = textFor(evidence, "latest-language-migration/final"); + boolean architecturePassed = containsStatus(architecture, "passed") + && integerField(architecture, "moduleCycles") == 0 + && integerField(architecture, + "packageSccsLargerThanOne") == 0 + && integerField(architecture, "splitPackageCount") == 0 + && integerField(architecture, "undeclaredModuleEdges") == 0; + boolean conformancePassed = totals.complete(); + boolean dependencyEvidencePassed = sectionPassed( + conformance, "resolution"); + boolean conformanceReleaseReady = booleanField( + conformance, "releaseReady"); + boolean semanticEvidencePassed = sectionPassed( + conformance, "representationMatrixResult") + && sectionPassed(conformance, "hostedLocalLimitCapability") + && sectionPassed(conformance, + "cyclicProofUnavailabilityCapability") + && sectionPassed(conformance, + "semanticBoundaryInvocationEvidence") + && sectionPassed(conformance, "ledgerLifecycleEvidence") + && sectionPassed(conformance, "gasExhaustionEvidence") + && sectionPassed(conformance, "cyclicProofEvidence") + && sectionPassed(conformance, "intrinsicEvidence") + && sectionPassed(conformance, + "referenceEvidenceClassificationEvidence"); + boolean benchmarkPresent = jmh.startsWith("[") + && jmh.contains("gc.alloc.rate.norm") + && jmh.contains("scoreConfidence") + && !jmh.contains("NaN") + && jmhEnvironment.contains( + "\"schema\": \"blue-bex-jmh-environment/1.0\"") + && jmhEnvironment.contains("\"profilers\":[\"gc\"]"); + boolean apiPresent = evidence.stream().anyMatch(item -> + item.path.contains("public-api-classification")) + && evidence.stream().anyMatch(item -> + item.path.contains("latest-language-api-migration")) + && working.contains("\"apiEvidence\":") + && working.matches("(?s).*\"apiEvidence\"\\s*:\\s*\\{" + + ".*?\"status\"\\s*:\\s*\"passed\".*"); + boolean workingPassed = working.contains( + "\"workingReady\": true"); + boolean documentationPresent = documentationCount(evidence) == 14; + SourceMetrics sourceMetrics = sourceMetrics(getSourceFiles()); + boolean modernizationReady = tests.executed > 0 + && tests.failed == 0 && tests.skipped == 0 + && tests.unclassified == 0 + && architecturePassed && conformancePassed + && dependencyEvidencePassed + && semanticEvidencePassed + && tests.concurrencyPassed && tests.propertiesPassed + && benchmarkPresent && apiPresent && workingPassed + && documentationPresent && sourceMetrics.fileCount > 0 + && !artifacts.isEmpty(); + String publishedStatus = stringField(published, "status"); + boolean releaseReady = modernizationReady + && "passed".equals(publishedStatus) + && conformanceReleaseReady; + + String json = "{\n" + + " \"schema\": \"blue-bex-modernization-report/1.0\",\n" + + " \"tests\": {\"executed\":" + tests.executed + + ",\"failed\":" + tests.failed + + ",\"skipped\":" + tests.skipped + + ",\"unclassified\":" + tests.unclassified + "},\n" + + " \"conformance\": " + totals.json() + ",\n" + + " \"dependencyEvidenceStatus\": " + + quote(dependencyEvidencePassed ? "passed" : "failed") + + ",\n" + + " \"conformanceReleaseReady\": " + + conformanceReleaseReady + ",\n" + + " \"semanticEvidenceStatus\": " + + quote(semanticEvidencePassed ? "passed" : "failed") + ",\n" + + " \"architectureStatus\": " + + quote(architecturePassed ? "passed" : "failed") + ",\n" + + " \"architecture\": " + jsonOrEmpty(architecture) + ",\n" + + " \"benchmarkEvidence\": " + benchmarkPresent + ",\n" + + " \"benchmarkEnvironment\": " + + jsonOrEmpty(jmhEnvironment) + ",\n" + + " \"apiEvidence\": " + apiPresent + ",\n" + + " \"workingEvidence\": " + workingPassed + ",\n" + + " \"documentationGuideCount\": " + + documentationCount(evidence) + ",\n" + + " \"sourceMetrics\": " + sourceMetrics.json() + ",\n" + + " \"concurrencyProperties\": {\"concurrency\":" + + tests.concurrencyPassed + ",\"properties\":" + + tests.propertiesPassed + "},\n" + + " \"evidence\": " + fileEvidenceJson(evidence) + ",\n" + + " \"artifacts\": " + fileEvidenceJson(artifacts) + ",\n" + + " \"modernizationReady\": " + modernizationReady + ",\n" + + " \"publishedModeStatus\": " + + quote(publishedStatus.isEmpty() + ? "not-executed" : publishedStatus) + ",\n" + + " \"releaseReady\": " + releaseReady + "\n" + + "}\n"; + File jsonFile = getJsonOutputFile().get().getAsFile(); + jsonFile.getParentFile().mkdirs(); + Files.write(jsonFile.toPath(), json.getBytes(StandardCharsets.UTF_8)); + + String markdown = "# BEX modernization evidence\n\n" + + "- Tests: " + tests.executed + " executed, " + + tests.failed + " failed, " + tests.skipped + " skipped\n" + + "- Normative vectors: " + totals.vectors + "/" + + totals.requiredVectors + "\n" + + "- Behavior fixtures: " + totals.behavior + "/" + + totals.requiredBehavior + "\n" + + "- Gas microfixtures: " + totals.gas + "/" + + totals.requiredGas + "\n" + + "- Operators: " + totals.operators + "/" + + totals.requiredOperators + "\n" + + "- Architecture: " + + (architecturePassed ? "passed" : "failed") + "\n" + + "- JMH smoke evidence: " + + (benchmarkPresent ? "serious campaign present" + : "missing or incomplete") + "\n" + + "- Concurrency/property gates: " + + tests.concurrencyPassed + "/" + tests.propertiesPassed + "\n" + + "- Developer guides: " + documentationCount(evidence) + + "/14\n" + + "- Modernization ready: " + modernizationReady + "\n" + + "- Public release ready: " + releaseReady + "\n\n" + + (releaseReady ? "Published dependency evidence is complete." + : "Published release remains fail-closed until matching " + + "Language artifacts and differential evidence exist.") + + "\n"; + File markdownFile = getMarkdownOutputFile().get().getAsFile(); + markdownFile.getParentFile().mkdirs(); + Files.write(markdownFile.toPath(), + markdown.getBytes(StandardCharsets.UTF_8)); + + if (getFailOnIncomplete().get() && !modernizationReady) { + throw new GradleException( + "BEX modernization evidence is incomplete; see " + jsonFile); + } + } catch (GradleException exception) { + throw exception; + } catch (Exception exception) { + throw new GradleException("Cannot generate modernization report", exception); + } + } + + private static TestTotals readTests(File directory) throws Exception { + TestTotals totals = new TestTotals(); + if (!directory.isDirectory()) { + return totals; + } + Files.walk(directory.toPath()) + .filter(path -> path.getFileName().toString().startsWith("TEST-")) + .filter(path -> path.toString().endsWith(".xml")) + .sorted() + .forEach(path -> { + try { + Element root = DocumentBuilderFactory.newInstance() + .newDocumentBuilder().parse(path.toFile()) + .getDocumentElement(); + int declared = integer(root, "tests"); + int cases = root.getElementsByTagName("testcase") + .getLength(); + totals.executed += cases; + totals.failed += integer(root, "failures") + + integer(root, "errors"); + totals.skipped += integer(root, "skipped"); + totals.unclassified += Math.max(0, declared - cases); + String name = path.getFileName().toString(); + boolean passed = integer(root, "failures") == 0 + && integer(root, "errors") == 0 + && integer(root, "skipped") == 0 + && cases > 0; + if (name.contains("BexConcurrentEngineIsolationTest")) { + totals.concurrencyPassed |= passed; + } + if (name.contains("BexModernizationPropertyTest")) { + totals.propertiesPassed |= passed; + } + } catch (Exception exception) { + throw new ReportReadException(exception); + } + }); + return totals; + } + + private static int integer(Element element, String name) { + String value = element.getAttribute(name); + return value.isEmpty() ? 0 : Integer.parseInt(value); + } + + private static List describe(ConfigurableFileCollection files) + throws IOException, NoSuchAlgorithmException { + List ordered = new ArrayList<>(files.getFiles()); + ordered.removeIf(file -> !file.isFile()); + ordered.sort(Comparator.comparing(File::getName) + .thenComparing(File::getAbsolutePath)); + List result = new ArrayList<>(); + for (File file : ordered) { + result.add(new FileEvidence( + file.getPath().replace(File.separatorChar, '/'), + file.length(), sha256(file))); + } + return result; + } + + private static String textFor(List files, String fragment) + throws IOException { + for (FileEvidence item : files) { + if (item.path.contains(fragment)) { + return new String(Files.readAllBytes(new File(item.path).toPath()), + StandardCharsets.UTF_8); + } + } + return ""; + } + + private static boolean containsStatus(String text, String status) { + return text.matches("(?s).*\\\"status\\\"\\s*:\\s*\\\"" + + Pattern.quote(status) + "\\\".*"); + } + + private static int integerField(String text, String field) { + Matcher matcher = Pattern.compile("\\\"" + Pattern.quote(field) + + "\\\"\\s*:\\s*(-?\\d+)").matcher(text); + return matcher.find() ? Integer.parseInt(matcher.group(1)) : -1; + } + + private static String stringField(String text, String field) { + Matcher matcher = Pattern.compile("\\\"" + Pattern.quote(field) + + "\\\"\\s*:\\s*\\\"([^\\\"]*)\\\"").matcher(text); + return matcher.find() ? matcher.group(1) : ""; + } + + private static boolean booleanField(String text, String field) { + return text.matches("(?s).*\\\"" + Pattern.quote(field) + + "\\\"\\s*:\\s*true.*"); + } + + private static boolean sectionPassed(String text, String name) { + String section = objectSection(text, name); + return containsStatus(section, "passed") + && !containsStatus(section, "failed") + && !containsStatus(section, "not-executed"); + } + + private static String objectSection(String text, String name) { + int key = text.indexOf("\"" + name + "\""); + if (key < 0) { + return ""; + } + int start = text.indexOf('{', key); + if (start < 0) { + return ""; + } + int depth = 0; + boolean quoted = false; + boolean escaped = false; + for (int index = start; index < text.length(); index++) { + char value = text.charAt(index); + if (quoted) { + if (escaped) { + escaped = false; + } else if (value == '\\') { + escaped = true; + } else if (value == '"') { + quoted = false; + } + } else if (value == '"') { + quoted = true; + } else if (value == '{') { + depth++; + } else if (value == '}' && --depth == 0) { + return text.substring(start, index + 1); + } + } + return ""; + } + + private static int documentationCount(List evidence) { + String[] names = { + "start-here.md", "architecture.md", "program-model.md", + "values-and-identity.md", "compiler-and-ir.md", + "runtime-and-context.md", "blue-output-boundary.md", + "gas-and-exhaustion.md", "intrinsics.md", + "adding-an-operator.md", "contracts-hosting.md", + "migrating-to-modular-blue-language.md", "conformance.md", + "release.md" + }; + int count = 0; + for (String name : names) { + if (evidence.stream().anyMatch(item -> + item.path.endsWith("/docs/" + name))) { + count++; + } + } + return count; + } + + private static SourceMetrics sourceMetrics(ConfigurableFileCollection files) + throws IOException { + SourceMetrics metrics = new SourceMetrics(); + for (File file : files.getFiles()) { + if (!file.isFile() || !file.getName().endsWith(".java")) { + continue; + } + String text = new String(Files.readAllBytes(file.toPath()), + StandardCharsets.UTF_8); + metrics.fileCount++; + metrics.lineCount += text.split("\\R").length; + metrics.publicTypeCount += matches(text, + "(?m)^public\\s+(?:abstract\\s+|final\\s+)?" + + "(?:class|interface|enum)\\s+"); + metrics.publicMethodCount += matches(text, + "(?m)^\\s*public\\s+(?!class|interface|enum)" + + "[^=;{}]+\\([^;{}]*\\)\\s*(?:\\{|;)"); + } + return metrics; + } + + private static int matches(String text, String expression) { + Matcher matcher = Pattern.compile(expression).matcher(text); + int count = 0; + while (matcher.find()) { + count++; + } + return count; + } + + private static String jsonOrEmpty(String text) { + return text.trim().isEmpty() ? "{}" : text.trim(); + } + + private static ConformanceTotals conformanceTotals(String text) { + ConformanceTotals totals = new ConformanceTotals(); + totals.vectors = pair(text, "normativeVectors", "executedAndPassing"); + totals.requiredVectors = pair(text, "normativeVectors", "required"); + totals.behavior = pair(text, "behaviorFixtures", "executedAndPassing"); + totals.requiredBehavior = pair(text, "behaviorFixtures", "required"); + totals.gas = pair(text, "gasMicrofixtures", "executedAndPassing"); + totals.requiredGas = pair(text, "gasMicrofixtures", "required"); + totals.operators = pair(text, "operators", "executedAndPassing"); + totals.requiredOperators = pair(text, "operators", "required"); + return totals; + } + + private static int pair(String text, String section, String field) { + Pattern pattern = Pattern.compile("\\\"" + Pattern.quote(section) + + "\\\"\\s*:\\s*\\{([^{}]|\\{[^{}]*\\})*?\\\"" + + Pattern.quote(field) + "\\\"\\s*:\\s*(\\d+)"); + Matcher matcher = pattern.matcher(text); + return matcher.find() ? Integer.parseInt(matcher.group(2)) : -1; + } + + private static String fileEvidenceJson(List files) { + List values = new ArrayList<>(); + for (FileEvidence item : files) { + values.add("{\"path\":" + quote(item.path) + + ",\"bytes\":" + item.bytes + + ",\"sha256\":" + quote(item.sha256) + "}"); + } + return "[" + String.join(",", values) + "]"; + } + + private static String sha256(File file) + throws IOException, NoSuchAlgorithmException { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + try (FileInputStream input = new FileInputStream(file)) { + byte[] buffer = new byte[8192]; + int read; + while ((read = input.read(buffer)) >= 0) { + digest.update(buffer, 0, read); + } + } + StringBuilder value = new StringBuilder(); + for (byte item : digest.digest()) { + value.append(String.format("%02x", item)); + } + return value.toString(); + } + + private static String quote(String value) { + return "\"" + value.replace("\\", "\\\\") + .replace("\"", "\\\"") + "\""; + } + + private static final class TestTotals { + private int executed; + private int failed; + private int skipped; + private int unclassified; + private boolean concurrencyPassed; + private boolean propertiesPassed; + } + + private static final class SourceMetrics { + private int fileCount; + private int lineCount; + private int publicTypeCount; + private int publicMethodCount; + + private String json() { + return "{\"productionJavaFiles\":" + fileCount + + ",\"productionJavaLines\":" + lineCount + + ",\"publicTopLevelTypes\":" + publicTypeCount + + ",\"publicMethods\":" + publicMethodCount + "}"; + } + } + + private static final class FileEvidence { + private final String path; + private final long bytes; + private final String sha256; + + private FileEvidence(String path, long bytes, String sha256) { + this.path = path; + this.bytes = bytes; + this.sha256 = sha256; + } + } + + private static final class ConformanceTotals { + private int vectors = -1; + private int requiredVectors = -1; + private int behavior = -1; + private int requiredBehavior = -1; + private int gas = -1; + private int requiredGas = -1; + private int operators = -1; + private int requiredOperators = -1; + + private boolean complete() { + return vectors == 60 && requiredVectors == 60 + && behavior == 105 && requiredBehavior == 105 + && gas == 30 && requiredGas == 30 + && operators == 86 && requiredOperators == 86; + } + + private String json() { + return "{\"vectors\":{" + counts(vectors, requiredVectors) + + "},\"behaviorFixtures\":{" + counts(behavior, requiredBehavior) + + "},\"gasMicrofixtures\":{" + counts(gas, requiredGas) + + "},\"operators\":{" + counts(operators, requiredOperators) + + "}}"; + } + + private static String counts(int executed, int required) { + return "\"executedAndPassing\":" + executed + + ",\"required\":" + required; + } + } + + private static final class ReportReadException extends RuntimeException { + private ReportReadException(Exception cause) { + super(cause); + } + } +} diff --git a/build-logic/src/main/java/blue/bex/buildlogic/tasks/GenerateReleaseReportTask.java b/build-logic/src/main/java/blue/bex/buildlogic/tasks/GenerateReleaseReportTask.java new file mode 100644 index 0000000..e9cc2a0 --- /dev/null +++ b/build-logic/src/main/java/blue/bex/buildlogic/tasks/GenerateReleaseReportTask.java @@ -0,0 +1,283 @@ +package blue.bex.buildlogic.tasks; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import org.gradle.api.DefaultTask; +import org.gradle.api.GradleException; +import org.gradle.api.file.DirectoryProperty; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.InputFile; +import org.gradle.api.tasks.Internal; +import org.gradle.api.tasks.Optional; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.PathSensitive; +import org.gradle.api.tasks.PathSensitivity; +import org.gradle.api.tasks.TaskAction; + +/** Writes and enforces the single strict, fail-closed BEX release decision. */ +public abstract class GenerateReleaseReportTask extends DefaultTask { + @InputFile + @PathSensitive(PathSensitivity.RELATIVE) + public abstract RegularFileProperty getModernizationReport(); + + @InputFile + @PathSensitive(PathSensitivity.RELATIVE) + public abstract RegularFileProperty getPublishedLanguageReport(); + + @InputFile + @Optional + @PathSensitive(PathSensitivity.NONE) + public abstract RegularFileProperty getIndependentCleanBuildReport(); + + @InputFile + @Optional + @PathSensitive(PathSensitivity.NONE) + public abstract RegularFileProperty getDifferentialReport(); + + @Internal + public abstract DirectoryProperty getRepositoryDirectory(); + + @Input + public abstract Property getExpectedReleaseTag(); + + @OutputFile + public abstract RegularFileProperty getJsonOutputFile(); + + @OutputFile + public abstract RegularFileProperty getMarkdownOutputFile(); + + @TaskAction + public void generate() { + try { + String modernization = read( + getModernizationReport().get().getAsFile()); + String published = read( + getPublishedLanguageReport().get().getAsFile()); + String independent = optionalText(getIndependentCleanBuildReport()); + String differential = optionalText(getDifferentialReport()); + File repository = getRepositoryDirectory().get().getAsFile(); + String commit = gitText(repository, "rev-parse", "HEAD").trim(); + boolean clean = gitBytes(repository, "status", "--porcelain", "-z") + .length == 0; + List tags = lines(gitText( + repository, "tag", "--points-at", "HEAD")); + boolean exactTag = tags.contains(getExpectedReleaseTag().get()); + + boolean modernizationReady = booleanField( + modernization, "modernizationReady"); + boolean conformanceReleaseReady = booleanField( + modernization, "conformanceReleaseReady"); + boolean publishedReady = "passed".equals( + stringField(published, "status")); + boolean independentReady = "passed".equals( + stringField(independent, "status")) + && sectionPassed(independent, "standalonePublished") + && sectionPassed(independent, "localComposite") + && commit.equals(stringField(independent, "bexCommit")); + boolean differentialReady = "passed".equals( + stringField(differential, "status")) + && fieldPassed(differential, "semanticAndGasParity") + && fieldPassed(differential, "exactGasTraceParity") + && commit.equals(stringField(differential, "bexCommit")); + + List blockers = new ArrayList<>(); + addBlocker(blockers, modernizationReady, + "modernization evidence is not ready"); + addBlocker(blockers, conformanceReleaseReady, + "detailed conformance report is not release-ready"); + addBlocker(blockers, publishedReady, + "matching published Language artifacts are not authenticated"); + addBlocker(blockers, independentReady, + "two isolated clean-build pairs are absent or do not match"); + addBlocker(blockers, differentialReady, + "local/published semantic and exact-gas differential is absent"); + addBlocker(blockers, clean, + "BEX source checkout is dirty"); + addBlocker(blockers, exactTag, + "HEAD is not tagged exactly " + getExpectedReleaseTag().get()); + boolean releaseReady = blockers.isEmpty(); + + String json = "{\n" + + " \"schema\": \"blue-bex-strict-release/1.0\",\n" + + " \"bexCommit\": " + quote(commit) + ",\n" + + " \"sourceState\": {\"clean\":" + clean + + ",\"expectedTag\":" + + quote(getExpectedReleaseTag().get()) + + ",\"tagsAtHead\":" + jsonStrings(tags) + + ",\"exactReleaseTag\":" + exactTag + "},\n" + + " \"modernizationStatus\": " + + quote(modernizationReady ? "passed" : "failed") + ",\n" + + " \"conformanceReleaseStatus\": " + + quote(conformanceReleaseReady ? "passed" : "failed") + + ",\n" + + " \"publishedLanguageStatus\": " + + quote(stringField(published, "status")) + ",\n" + + " \"independentCleanBuildStatus\": " + + quote(independentReady ? "passed" : "not-executed") + + ",\n" + + " \"localPublishedDifferentialStatus\": " + + quote(differentialReady ? "passed" : "not-executed") + + ",\n" + + " \"blockers\": " + jsonStrings(blockers) + ",\n" + + " \"releaseReady\": " + releaseReady + "\n" + + "}\n"; + File jsonFile = getJsonOutputFile().get().getAsFile(); + jsonFile.getParentFile().mkdirs(); + Files.write(jsonFile.toPath(), json.getBytes(StandardCharsets.UTF_8)); + + String markdown = "# BEX strict release evidence\n\n" + + "- Commit: `" + commit + "`\n" + + "- Modernization: " + pass(modernizationReady) + "\n" + + "- Published Language: " + pass(publishedReady) + "\n" + + "- Independent clean builds: " + pass(independentReady) + + "\n" + + "- Local/published differential: " + + pass(differentialReady) + "\n" + + "- Clean exact tagged source: " + + pass(clean && exactTag) + "\n" + + "- `releaseReady`: `" + releaseReady + "`\n\n" + + (blockers.isEmpty() ? "No blockers." + : "Blockers:\n\n" + blockers.stream() + .map(value -> "- " + value) + .collect(Collectors.joining("\n"))) + "\n"; + File markdownFile = getMarkdownOutputFile().get().getAsFile(); + markdownFile.getParentFile().mkdirs(); + Files.write(markdownFile.toPath(), + markdown.getBytes(StandardCharsets.UTF_8)); + + if (!releaseReady) { + throw new GradleException( + "BEX public release remains fail-closed; see " + jsonFile); + } + } catch (GradleException exception) { + throw exception; + } catch (Exception exception) { + throw new GradleException("Cannot generate strict release report", + exception); + } + } + + private static String optionalText(RegularFileProperty property) + throws IOException { + return property.isPresent() && property.get().getAsFile().isFile() + ? read(property.get().getAsFile()) : ""; + } + + private static void addBlocker(List blockers, boolean passed, + String blocker) { + if (!passed) { + blockers.add(blocker); + } + } + + private static String pass(boolean value) { + return value ? "passed" : "not passed"; + } + + private static boolean fieldPassed(String text, String field) { + return text.matches("(?s).*\\\"" + Pattern.quote(field) + + "\\\"\\s*:\\s*(?:\\\"passed\\\"|true).*?"); + } + + private static boolean sectionPassed(String text, String section) { + return "passed".equals(stringField(objectSection(text, section), + "status")); + } + + private static boolean booleanField(String text, String field) { + return text.matches("(?s).*\\\"" + Pattern.quote(field) + + "\\\"\\s*:\\s*true.*"); + } + + private static String stringField(String text, String field) { + Matcher matcher = Pattern.compile("\\\"" + Pattern.quote(field) + + "\\\"\\s*:\\s*\\\"([^\\\"]*)\\\"").matcher(text); + return matcher.find() ? matcher.group(1) : ""; + } + + private static String objectSection(String text, String name) { + int key = text.indexOf("\"" + name + "\""); + int start = key < 0 ? -1 : text.indexOf('{', key); + if (start < 0) { + return ""; + } + int depth = 0; + boolean quoted = false; + boolean escaped = false; + for (int index = start; index < text.length(); index++) { + char value = text.charAt(index); + if (quoted) { + if (escaped) { + escaped = false; + } else if (value == '\\') { + escaped = true; + } else if (value == '"') { + quoted = false; + } + } else if (value == '"') { + quoted = true; + } else if (value == '{') { + depth++; + } else if (value == '}' && --depth == 0) { + return text.substring(start, index + 1); + } + } + return ""; + } + + private static List lines(String text) { + String trimmed = text.trim(); + return trimmed.isEmpty() ? new ArrayList<>() + : Arrays.asList(trimmed.split("\\R")); + } + + private static String read(File file) throws IOException { + return new String(Files.readAllBytes(file.toPath()), + StandardCharsets.UTF_8); + } + + private static String gitText(File directory, String... arguments) + throws IOException, InterruptedException { + return new String(gitBytes(directory, arguments), StandardCharsets.UTF_8); + } + + private static byte[] gitBytes(File directory, String... arguments) + throws IOException, InterruptedException { + List command = new ArrayList<>(); + command.add("git"); + command.addAll(Arrays.asList(arguments)); + Process process = new ProcessBuilder(command).directory(directory) + .redirectErrorStream(true).start(); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + byte[] buffer = new byte[8192]; + int read; + while ((read = process.getInputStream().read(buffer)) >= 0) { + output.write(buffer, 0, read); + } + if (process.waitFor() != 0) { + throw new IOException(output.toString(StandardCharsets.UTF_8.name())); + } + return output.toByteArray(); + } + + private static String jsonStrings(List values) { + return values.stream().map(GenerateReleaseReportTask::quote) + .collect(Collectors.joining(",", "[", "]")); + } + + private static String quote(String value) { + return "\"" + value.replace("\\", "\\\\") + .replace("\"", "\\\"") + "\""; + } +} diff --git a/build-logic/src/main/java/blue/bex/buildlogic/tasks/GenerateSourceFingerprintTask.java b/build-logic/src/main/java/blue/bex/buildlogic/tasks/GenerateSourceFingerprintTask.java new file mode 100644 index 0000000..fcdfe96 --- /dev/null +++ b/build-logic/src/main/java/blue/bex/buildlogic/tasks/GenerateSourceFingerprintTask.java @@ -0,0 +1,171 @@ +package blue.bex.buildlogic.tasks; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import org.gradle.api.DefaultTask; +import org.gradle.api.GradleException; +import org.gradle.api.file.DirectoryProperty; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.tasks.Internal; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.TaskAction; + +/** Hashes tracked plus non-ignored untracked source with path-safe framing. */ +public abstract class GenerateSourceFingerprintTask extends DefaultTask { + @Internal + public abstract DirectoryProperty getRepositoryDirectory(); + + @OutputFile + public abstract RegularFileProperty getOutputFile(); + + @TaskAction + public void generate() { + File repository = getRepositoryDirectory().get().getAsFile(); + try { + byte[] names = git(repository, "ls-files", "-z", "--cached", + "--others", "--exclude-standard"); + List paths = splitNul(names); + paths.sort(GenerateSourceFingerprintTask::compareUnsigned); + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + int files = 0; + int symlinks = 0; + for (byte[] rawPath : paths) { + String relative = new String(rawPath, StandardCharsets.UTF_8); + Path path = repository.toPath().resolve(relative); + byte type; + byte[] content; + if (Files.isSymbolicLink(path)) { + type = 'L'; + content = Files.readSymbolicLink(path).toString() + .getBytes(StandardCharsets.UTF_8); + symlinks++; + } else if (Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)) { + type = 'F'; + content = Files.readAllBytes(path); + files++; + } else { + throw new GradleException( + "Unsupported source path type: " + relative); + } + updateInt(digest, rawPath.length); + digest.update(rawPath); + digest.update(type); + updateLong(digest, content.length); + digest.update(content); + } + String head = new String( + git(repository, "rev-parse", "HEAD"), + StandardCharsets.UTF_8).trim(); + byte[] status = git(repository, "status", "--porcelain", "-z"); + String json = "{\n" + + " \"schema\": \"blue-bex-source-fingerprint/1.0\",\n" + + " \"commit\": \"" + head + "\",\n" + + " \"dirty\": " + (status.length != 0) + ",\n" + + " \"pathCount\": " + paths.size() + ",\n" + + " \"fileCount\": " + files + ",\n" + + " \"symlinkCount\": " + symlinks + ",\n" + + " \"statusSha256\": \"" + hex(sha256(status)) + "\",\n" + + " \"workspaceSha256\": \"" + + hex(digest.digest()) + "\"\n" + + "}\n"; + File output = getOutputFile().get().getAsFile(); + output.getParentFile().mkdirs(); + Files.write(output.toPath(), json.getBytes(StandardCharsets.UTF_8)); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new GradleException("Cannot fingerprint BEX source", e); + } catch (IOException | NoSuchAlgorithmException e) { + throw new GradleException("Cannot fingerprint BEX source", e); + } + } + + private static byte[] git(File repository, String... arguments) + throws IOException, InterruptedException { + List command = new ArrayList<>(); + command.add("git"); + command.addAll(Arrays.asList(arguments)); + Process process = new ProcessBuilder(command) + .directory(repository) + .redirectErrorStream(true) + .start(); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + byte[] buffer = new byte[8192]; + int read; + while ((read = process.getInputStream().read(buffer)) >= 0) { + output.write(buffer, 0, read); + } + if (process.waitFor() != 0) { + throw new IOException( + "git command failed: " + + new String(output.toByteArray(), StandardCharsets.UTF_8)); + } + return output.toByteArray(); + } + + private static List splitNul(byte[] bytes) { + List result = new ArrayList<>(); + int start = 0; + for (int index = 0; index < bytes.length; index++) { + if (bytes[index] == 0) { + result.add(Arrays.copyOfRange(bytes, start, index)); + start = index + 1; + } + } + return result; + } + + private static int compareUnsigned(byte[] left, byte[] right) { + int length = Math.min(left.length, right.length); + for (int index = 0; index < length; index++) { + int comparison = Integer.compare( + Byte.toUnsignedInt(left[index]), + Byte.toUnsignedInt(right[index])); + if (comparison != 0) { + return comparison; + } + } + return Integer.compare(left.length, right.length); + } + + private static void updateInt(MessageDigest digest, int value) + throws IOException { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(4); + try (DataOutputStream data = new DataOutputStream(bytes)) { + data.writeInt(value); + } + digest.update(bytes.toByteArray()); + } + + private static void updateLong(MessageDigest digest, long value) + throws IOException { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(8); + try (DataOutputStream data = new DataOutputStream(bytes)) { + data.writeLong(value); + } + digest.update(bytes.toByteArray()); + } + + private static byte[] sha256(byte[] bytes) throws NoSuchAlgorithmException { + return MessageDigest.getInstance("SHA-256").digest(bytes); + } + + private static String hex(byte[] bytes) { + StringBuilder value = new StringBuilder(bytes.length * 2); + for (byte item : bytes) { + value.append(String.format("%02x", item)); + } + return value.toString(); + } +} diff --git a/build-logic/src/main/java/blue/bex/buildlogic/tasks/GenerateWorkingReportTask.java b/build-logic/src/main/java/blue/bex/buildlogic/tasks/GenerateWorkingReportTask.java new file mode 100644 index 0000000..0fe84b4 --- /dev/null +++ b/build-logic/src/main/java/blue/bex/buildlogic/tasks/GenerateWorkingReportTask.java @@ -0,0 +1,779 @@ +package blue.bex.buildlogic.tasks; + +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.security.MessageDigest; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import javax.xml.parsers.DocumentBuilderFactory; +import org.gradle.api.DefaultTask; +import org.gradle.api.GradleException; +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.file.DirectoryProperty; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.provider.ListProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.InputDirectory; +import org.gradle.api.tasks.InputFiles; +import org.gradle.api.tasks.Internal; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.PathSensitive; +import org.gradle.api.tasks.PathSensitivity; +import org.gradle.api.tasks.TaskAction; +import org.w3c.dom.Element; + +/** Writes the exact local-composite BEX working-readiness receipt. */ +public abstract class GenerateWorkingReportTask extends DefaultTask { + private static final Pattern FORBIDDEN_IMPORT = Pattern.compile( + "(?m)^import\\s+blue\\.language\\.(?:utils\\.|" + + "snapshot\\.ResolvedSnapshot|NodeProvider|" + + "BlueOperationLimits|BlueOperationOutcome|" + + "BlueOperationResult)"); + + @InputDirectory + public abstract DirectoryProperty getTestResultsDirectory(); + + @InputFiles + @PathSensitive(PathSensitivity.RELATIVE) + public abstract ConfigurableFileCollection getEvidenceFiles(); + + @InputFiles + @PathSensitive(PathSensitivity.RELATIVE) + public abstract ConfigurableFileCollection getPrimaryArtifacts(); + + @InputFiles + @PathSensitive(PathSensitivity.RELATIVE) + public abstract ConfigurableFileCollection getReplicaArtifacts(); + + @InputFiles + @PathSensitive(PathSensitivity.RELATIVE) + public abstract ConfigurableFileCollection getProductionSources(); + + @Internal + public abstract DirectoryProperty getBexRepository(); + + @Input + public abstract Property getLanguageRepositoryPath(); + + @Input + public abstract Property getExpectedLanguageCommit(); + + @Input + public abstract Property getVerifiedImplementationCommit(); + + @Input + public abstract ListProperty getAllowedLanguageDeltaPaths(); + + @Input + public abstract Property getLocalCompositeCommand(); + + @Input + public abstract Property getFailOnIncomplete(); + + @OutputFile + public abstract RegularFileProperty getOutputFile(); + + @TaskAction + public void generate() { + try { + File bex = getBexRepository().get().getAsFile(); + File language = new File(getLanguageRepositoryPath().get()) + .getCanonicalFile(); + GitState bexState = gitState(bex); + GitState languageState = gitState(language); + + List evidence = regularFiles(getEvidenceFiles()); + String conformance = textFor(evidence, "/bex-conformance/report.json"); + String architecture = textFor(evidence, "/bex-modernization/architecture.json"); + String baseline = textFor(evidence, "latest-language-baseline.json"); + String generatedApiClassification = textFor( + evidence, "/blue-bex-conformance/build/reports/bex-release/" + + "public-api-classification.json"); + String reviewedApiClassification = textFor( + evidence, "/docs/public-api-classification.json"); + String apiMigration = textFor( + evidence, "latest-language-api-migration.json"); + String generatedApi = textFor( + evidence, "/bex-release/public-api.txt"); + String requiredApi = textFor( + evidence, "/hosted-release/required-public-api.txt"); + String jmh = textFor(evidence, "/jmh/smoke-results.json"); + String published = textFor(evidence, "published-language.json"); + + TestTotals tests = readTests( + getTestResultsDirectory().get().getAsFile()); + ConformanceTotals totals = conformanceTotals(conformance); + boolean architecturePassed = hasStatus(architecture, "passed"); + int descriptorCount = manifestDescriptorCount(generatedApi); + int publicTypeCount = manifestTypeCount(generatedApi); + String manifestSha = sha256( + generatedApi.getBytes(StandardCharsets.UTF_8)); + boolean apiPassed = !generatedApi.isEmpty() + && generatedApi.equals(requiredApi) + && generatedApiClassification.equals( + reviewedApiClassification) + && hasInventory(generatedApiClassification, + publicTypeCount, descriptorCount) + && apiMigration.contains( + "\"afterSha256\": \"" + manifestSha + "\""); + boolean benchmarkPassed = jmh.trim().startsWith("[") + && jmh.contains("BexCoreBenchmark") + && jmh.contains("BexHostedGasBenchmark"); + + List dependencyFiles = evidence.stream() + .filter(file -> unix(file).contains( + "/reports/dependencies/language.json")) + .sorted(Comparator.comparing(GenerateWorkingReportTask::unix)) + .collect(Collectors.toList()); + boolean dependenciesPassed = dependencyFiles.size() == 5; + List dependencyJson = new ArrayList<>(); + for (File file : dependencyFiles) { + String text = read(file); + dependencyJson.add(text.trim()); + dependenciesPassed &= text.contains( + "\"mode\": \"local-composite\"") + && text.contains("\"languageCommit\": \"" + + getExpectedLanguageCommit().get() + "\"") + && text.contains( + "\"languageCheckoutState\": \"clean\"") + && text.contains("\"bexCommit\": \"" + + bexState.head + "\"") + && shaCount(text) > 0; + } + + Set actualDelta = new LinkedHashSet<>(gitLines( + language, "diff", "--name-only", + getVerifiedImplementationCommit().get() + "..HEAD")); + Set allowedDelta = new LinkedHashSet<>( + getAllowedLanguageDeltaPaths().get()); + boolean languageCodeEquivalent = languageState.head.equals( + getExpectedLanguageCommit().get()) + && !languageState.dirty + && actualDelta.equals(allowedDelta); + + String expectedBexCz = stringAfter( + baseline, "\"bex\"", "\"czTomlSha256\""); + String expectedLanguageCz = stringAfter( + baseline, "\"language\"", "\"czTomlSha256\""); + String actualBexCz = sha256(new File(bex, ".cz.toml")); + String actualLanguageCz = sha256(new File(language, ".cz.toml")); + boolean versionAutomationUntouched = actualBexCz.equals(expectedBexCz) + && actualLanguageCz.equals(expectedLanguageCz); + + LegacyTotals legacy = legacyTotals(getProductionSources()); + int legacyBeforeLines = integerAfter( + baseline, "\"allForbiddenLegacyImports\"", "\"lines\""); + int legacyBeforeFiles = integerAfter( + baseline, "\"allForbiddenLegacyImports\"", "\"files\""); + boolean legacyPassed = legacy.lines == 0 && legacy.files == 0 + && legacyBeforeLines >= 0 && legacyBeforeFiles >= 0; + + List primary = describe(getPrimaryArtifacts()); + List replicas = describe(getReplicaArtifacts()); + boolean artifactsComplete = requiredArtifacts(primary); + boolean reproducible = replicasMatch(primary, replicas); + BytecodeEvidence bytecode = bytecode(primary); + boolean hostedPassed = hostedTestsPassed( + getTestResultsDirectory().get().getAsFile()); + boolean criticalSemanticEvidence = sectionPassed( + conformance, "representationMatrixResult") + && sectionPassed(conformance, + "hostedLocalLimitCapability") + && sectionPassed(conformance, + "cyclicProofUnavailabilityCapability") + && sectionPassed(conformance, + "semanticBoundaryInvocationEvidence") + && sectionPassed(conformance, + "ledgerLifecycleEvidence") + && sectionPassed(conformance, + "gasExhaustionEvidence") + && sectionPassed(conformance, "cyclicProofEvidence") + && sectionPassed(conformance, "intrinsicEvidence") + && sectionPassed(conformance, + "referenceEvidenceClassificationEvidence"); + boolean semanticAndGasParity = tests.failed == 0 + && tests.skipped == 0 && tests.unclassified == 0 + && totals.complete() && criticalSemanticEvidence; + String publishedStatus = hasStatus(published, "passed") + ? "passed" : "not-executed"; + boolean workingReady = !bexState.dirty + && languageCodeEquivalent + && versionAutomationUntouched + && dependenciesPassed + && tests.executed > 0 + && semanticAndGasParity + && hostedPassed + && architecturePassed + && legacyPassed + && apiPassed + && benchmarkPassed + && bytecode.passed + && artifactsComplete + && reproducible; + + String json = "{\n" + + " \"schema\": \"blue-bex-latest-language-working/1.0\",\n" + + " \"bex\": {\"commit\":" + quote(bexState.head) + + ",\"dirty\":" + bexState.dirty + + ",\"statusSha256\":" + quote(bexState.statusSha256) + + "},\n" + + " \"language\": {\"exactCommit\":" + + quote(languageState.head) + ",\"dirty\":" + + languageState.dirty + ",\"verifiedImplementationCommit\":" + + quote(getVerifiedImplementationCommit().get()) + + ",\"codeEquivalent\":" + languageCodeEquivalent + + ",\"deltaPaths\":" + jsonStrings(actualDelta) + "},\n" + + " \"languageModuleBaseline\": " + + jsonOrEmpty(baseline) + ",\n" + + " \"versionAutomation\": {\"status\":" + + quote(versionAutomationUntouched ? "passed" : "failed") + + ",\"bexCzTomlSha256\":" + quote(actualBexCz) + + ",\"languageCzTomlSha256\":" + + quote(actualLanguageCz) + "},\n" + + " \"dependencyEvidence\": " + + jsonObjects(dependencyJson) + ",\n" + + " \"legacyImports\": {\"before\":{\"lines\":" + + legacyBeforeLines + ",\"files\":" + legacyBeforeFiles + + "},\"after\":{\"lines\":" + legacy.lines + + ",\"files\":" + legacy.files + "}},\n" + + " \"tests\": {\"executed\":" + tests.executed + + ",\"passed\":" + tests.passed + ",\"failed\":" + + tests.failed + ",\"skipped\":" + tests.skipped + + ",\"unclassified\":" + tests.unclassified + "},\n" + + " \"conformance\": " + totals.json() + ",\n" + + " \"semanticAndGasParity\": " + + quote(semanticAndGasParity ? "passed" : "failed") + ",\n" + + " \"hostedBoundaryTests\": " + + quote(hostedPassed ? "passed" : "failed") + ",\n" + + " \"java8Bytecode\": {\"status\":" + + quote(bytecode.passed ? "passed" : "failed") + + ",\"classCount\":" + bytecode.classCount + + ",\"maximumMajorVersion\":" + bytecode.maximumMajor + "},\n" + + " \"architecture\": " + jsonOrEmpty(architecture) + ",\n" + + " \"apiEvidence\": {\"status\":" + + quote(apiPassed ? "passed" : "failed") + + ",\"publicTypeCount\":" + publicTypeCount + + ",\"publicDescriptorCount\":" + descriptorCount + + ",\"manifestSha256\":" + quote(manifestSha) + + ",\"classificationSha256\":" + + quote(sha256(generatedApiClassification.getBytes( + StandardCharsets.UTF_8))) + + ",\"migrationLedgerSha256\":" + + quote(sha256(apiMigration.getBytes(StandardCharsets.UTF_8))) + + "},\n" + + " \"benchmarkSmoke\": " + + quote(benchmarkPassed ? "passed" : "failed") + ",\n" + + " \"artifacts\": " + fileEvidenceJson(primary) + ",\n" + + " \"reproducibility\": {\"status\":" + + quote(reproducible ? "passed" : "failed") + + ",\"replicas\":" + fileEvidenceJson(replicas) + "},\n" + + " \"localComposite\": {\"command\":" + + quote(getLocalCompositeCommand().get()) + + ",\"outcome\":" + + quote(workingReady ? "passed" : "failed") + "},\n" + + " \"workingReady\": " + workingReady + ",\n" + + " \"publishedModeStatus\": " + + quote(publishedStatus) + "\n" + + "}\n"; + File output = getOutputFile().get().getAsFile(); + output.getParentFile().mkdirs(); + Files.write(output.toPath(), json.getBytes(StandardCharsets.UTF_8)); + if (getFailOnIncomplete().get() && !workingReady) { + throw new GradleException( + "BEX working evidence is incomplete; see " + output); + } + } catch (GradleException exception) { + throw exception; + } catch (Exception exception) { + throw new GradleException("Cannot generate BEX working report", exception); + } + } + + private static List regularFiles(ConfigurableFileCollection files) { + return files.getFiles().stream().filter(File::isFile) + .sorted(Comparator.comparing(GenerateWorkingReportTask::unix)) + .collect(Collectors.toList()); + } + + private static List describe(ConfigurableFileCollection files) + throws Exception { + List result = new ArrayList<>(); + for (File file : regularFiles(files)) { + result.add(new FileEvidence(file, file.length(), sha256(file))); + } + return result; + } + + private static boolean requiredArtifacts(List files) { + Set names = files.stream().map(item -> item.file.getName()) + .collect(Collectors.toSet()); + return names.stream().anyMatch(name -> name.matches( + "blue-bex-core-.+\\.jar")) + && names.stream().anyMatch(name -> name.matches( + "blue-bex-contracts-.+\\.jar")) + && names.stream().anyMatch(name -> name.matches( + "blue-bex-java-.+\\.jar")) + && names.stream().anyMatch(name -> name.matches( + "blue-bex-java-.+-sources\\.jar")) + && names.stream().anyMatch(name -> name.matches( + "blue-bex-java-.+-javadoc\\.jar")) + && names.stream().anyMatch(name -> name.matches( + "blue-bex-java-.+-source-release\\.zip")); + } + + private static boolean replicasMatch( + List primary, List replicas) { + Map byName = new LinkedHashMap<>(); + for (FileEvidence replica : replicas) { + byName.put(replica.file.getName(), replica); + } + return !primary.isEmpty() && primary.stream().allMatch(item -> + byName.containsKey(item.file.getName()) + && item.sha256.equals(byName.get( + item.file.getName()).sha256)); + } + + private static BytecodeEvidence bytecode(List artifacts) + throws IOException { + int classes = 0; + int maximum = 0; + boolean passed = true; + for (FileEvidence artifact : artifacts) { + String name = artifact.file.getName(); + if (!name.endsWith(".jar") || name.contains("-sources") + || name.contains("-javadoc")) { + continue; + } + try (JarFile jar = new JarFile(artifact.file)) { + for (JarEntry entry : java.util.Collections.list(jar.entries())) { + if (!entry.getName().endsWith(".class")) { + continue; + } + classes++; + try (DataInputStream input = new DataInputStream( + jar.getInputStream(entry))) { + if (input.readInt() != 0xCAFEBABE) { + passed = false; + continue; + } + input.readUnsignedShort(); + int major = input.readUnsignedShort(); + maximum = Math.max(maximum, major); + passed &= major <= 52; + } + } + } + } + return new BytecodeEvidence(passed && classes > 0, classes, maximum); + } + + private static TestTotals readTests(File directory) throws Exception { + TestTotals totals = new TestTotals(); + if (!directory.isDirectory()) { + return totals; + } + for (java.nio.file.Path path : Files.walk(directory.toPath()) + .filter(item -> item.getFileName().toString().startsWith("TEST-")) + .filter(item -> item.toString().endsWith(".xml")) + .collect(Collectors.toList())) { + Element root = DocumentBuilderFactory.newInstance() + .newDocumentBuilder().parse(path.toFile()).getDocumentElement(); + int declared = integer(root, "tests"); + int executed = root.getElementsByTagName("testcase").getLength(); + int failed = integer(root, "failures") + integer(root, "errors"); + int skipped = integer(root, "skipped"); + totals.executed += executed; + totals.failed += failed; + totals.skipped += skipped; + totals.passed += executed - failed - skipped; + totals.unclassified += Math.max(0, declared - executed); + } + return totals; + } + + private static boolean hostedTestsPassed(File directory) throws Exception { + TestTotals totals = new TestTotals(); + int files = 0; + for (java.nio.file.Path path : Files.walk(directory.toPath()) + .filter(item -> item.getFileName().toString().matches( + "TEST-.*(?:HostedRuntimeWorkSession|" + + "SemanticIdentityIntegration|" + + "CompositeExhaustionEvidence).*\\.xml")) + .collect(Collectors.toList())) { + files++; + Element root = DocumentBuilderFactory.newInstance() + .newDocumentBuilder().parse(path.toFile()).getDocumentElement(); + totals.executed += integer(root, "tests"); + totals.failed += integer(root, "failures") + integer(root, "errors"); + totals.skipped += integer(root, "skipped"); + } + return files >= 3 && totals.executed > 0 + && totals.failed == 0 && totals.skipped == 0; + } + + private static int integer(Element element, String name) { + String value = element.getAttribute(name); + return value.isEmpty() ? 0 : Integer.parseInt(value); + } + + private static ConformanceTotals conformanceTotals(String text) { + return new ConformanceTotals( + pair(text, "normativeVectors", "executedAndPassing"), + pair(text, "normativeVectors", "required"), + pair(text, "behaviorFixtures", "executedAndPassing"), + pair(text, "behaviorFixtures", "required"), + pair(text, "gasMicrofixtures", "executedAndPassing"), + pair(text, "gasMicrofixtures", "required"), + pair(text, "operators", "executedAndPassing"), + pair(text, "operators", "required")); + } + + private static int pair(String text, String section, String field) { + Matcher matcher = Pattern.compile("\\\"" + Pattern.quote(section) + + "\\\"\\s*:\\s*\\{([^{}]|\\{[^{}]*\\})*?\\\"" + + Pattern.quote(field) + "\\\"\\s*:\\s*(\\d+)") + .matcher(text); + return matcher.find() ? Integer.parseInt(matcher.group(2)) : -1; + } + + private static boolean hasStatus(String text, String status) { + return text.matches("(?s).*\\\"status\\\"\\s*:\\s*\\\"" + + Pattern.quote(status) + "\\\".*"); + } + + private static boolean sectionPassed(String text, String name) { + String section = objectSection(text, name); + return hasStatus(section, "passed") + && !hasStatus(section, "failed") + && !hasStatus(section, "not-executed"); + } + + private static String objectSection(String text, String name) { + int key = text.indexOf("\"" + name + "\""); + if (key < 0) { + return ""; + } + int start = text.indexOf('{', key); + if (start < 0) { + return ""; + } + int depth = 0; + boolean quoted = false; + boolean escaped = false; + for (int index = start; index < text.length(); index++) { + char value = text.charAt(index); + if (quoted) { + if (escaped) { + escaped = false; + } else if (value == '\\') { + escaped = true; + } else if (value == '"') { + quoted = false; + } + } else if (value == '"') { + quoted = true; + } else if (value == '{') { + depth++; + } else if (value == '}' && --depth == 0) { + return text.substring(start, index + 1); + } + } + return ""; + } + + private static boolean hasInventory(String text, int types, int descriptors) { + return text.matches("(?s).*\\\"publicTypeCount\\\"\\s*:\\s*" + + types + ".*") + && text.matches("(?s).*\\\"publicDescriptorCount\\\"" + + "\\s*:\\s*" + descriptors + ".*"); + } + + private static int manifestDescriptorCount(String text) { + if (text.isEmpty()) { + return 0; + } + return (int) Arrays.stream(text.split("\\R")) + .filter(line -> !line.isEmpty()) + .filter(line -> !line.startsWith("schema=")) + .count(); + } + + private static int manifestTypeCount(String text) { + return (int) Arrays.stream(text.split("\\R")) + .filter(line -> line.startsWith("class ")) + .count(); + } + + private static int shaCount(String text) { + Matcher matcher = Pattern.compile( + "\\\"sha256\\\"\\s*:\\s*\\\"[0-9a-f]{64}\\\"") + .matcher(text); + int count = 0; + while (matcher.find()) { + count++; + } + return count; + } + + private static LegacyTotals legacyTotals(ConfigurableFileCollection sources) + throws IOException { + int lines = 0; + int files = 0; + for (File file : regularFiles(sources)) { + String text = read(file); + Matcher matcher = FORBIDDEN_IMPORT.matcher(text); + int fileLines = 0; + while (matcher.find()) { + fileLines++; + } + if (fileLines > 0) { + files++; + lines += fileLines; + } + } + return new LegacyTotals(lines, files); + } + + private static int integerAfter(String text, String section, String field) { + int start = text.indexOf(section); + if (start < 0) { + return -1; + } + Matcher matcher = Pattern.compile(Pattern.quote(field) + + "\\s*:\\s*(\\d+)").matcher(text.substring(start)); + return matcher.find() ? Integer.parseInt(matcher.group(1)) : -1; + } + + private static String stringAfter(String text, String section, String field) { + int start = text.indexOf(section); + if (start < 0) { + return ""; + } + Matcher matcher = Pattern.compile(Pattern.quote(field) + + "\\s*:\\s*\\\"([^\\\"]+)\\\"") + .matcher(text.substring(start)); + return matcher.find() ? matcher.group(1) : ""; + } + + private static GitState gitState(File directory) throws Exception { + String head = gitText(directory, "rev-parse", "HEAD").trim(); + byte[] status = git(directory, "status", "--porcelain", "-z"); + return new GitState(head, status.length != 0, sha256(status)); + } + + private static List gitLines(File directory, String... args) + throws Exception { + String text = gitText(directory, args).trim(); + return text.isEmpty() ? new ArrayList<>() + : Arrays.asList(text.split("\\R")); + } + + private static String gitText(File directory, String... args) + throws Exception { + return new String(git(directory, args), StandardCharsets.UTF_8); + } + + private static byte[] git(File directory, String... args) throws Exception { + List command = new ArrayList<>(); + command.add("git"); + command.addAll(Arrays.asList(args)); + Process process = new ProcessBuilder(command).directory(directory) + .redirectErrorStream(true).start(); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + byte[] buffer = new byte[8192]; + int read; + while ((read = process.getInputStream().read(buffer)) >= 0) { + output.write(buffer, 0, read); + } + if (process.waitFor() != 0) { + throw new IOException(output.toString(StandardCharsets.UTF_8.name())); + } + return output.toByteArray(); + } + + private static String textFor(List files, String fragment) + throws IOException { + for (File file : files) { + if (unix(file).contains(fragment)) { + return read(file); + } + } + return ""; + } + + private static String read(File file) throws IOException { + return new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8); + } + + private static String unix(File file) { + return file.getAbsolutePath().replace(File.separatorChar, '/'); + } + + private static String fileEvidenceJson(List files) { + return files.stream().map(item -> "{\"path\":" + quote(unix(item.file)) + + ",\"bytes\":" + item.bytes + ",\"sha256\":" + + quote(item.sha256) + "}").collect(Collectors.joining(",", "[", "]")); + } + + private static String jsonObjects(List values) { + return values.stream().filter(value -> !value.isEmpty()) + .collect(Collectors.joining(",", "[", "]")); + } + + private static String jsonStrings(Iterable values) { + List result = new ArrayList<>(); + for (String value : values) { + result.add(quote(value)); + } + return "[" + String.join(",", result) + "]"; + } + + private static String jsonOrEmpty(String value) { + return value.trim().isEmpty() ? "{}" : value.trim(); + } + + private static String quote(String value) { + return "\"" + value.replace("\\", "\\\\") + .replace("\"", "\\\"") + "\""; + } + + private static String sha256(File file) throws Exception { + try (FileInputStream input = new FileInputStream(file)) { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] buffer = new byte[8192]; + int read; + while ((read = input.read(buffer)) >= 0) { + digest.update(buffer, 0, read); + } + return hex(digest.digest()); + } + } + + private static String sha256(byte[] value) throws Exception { + return hex(MessageDigest.getInstance("SHA-256").digest(value)); + } + + private static String hex(byte[] value) { + StringBuilder result = new StringBuilder(); + for (byte item : value) { + result.append(String.format("%02x", item)); + } + return result.toString(); + } + + private static final class TestTotals { + private int executed; + private int passed; + private int failed; + private int skipped; + private int unclassified; + } + + private static final class ConformanceTotals { + private final int vectors; + private final int requiredVectors; + private final int behavior; + private final int requiredBehavior; + private final int gas; + private final int requiredGas; + private final int operators; + private final int requiredOperators; + + private ConformanceTotals(int vectors, int requiredVectors, + int behavior, int requiredBehavior, int gas, int requiredGas, + int operators, int requiredOperators) { + this.vectors = vectors; + this.requiredVectors = requiredVectors; + this.behavior = behavior; + this.requiredBehavior = requiredBehavior; + this.gas = gas; + this.requiredGas = requiredGas; + this.operators = operators; + this.requiredOperators = requiredOperators; + } + + private boolean complete() { + return vectors == 60 && requiredVectors == 60 + && behavior == 105 && requiredBehavior == 105 + && gas == 30 && requiredGas == 30 + && operators == 86 && requiredOperators == 86; + } + + private String json() { + return "{\"normativeVectors\":" + counts(vectors, requiredVectors) + + ",\"behaviorFixtures\":" + counts(behavior, requiredBehavior) + + ",\"gasMicrofixtures\":" + counts(gas, requiredGas) + + ",\"operators\":" + counts(operators, requiredOperators) + + "}"; + } + + private static String counts(int actual, int required) { + return "{\"executedAndPassing\":" + actual + + ",\"required\":" + required + "}"; + } + } + + private static final class GitState { + private final String head; + private final boolean dirty; + private final String statusSha256; + + private GitState(String head, boolean dirty, String statusSha256) { + this.head = head; + this.dirty = dirty; + this.statusSha256 = statusSha256; + } + } + + private static final class LegacyTotals { + private final int lines; + private final int files; + + private LegacyTotals(int lines, int files) { + this.lines = lines; + this.files = files; + } + } + + private static final class FileEvidence { + private final File file; + private final long bytes; + private final String sha256; + + private FileEvidence(File file, long bytes, String sha256) { + this.file = file; + this.bytes = bytes; + this.sha256 = sha256; + } + } + + private static final class BytecodeEvidence { + private final boolean passed; + private final int classCount; + private final int maximumMajor; + + private BytecodeEvidence(boolean passed, int classCount, + int maximumMajor) { + this.passed = passed; + this.classCount = classCount; + this.maximumMajor = maximumMajor; + } + } +} diff --git a/build-logic/src/main/java/blue/bex/buildlogic/tasks/VerifyBexArchitectureTask.java b/build-logic/src/main/java/blue/bex/buildlogic/tasks/VerifyBexArchitectureTask.java new file mode 100644 index 0000000..ae0133c --- /dev/null +++ b/build-logic/src/main/java/blue/bex/buildlogic/tasks/VerifyBexArchitectureTask.java @@ -0,0 +1,351 @@ +package blue.bex.buildlogic.tasks; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Deque; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; +import org.gradle.api.DefaultTask; +import org.gradle.api.GradleException; +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.file.DirectoryProperty; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.provider.ListProperty; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.InputFiles; +import org.gradle.api.tasks.Internal; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.PathSensitive; +import org.gradle.api.tasks.PathSensitivity; +import org.gradle.api.tasks.TaskAction; + +/** Verifies module/package ownership, acyclicity, and cohesion constraints. */ +public abstract class VerifyBexArchitectureTask extends DefaultTask { + private static final Pattern PACKAGE = + Pattern.compile("(?m)^package\\s+([a-zA-Z0-9_.]+)\\s*;"); + private static final Pattern IMPORT = + Pattern.compile("(?m)^import\\s+(?:static\\s+)?([a-zA-Z0-9_.*]+)\\s*;"); + private static final Pattern PROJECT_DEPENDENCY = + Pattern.compile("project\\(\\s*\"(:[a-zA-Z0-9_-]+)\"\\s*\\)"); + + @Internal + public abstract DirectoryProperty getRepositoryDirectory(); + + @InputFiles + @PathSensitive(PathSensitivity.RELATIVE) + public abstract ConfigurableFileCollection getArchitectureInputs(); + + @Input + public abstract ListProperty getModuleNames(); + + @Input + public abstract ListProperty getModuleEdges(); + + @OutputFile + public abstract RegularFileProperty getOutputFile(); + + @TaskAction + public void verify() throws IOException { + File root = getRepositoryDirectory().get().getAsFile(); + List failures = new ArrayList<>(); + Map> packageOwners = new LinkedHashMap<>(); + Map> packageEdges = new LinkedHashMap<>(); + List sources = new ArrayList<>(); + + for (String module : getModuleNames().get()) { + File sourceRoot = new File(root, module + "/src/main/java"); + if (!sourceRoot.isDirectory()) { + failures.add("missing-main-source-root:" + module); + continue; + } + try (Stream paths = Files.walk(sourceRoot.toPath())) { + paths.filter(path -> path.toString().endsWith(".java")) + .sorted() + .forEach(path -> { + try { + String text = new String( + Files.readAllBytes(path), StandardCharsets.UTF_8); + Matcher packageMatcher = PACKAGE.matcher(text); + if (!packageMatcher.find()) { + failures.add("missing-package:" + root.toPath() + .relativize(path)); + return; + } + String packageName = packageMatcher.group(1); + packageOwners.computeIfAbsent( + packageName, ignored -> new LinkedHashSet<>()) + .add(module); + sources.add(new Source(module, path.toFile(), + packageName, text)); + packageEdges.computeIfAbsent( + packageName, ignored -> new LinkedHashSet<>()); + } catch (IOException exception) { + throw new ArchitectureReadException(exception); + } + }); + } + } + + for (Map.Entry> entry : packageOwners.entrySet()) { + if (entry.getValue().size() > 1) { + failures.add("split-package:" + entry.getKey() + ":" + + String.join(",", entry.getValue())); + } + } + + Set packages = packageOwners.keySet(); + for (Source source : sources) { + if (source.text.contains("import blue.language.utils.")) { + failures.add("legacy-language-utils:" + relative(root, source.file)); + } + if (source.module.equals("blue-bex-core") + && source.text.contains("import blue.language.processor.")) { + failures.add("core-processor-import:" + relative(root, source.file)); + } + Matcher imports = IMPORT.matcher(source.text); + while (imports.find()) { + String imported = imports.group(1).replace(".*", ""); + String target = longestPackagePrefix(imported, packages); + if (target != null && !target.equals(source.packageName)) { + packageEdges.get(source.packageName).add(target); + } + } + int lines = source.text.split("\\R").length; + String name = source.file.getName(); + if (name.equals("BexCompiler.java") && lines > 450) { + failures.add("BexCompiler-lines:" + lines); + } + if (name.equals("BexExpressions.java")) { + failures.add("BexExpressions-still-present"); + } + if (name.equals("BexBlueTypeMatcher.java") && lines > 450) { + failures.add("BexBlueTypeMatcher-lines:" + lines); + } + if (name.equals("BexGasMeter.java") && lines > 500) { + failures.add("BexGasMeter-lines:" + lines); + } + if (lines > 800) { + failures.add("production-file-over-800-lines:" + + relative(root, source.file) + ":" + lines); + } + } + + List> packageComponents = stronglyConnected(packageEdges); + long packageCycles = packageComponents.stream() + .filter(component -> component.size() > 1) + .count(); + if (packageCycles != 0) { + failures.add("package-sccs-larger-than-one:" + packageCycles); + } + + Map> moduleGraph = new LinkedHashMap<>(); + for (String module : getModuleNames().get()) { + moduleGraph.put(module, new LinkedHashSet<>()); + } + for (String edge : getModuleEdges().get()) { + String[] parts = edge.split("->", -1); + if (parts.length != 2 || !moduleGraph.containsKey(parts[0]) + || !moduleGraph.containsKey(parts[1])) { + failures.add("invalid-module-edge:" + edge); + } else { + moduleGraph.get(parts[0]).add(parts[1]); + } + } + int undeclaredModuleEdges = 0; + for (String module : getModuleNames().get()) { + File build = new File(root, module + "/build.gradle.kts"); + if (!build.isFile()) { + failures.add("missing-module-build:" + module); + continue; + } + String text = new String( + Files.readAllBytes(build.toPath()), StandardCharsets.UTF_8); + Matcher matcher = PROJECT_DEPENDENCY.matcher(text); + while (matcher.find()) { + String target = matcher.group(1).substring(1); + if (target.equals(module)) { + failures.add("self-module-edge:" + module); + undeclaredModuleEdges++; + } else if (!moduleGraph.get(module).contains(target)) { + failures.add("undeclared-module-edge:" + module + "->" + target); + undeclaredModuleEdges++; + } + } + } + long moduleCycles = stronglyConnected(moduleGraph).stream() + .filter(component -> component.size() > 1) + .count(); + if (moduleCycles != 0) { + failures.add("module-cycles:" + moduleCycles); + } + + File rootBuild = new File(root, "build.gradle.kts"); + long rootBuildLines = -1; + if (rootBuild.isFile()) { + try (Stream lines = Files.lines(rootBuild.toPath())) { + rootBuildLines = lines.count(); + } + } + if (rootBuildLines < 0 || rootBuildLines > 300) { + failures.add("root-build-lines:" + rootBuildLines); + } + + String json = "{\n" + + " \"schema\": \"blue-bex-architecture/1.0\",\n" + + " \"status\": \"" + (failures.isEmpty() ? "passed" : "failed") + + "\",\n" + + " \"moduleCount\": " + moduleGraph.size() + ",\n" + + " \"moduleCycles\": " + moduleCycles + ",\n" + + " \"moduleEdges\": " + + jsonArray(sortedEdges(moduleGraph)) + ",\n" + + " \"undeclaredModuleEdges\": " + + undeclaredModuleEdges + ",\n" + + " \"packageCount\": " + packageOwners.size() + ",\n" + + " \"packageSccsLargerThanOne\": " + packageCycles + ",\n" + + " \"packageEdges\": " + + jsonArray(sortedEdges(packageEdges)) + ",\n" + + " \"splitPackageCount\": " + + packageOwners.values().stream().filter(owners -> owners.size() > 1) + .count() + ",\n" + + " \"rootBuildLines\": " + rootBuildLines + ",\n" + + " \"failures\": " + jsonArray(failures) + "\n" + + "}\n"; + File output = getOutputFile().get().getAsFile(); + output.getParentFile().mkdirs(); + Files.write(output.toPath(), json.getBytes(StandardCharsets.UTF_8)); + if (!failures.isEmpty()) { + throw new GradleException( + "BEX architecture verification failed: " + + String.join("; ", failures)); + } + } + + private static String longestPackagePrefix( + String imported, Collection packages) { + String result = null; + for (String packageName : packages) { + if ((imported.equals(packageName) + || imported.startsWith(packageName + ".")) + && (result == null || packageName.length() > result.length())) { + result = packageName; + } + } + return result; + } + + private static List> stronglyConnected( + Map> graph) { + Tarjan tarjan = new Tarjan<>(graph); + return tarjan.components(); + } + + private static String relative(File root, File file) { + return root.toPath().relativize(file.toPath()).toString(); + } + + private static String jsonArray(List values) { + List escaped = new ArrayList<>(); + for (String value : values) { + escaped.add("\"" + value.replace("\\", "\\\\") + .replace("\"", "\\\"") + "\""); + } + return "[" + String.join(",", escaped) + "]"; + } + + private static List sortedEdges(Map> graph) { + List result = new ArrayList<>(); + for (Map.Entry> entry : graph.entrySet()) { + for (String target : entry.getValue()) { + result.add(entry.getKey() + "->" + target); + } + } + Collections.sort(result); + return result; + } + + private static final class Source { + private final String module; + private final File file; + private final String packageName; + private final String text; + + private Source(String module, File file, String packageName, String text) { + this.module = module; + this.file = file; + this.packageName = packageName; + this.text = text; + } + } + + private static final class ArchitectureReadException + extends RuntimeException { + private ArchitectureReadException(IOException cause) { + super(cause); + } + } + + private static final class Tarjan { + private final Map> graph; + private final Map indexes = new HashMap<>(); + private final Map lowLinks = new HashMap<>(); + private final Deque stack = new ArrayDeque<>(); + private final Set onStack = new HashSet<>(); + private final List> components = new ArrayList<>(); + private int nextIndex; + + private Tarjan(Map> graph) { + this.graph = graph; + } + + private List> components() { + for (T node : graph.keySet()) { + if (!indexes.containsKey(node)) { + visit(node); + } + } + return components; + } + + private void visit(T node) { + indexes.put(node, nextIndex); + lowLinks.put(node, nextIndex); + nextIndex++; + stack.push(node); + onStack.add(node); + for (T target : graph.getOrDefault(node, Collections.emptySet())) { + if (!indexes.containsKey(target)) { + visit(target); + lowLinks.put(node, Math.min( + lowLinks.get(node), lowLinks.get(target))); + } else if (onStack.contains(target)) { + lowLinks.put(node, Math.min( + lowLinks.get(node), indexes.get(target))); + } + } + if (lowLinks.get(node).equals(indexes.get(node))) { + Set component = new LinkedHashSet<>(); + T item; + do { + item = stack.pop(); + onStack.remove(item); + component.add(item); + } while (!item.equals(node)); + components.add(component); + } + } + } +} diff --git a/build-logic/src/main/java/blue/bex/buildlogic/tasks/VerifyJava8BytecodeTask.java b/build-logic/src/main/java/blue/bex/buildlogic/tasks/VerifyJava8BytecodeTask.java new file mode 100644 index 0000000..f775560 --- /dev/null +++ b/build-logic/src/main/java/blue/bex/buildlogic/tasks/VerifyJava8BytecodeTask.java @@ -0,0 +1,69 @@ +package blue.bex.buildlogic.tasks; + +import java.io.DataInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import org.gradle.api.DefaultTask; +import org.gradle.api.GradleException; +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.InputFiles; +import org.gradle.api.tasks.PathSensitive; +import org.gradle.api.tasks.PathSensitivity; +import org.gradle.api.tasks.TaskAction; + +/** Verifies that every emitted production class is valid Java 8 bytecode. */ +public abstract class VerifyJava8BytecodeTask extends DefaultTask { + @Input + public abstract Property getAllowEmpty(); + + @InputFiles + @PathSensitive(PathSensitivity.RELATIVE) + public abstract ConfigurableFileCollection getClassDirectories(); + + @TaskAction + public void verify() throws IOException { + List failures = new ArrayList<>(); + int[] count = {0}; + for (File root : getClassDirectories().getFiles()) { + if (!root.isDirectory()) { + continue; + } + java.nio.file.Files.walk(root.toPath()) + .filter(path -> path.toString().endsWith(".class")) + .sorted() + .forEach(path -> { + count[0]++; + try (DataInputStream input = new DataInputStream( + new FileInputStream(path.toFile()))) { + int magic = input.readInt(); + int minor = input.readUnsignedShort(); + int major = input.readUnsignedShort(); + if (magic != 0xCAFEBABE || major > 52) { + failures.add(root.toPath().relativize(path) + + ":major=" + major + ":minor=" + minor); + } + } catch (IOException exception) { + throw new BytecodeReadException(exception); + } + }); + } + if (count[0] == 0 && !getAllowEmpty().get()) { + throw new GradleException("No production class files were verified"); + } + if (!failures.isEmpty()) { + throw new GradleException( + "Non-Java-8 BEX bytecode: " + String.join("; ", failures)); + } + } + + private static final class BytecodeReadException extends RuntimeException { + private BytecodeReadException(IOException cause) { + super(cause); + } + } +} diff --git a/build-logic/src/main/java/blue/bex/buildlogic/tasks/VerifyLegacyLanguageImportsTask.java b/build-logic/src/main/java/blue/bex/buildlogic/tasks/VerifyLegacyLanguageImportsTask.java new file mode 100644 index 0000000..05527d9 --- /dev/null +++ b/build-logic/src/main/java/blue/bex/buildlogic/tasks/VerifyLegacyLanguageImportsTask.java @@ -0,0 +1,69 @@ +package blue.bex.buildlogic.tasks; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.gradle.api.DefaultTask; +import org.gradle.api.GradleException; +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.tasks.InputFiles; +import org.gradle.api.tasks.PathSensitive; +import org.gradle.api.tasks.PathSensitivity; +import org.gradle.api.tasks.TaskAction; + +/** Fails when a removed pre-modular Language package returns to production. */ +public abstract class VerifyLegacyLanguageImportsTask extends DefaultTask { + private static final List FORBIDDEN = Arrays.asList( + "blue.language.utils.", + "blue.language.snapshot.ResolvedSnapshot", + "blue.language.NodeProvider", + "blue.language.BlueOperationLimits", + "blue.language.BlueOperationOutcome", + "blue.language.BlueOperationResult"); + + @InputFiles + @PathSensitive(PathSensitivity.RELATIVE) + public abstract ConfigurableFileCollection getSourceDirectories(); + + @TaskAction + public void verify() throws IOException { + List failures = new ArrayList<>(); + for (File root : getSourceDirectories().getFiles()) { + if (!root.isDirectory()) { + continue; + } + Files.walk(root.toPath()) + .filter(path -> path.toString().endsWith(".java")) + .sorted() + .forEach(path -> { + try { + String source = new String( + Files.readAllBytes(path), StandardCharsets.UTF_8); + for (String forbidden : FORBIDDEN) { + if (source.contains("import " + forbidden)) { + failures.add(root.toPath().relativize(path) + + ":" + forbidden); + } + } + } catch (IOException exception) { + throw new SourceReadException(exception); + } + }); + } + if (!failures.isEmpty()) { + throw new GradleException( + "Removed Language imports in BEX production source: " + + String.join("; ", failures)); + } + } + + private static final class SourceReadException extends RuntimeException { + private SourceReadException(IOException cause) { + super(cause); + } + } +} diff --git a/build-logic/src/main/java/blue/bex/buildlogic/tasks/VerifyPublishedLanguageTask.java b/build-logic/src/main/java/blue/bex/buildlogic/tasks/VerifyPublishedLanguageTask.java new file mode 100644 index 0000000..b2fcfbd --- /dev/null +++ b/build-logic/src/main/java/blue/bex/buildlogic/tasks/VerifyPublishedLanguageTask.java @@ -0,0 +1,316 @@ +package blue.bex.buildlogic.tasks; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.Reader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Enumeration; +import java.util.List; +import java.util.Properties; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import org.gradle.api.DefaultTask; +import org.gradle.api.GradleException; +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.InputFile; +import org.gradle.api.tasks.InputFiles; +import org.gradle.api.tasks.Optional; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.PathSensitive; +import org.gradle.api.tasks.PathSensitivity; +import org.gradle.api.tasks.TaskAction; + +/** + * Authenticates published Language evidence against a reviewed inspection. + * + *

Caller-supplied coordinates and digests are assertions, not evidence. + * This task only passes when they agree with the source-controlled inspection, + * the resolved artifact bytes, and a same-source local/published differential + * report. Missing publication inputs remain explicitly {@code not-executed}. + */ +public abstract class VerifyPublishedLanguageTask extends DefaultTask { + private static final String COMPATIBLE_STATUS = + "compatible-with-final-hosted-adapter"; + + @Input + public abstract Property getRequired(); + + @Input + @Optional + public abstract Property getCoordinate(); + + @Input + @Optional + public abstract Property getArtifactSha256(); + + @InputFile + @PathSensitive(PathSensitivity.RELATIVE) + public abstract RegularFileProperty getInspectionFile(); + + @InputFiles + @Optional + @PathSensitive(PathSensitivity.NAME_ONLY) + public abstract ConfigurableFileCollection getArtifacts(); + + @InputFile + @Optional + @PathSensitive(PathSensitivity.NONE) + public abstract RegularFileProperty getDifferentialReport(); + + @OutputFile + public abstract RegularFileProperty getOutputFile(); + + @TaskAction + public void verify() { + try { + File inspectionFile = getInspectionFile().get().getAsFile(); + Properties inspection = new Properties(); + try (Reader reader = Files.newBufferedReader( + inspectionFile.toPath(), StandardCharsets.UTF_8)) { + inspection.load(reader); + } + + String reviewedStatus = property(inspection, "status"); + String reviewedCoordinate = property(inspection, "coordinate"); + String reviewedSha = property(inspection, "artifact.sha256"); + String sourceCommit = property(inspection, "source.commit"); + String sourceTag = property(inspection, "source.tag"); + String assertedCoordinate = getCoordinate().getOrElse("").trim(); + String assertedSha = getArtifactSha256().getOrElse("").trim(); + String configuredCoordinate = assertedCoordinate.isEmpty() + ? reviewedCoordinate : assertedCoordinate; + String configuredSha = assertedSha.isEmpty() + ? reviewedSha : assertedSha; + + List artifacts = new ArrayList<>(getArtifacts().getFiles()); + artifacts.removeIf(file -> !file.isFile()); + artifacts.sort(Comparator.comparing(File::getName) + .thenComparing(File::getAbsolutePath)); + List artifactEvidence = new ArrayList<>(); + for (File artifact : artifacts) { + artifactEvidence.add(new ArtifactEvidence( + artifact, sha256(artifact))); + } + + List reasons = new ArrayList<>(); + boolean reviewedCompatible = COMPATIBLE_STATUS.equals(reviewedStatus); + if (!reviewedCompatible) { + reasons.add("reviewed published API status is " + reviewedStatus); + } + boolean reviewedIdentityComplete = reviewedCoordinate.matches( + "[^:]+:[^:]+:[^:]+") + && reviewedSha.matches("[0-9a-f]{64}") + && sourceCommit.matches("[0-9a-f]{40}") + && sourceTag.matches("v?[A-Za-z0-9][A-Za-z0-9._-]*"); + if (!reviewedIdentityComplete) { + reasons.add("reviewed coordinate, artifact hash, or source identity " + + "is incomplete"); + } + + boolean configured = !configuredCoordinate.isEmpty() + && !configuredSha.isEmpty(); + boolean assertionsMatch = configured + && reviewedCoordinate.equals(configuredCoordinate) + && reviewedSha.equals(configuredSha); + if (configured && !assertionsMatch) { + reasons.add("caller assertions differ from reviewed publication " + + "identity"); + } + + boolean artifactHashMatches = artifactEvidence.stream() + .anyMatch(item -> reviewedSha.equals(item.sha256)); + if (!artifacts.isEmpty() && !artifactHashMatches) { + reasons.add("no resolved artifact matches the reviewed SHA-256"); + } + boolean reviewedApiClaimsPass = reviewedApiClaimsPass( + inspection, artifacts, reasons); + + String differential = getDifferentialReport().isPresent() + && getDifferentialReport().get().getAsFile().isFile() + ? read(getDifferentialReport().get().getAsFile()) : ""; + boolean differentialPassed = containsStatus(differential, "passed") + && fieldPassed(differential, "semanticAndGasParity") + && fieldPassed(differential, "exactGasTraceParity"); + if (!differential.isEmpty() && !differentialPassed) { + reasons.add("local/published semantic and exact-gas differential " + + "did not pass"); + } + + boolean inputsPresent = configured && !artifacts.isEmpty() + && !differential.isEmpty(); + boolean passed = reviewedCompatible && reviewedIdentityComplete + && assertionsMatch && artifactHashMatches + && reviewedApiClaimsPass && differentialPassed; + String status; + if (!reviewedCompatible) { + status = "incompatible"; + } else if (!inputsPresent) { + status = "not-executed"; + reasons.add("resolved artifacts and same-run differential evidence " + + "are required"); + } else { + status = passed ? "passed" : "failed"; + } + + String json = "{\n" + + " \"schema\": \"blue-bex-published-language/2.0\",\n" + + " \"status\": " + quote(status) + ",\n" + + " \"coordinate\": " + quote(reviewedCoordinate) + ",\n" + + " \"artifactSha256\": " + quote(reviewedSha) + ",\n" + + " \"sourceCommit\": " + quote(sourceCommit) + ",\n" + + " \"sourceTag\": " + quote(sourceTag) + ",\n" + + " \"inspection\": {\"path\":" + + quote(unix(inspectionFile)) + ",\"sha256\":" + + quote(sha256(inspectionFile)) + ",\"reviewedStatus\":" + + quote(reviewedStatus) + "},\n" + + " \"configuredAssertionsMatch\": " + + assertionsMatch + ",\n" + + " \"resolvedArtifacts\": " + + artifactsJson(artifactEvidence) + ",\n" + + " \"apiInspectionPassed\": " + + reviewedApiClaimsPass + ",\n" + + " \"differentialStatus\": " + + quote(differentialPassed ? "passed" : "not-executed") + + ",\n" + + " \"blockers\": " + jsonStrings(reasons) + "\n" + + "}\n"; + File output = getOutputFile().get().getAsFile(); + output.getParentFile().mkdirs(); + Files.write(output.toPath(), json.getBytes(StandardCharsets.UTF_8)); + if (getRequired().get() && !passed) { + throw new GradleException( + "Published Blue Language evidence is not release-ready; " + + "see " + output); + } + } catch (GradleException exception) { + throw exception; + } catch (Exception exception) { + throw new GradleException( + "Cannot verify published Blue Language evidence", exception); + } + } + + private static boolean reviewedApiClaimsPass(Properties inspection, + List artifacts, List reasons) throws IOException { + boolean passed = "passed".equals(property( + inspection, "standaloneCompile")); + if (!passed) { + reasons.add("reviewed standalone compile did not pass"); + } + for (String key : inspection.stringPropertyNames().stream() + .sorted().collect(Collectors.toList())) { + if ((key.startsWith("class.") || key.startsWith("method.") + || key.startsWith("visibility.")) + && !"true".equals(inspection.getProperty(key))) { + passed = false; + reasons.add("reviewed API claim is false: " + key); + } + if (key.startsWith("class.") + && "true".equals(inspection.getProperty(key))) { + String entry = key.substring("class.".length()) + .replace('.', '/') + ".class"; + if (!containsJarEntry(artifacts, entry)) { + passed = false; + reasons.add("resolved artifacts do not contain " + entry); + } + } + } + return passed; + } + + private static boolean containsJarEntry(List files, String name) + throws IOException { + for (File file : files) { + if (!file.getName().endsWith(".jar")) { + continue; + } + try (JarFile jar = new JarFile(file)) { + Enumeration entries = jar.entries(); + while (entries.hasMoreElements()) { + if (name.equals(entries.nextElement().getName())) { + return true; + } + } + } + } + return false; + } + + private static String property(Properties properties, String key) { + return properties.getProperty(key, "").trim(); + } + + private static boolean containsStatus(String text, String status) { + return text.matches("(?s).*\\\"status\\\"\\s*:\\s*\\\"" + + Pattern.quote(status) + "\\\".*"); + } + + private static boolean fieldPassed(String text, String field) { + return text.matches("(?s).*\\\"" + Pattern.quote(field) + + "\\\"\\s*:\\s*(?:\\\"passed\\\"|true).*?"); + } + + private static String artifactsJson(List artifacts) { + return artifacts.stream().map(item -> "{\"path\":" + + quote(unix(item.file)) + ",\"bytes\":" + item.file.length() + + ",\"sha256\":" + quote(item.sha256) + "}") + .collect(Collectors.joining(",", "[", "]")); + } + + private static String jsonStrings(List values) { + return values.stream().map(VerifyPublishedLanguageTask::quote) + .collect(Collectors.joining(",", "[", "]")); + } + + private static String read(File file) throws IOException { + return new String(Files.readAllBytes(file.toPath()), + StandardCharsets.UTF_8); + } + + private static String sha256(File file) + throws IOException, NoSuchAlgorithmException { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + try (FileInputStream input = new FileInputStream(file)) { + byte[] buffer = new byte[8192]; + int read; + while ((read = input.read(buffer)) >= 0) { + digest.update(buffer, 0, read); + } + } + StringBuilder value = new StringBuilder(); + for (byte item : digest.digest()) { + value.append(String.format("%02x", item)); + } + return value.toString(); + } + + private static String unix(File file) { + return file.getAbsolutePath().replace(File.separatorChar, '/'); + } + + private static String quote(String value) { + return "\"" + value.replace("\\", "\\\\") + .replace("\"", "\\\"") + "\""; + } + + private static final class ArtifactEvidence { + private final File file; + private final String sha256; + + private ArtifactEvidence(File file, String sha256) { + this.file = file; + this.sha256 = sha256; + } + } +} diff --git a/build-logic/src/main/java/blue/bex/buildlogic/tasks/package-info.java b/build-logic/src/main/java/blue/bex/buildlogic/tasks/package-info.java new file mode 100644 index 0000000..1f42245 --- /dev/null +++ b/build-logic/src/main/java/blue/bex/buildlogic/tasks/package-info.java @@ -0,0 +1,11 @@ +/** + * Cache-aware Gradle tasks that fingerprint sources and verify architecture and + * published Language evidence. + * + *

Task instances are Gradle-owned and may rely only on declared inputs and + * outputs; they are not general thread-safe utilities. Required file/property + * inputs are non-null, and absent, stale, ambiguous, or malformed evidence fails + * closed. These tasks consume no portable BEX gas and must never report an + * unexecuted check as successful.

+ */ +package blue.bex.buildlogic.tasks; diff --git a/build.gradle.kts b/build.gradle.kts index 53f9603..8893929 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,3982 +1,16 @@ -import java.io.File -import java.nio.ByteBuffer -import java.nio.charset.StandardCharsets -import java.nio.file.Files -import java.nio.file.LinkOption -import java.security.MessageDigest -import java.time.Instant -import java.util.Properties -import java.util.zip.ZipFile -import groovy.json.JsonOutput -import groovy.json.JsonSlurper -import org.apache.commons.compress.archivers.zip.ZipFile as CommonsZipFile -import org.gradle.api.artifacts.Configuration -import org.gradle.api.artifacts.component.ProjectComponentIdentifier -import org.gradle.api.artifacts.result.ResolvedDependencyResult -import org.gradle.api.tasks.javadoc.Javadoc -import org.gradle.external.javadoc.StandardJavadocDocletOptions -import org.gradle.api.tasks.bundling.Jar -import org.gradle.api.tasks.bundling.Zip -import org.gradle.api.tasks.testing.Test - plugins { - `java-library` - `maven-publish` - signing + id("blue.bex.root-orchestration") id("org.jreleaser") version "1.24.0" } group = "blue.bex" -version = determineProjectVersion() - -val blueLanguagePublishedVersion = "3.1.0-rc.19" -val blueLanguageModelDeclaredCoordinate = - "blue.language:blue-language-model:$blueLanguagePublishedVersion" -val blueLanguageCoreDeclaredCoordinate = - "blue.language:blue-language-core:$blueLanguagePublishedVersion" -val blueLanguageMappingDeclaredCoordinate = - "blue.language:blue-language-mapping:$blueLanguagePublishedVersion" -val blueContractsCoreDeclaredCoordinate = - "blue.language:blue-contracts-core:$blueLanguagePublishedVersion" -val blueLanguageDeclaredCoordinate = - "blue.language:blue-language-java:$blueLanguagePublishedVersion" -val blueLanguageFocusedCoordinates = - listOf( - blueLanguageModelDeclaredCoordinate, - blueLanguageCoreDeclaredCoordinate, - blueLanguageMappingDeclaredCoordinate, - blueContractsCoreDeclaredCoordinate - ) -val blueLanguageFocusedModuleNames = - linkedSetOf( - "blue-language-model", - "blue-language-core", - "blue-language-mapping", - "blue-contracts-core" - ) -val blueLanguageFocusedProjectPaths = - blueLanguageFocusedModuleNames.associateWith { ":$it" } -val requiredBexRegistryIdentity = - "sha256:23d282ec1c0bb016263922b1b49c369fdd537efdcf23e005eceeb888d7763fe1" -val requiredBexGasManifestIdentity = - "sha256:41247c820d91a12fdfc17fd9e787a5d8d668d8acc5954fdcb131715bf9e6147d" -val requiredBexFixturePackageIdentity = - "sha256:a1b7bb2b3687389409bc9d0aa450c734f7856d2bcb818c95f4d7ecb19095d20e" -val latestLanguageMigrationLock = - layout.projectDirectory.file( - "gradle/verification/latest-language-baseline.json" - ) -val blueLanguageCompositePath = - providers.gradleProperty("blueLanguageCompositePath") - .orNull - ?.trim() - ?.takeIf { it.isNotEmpty() } -val blueLanguageDependencyMode = - if (blueLanguageCompositePath == null) { - "standalone-published" - } else { - "local-composite" - } -val publishedBlueLanguageInspection = - Properties().apply { - file( - "src/test/resources/hosted-release/" + - "published-api-inspection.properties" - ).inputStream().use(::load) - } -val publishedBlueLanguageCoordinate = - publishedBlueLanguageInspection.getProperty("coordinate") -val publishedBlueLanguageSha256 = - publishedBlueLanguageInspection.getProperty("artifact.sha256") -val publishedBlueLanguageRepository = - publishedBlueLanguageInspection.getProperty("repository") -val blueLanguageModuleVersionCache = - File( - gradle.gradleUserHomeDir, - "caches/modules-2/files-2.1/blue.language/" + - "blue-language-java/$blueLanguagePublishedVersion" - ) -val blueLanguageModuleVersionCacheInitiallyAbsent = - !blueLanguageModuleVersionCache.exists() -val blueLanguageFocusedModuleVersionCaches = - blueLanguageFocusedModuleNames.associateWith { moduleName -> - File( - gradle.gradleUserHomeDir, - "caches/modules-2/files-2.1/blue.language/" + - "$moduleName/$blueLanguagePublishedVersion" - ) - } -val blueLanguageFocusedModuleVersionCachesInitiallyAbsent = - blueLanguageFocusedModuleVersionCaches.mapValues { (_, cache) -> - !cache.exists() - } -val blueLanguageRequireFreshModuleCache = - providers.gradleProperty("blueLanguageRequireFreshModuleCache") - .map(String::toBoolean) - .orElse(false) - -base { - archivesName.set("blue-bex-java") -} - -repositories { - // Release and developer resolution intentionally share one policy. - // A same-GAV artifact from ~/.m2 must never masquerade as the published - // Blue Language artifact in standalone-published mode. - mavenCentral() -} - -java { - withJavadocJar() - withSourcesJar() - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} - -tasks.withType().configureEach { - options.encoding = "UTF-8" - options.release.set(8) -} - -tasks.withType().configureEach { - isPreserveFileTimestamps = false - isReproducibleFileOrder = true -} - -tasks.withType().configureEach { - javadocTool.set( - javaToolchains.javadocToolFor { - languageVersion.set(JavaLanguageVersion.of(8)) - } - ) - (options as StandardJavadocDocletOptions).apply { - encoding = "UTF-8" - charSet = "UTF-8" - addBooleanOption("notimestamp", true) - } -} - -fun sha256(file: File): String { - val digest = MessageDigest.getInstance("SHA-256") - file.inputStream().buffered().use { input -> - val buffer = ByteArray(8192) - while (true) { - val read = input.read(buffer) - if (read < 0) { - break - } - digest.update(buffer, 0, read) - } - } - return digest.digest().joinToString("") { "%02x".format(it) } -} +version = configuredVersion() -fun sha256(bytes: ByteArray): String = - MessageDigest.getInstance("SHA-256") - .digest(bytes) - .joinToString("") { "%02x".format(it) } - -fun byteIdentical(left: File, right: File): Boolean { - if (left.length() != right.length()) { - return false - } - left.inputStream().buffered().use { leftInput -> - right.inputStream().buffered().use { rightInput -> - val leftBuffer = ByteArray(8192) - val rightBuffer = ByteArray(8192) - while (true) { - val leftRead = leftInput.read(leftBuffer) - val rightRead = rightInput.read(rightBuffer) - if (leftRead != rightRead) { - return false - } - if (leftRead < 0) { - return true - } - for (index in 0 until leftRead) { - if (leftBuffer[index] != rightBuffer[index]) { - return false - } - } - } - } - } -} - -fun writeEvidence(file: File, values: Map) { - file.parentFile.mkdirs() - file.writeText( - values.toSortedMap().entries.joinToString( - separator = "\n", - postfix = "\n" - ) { (key, value) -> "$key=$value" } - ) -} - -fun readEvidence(file: File): Map { - check(file.isFile) { - "Evidence file does not exist: ${file.canonicalPath}" - } - val properties = Properties() - file.inputStream().use(properties::load) - return properties.stringPropertyNames().associateWith { - properties.getProperty(it) - } -} - -fun commandBytes( - directory: File, - vararg command: String -): ByteArray { - val process = - ProcessBuilder(command.toList()) - .directory(directory) - .redirectErrorStream(true) - .start() - val output = - process.inputStream.buffered().use { - it.readBytes() - } - val exitCode = process.waitFor() - check(exitCode == 0) { - "Command failed ($exitCode): " + - command.joinToString(" ") + - "\n" + - String(output, StandardCharsets.UTF_8) - } - return output -} - -fun commandOutput(directory: File, vararg command: String): String = - String( - commandBytes(directory, *command), - StandardCharsets.UTF_8 - ) - -data class GitWorkspaceFingerprint( - val commit: String, - val dirty: Boolean, - val statusSha256: String, - val workspaceSha256: String, - val pathCount: Int -) - -fun splitNul(bytes: ByteArray): List { - val values = mutableListOf() - var start = 0 - for (index in bytes.indices) { - if (bytes[index].toInt() == 0) { - if (index > start) { - values.add( - String( - bytes, - start, - index - start, - StandardCharsets.UTF_8 - ) - ) - } - start = index + 1 - } - } - if (start < bytes.size) { - values.add( - String( - bytes, - start, - bytes.size - start, - StandardCharsets.UTF_8 - ) - ) - } - return values +allprojects { + group = rootProject.group + version = rootProject.version } -fun updateLength( - digest: MessageDigest, - length: Long -) { - digest.update( - ByteBuffer.allocate(8) - .putLong(length) - .array() - ) -} - -fun gitWorkspaceFingerprint( - directory: File -): GitWorkspaceFingerprint { - val root = directory.canonicalFile - val commit = - commandOutput(root, "git", "rev-parse", "HEAD") - .trim() - .lowercase() - check(commit.matches(Regex("[0-9a-f]{40}"))) { - "Source fingerprint requires an exact Git commit: $root" - } - val status = - commandBytes( - root, - "git", - "status", - "--porcelain", - "-z", - "--untracked-files=all" - ) - val listed = - commandBytes( - root, - "git", - "ls-files", - "-z", - "--cached", - "--others", - "--exclude-standard" - ) - val ignoredReleaseInputs = - commandBytes( - root, - "git", - "ls-files", - "-z", - "--others", - "--ignored", - "--exclude-standard", - "--", - ".github", - "docs", - "gradle", - "specifications", - "src" - ) - check(ignoredReleaseInputs.isEmpty()) { - "Source fingerprint rejects ignored release inputs: " + - splitNul(ignoredReleaseInputs).joinToString(", ") - } - val paths = splitNul(listed).sorted() - check(paths.isNotEmpty()) { - "Source fingerprint has no tracked or non-ignored files: $root" - } - val digest = MessageDigest.getInstance("SHA-256") - for (relativePath in paths) { - val pathBytes = - relativePath.toByteArray(StandardCharsets.UTF_8) - updateLength(digest, pathBytes.size.toLong()) - digest.update(pathBytes) - val source = File(root, relativePath) - .toPath() - .toAbsolutePath() - .normalize() - check(source.startsWith(root.toPath())) { - "Source path escapes checkout: $relativePath" - } - check( - Files.isRegularFile( - source, - LinkOption.NOFOLLOW_LINKS - ) - ) { - "Source fingerprint rejects missing, symlink, or " + - "non-regular path: $relativePath" - } - digest.update(byteArrayOf(1)) - val length = Files.size(source) - updateLength(digest, length) - Files.newInputStream(source).buffered().use { input -> - val buffer = ByteArray(8192) - while (true) { - val read = input.read(buffer) - if (read < 0) { - break - } - digest.update(buffer, 0, read) - } - } - } - return GitWorkspaceFingerprint( - commit, - status.isNotEmpty(), - sha256(status), - digest.digest().joinToString("") { - "%02x".format(it) - }, - paths.size - ) -} - -data class FocusedLanguageArtifactEvidence( - val coordinate: String, - val component: String, - val projectPath: String?, - val includedBuildProject: Boolean, - val file: File, - val bytes: Long, - val sha256: String -) - -data class FocusedLanguageResolutionEvidence( - val components: List>, - val edges: List>, - val artifacts: List, - val graphSha256: String -) - -fun resolveFocusedLanguageEvidence( - configuration: Configuration, - requiredProjectPaths: Map, - requireIncludedBuildProjects: Boolean -): FocusedLanguageResolutionEvidence { - val resolution = configuration.incoming.resolutionResult - val components = - resolution.allComponents.map { component -> - val identifier = component.id - val moduleVersion = component.moduleVersion - linkedMapOf( - "id" to identifier.displayName, - "group" to moduleVersion?.group, - "name" to moduleVersion?.name, - "version" to moduleVersion?.version, - "origin" to - if (identifier is ProjectComponentIdentifier) { - if (identifier.build.buildPath == ":") { - "current-build-project" - } else { - "included-build-project" - } - } else { - "external-module" - }, - "projectPath" to - (identifier as? ProjectComponentIdentifier) - ?.projectPath - ) - }.sortedBy { it["id"].toString() } - val edges = - resolution.allDependencies - .filterIsInstance() - .map { dependency -> - linkedMapOf( - "from" to dependency.from.id.displayName, - "requested" to dependency.requested.displayName, - "selected" to dependency.selected.id.displayName - ) - } - .sortedWith( - compareBy>( - { it.getValue("from") }, - { it.getValue("requested") }, - { it.getValue("selected") } - ) - ) - val artifacts = - configuration.resolvedConfiguration.resolvedArtifacts - .filter { it.extension == "jar" } - .map { artifact -> - val identifier = artifact.id.componentIdentifier - val coordinate = - artifact.moduleVersion.id.group + ":" + - artifact.name + ":" + - artifact.moduleVersion.id.version - FocusedLanguageArtifactEvidence( - coordinate = coordinate, - component = identifier.displayName, - projectPath = - (identifier as? ProjectComponentIdentifier) - ?.projectPath, - includedBuildProject = - identifier is ProjectComponentIdentifier && - identifier.build.buildPath != ":", - file = artifact.file.canonicalFile, - bytes = artifact.file.length(), - sha256 = sha256(artifact.file) - ) - } - .sortedWith( - compareBy( - { it.coordinate }, - { it.file.name } - ) - ) - check(artifacts.isNotEmpty()) { - "Focused Blue Language resolution produced no JAR artifacts" - } - for ((moduleName, projectPath) in requiredProjectPaths) { - val matches = - artifacts.filter { - it.coordinate.substringBefore(':') == "blue.language" && - it.coordinate.substringAfter(':') - .substringBefore(':') == moduleName - } - check(matches.size == 1) { - "Expected exactly one focused $moduleName JAR, found " + - matches.joinToString { it.file.path } - } - if (requireIncludedBuildProjects) { - val match = matches.single() - check( - match.includedBuildProject && - match.projectPath == projectPath - ) { - "Focused module $moduleName must resolve directly from " + - "included-build project $projectPath, but resolved " + - "${match.component} (projectPath=${match.projectPath})" - } - } - } - val canonicalGraph = - buildList { - components.forEach { - add( - "component|${it["id"]}|${it["group"]}|" + - "${it["name"]}|${it["version"]}|" + - "${it["origin"]}|${it["projectPath"]}" - ) - } - edges.forEach { - add( - "edge|${it.getValue("from")}|" + - "${it.getValue("requested")}|" + - it.getValue("selected") - ) - } - artifacts.forEach { - add( - "artifact|${it.coordinate}|${it.component}|" + - "${it.projectPath}|${it.bytes}|${it.sha256}" - ) - } - }.joinToString("\n", postfix = "\n") - return FocusedLanguageResolutionEvidence( - components = components, - edges = edges, - artifacts = artifacts, - graphSha256 = sha256( - canonicalGraph.toByteArray(StandardCharsets.UTF_8) - ) - ) -} - -fun Configuration.containsBlueLanguageAggregate(): Boolean = - incoming.resolutionResult.allComponents.any { component -> - component.moduleVersion?.let { - it.group == "blue.language" && - it.name == "blue-language-java" - } == true - } - -fun focusedEvidenceJson( - evidence: FocusedLanguageResolutionEvidence, - mode: String, - declaredCoordinates: List, - compileAggregatePresent: Boolean, - runtimeAggregatePresent: Boolean -): Map = - linkedMapOf( - "schema" to "blue-bex-focused-language-resolution/1.0", - "status" to "passed", - "mode" to mode, - "declaredCoordinates" to declaredCoordinates, - "graphSha256" to evidence.graphSha256, - "componentCount" to evidence.components.size, - "edgeCount" to evidence.edges.size, - "artifactCount" to evidence.artifacts.size, - "components" to evidence.components, - "edges" to evidence.edges, - "artifacts" to evidence.artifacts.map { - linkedMapOf( - "coordinate" to it.coordinate, - "component" to it.component, - "origin" to - if (it.includedBuildProject) { - "included-build-project" - } else { - "external-module" - }, - "projectPath" to it.projectPath, - "path" to it.file.path, - "bytes" to it.bytes, - "sha256" to it.sha256 - ) - }, - "productionClasspaths" to - linkedMapOf( - "compile" to - linkedMapOf( - "aggregatePresent" to compileAggregatePresent - ), - "runtime" to - linkedMapOf( - "aggregatePresent" to runtimeAggregatePresent - ) - ) - ) - -data class JavaImportInventory( - val lineCount: Int, - val fileCount: Int, - val files: Map, - val imports: Map -) - -fun JavaImportInventory.toJson(): Map = - linkedMapOf( - "lineCount" to lineCount, - "fileCount" to fileCount, - "files" to files.toSortedMap(), - "imports" to imports.toSortedMap() - ) - -val allBlueLanguageImportPattern = - Regex("^import\\s+(?:static\\s+)?blue\\.language\\.") -val legacyUtilsImportPattern = - Regex("^import\\s+(?:static\\s+)?blue\\.language\\.utils\\.") -val forbiddenLegacyImportPatterns = - listOf( - legacyUtilsImportPattern, - Regex( - "^import\\s+blue\\.language\\.snapshot\\." + - "ResolvedSnapshot;" - ), - Regex("^import\\s+blue\\.language\\.NodeProvider;"), - Regex( - "^import\\s+blue\\.language\\.BlueOperation" + - "(?:Limits|Outcome|Result);" - ) - ) -val allBlueLanguageImportGitPattern = - "^import (static )?blue\\.language\\." -val legacyUtilsImportGitPattern = - "^import (static )?blue\\.language\\.utils\\." -val forbiddenLegacyImportGitPattern = - "^import (static )?blue\\.language\\." + - "(utils\\.|snapshot\\.ResolvedSnapshot;|NodeProvider;|" + - "BlueOperation(Limits|Outcome|Result);)" - -fun sourceImportInventory( - checkout: File, - sourceRoot: String, - matches: (String) -> Boolean -): JavaImportInventory { - val root = File(checkout, sourceRoot) - val lines = mutableListOf>() - if (root.isDirectory) { - root.walkTopDown() - .filter { it.isFile && it.extension == "java" } - .forEach { source -> - source.useLines { sourceLines -> - sourceLines.forEach { rawLine -> - val line = rawLine.trim() - if (matches(line)) { - lines.add( - source.relativeTo(checkout) - .invariantSeparatorsPath to line - ) - } - } - } - } - } - return JavaImportInventory( - lineCount = lines.size, - fileCount = lines.map { it.first }.toSet().size, - files = lines.groupingBy { it.first }.eachCount(), - imports = lines.groupingBy { it.second }.eachCount() - ) -} - -fun gitImportInventory( - checkout: File, - revision: String, - sourceRoot: String, - pattern: String -): JavaImportInventory { - val output = - commandOutput( - checkout, - "git", - "grep", - "-n", - "-E", - pattern, - revision, - "--", - sourceRoot - ).trim() - val lines = - if (output.isEmpty()) { - emptyList() - } else { - output.lineSequence().map { rawLine -> - val withoutRevision = - rawLine.removePrefix("$revision:") - val pathSeparator = withoutRevision.indexOf(':') - val lineSeparator = - withoutRevision.indexOf(':', pathSeparator + 1) - check(pathSeparator > 0 && lineSeparator > pathSeparator) { - "Unexpected git grep evidence line: $rawLine" - } - val path = withoutRevision.substring(0, pathSeparator) - val imported = - withoutRevision.substring(lineSeparator + 1).trim() - path to imported - }.toList() - } - return JavaImportInventory( - lineCount = lines.size, - fileCount = lines.map { it.first }.toSet().size, - files = lines.groupingBy { it.first }.eachCount(), - imports = lines.groupingBy { it.second }.eachCount() - ) -} - -val blueLanguageFocusedResolution by configurations.creating { - isCanBeConsumed = false - isCanBeResolved = true - isTransitive = true - description = - "Resolves the complete focused Blue Language component graph for " + - "provenance, version, and JAR hash evidence." -} - -val blueLanguageAggregateCompatibility by configurations.creating { - isCanBeConsumed = false - isCanBeResolved = true - description = - "Resolves the aggregate Blue Language facade only for compatibility " + - "and provenance checks; it is not on a BEX production classpath." -} - -dependencies { - api(blueLanguageModelDeclaredCoordinate) - api(blueLanguageCoreDeclaredCoordinate) - implementation(blueLanguageMappingDeclaredCoordinate) - api(blueContractsCoreDeclaredCoordinate) - - blueLanguageFocusedCoordinates.forEach { coordinate -> - add(blueLanguageFocusedResolution.name, coordinate) - } - - add( - blueLanguageAggregateCompatibility.name, - blueLanguageDeclaredCoordinate - ) - - testImplementation(platform("org.junit:junit-bom:5.10.2")) - testImplementation("org.junit.jupiter:junit-jupiter") - testImplementation("org.yaml:snakeyaml:1.31") - testRuntimeOnly("org.junit.platform:junit-platform-launcher") -} - -val blueLanguageAggregateCompatibilityEvidence = - layout.buildDirectory.file( - "reports/latest-language-migration/" + - "aggregate-compatibility.properties" - ) -val blueLanguageFocusedResolutionEvidence = - layout.buildDirectory.file( - "reports/latest-language-migration/" + - "focused-language-resolution.json" - ) -val writeFocusedLanguageResolutionEvidence by tasks.registering { - group = "verification" - description = - "Records the complete focused Language graph, exact versions, " + - "project origins, and JAR SHA-256 values and rejects the " + - "aggregate facade on BEX production classpaths." - inputs.property("dependency.mode", blueLanguageDependencyMode) - inputs.property( - "dependency.coordinates", - blueLanguageFocusedCoordinates.joinToString(",") - ) - inputs.file(latestLanguageMigrationLock) - inputs.files(blueLanguageFocusedResolution) - outputs.file(blueLanguageFocusedResolutionEvidence) - outputs.upToDateWhen { false } - doFirst { - blueLanguageFocusedResolutionEvidence.get().asFile.delete() - } - doLast { - val compileClasspath = configurations.compileClasspath.get() - val runtimeClasspath = configurations.runtimeClasspath.get() - val compileAggregatePresent = - compileClasspath.containsBlueLanguageAggregate() - val runtimeAggregatePresent = - runtimeClasspath.containsBlueLanguageAggregate() - check(!compileAggregatePresent && !runtimeAggregatePresent) { - "blue-language-java is forbidden on BEX production " + - "compile/runtime classpaths" - } - val evidence = - resolveFocusedLanguageEvidence( - blueLanguageFocusedResolution, - blueLanguageFocusedProjectPaths, - blueLanguageDependencyMode == "local-composite" - ) - @Suppress("UNCHECKED_CAST") - val lock = - JsonSlurper().parse(latestLanguageMigrationLock.asFile) - as Map - val languageLock = lock["language"] as? Map<*, *> - ?: throw GradleException("Language baseline lock is malformed") - val lockedModules = - (languageLock["focusedModules"] as? List<*>) - ?.map { it as Map<*, *> } - ?: throw GradleException( - "Language baseline lock has no focused modules" - ) - check( - lockedModules.map { it["coordinate"] }.toSet() == - blueLanguageFocusedCoordinates.toSet() - ) { - "Focused dependency declarations differ from the BEX-owned lock" - } - if (blueLanguageDependencyMode == "local-composite") { - for (lockedModule in lockedModules) { - val coordinate = lockedModule["coordinate"].toString() - val moduleName = coordinate.split(':')[1] - val artifact = - evidence.artifacts.single { - it.coordinate.substringAfter(':') - .substringBefore(':') == moduleName - } - check( - artifact.projectPath == lockedModule["projectPath"] - ) { - "$moduleName resolved from ${artifact.projectPath}, " + - "not the locked project path " + - lockedModule["projectPath"] - } - check( - artifact.sha256 == - lockedModule["verifiedArtifactSha256"] - ) { - "$moduleName JAR differs from the verified Language " + - "implementation artifact: ${artifact.sha256}" - } - } - } - val output = LinkedHashMap( - focusedEvidenceJson( - evidence, - blueLanguageDependencyMode, - blueLanguageFocusedCoordinates, - compileAggregatePresent, - runtimeAggregatePresent - ) - ) - output["lock"] = - linkedMapOf( - "path" to - latestLanguageMigrationLock.asFile - .relativeTo(projectDir) - .invariantSeparatorsPath, - "sha256" to sha256(latestLanguageMigrationLock.asFile), - "focusedModulesMatch" to true, - "localArtifactsMatchVerifiedImplementation" to - (blueLanguageDependencyMode == "local-composite") - ) - val outputFile = - blueLanguageFocusedResolutionEvidence.get().asFile - outputFile.parentFile.mkdirs() - outputFile.writeText( - JsonOutput.prettyPrint(JsonOutput.toJson(output)) + "\n" - ) - } -} -val latestLanguageMigrationBaselineEvidence = - layout.buildDirectory.file( - "reports/latest-language-migration/baseline.json" - ) -val writeLatestLanguageMigrationBaseline by tasks.registering { - group = "verification" - description = - "Regenerates the migration baseline from the source-controlled " + - "Language/BEX lock and live source inventories." - inputs.file(latestLanguageMigrationLock) - inputs.files( - fileTree("src/main/java") { include("**/*.java") }, - fileTree("src/test/java") { include("**/*.java") }, - layout.projectDirectory.file(".cz.toml") - ) - blueLanguageCompositePath?.let { path -> - inputs.file(file(path).resolve(".cz.toml")) - } - outputs.file(latestLanguageMigrationBaselineEvidence) - outputs.upToDateWhen { false } - doFirst { - latestLanguageMigrationBaselineEvidence.get().asFile.delete() - } - doLast { - @Suppress("UNCHECKED_CAST") - val lock = - JsonSlurper().parse(latestLanguageMigrationLock.asFile) - as Map - val bexLock = lock["bex"] as? Map<*, *> - ?: throw GradleException("BEX migration lock is malformed") - val languageLock = lock["language"] as? Map<*, *> - ?: throw GradleException("Language migration lock is malformed") - val migrationLock = lock["migration"] as? Map<*, *> - ?: throw GradleException("Migration inventory lock is malformed") - val baselineCommit = - bexLock["migrationBaselineCommit"].toString() - val expectedLanguageHead = - languageLock["exactHead"].toString() - val verifiedImplementationCommit = - languageLock["verifiedImplementationCommit"].toString() - val allowedLanguageDiffPaths = - (languageLock["documentationOnlyDiffPaths"] as? List<*>) - ?.map(Any?::toString) - ?.sorted() - ?: emptyList() - val failures = mutableListOf() - fun requireBaseline(value: Boolean, failure: String) { - if (!value) failures.add(failure) - } - fun expectedInventory( - section: String, - scope: String - ): Pair { - val sectionMap = migrationLock[section] as? Map<*, *> - ?: throw GradleException("Missing migration lock: $section") - val scopeMap = sectionMap[scope] as? Map<*, *> - ?: throw GradleException( - "Missing migration lock: $section.$scope" - ) - return ( - (scopeMap["lines"] as Number).toInt() to - (scopeMap["files"] as Number).toInt() - ) - } - fun matchesExpected( - inventory: JavaImportInventory, - expected: Pair - ): Boolean = - inventory.lineCount == expected.first && - inventory.fileCount == expected.second - - commandOutput( - projectDir, - "git", - "cat-file", - "-e", - "$baselineCommit^{commit}" - ) - val bexFingerprint = gitWorkspaceFingerprint(projectDir) - val bexCzToml = file(".cz.toml") - val actualBexCzTomlSha256 = sha256(bexCzToml) - requireBaseline( - actualBexCzTomlSha256 == bexLock["czTomlSha256"], - "bex-cz-toml-differs-from-lock" - ) - - val languageDirectory = - blueLanguageCompositePath - ?.let { file(it).canonicalFile } - val languageFingerprint = - languageDirectory - ?.takeIf(File::isDirectory) - ?.let(::gitWorkspaceFingerprint) - requireBaseline( - languageFingerprint != null, - "language-composite-checkout-unavailable" - ) - val actualLanguageCzTomlSha256 = - languageDirectory - ?.resolve(".cz.toml") - ?.takeIf(File::isFile) - ?.let(::sha256) - requireBaseline( - actualLanguageCzTomlSha256 == - languageLock["czTomlSha256"], - "language-cz-toml-differs-from-lock" - ) - requireBaseline( - languageFingerprint?.commit == expectedLanguageHead, - "language-head-differs-from-lock" - ) - requireBaseline( - languageFingerprint != null && !languageFingerprint.dirty, - "language-git-worktree-dirty" - ) - val changedLanguagePaths = - languageDirectory?.let { - commandOutput( - it, - "git", - "diff", - "--name-only", - "$verifiedImplementationCommit..$expectedLanguageHead" - ).lineSequence() - .map(String::trim) - .filter(String::isNotEmpty) - .sorted() - .toList() - } ?: emptyList() - val codeEquivalent = - languageFingerprint?.commit == expectedLanguageHead && - changedLanguagePaths == allowedLanguageDiffPaths && - actualLanguageCzTomlSha256 == - languageLock["czTomlSha256"] - requireBaseline( - codeEquivalent, - "language-head-not-code-equivalent-to-verified-implementation" - ) - - val baselineProductionImports = - gitImportInventory( - projectDir, - baselineCommit, - "src/main/java", - allBlueLanguageImportGitPattern - ) - val baselineTestImports = - gitImportInventory( - projectDir, - baselineCommit, - "src/test/java", - allBlueLanguageImportGitPattern - ) - val baselineProductionUtils = - gitImportInventory( - projectDir, - baselineCommit, - "src/main/java", - legacyUtilsImportGitPattern - ) - val baselineTestUtils = - gitImportInventory( - projectDir, - baselineCommit, - "src/test/java", - legacyUtilsImportGitPattern - ) - val baselineProductionForbidden = - gitImportInventory( - projectDir, - baselineCommit, - "src/main/java", - forbiddenLegacyImportGitPattern - ) - val baselineTestForbidden = - gitImportInventory( - projectDir, - baselineCommit, - "src/test/java", - forbiddenLegacyImportGitPattern - ) - requireBaseline( - matchesExpected( - baselineProductionImports, - expectedInventory( - "languageImportInventory", - "production" - ) - ) && matchesExpected( - baselineTestImports, - expectedInventory("languageImportInventory", "test") - ), - "baseline-language-import-inventory-differs-from-lock" - ) - requireBaseline( - matchesExpected( - baselineProductionUtils, - expectedInventory("legacyUtilsImports", "production") - ) && matchesExpected( - baselineTestUtils, - expectedInventory("legacyUtilsImports", "test") - ), - "baseline-utils-import-ledger-differs-from-lock" - ) - requireBaseline( - matchesExpected( - baselineProductionForbidden, - expectedInventory( - "allForbiddenLegacyImports", - "production" - ) - ) && matchesExpected( - baselineTestForbidden, - expectedInventory( - "allForbiddenLegacyImports", - "test" - ) - ), - "baseline-forbidden-import-ledger-differs-from-lock" - ) - - val currentProductionImports = - sourceImportInventory( - projectDir, - "src/main/java" - ) { allBlueLanguageImportPattern.containsMatchIn(it) } - val currentTestImports = - sourceImportInventory( - projectDir, - "src/test/java" - ) { allBlueLanguageImportPattern.containsMatchIn(it) } - val currentProductionForbidden = - sourceImportInventory( - projectDir, - "src/main/java" - ) { line -> - forbiddenLegacyImportPatterns.any { - it.containsMatchIn(line) - } - } - val currentTestForbidden = - sourceImportInventory( - projectDir, - "src/test/java" - ) { line -> - forbiddenLegacyImportPatterns.any { - it.containsMatchIn(line) - } - } - - val output = - linkedMapOf( - "schema" to - "blue-bex-latest-language-migration-baseline/1.0", - "status" to - if (failures.isEmpty()) "passed" else "failed", - "failures" to failures, - "lock" to - linkedMapOf( - "path" to - latestLanguageMigrationLock.asFile - .relativeTo(projectDir) - .invariantSeparatorsPath, - "sha256" to - sha256(latestLanguageMigrationLock.asFile) - ), - "toolchain" to - linkedMapOf( - "gradle" to gradle.gradleVersion, - "java" to System.getProperty("java.version"), - "os" to System.getProperty("os.name"), - "architecture" to System.getProperty("os.arch") - ), - "bex" to - linkedMapOf( - "baselineCommit" to baselineCommit, - "currentCommit" to bexFingerprint.commit, - "gitWorktreeDirty" to bexFingerprint.dirty, - "workspaceSha256" to - bexFingerprint.workspaceSha256, - "czToml" to - linkedMapOf( - "expectedSha256" to - bexLock["czTomlSha256"], - "actualSha256" to - actualBexCzTomlSha256, - "matches" to - (actualBexCzTomlSha256 == - bexLock["czTomlSha256"]) - ) - ), - "language" to - linkedMapOf( - "path" to languageDirectory?.path, - "exactCommit" to languageFingerprint?.commit, - "gitWorktreeDirty" to languageFingerprint?.dirty, - "includedBuildCleanClaim" to "not-made", - "verifiedImplementationCommit" to - verifiedImplementationCommit, - "changedPathsSinceVerifiedImplementation" to - changedLanguagePaths, - "allowedDocumentationOnlyPaths" to - allowedLanguageDiffPaths, - "codeEquivalent" to codeEquivalent, - "czToml" to - linkedMapOf( - "expectedSha256" to - languageLock["czTomlSha256"], - "actualSha256" to - actualLanguageCzTomlSha256, - "matches" to - (actualLanguageCzTomlSha256 == - languageLock["czTomlSha256"]) - ), - "focusedModules" to - languageLock["focusedModules"], - "hostingPackageIdentities" to - languageLock["hostingPackageIdentities"] - ), - "sourceApiInventory" to - linkedMapOf( - "baseline" to - linkedMapOf( - "revision" to baselineCommit, - "production" to - baselineProductionImports.toJson(), - "test" to baselineTestImports.toJson() - ), - "current" to - linkedMapOf( - "production" to - currentProductionImports.toJson(), - "test" to currentTestImports.toJson() - ) - ), - "migrationLedger" to - linkedMapOf( - "legacyUtils" to - linkedMapOf( - "before" to - linkedMapOf( - "production" to - baselineProductionUtils.toJson(), - "test" to - baselineTestUtils.toJson() - ), - "after" to - linkedMapOf( - "production" to - sourceImportInventory( - projectDir, - "src/main/java" - ) { - legacyUtilsImportPattern - .containsMatchIn(it) - }.toJson(), - "test" to - sourceImportInventory( - projectDir, - "src/test/java" - ) { - legacyUtilsImportPattern - .containsMatchIn(it) - }.toJson() - ) - ), - "allForbiddenLegacyImports" to - linkedMapOf( - "before" to - linkedMapOf( - "production" to - baselineProductionForbidden - .toJson(), - "test" to - baselineTestForbidden.toJson() - ), - "after" to - linkedMapOf( - "production" to - currentProductionForbidden - .toJson(), - "test" to - currentTestForbidden.toJson() - ) - ) - ) - ) - val outputFile = - latestLanguageMigrationBaselineEvidence.get().asFile - outputFile.parentFile.mkdirs() - outputFile.writeText( - JsonOutput.prettyPrint(JsonOutput.toJson(output)) + "\n" - ) - } -} -val verifyBlueLanguageAggregateCompatibility by tasks.registering { - group = "verification" - description = - "Resolves and inspects the aggregate Blue Language facade without " + - "adding it to a BEX production classpath." - inputs.property("dependency.mode", blueLanguageDependencyMode) - inputs.property( - "dependency.coordinate", - blueLanguageDeclaredCoordinate - ) - inputs.files(blueLanguageAggregateCompatibility) - outputs.file(blueLanguageAggregateCompatibilityEvidence) - outputs.upToDateWhen { false } - doFirst { - blueLanguageAggregateCompatibilityEvidence.get().asFile.delete() - } - doLast { - val matches = - blueLanguageAggregateCompatibility - .resolvedConfiguration - .resolvedArtifacts - .filter { - it.moduleVersion.id.group == "blue.language" && - it.name == "blue-language-java" && - it.extension == "jar" - } - check(matches.size == 1) { - "Expected exactly one aggregate blue-language-java artifact, " + - "found " + - matches.joinToString { it.file.absolutePath } - } - val artifact = matches.single() - check(artifact.file.isFile && artifact.file.length() > 0L) { - "Aggregate Blue Language artifact is missing or empty: " + - artifact.file - } - ZipFile(artifact.file).use { archive -> - check(archive.getEntry("blue/language/Blue.class") != null) { - "Aggregate Blue Language artifact does not expose the " + - "compatibility facade blue.language.Blue: " + - artifact.file - } - } - val component = artifact.id.componentIdentifier - if (blueLanguageDependencyMode == "local-composite") { - val projectComponent = - component as? ProjectComponentIdentifier - check( - projectComponent != null && - projectComponent.build.buildPath != ":" && - projectComponent.projectPath == - ":blue-language-java" - ) { - "Local aggregate compatibility artifact did not resolve " + - "from :blue-language-java: " + - component.displayName - } - } - writeEvidence( - blueLanguageAggregateCompatibilityEvidence.get().asFile, - mapOf( - "schema" to - "blue-bex-language-aggregate-compatibility/1.0", - "status" to "passed", - "mode" to blueLanguageDependencyMode, - "declared.coordinate" to - blueLanguageDeclaredCoordinate, - "effective.component" to component.displayName, - "effective.coordinate" to - ( - artifact.moduleVersion.id.group + - ":" + artifact.name + ":" + - artifact.moduleVersion.id.version - ), - "artifact.path" to artifact.file.canonicalPath, - "artifact.bytes" to artifact.file.length().toString(), - "artifact.sha256" to sha256(artifact.file) - ) - ) - } -} - -tasks.test { - javaLauncher.set( - javaToolchains.launcherFor { - languageVersion.set(JavaLanguageVersion.of(8)) - } - ) - useJUnitPlatform() - reports { - junitXml.required.set(true) - html.required.set(true) - } - testLogging { - events("PASSED", "FAILED", "SKIPPED") - showStandardStreams = true - } -} - -val mainJar = tasks.named("jar") -val sourcesJarTask = tasks.named("sourcesJar") -val javadocJarTask = tasks.named("javadocJar") -val javadocTask = tasks.named("javadoc") -val sourceReleaseIncludes = - listOf( - ".cz.toml", - ".github/**", - "LICENSE", - "README.md", - "build.gradle.kts", - "docs/**", - "gradle.properties", - "gradle/**", - "gradlew", - "gradlew.bat", - "settings.gradle.kts", - "specifications/**", - "src/**" - ) -val sourceReleaseExcludes = - listOf( - ".git/**", - ".gradle/**", - ".idea/**", - "build/**", - "out/**", - "target/**", - "work-status.txt", - "*.zip", - "*.tar", - "*.tar.gz", - "*.tgz", - "*.7z", - "**/*.zip", - "**/*.tar", - "**/*.tar.gz", - "**/*.tgz", - "**/*.7z", - "**/*.class", - "**/*.log", - "**/*.tmp", - "**/*.bak", - "**/*.swp", - "**/*~", - ".DS_Store", - "**/.DS_Store" - ) -val sourceReleaseInputs = - fileTree(projectDir) { - include(sourceReleaseIncludes) - exclude(sourceReleaseExcludes) - } -val sourceReleaseRoot = - "${base.archivesName.get()}-${project.version}" -val sourceReleaseArchive by tasks.registering(Zip::class) { - group = "distribution" - description = - "Assembles the reproducible BEX source release from release inputs." - archiveBaseName.set(base.archivesName) - archiveVersion.set(project.version.toString()) - archiveClassifier.set("source-release") - destinationDirectory.set(layout.buildDirectory.dir("distributions")) - isPreserveFileTimestamps = false - isReproducibleFileOrder = true - from(sourceReleaseInputs) { - exclude( - "gradlew", - ".github/scripts/run-final-publication-gates.sh" - ) - into(sourceReleaseRoot) - } - from("gradlew") { - into(sourceReleaseRoot) - filePermissions { - unix("rwxr-xr-x") - } - } - from(".github/scripts/run-final-publication-gates.sh") { - into("$sourceReleaseRoot/.github/scripts") - filePermissions { - unix("rwxr-xr-x") - } - } -} -val rebuiltSourceReleaseArchive by tasks.registering(Zip::class) { - group = "verification" - description = - "Independently reassembles the source release from the same working-tree inputs." - archiveFileName.set(sourceReleaseArchive.flatMap { it.archiveFileName }) - destinationDirectory.set( - layout.buildDirectory.dir("reproducibility/source-release") - ) - isPreserveFileTimestamps = false - isReproducibleFileOrder = true - from(sourceReleaseInputs) { - exclude( - "gradlew", - ".github/scripts/run-final-publication-gates.sh" - ) - into(sourceReleaseRoot) - } - from("gradlew") { - into(sourceReleaseRoot) - filePermissions { - unix("rwxr-xr-x") - } - } - from(".github/scripts/run-final-publication-gates.sh") { - into("$sourceReleaseRoot/.github/scripts") - filePermissions { - unix("rwxr-xr-x") - } - } -} -val rebuiltMainJar by tasks.registering(Jar::class) { - group = "verification" - description = - "Repackages the current compiled main output for archive byte comparison." - archiveFileName.set(mainJar.flatMap { it.archiveFileName }) - destinationDirectory.set( - layout.buildDirectory.dir("reproducibility/main") - ) - from(sourceSets.main.get().output) -} -val rebuiltSourcesJar by tasks.registering(Jar::class) { - group = "verification" - description = - "Repackages the current source inputs for archive byte comparison." - archiveFileName.set(sourcesJarTask.flatMap { it.archiveFileName }) - destinationDirectory.set( - layout.buildDirectory.dir("reproducibility/sources") - ) - from(sourceSets.main.get().allSource) -} -val rebuiltJavadoc by tasks.registering(Javadoc::class) { - group = "verification" - description = - "Freshly regenerates the public Javadoc for deterministic comparison." - source = sourceSets.main.get().allJava - classpath = sourceSets.main.get().compileClasspath - destinationDir = - layout.buildDirectory.dir( - "reproducibility/javadoc-content" - ).get().asFile -} -val rebuiltJavadocJar by tasks.registering(Jar::class) { - group = "verification" - description = - "Packages freshly regenerated Javadoc for byte comparison." - dependsOn(rebuiltJavadoc) - archiveFileName.set(javadocJarTask.flatMap { it.archiveFileName }) - destinationDirectory.set( - layout.buildDirectory.dir("reproducibility/javadoc") - ) - from(rebuiltJavadoc.map { it.destinationDir }) -} - -val deterministicArchiveEvidence = - layout.buildDirectory.file( - "reports/bex-release/deterministic-archives.properties" - ) -val sourceReleaseEvidence = - layout.buildDirectory.file( - "reports/bex-release/source-release.properties" - ) -val sourceReleaseChecksum = - sourceReleaseArchive.flatMap { archive -> - archive.archiveFile.map { file -> - File(file.asFile.parentFile, "${file.asFile.name}.sha256") - } - } -val verifyDeterministicArchives by tasks.registering { - group = "verification" - description = - "Checks JAR packaging determinism and independently reassembles the source release from the same working tree; this is not an independent clean compilation or checkout." - dependsOn( - mainJar, - sourcesJarTask, - javadocJarTask, - javadocTask, - rebuiltMainJar, - rebuiltSourcesJar, - rebuiltJavadocJar, - sourceReleaseArchive, - rebuiltSourceReleaseArchive - ) - outputs.file(deterministicArchiveEvidence) - outputs.file(sourceReleaseEvidence) - outputs.file(sourceReleaseChecksum) - outputs.upToDateWhen { false } - doFirst { - deterministicArchiveEvidence.get().asFile.delete() - sourceReleaseEvidence.get().asFile.delete() - sourceReleaseChecksum.get().delete() - } - doLast { - val originalMain = mainJar.get().archiveFile.get().asFile - val rebuiltMain = rebuiltMainJar.get().archiveFile.get().asFile - val originalSources = - sourcesJarTask.get().archiveFile.get().asFile - val rebuiltSources = - rebuiltSourcesJar.get().archiveFile.get().asFile - val originalJavadoc = - javadocJarTask.get().archiveFile.get().asFile - val regeneratedJavadoc = - rebuiltJavadocJar.get().archiveFile.get().asFile - val originalSourceRelease = - sourceReleaseArchive.get().archiveFile.get().asFile - val rebuiltSourceRelease = - rebuiltSourceReleaseArchive.get().archiveFile.get().asFile - val originalMainHash = sha256(originalMain) - val rebuiltMainHash = sha256(rebuiltMain) - val originalSourcesHash = sha256(originalSources) - val rebuiltSourcesHash = sha256(rebuiltSources) - val originalJavadocHash = sha256(originalJavadoc) - val regeneratedJavadocHash = sha256(regeneratedJavadoc) - val originalSourceReleaseHash = sha256(originalSourceRelease) - val rebuiltSourceReleaseHash = sha256(rebuiltSourceRelease) - val sourceReleaseByteIdentity = - byteIdentical(originalSourceRelease, rebuiltSourceRelease) - val originalExecutableModes = - sourceReleaseExecutableModes( - originalSourceRelease, - sourceReleaseRoot - ) - val rebuiltExecutableModes = - sourceReleaseExecutableModes( - rebuiltSourceRelease, - sourceReleaseRoot - ) - check(originalMainHash == rebuiltMainHash) { - "Main JAR rebuild differs: $originalMainHash != $rebuiltMainHash" - } - check(originalSourcesHash == rebuiltSourcesHash) { - "Source JAR rebuild differs: $originalSourcesHash != $rebuiltSourcesHash" - } - check(originalJavadocHash == regeneratedJavadocHash) { - "Javadoc JAR rebuild differs: " + - "$originalJavadocHash != $regeneratedJavadocHash" - } - check(sourceReleaseByteIdentity) { - "Source-release ZIP replica is not byte-identical to the original" - } - check(originalSourceReleaseHash == rebuiltSourceReleaseHash) { - "Source-release ZIP replica differs: " + - "$originalSourceReleaseHash != $rebuiltSourceReleaseHash" - } - check(originalExecutableModes == rebuiltExecutableModes) { - "Source-release executable modes differ between assemblies" - } - sourceReleaseChecksum.get().writeText( - "$originalSourceReleaseHash ${originalSourceRelease.name}\n" - ) - writeEvidence( - sourceReleaseEvidence.get().asFile, - mapOf( - "archive.bytes" to - originalSourceRelease.length().toString(), - "archive.path" to - originalSourceRelease - .relativeTo(projectDir) - .invariantSeparatorsPath, - "archive.sha256" to originalSourceReleaseHash, - "assembly" to - "two-independent-gradle-zip-tasks", - "byteIdentity" to sourceReleaseByteIdentity.toString(), - "checksum.path" to - sourceReleaseChecksum.get() - .relativeTo(projectDir) - .invariantSeparatorsPath, - "checksum.sha256" to - sha256(sourceReleaseChecksum.get()), - "excluded.patterns" to - sourceReleaseExcludes.joinToString(","), - "hashIdentity" to - (originalSourceReleaseHash == - rebuiltSourceReleaseHash).toString(), - "included.patterns" to - sourceReleaseIncludes.joinToString(","), - "independentCleanCheckout" to "false", - "executable.gradlew.mode" to - originalExecutableModes.getValue("gradlew"), - "executable.publicationGate.mode" to - originalExecutableModes.getValue( - ".github/scripts/run-final-publication-gates.sh" - ), - "replica.bytes" to - rebuiltSourceRelease.length().toString(), - "replica.path" to - rebuiltSourceRelease - .relativeTo(projectDir) - .invariantSeparatorsPath, - "replica.sha256" to rebuiltSourceReleaseHash, - "rootDirectory" to sourceReleaseRoot, - "schema" to - "blue-bex-source-release-evidence/1.0", - "scope" to - "independent-archive-assembly-from-the-same-working-tree-inputs", - "status" to "passed" - ) - ) - writeEvidence( - deterministicArchiveEvidence.get().asFile, - mapOf( - "main.original.path" to - originalMain.relativeTo(projectDir).invariantSeparatorsPath, - "main.original.sha256" to originalMainHash, - "main.rebuild.path" to - rebuiltMain.relativeTo(projectDir).invariantSeparatorsPath, - "main.rebuild.sha256" to rebuiltMainHash, - "sources.original.path" to - originalSources.relativeTo(projectDir).invariantSeparatorsPath, - "sources.original.sha256" to originalSourcesHash, - "sources.rebuild.path" to - rebuiltSources.relativeTo(projectDir).invariantSeparatorsPath, - "sources.rebuild.sha256" to rebuiltSourcesHash, - "javadoc.original.path" to - originalJavadoc.relativeTo(projectDir).invariantSeparatorsPath, - "javadoc.original.sha256" to originalJavadocHash, - "javadoc.rebuild.path" to - regeneratedJavadoc.relativeTo(projectDir).invariantSeparatorsPath, - "javadoc.rebuild.sha256" to regeneratedJavadocHash, - "javadoc.freshlyRegenerated" to "true", - "sourceRelease.original.path" to - originalSourceRelease - .relativeTo(projectDir) - .invariantSeparatorsPath, - "sourceRelease.original.sha256" to - originalSourceReleaseHash, - "sourceRelease.replica.path" to - rebuiltSourceRelease - .relativeTo(projectDir) - .invariantSeparatorsPath, - "sourceRelease.replica.sha256" to - rebuiltSourceReleaseHash, - "sourceRelease.byteIdentity" to - sourceReleaseByteIdentity.toString(), - "sourceRelease.hashIdentity" to - (originalSourceReleaseHash == - rebuiltSourceReleaseHash).toString(), - "sourceRelease.independentAssembly" to "true", - "sourceRelease.independentCleanCheckout" to "false", - "scope" to - "jar-packaging-determinism-and-source-release-reassembly-from-the-same-working-tree", - "independentCleanCompilation" to "false", - "status" to "passed" - ) - ) - } -} - -val cleanBuildArtifactEvidence = - layout.buildDirectory.file( - "reports/bex-release/clean-build-artifacts.properties" - ) -val cleanBuildDependencyArtifact = - layout.buildDirectory.file( - "reports/bex-release/clean-build-inputs/" + - "blue-language-java.jar" - ) -val cleanBuildFocusedDependencyDirectory = - layout.buildDirectory.dir( - "reports/bex-release/clean-build-inputs/focused-language" - ) -val invalidateCleanBuildArtifactEvidence by tasks.registering { - group = "verification" - description = - "Invalidates any prior clean-build receipt before artifact work starts." - outputs.upToDateWhen { false } - doLast { - cleanBuildArtifactEvidence.get().asFile.delete() - cleanBuildDependencyArtifact.get().asFile.delete() - project.delete(cleanBuildFocusedDependencyDirectory) - } -} -listOf( - mainJar, - sourcesJarTask, - javadocJarTask, - sourceReleaseArchive -).forEach { artifactTask -> - artifactTask.configure { - mustRunAfter(invalidateCleanBuildArtifactEvidence) - } -} -verifyBlueLanguageAggregateCompatibility { - mustRunAfter(invalidateCleanBuildArtifactEvidence) -} -writeFocusedLanguageResolutionEvidence { - mustRunAfter(invalidateCleanBuildArtifactEvidence) -} -val writeCleanBuildArtifactHashes by tasks.registering { - group = "verification" - description = - "Records all four release hashes from one clean committed checkout." - dependsOn( - invalidateCleanBuildArtifactEvidence, - verifyBlueLanguageAggregateCompatibility, - writeFocusedLanguageResolutionEvidence, - mainJar, - sourcesJarTask, - javadocJarTask, - sourceReleaseArchive - ) - outputs.file(cleanBuildArtifactEvidence) - outputs.file(cleanBuildDependencyArtifact) - outputs.dir(cleanBuildFocusedDependencyDirectory) - outputs.upToDateWhen { false } - doFirst { - cleanBuildArtifactEvidence.get().asFile.delete() - cleanBuildDependencyArtifact.get().asFile.delete() - project.delete(cleanBuildFocusedDependencyDirectory) - } - doLast { - val checkout = - gitWorkspaceFingerprint(projectDir) - check(!checkout.dirty) { - "Clean-build evidence requires a completely clean checkout:\n" + - commandOutput( - projectDir, - "git", - "status", - "--short" - ) - } - val gitDirectory = - commandOutput( - projectDir, - "git", - "rev-parse", - "--absolute-git-dir" - ).trim() - val languageArtifacts = - blueLanguageAggregateCompatibility - .resolvedConfiguration - .resolvedArtifacts - .filter { - it.moduleVersion.id.group == "blue.language" && - it.name == "blue-language-java" && - it.extension == "jar" - } - check(languageArtifacts.size == 1) { - "Expected exactly one Blue Language dependency artifact" - } - val languageArtifact = languageArtifacts.single() - val languageArtifactCopy = - cleanBuildDependencyArtifact.get().asFile - languageArtifactCopy.parentFile.mkdirs() - languageArtifact.file.copyTo( - languageArtifactCopy, - overwrite = true - ) - check( - languageArtifactCopy.isFile && - languageArtifactCopy.length() == - languageArtifact.file.length() && - sha256(languageArtifactCopy) == - sha256(languageArtifact.file) - ) { - "Failed to preserve the exact Blue Language dependency " + - "artifact with the clean-build receipt" - } - val focusedResolution = - resolveFocusedLanguageEvidence( - blueLanguageFocusedResolution, - blueLanguageFocusedProjectPaths, - blueLanguageDependencyMode == "local-composite" - ) - val focusedCopyDirectory = - cleanBuildFocusedDependencyDirectory.get().asFile - focusedCopyDirectory.mkdirs() - val focusedArtifactCopies = - focusedResolution.artifacts.mapIndexed { index, artifact -> - val safeCoordinate = - artifact.coordinate.replace( - Regex("[^A-Za-z0-9._-]"), - "_" - ) - val copy = - File( - focusedCopyDirectory, - "%03d-%s.jar".format(index, safeCoordinate) - ) - artifact.file.copyTo(copy, overwrite = true) - check( - copy.length() == artifact.bytes && - sha256(copy) == artifact.sha256 - ) { - "Failed to preserve focused dependency artifact " + - artifact.coordinate - } - artifact to copy - } - val compositeDirectory = - blueLanguageCompositePath - ?.let { file(it).canonicalFile } - val compositeFingerprint = - compositeDirectory - ?.let(::gitWorkspaceFingerprint) - if (blueLanguageDependencyMode == "local-composite") { - check(compositeDirectory != null) { - "Local-composite clean-build evidence requires a source path" - } - check(compositeFingerprint != null) { - "Local-composite source fingerprint is unavailable" - } - check(!compositeFingerprint.dirty) { - "Local-composite clean-build evidence rejects a dirty " + - "Blue Language checkout" - } - } - val artifacts = - mapOf( - "main" to mainJar.get().archiveFile.get().asFile, - "sources" to - sourcesJarTask.get().archiveFile.get().asFile, - "javadoc" to - javadocJarTask.get().archiveFile.get().asFile, - "sourceRelease" to - sourceReleaseArchive.get().archiveFile.get().asFile - ) - val values = - linkedMapOf( - "schema" to - "blue-bex-clean-build-artifacts/1.1", - "status" to "passed", - "commit" to checkout.commit, - "checkout.clean" to "true", - "checkout.root" to projectDir.canonicalPath, - "checkout.gitDirectory" to - File(gitDirectory).canonicalPath, - "checkout.gitStatusSha256" to - checkout.statusSha256, - "checkout.workspaceSha256" to - checkout.workspaceSha256, - "checkout.pathCount" to - checkout.pathCount.toString(), - "project.version" to project.version.toString(), - "dependency.mode" to blueLanguageDependencyMode, - "dependency.coordinate" to - blueLanguageDeclaredCoordinate, - "dependency.effectiveCoordinate" to - ( - languageArtifact.moduleVersion.id.group + - ":" + - languageArtifact.name + - ":" + - languageArtifact.moduleVersion.id.version - ), - "dependency.artifact.bytes" to - languageArtifactCopy.length().toString(), - "dependency.artifact.path" to - languageArtifactCopy.relativeTo(projectDir) - .invariantSeparatorsPath, - "dependency.artifact.sha256" to - sha256(languageArtifactCopy), - "dependency.aggregateCompatibilityOnly" to "true", - "dependency.focused.declaredCoordinates" to - blueLanguageFocusedCoordinates.joinToString(","), - "dependency.focused.graphSha256" to - focusedResolution.graphSha256, - "dependency.focused.componentCount" to - focusedResolution.components.size.toString(), - "dependency.focused.edgeCount" to - focusedResolution.edges.size.toString(), - "dependency.focused.artifactCount" to - focusedArtifactCopies.size.toString(), - "composite.path" to - (compositeDirectory?.path ?: ""), - "composite.commit" to - (compositeFingerprint?.commit ?: ""), - "composite.dirty" to - (compositeFingerprint?.dirty - ?.toString() - ?: "false"), - "composite.gitStatusSha256" to - ( - compositeFingerprint - ?.statusSha256 - ?: "" - ), - "composite.workspaceSha256" to - ( - compositeFingerprint - ?.workspaceSha256 - ?: "" - ), - "composite.pathCount" to - ( - compositeFingerprint - ?.pathCount - ?.toString() - ?: "0" - ) - ) - focusedArtifactCopies.forEachIndexed { index, pair -> - val (artifact, copy) = pair - val prefix = - "dependency.focused.artifact.%03d".format(index) - values["$prefix.coordinate"] = artifact.coordinate - values["$prefix.component"] = artifact.component - values["$prefix.projectPath"] = - artifact.projectPath.orEmpty() - values["$prefix.origin"] = - if (artifact.includedBuildProject) { - "included-build-project" - } else { - "external-module" - } - values["$prefix.path"] = - copy.relativeTo(projectDir).invariantSeparatorsPath - values["$prefix.bytes"] = copy.length().toString() - values["$prefix.sha256"] = sha256(copy) - } - for ((name, artifact) in artifacts) { - values["artifact.$name.path"] = - artifact.relativeTo(projectDir) - .invariantSeparatorsPath - values["artifact.$name.bytes"] = - artifact.length().toString() - values["artifact.$name.sha256"] = - sha256(artifact) - } - writeEvidence( - cleanBuildArtifactEvidence.get().asFile, - values - ) - } -} - -val independentCleanBuildEvidence = - layout.projectDirectory.file( - ".gradle/bex-hosted-release/" + - "independent-clean-builds-" + - "$blueLanguageDependencyMode.properties" - ) -val verifyIndependentCleanBuildReproducibility by tasks.registering { - group = "verification" - description = - "Compares main, sources, Javadoc, and source-release hashes from two clean checkouts of the same commit." - dependsOn( - verifyBlueLanguageAggregateCompatibility, - writeFocusedLanguageResolutionEvidence - ) - val firstEvidencePath = - providers.gradleProperty("cleanBuildEvidenceOne") - val secondEvidencePath = - providers.gradleProperty("cleanBuildEvidenceTwo") - inputs.property( - "cleanBuildEvidenceOne", - firstEvidencePath.orElse("") - ) - inputs.property( - "cleanBuildEvidenceTwo", - secondEvidencePath.orElse("") - ) - outputs.file(independentCleanBuildEvidence) - outputs.upToDateWhen { false } - doFirst { - independentCleanBuildEvidence.asFile.delete() - } - doLast { - val firstPath = - firstEvidencePath.orNull?.trim() - ?.takeIf { it.isNotEmpty() } - ?.let(::file) - ?: throw GradleException( - "-PcleanBuildEvidenceOne is required" - ) - val secondPath = - secondEvidencePath.orNull?.trim() - ?.takeIf { it.isNotEmpty() } - ?.let(::file) - ?: throw GradleException( - "-PcleanBuildEvidenceTwo is required" - ) - check( - firstPath.canonicalFile != - secondPath.canonicalFile - ) { - "Independent-build evidence files must be distinct" - } - val first = readEvidence(firstPath) - val second = readEvidence(secondPath) - val currentCommit = - commandOutput( - projectDir, - "git", - "rev-parse", - "HEAD" - ).trim().lowercase() - val expectedSchema = - "blue-bex-clean-build-artifacts/1.1" - val artifactNames = - listOf( - "main", - "sources", - "javadoc", - "sourceRelease" - ) - val artifactPrefix = - "blue-bex-java-${project.version}" - val expectedArtifactPaths = - mapOf( - "main" to - "build/libs/$artifactPrefix.jar", - "sources" to - "build/libs/$artifactPrefix-sources.jar", - "javadoc" to - "build/libs/$artifactPrefix-javadoc.jar", - "sourceRelease" to - "build/distributions/" + - "$artifactPrefix-source-release.zip" - ) - val expectedDependencyArtifactPath = - "build/reports/bex-release/clean-build-inputs/" + - "blue-language-java.jar" - val authenticatedRoots = - linkedMapOf() - val authenticatedGitDirectories = - linkedMapOf() - val authenticatedFingerprints = - linkedMapOf() - val authenticatedArtifacts = - linkedMapOf< - String, - Map> - >() - val authenticatedDependencyArtifacts = - linkedMapOf>() - val authenticatedFocusedArtifacts = - linkedMapOf>>() - val authenticatedFocusedGraphHashes = - linkedMapOf() - for ((label, evidence) in - listOf("first" to first, "second" to second)) { - check(evidence["schema"] == expectedSchema) { - "$label clean-build evidence has the wrong schema" - } - check(evidence["status"] == "passed") { - "$label clean-build evidence did not pass" - } - check(evidence["checkout.clean"] == "true") { - "$label build was not produced from a clean checkout" - } - val recordedRoot = - evidence["checkout.root"] - ?.let(::File) - ?.takeIf(File::isAbsolute) - ?.canonicalFile - check(recordedRoot?.isDirectory == true) { - "$label checkout root is unavailable" - } - val actualRoot = - File( - commandOutput( - recordedRoot, - "git", - "rev-parse", - "--show-toplevel" - ).trim() - ).canonicalFile - check(actualRoot == recordedRoot) { - "$label checkout root is not its Git top level" - } - val recordedGitDirectory = - evidence["checkout.gitDirectory"] - ?.let(::File) - ?.takeIf(File::isAbsolute) - ?.canonicalFile - val actualGitDirectory = - File( - commandOutput( - recordedRoot, - "git", - "rev-parse", - "--absolute-git-dir" - ).trim() - ).canonicalFile - check( - recordedGitDirectory?.isDirectory == true - && recordedGitDirectory == - actualGitDirectory - ) { - "$label Git directory is unavailable" - } - val fingerprint = - gitWorkspaceFingerprint(recordedRoot) - check(!fingerprint.dirty) { - "$label checkout is no longer clean" - } - check( - evidence["checkout.workspaceSha256"] == - fingerprint.workspaceSha256 - ) { - "$label checkout source fingerprint changed" - } - check( - evidence["checkout.pathCount"] == - fingerprint.pathCount.toString() - ) { - "$label checkout source path count changed" - } - check( - evidence["checkout.gitStatusSha256"] == - fingerprint.statusSha256 - ) { - "$label checkout Git status fingerprint changed" - } - check(evidence["commit"] == currentCommit) { - "$label build commit ${evidence["commit"]} " + - "does not match $currentCommit" - } - check( - evidence["project.version"] == - project.version.toString() - ) { - "$label build used a different project version" - } - check( - evidence["dependency.mode"] == - blueLanguageDependencyMode - ) { - "$label build used a different dependency mode" - } - check( - evidence["dependency.coordinate"] == - blueLanguageDeclaredCoordinate - ) { - "$label build used a different declared dependency" - } - check( - evidence["dependency.effectiveCoordinate"] - ?.isNotEmpty() == true - ) { - "$label build has no effective dependency coordinate" - } - val buildArtifacts = - linkedMapOf>() - for (artifactName in artifactNames) { - val pathKey = - "artifact.$artifactName.path" - val bytesKey = - "artifact.$artifactName.bytes" - val hashKey = - "artifact.$artifactName.sha256" - val relativePath = - evidence[pathKey] - ?: throw GradleException( - "$label $artifactName path is unavailable" - ) - check( - relativePath == - expectedArtifactPaths.getValue( - artifactName - ) - ) { - "$label $artifactName path is not the expected " + - "release output: $relativePath" - } - check(!File(relativePath).isAbsolute) { - "$label $artifactName path must be relative" - } - val artifact = - File(recordedRoot, relativePath) - .canonicalFile - check( - artifact.toPath().startsWith( - recordedRoot.toPath() - ) && - artifact.isFile - ) { - "$label $artifactName artifact is unavailable " + - "under its authenticated checkout" - } - val recordedBytes = - evidence[bytesKey]?.toLongOrNull() - check( - recordedBytes != null && - recordedBytes == artifact.length() - ) { - "$label $artifactName byte length differs from " + - "its receipt" - } - val recordedHash = evidence[hashKey] - val actualHash = sha256(artifact) - check( - recordedHash?.matches( - Regex("[0-9a-f]{64}") - ) == true && - recordedHash == actualHash - ) { - "$label $artifactName artifact hash differs from " + - "its receipt" - } - buildArtifacts[artifactName] = - mapOf( - "path" to relativePath, - "bytes" to recordedBytes.toString(), - "sha256" to actualHash - ) - } - val dependencyPath = - evidence["dependency.artifact.path"] - ?: throw GradleException( - "$label Blue Language artifact path is unavailable" - ) - check( - dependencyPath == - expectedDependencyArtifactPath && - !File(dependencyPath).isAbsolute - ) { - "$label Blue Language artifact path is not the " + - "expected receipt-owned copy" - } - val dependencyArtifact = - File(recordedRoot, dependencyPath) - .canonicalFile - check( - dependencyArtifact.toPath().startsWith( - recordedRoot.toPath() - ) && - dependencyArtifact.isFile - ) { - "$label Blue Language artifact copy is unavailable " + - "under its authenticated checkout" - } - val dependencyBytes = - evidence["dependency.artifact.bytes"] - ?.toLongOrNull() - check( - dependencyBytes != null && - dependencyBytes == - dependencyArtifact.length() - ) { - "$label Blue Language artifact byte length differs " + - "from its receipt" - } - val dependencyHash = - evidence["dependency.artifact.sha256"] - val actualDependencyHash = - sha256(dependencyArtifact) - check( - dependencyHash?.matches( - Regex("[0-9a-f]{64}") - ) == true && - dependencyHash == - actualDependencyHash - ) { - "$label Blue Language artifact hash differs from " + - "its receipt" - } - check( - evidence["dependency.aggregateCompatibilityOnly"] == - "true" - ) { - "$label aggregate artifact is not labelled smoke-only" - } - check( - evidence["dependency.focused.declaredCoordinates"] == - blueLanguageFocusedCoordinates.joinToString(",") - ) { - "$label focused dependency coordinates differ" - } - val focusedGraphHash = - evidence["dependency.focused.graphSha256"] - check( - focusedGraphHash?.matches(Regex("[0-9a-f]{64}")) == - true - ) { - "$label focused dependency graph hash is unavailable" - } - val focusedArtifactCount = - evidence["dependency.focused.artifactCount"] - ?.toIntOrNull() - check( - focusedArtifactCount != null && - focusedArtifactCount > 0 - ) { - "$label focused dependency artifact count is invalid" - } - val focusedArtifacts = - (0 until focusedArtifactCount).map { index -> - val prefix = - "dependency.focused.artifact.%03d".format(index) - val coordinate = evidence["$prefix.coordinate"] - val component = evidence["$prefix.component"] - val projectPath = - evidence["$prefix.projectPath"].orEmpty() - val origin = evidence["$prefix.origin"] - val relativePath = evidence["$prefix.path"] - check( - coordinate?.split(':')?.size == 3 && - component?.isNotEmpty() == true && - origin in setOf( - "included-build-project", - "external-module" - ) && - relativePath?.startsWith( - "build/reports/bex-release/" + - "clean-build-inputs/focused-language/" - ) == true && - !File(relativePath).isAbsolute - ) { - "$label focused artifact $index metadata is invalid" - } - val copiedArtifact = - File(recordedRoot, relativePath).canonicalFile - check( - copiedArtifact.toPath().startsWith( - recordedRoot.toPath() - ) && copiedArtifact.isFile - ) { - "$label focused artifact $coordinate is unavailable" - } - val recordedBytes = - evidence["$prefix.bytes"]?.toLongOrNull() - val recordedHash = evidence["$prefix.sha256"] - check( - recordedBytes == copiedArtifact.length() && - recordedHash?.matches( - Regex("[0-9a-f]{64}") - ) == true && - recordedHash == sha256(copiedArtifact) - ) { - "$label focused artifact $coordinate differs " + - "from its receipt" - } - val authenticatedCoordinate = - requireNotNull(coordinate) - val authenticatedComponent = - requireNotNull(component) - val authenticatedOrigin = requireNotNull(origin) - val authenticatedHash = requireNotNull(recordedHash) - linkedMapOf( - "coordinate" to authenticatedCoordinate, - "component" to authenticatedComponent, - "projectPath" to projectPath, - "origin" to authenticatedOrigin, - "bytes" to recordedBytes.toString(), - "sha256" to authenticatedHash - ) - } - for ((moduleName, expectedPath) in - blueLanguageFocusedProjectPaths) { - val matches = focusedArtifacts.filter { - it.getValue("coordinate") - .substringAfter(':') - .substringBefore(':') == moduleName - } - check(matches.size == 1) { - "$label receipt does not contain exactly one " + - "$moduleName artifact" - } - if (blueLanguageDependencyMode == "local-composite") { - check( - matches.single().getValue("origin") == - "included-build-project" && - matches.single().getValue("projectPath") == - expectedPath - ) { - "$label $moduleName did not originate from " + - "$expectedPath" - } - } - } - authenticatedRoots[label] = recordedRoot - authenticatedGitDirectories[label] = - actualGitDirectory - authenticatedFingerprints[label] = - fingerprint - authenticatedArtifacts[label] = - buildArtifacts - authenticatedDependencyArtifacts[label] = - mapOf( - "path" to dependencyPath, - "bytes" to dependencyBytes.toString(), - "sha256" to actualDependencyHash - ) - authenticatedFocusedArtifacts[label] = focusedArtifacts - authenticatedFocusedGraphHashes[label] = focusedGraphHash - } - check( - authenticatedRoots.getValue("first") != - authenticatedRoots.getValue("second") - ) { - "Independent builds used the same checkout root" - } - check( - authenticatedGitDirectories.getValue("first") != - authenticatedGitDirectories.getValue("second") - ) { - "Independent builds used the same Git directory" - } - check( - authenticatedFingerprints.getValue("first") - .workspaceSha256 == - authenticatedFingerprints.getValue("second") - .workspaceSha256 - ) { - "Independent builds used different source bytes" - } - check( - authenticatedFingerprints.getValue("first") - .pathCount == - authenticatedFingerprints.getValue("second") - .pathCount - ) { - "Independent builds used different source path sets" - } - val values = - linkedMapOf( - "schema" to - "blue-bex-independent-clean-builds/1.2", - "status" to "passed", - "commit" to currentCommit, - "first.checkout.clean" to "true", - "second.checkout.clean" to "true", - "first.checkout.root" to - authenticatedRoots.getValue("first") - .canonicalPath, - "second.checkout.root" to - authenticatedRoots.getValue("second") - .canonicalPath, - "first.checkout.gitDirectory" to - authenticatedGitDirectories - .getValue("first") - .canonicalPath, - "second.checkout.gitDirectory" to - authenticatedGitDirectories - .getValue("second") - .canonicalPath, - "first.checkout.gitStatusSha256" to - authenticatedFingerprints - .getValue("first") - .statusSha256, - "second.checkout.gitStatusSha256" to - authenticatedFingerprints - .getValue("second") - .statusSha256, - "first.checkout.workspaceSha256" to - authenticatedFingerprints - .getValue("first") - .workspaceSha256, - "second.checkout.workspaceSha256" to - authenticatedFingerprints - .getValue("second") - .workspaceSha256, - "first.checkout.pathCount" to - authenticatedFingerprints - .getValue("first") - .pathCount.toString(), - "second.checkout.pathCount" to - authenticatedFingerprints - .getValue("second") - .pathCount.toString(), - "first.evidence.path" to - firstPath.canonicalPath, - "second.evidence.path" to - secondPath.canonicalPath, - "first.evidence.sha256" to sha256(firstPath), - "second.evidence.sha256" to sha256(secondPath), - "project.version" to project.version.toString(), - "dependency.mode" to - first.getValue("dependency.mode"), - "dependency.coordinate" to - first.getValue("dependency.coordinate"), - "dependency.effectiveCoordinate" to - first.getValue( - "dependency.effectiveCoordinate" - ), - "dependency.artifact.sha256" to - authenticatedDependencyArtifacts - .getValue("first") - .getValue("sha256"), - "dependency.artifact.path" to - authenticatedDependencyArtifacts - .getValue("first") - .getValue("path"), - "dependency.artifact.bytes" to - authenticatedDependencyArtifacts - .getValue("first") - .getValue("bytes"), - "composite.path" to - first.getValue("composite.path"), - "composite.commit" to - first.getValue("composite.commit"), - "composite.dirty" to - first.getValue("composite.dirty"), - "composite.gitStatusSha256" to - first.getValue( - "composite.gitStatusSha256" - ), - "composite.workspaceSha256" to - first.getValue( - "composite.workspaceSha256" - ), - "composite.pathCount" to - first.getValue("composite.pathCount") - ) - for ((label, artifacts) in authenticatedArtifacts) { - for ((artifactName, artifact) in artifacts) { - for ((field, value) in artifact) { - values[ - "$label.artifact.$artifactName.$field" - ] = value - } - } - } - for ((label, artifact) in - authenticatedDependencyArtifacts) { - for ((field, value) in artifact) { - values[ - "$label.dependency.artifact.$field" - ] = value - } - } - check( - first["dependency.mode"] == - second["dependency.mode"] - ) { - "Clean builds used different dependency modes" - } - check( - first["dependency.coordinate"] == - second["dependency.coordinate"] - ) { - "Clean builds used different dependency coordinates" - } - check( - first["dependency.effectiveCoordinate"] == - second["dependency.effectiveCoordinate"] - ) { - "Clean builds resolved different effective dependency coordinates" - } - val firstDependencyHash = - authenticatedDependencyArtifacts - .getValue("first") - .getValue("sha256") - val secondDependencyHash = - authenticatedDependencyArtifacts - .getValue("second") - .getValue("sha256") - check( - firstDependencyHash.matches( - Regex("[0-9a-f]{64}") - ) - ) { - "First build has no exact Language artifact hash" - } - check( - firstDependencyHash == - secondDependencyHash - ) { - "Clean builds resolved different Language artifacts" - } - val firstFocusedGraphHash = - authenticatedFocusedGraphHashes.getValue("first") - val secondFocusedGraphHash = - authenticatedFocusedGraphHashes.getValue("second") - check(firstFocusedGraphHash == secondFocusedGraphHash) { - "Clean builds resolved different focused Language graphs" - } - check( - authenticatedFocusedArtifacts.getValue("first") == - authenticatedFocusedArtifacts.getValue("second") - ) { - "Clean builds resolved different focused Language JAR sets" - } - val verifierFocusedResolution = - resolveFocusedLanguageEvidence( - blueLanguageFocusedResolution, - blueLanguageFocusedProjectPaths, - blueLanguageDependencyMode == "local-composite" - ) - check( - verifierFocusedResolution.graphSha256 == - firstFocusedGraphHash - ) { - "Clean builds did not use the verifier's exact focused " + - "Language component graph" - } - values["dependency.focused.declaredCoordinates"] = - blueLanguageFocusedCoordinates.joinToString(",") - values["dependency.focused.graphSha256"] = - firstFocusedGraphHash - values["dependency.focused.artifactCount"] = - authenticatedFocusedArtifacts.getValue("first") - .size.toString() - authenticatedFocusedArtifacts.getValue("first") - .forEachIndexed { index, artifact -> - val prefix = - "dependency.focused.artifact.%03d".format(index) - artifact.forEach { (field, value) -> - values["$prefix.$field"] = value - } - } - val verifierLanguageArtifacts = - blueLanguageAggregateCompatibility - .resolvedConfiguration - .resolvedArtifacts - .filter { - it.moduleVersion.id.group == "blue.language" && - it.name == "blue-language-java" && - it.extension == "jar" - } - check(verifierLanguageArtifacts.size == 1) { - "Verifier did not resolve exactly one Blue Language artifact" - } - val verifierLanguageArtifact = - verifierLanguageArtifacts.single() - val verifierLanguageCoordinate = - ( - verifierLanguageArtifact.moduleVersion.id.group + - ":" + - verifierLanguageArtifact.name + - ":" + - verifierLanguageArtifact.moduleVersion.id.version - ) - check( - verifierLanguageCoordinate == - first["dependency.effectiveCoordinate"] - ) { - "Clean builds did not resolve the verifier's exact Blue " + - "Language coordinate" - } - check( - sha256(verifierLanguageArtifact.file) == - firstDependencyHash - ) { - "Clean builds did not use the verifier's exact Blue " + - "Language artifact" - } - val compositeKeys = - listOf( - "composite.path", - "composite.commit", - "composite.dirty", - "composite.gitStatusSha256", - "composite.workspaceSha256", - "composite.pathCount" - ) - for (key in compositeKeys) { - check(first[key] == second[key]) { - "Clean builds used different $key values" - } - } - if (blueLanguageDependencyMode == "local-composite") { - check( - first["composite.commit"] - ?.matches(Regex("[0-9a-f]{40}")) == - true - ) { - "Local-composite source commit is unavailable" - } - check( - first["composite.workspaceSha256"] - ?.matches(Regex("[0-9a-f]{64}")) == - true - ) { - "Local-composite source fingerprint is unavailable" - } - check( - first["composite.dirty"] == "false" - && second["composite.dirty"] == - "false" - ) { - "Local-composite clean builds require a clean " + - "Blue Language checkout" - } - check( - first["composite.gitStatusSha256"] == - sha256(ByteArray(0)) - ) { - "Local-composite Git status is not clean" - } - val activeCompositePath = - blueLanguageCompositePath - ?.let(::file) - ?.canonicalFile - val recordedCompositePath = - first["composite.path"] - ?.let(::File) - ?.canonicalFile - check( - activeCompositePath?.isDirectory == true - && recordedCompositePath == - activeCompositePath - ) { - "Local-composite source path changed" - } - val activeComposite = - gitWorkspaceFingerprint(activeCompositePath) - check( - !activeComposite.dirty - && activeComposite.commit == - first["composite.commit"] - && activeComposite.statusSha256 == - first["composite.gitStatusSha256"] - && activeComposite.workspaceSha256 == - first["composite.workspaceSha256"] - && activeComposite.pathCount.toString() == - first["composite.pathCount"] - ) { - "Local-composite source state changed" - } - } - for (artifactName in artifactNames) { - val firstHash = - authenticatedArtifacts - .getValue("first") - .getValue(artifactName) - .getValue("sha256") - val secondHash = - authenticatedArtifacts - .getValue("second") - .getValue(artifactName) - .getValue("sha256") - check( - firstHash.matches(Regex("[0-9a-f]{64}")) - ) { - "First $artifactName hash is unavailable" - } - check(firstHash == secondHash) { - "$artifactName differs across clean builds: " + - "$firstHash != $secondHash" - } - values["artifact.$artifactName.sha256"] = - firstHash - values["artifact.$artifactName.byteIdentical"] = - "true" - } - writeEvidence( - independentCleanBuildEvidence.asFile, - values - ) - } -} - -val binaryApiEvidence = - layout.buildDirectory.file( - "reports/bex-release/binary-api.properties" - ) -val binaryApiManifest = - layout.buildDirectory.file( - "reports/bex-release/public-api.txt" - ) -val requiredBinaryApi = - layout.projectDirectory.file( - "src/test/resources/hosted-release/" + - "required-public-api.txt" - ) -val generateBinaryApiManifest by tasks.registering(JavaExec::class) { - group = "verification" - description = - "Generates a deterministic descriptor-level public/protected API manifest from the packaged JAR." - dependsOn(tasks.testClasses, mainJar) - classpath = sourceSets.test.get().runtimeClasspath - mainClass.set( - "blue.bex.conformance.BexBinaryApiManifestMain" - ) - javaLauncher.set( - javaToolchains.launcherFor { - languageVersion.set(JavaLanguageVersion.of(8)) - } - ) - doFirst { - setArgs( - listOf( - mainJar.get().archiveFile.get().asFile.absolutePath, - binaryApiManifest.get().asFile.absolutePath - ) - ) - } - inputs.file(mainJar.flatMap { it.archiveFile }) - outputs.file(binaryApiManifest) - outputs.upToDateWhen { false } -} -val binaryApiCheck by tasks.registering(Test::class) { - group = "verification" - description = - "Runs the BEX 2.0 API-surface checks against the packaged binary JAR." - dependsOn( - tasks.testClasses, - mainJar, - generateBinaryApiManifest - ) - testClassesDirs = sourceSets.test.get().output.classesDirs - classpath = - sourceSets.test.get().output + - files(mainJar.flatMap { it.archiveFile }) + - configurations.testRuntimeClasspath.get() - useJUnitPlatform() - include("**/Bex20ApiSurfaceTest.class") - javaLauncher.set( - javaToolchains.launcherFor { - languageVersion.set(JavaLanguageVersion.of(8)) - } - ) - reports.junitXml.required.set(true) - reports.html.required.set(true) - inputs.file(requiredBinaryApi) - outputs.file(binaryApiEvidence) - outputs.upToDateWhen { false } - doFirst { - binaryApiEvidence.get().asFile.delete() - } - doLast { - val artifact = mainJar.get().archiveFile.get().asFile - val manifest = binaryApiManifest.get().asFile - val required = requiredBinaryApi.asFile - check(manifest.isFile) { - "Binary API manifest is missing: $manifest" - } - check(required.isFile) { - "Required binary API signature set is missing: $required" - } - val actualSignatures = manifest.readLines() - .map { it.trimEnd() } - val requiredSignatures = required.readLines() - .map { it.trimEnd() } - check(actualSignatures == requiredSignatures) { - val firstDifference = - (0 until maxOf( - actualSignatures.size, - requiredSignatures.size - )).firstOrNull { index -> - actualSignatures.getOrNull(index) != - requiredSignatures.getOrNull(index) - } - "Packaged JAR public/protected API differs from the exact " + - "first-public BEX 2.0 baseline at line " + - "${firstDifference?.plus(1) ?: 1}:\n" + - "expected=" + - requiredSignatures.getOrNull(firstDifference ?: 0) + - "\nactual=" + - actualSignatures.getOrNull(firstDifference ?: 0) - } - writeEvidence( - binaryApiEvidence.get().asFile, - mapOf( - "artifact.path" to - artifact.relativeTo(projectDir).invariantSeparatorsPath, - "artifact.sha256" to sha256(artifact), - "manifest.path" to - manifest.relativeTo(projectDir).invariantSeparatorsPath, - "manifest.sha256" to sha256(manifest), - "manifest.schema" to - "blue-bex-binary-api-manifest/1.0", - "required.path" to - required.relativeTo(projectDir).invariantSeparatorsPath, - "required.sha256" to sha256(required), - "required.signatureCount" to - requiredSignatures.size.toString(), - "required.missingCount" to "0", - "required.unexpectedCount" to "0", - "required.comparison" to "exact-match", - "status" to "passed", - "testClass" to "blue.bex.api.Bex20ApiSurfaceTest" - ) - ) - } -} - -val java8BytecodeEvidence = - layout.buildDirectory.file( - "reports/bex-release/java8-bytecode.properties" - ) -val java8BytecodeCheck by tasks.registering { - group = "verification" - description = - "Verifies that every class in the packaged main JAR is Java 8 bytecode." - dependsOn(mainJar) - inputs.file(mainJar.flatMap { it.archiveFile }) - outputs.file(java8BytecodeEvidence) - outputs.upToDateWhen { false } - doFirst { - java8BytecodeEvidence.get().asFile.delete() - } - doLast { - val artifact = mainJar.get().archiveFile.get().asFile - val expectedMagic = "cafebabe" - val expectedMajor = 52 - val observedMajors = sortedSetOf() - val classCount = - ZipFile(artifact).use { archive -> - val classEntries = - archive.entries().asSequence() - .filter { - !it.isDirectory && - it.name.endsWith(".class") - } - .sortedBy { it.name } - .toList() - check(classEntries.isNotEmpty()) { - "Packaged main JAR contains no class entries: $artifact" - } - classEntries.forEach { entry -> - val header = ByteArray(8) - val bytesRead = - archive.getInputStream(entry).buffered().use { input -> - var offset = 0 - while (offset < header.size) { - val read = - input.read( - header, - offset, - header.size - offset - ) - if (read < 0) { - break - } - offset += read - } - offset - } - check(bytesRead == header.size) { - "Truncated class header in ${entry.name}: " + - "$bytesRead bytes" - } - val magic = - header.take(4).joinToString("") { - "%02x".format(it.toInt() and 0xff) - } - check(magic == expectedMagic) { - "Invalid class magic in ${entry.name}: $magic" - } - val major = - ((header[6].toInt() and 0xff) shl 8) or - (header[7].toInt() and 0xff) - observedMajors.add(major) - check(major == expectedMajor) { - "Non-Java-8 bytecode in ${entry.name}: " + - "major $major (expected $expectedMajor)" - } - } - classEntries.size - } - writeEvidence( - java8BytecodeEvidence.get().asFile, - mapOf( - "artifact.path" to - artifact.relativeTo(projectDir).invariantSeparatorsPath, - "artifact.sha256" to sha256(artifact), - "classCount" to classCount.toString(), - "expected.magic" to expectedMagic.uppercase(), - "expected.major" to expectedMajor.toString(), - "observed.magic" to expectedMagic.uppercase(), - "observed.major" to observedMajors.joinToString(","), - "schema" to - "blue-bex-java8-bytecode-evidence/1.0", - "status" to "passed" - ) - ) - } -} - -val benchmarkCompilationEvidence = - layout.buildDirectory.file( - "reports/bex-release/benchmark-compilation.properties" - ) -val benchmarkCompilationCheck by tasks.registering { - group = "verification" - description = - "Verifies that the compile-only local benchmark builds under Java 8; it does not run timing." - dependsOn(tasks.testClasses) - val benchmarkSource = - layout.projectDirectory.file( - "src/test/java/blue/bex/BexLocalBenchmarkTest.java" - ) - val benchmarkClass = - layout.buildDirectory.file( - "classes/java/test/blue/bex/BexLocalBenchmarkTest.class" - ) - inputs.file(benchmarkSource) - inputs.file(benchmarkClass) - outputs.file(benchmarkCompilationEvidence) - outputs.upToDateWhen { false } - doFirst { - benchmarkCompilationEvidence.get().asFile.delete() - } - doLast { - val source = benchmarkSource.asFile - val compiled = benchmarkClass.get().asFile - check(source.isFile) { - "Benchmark source is missing: $source" - } - check(compiled.isFile) { - "Benchmark did not compile to: $compiled" - } - writeEvidence( - benchmarkCompilationEvidence.get().asFile, - mapOf( - "class.path" to - compiled.relativeTo(projectDir).invariantSeparatorsPath, - "class.sha256" to sha256(compiled), - "source.path" to - source.relativeTo(projectDir).invariantSeparatorsPath, - "source.sha256" to sha256(source), - "status" to "passed", - "timingExecuted" to "false" - ) - ) - } -} - -val dependencyResolutionEvidence = - layout.buildDirectory.file( - "reports/bex-release/dependency-resolution.properties" - ) -val writeDependencyResolutionEvidence by tasks.registering { - group = "verification" - description = - "Verifies focused Language resolution and the smoke-only aggregate " + - "facade against strict standalone provenance evidence." - dependsOn( - verifyBlueLanguageAggregateCompatibility, - writeFocusedLanguageResolutionEvidence - ) - outputs.file(dependencyResolutionEvidence) - outputs.upToDateWhen { false } - doFirst { - dependencyResolutionEvidence.get().asFile.delete() - } - doLast { - val matches = - blueLanguageAggregateCompatibility - .resolvedConfiguration - .resolvedArtifacts - .filter { - it.moduleVersion.id.group == "blue.language" && - it.name == "blue-language-java" && - it.extension == "jar" - } - check(matches.size == 1) { - "Expected exactly one smoke-only blue-language-java artifact, found " + - matches.joinToString { it.file.absolutePath } - } - val artifact = matches.single() - val component = artifact.id.componentIdentifier - val compositeDirectory = - blueLanguageCompositePath - ?.let { file(it).canonicalFile } - val artifactHash = sha256(artifact.file) - val focusedResolution = - resolveFocusedLanguageEvidence( - blueLanguageFocusedResolution, - blueLanguageFocusedProjectPaths, - blueLanguageDependencyMode == "local-composite" - ) - val provenanceStatus: String - val moduleVersionCacheAcceptance: String - if (blueLanguageDependencyMode == "standalone-published") { - check( - publishedBlueLanguageCoordinate == - blueLanguageDeclaredCoordinate - ) { - "Recorded Maven Central coordinate differs from the " + - "declared dependency: $publishedBlueLanguageCoordinate" - } - check( - publishedBlueLanguageRepository == - "https://repo1.maven.org/maven2" - ) { - "Unrecognized recorded Maven Central provenance: " + - publishedBlueLanguageRepository - } - check(artifactHash == publishedBlueLanguageSha256) { - "Resolved standalone artifact does not match the recorded " + - "Maven Central SHA-256: $artifactHash != " + - publishedBlueLanguageSha256 - } - provenanceStatus = - "verified-against-recorded-maven-central-hash" - // This is deliberately narrower than claiming that the entire - // Gradle cache was clean. Project configuration captures whether - // this exact Blue Language module/version directory was absent; - // resolution above then verifies the resulting JAR against the - // source-controlled Maven Central hash. - val allRequiredCachesInitiallyAbsent = - blueLanguageModuleVersionCacheInitiallyAbsent && - blueLanguageFocusedModuleVersionCachesInitiallyAbsent - .values.all { it } - moduleVersionCacheAcceptance = - if (!blueLanguageRequireFreshModuleCache.get()) { - "not-required-for-current-run" - } else if (allRequiredCachesInitiallyAbsent) { - "passed" - } else { - "failed" - } - } else { - provenanceStatus = "not-applicable-local-composite" - moduleVersionCacheAcceptance = "not-executed" - } - val values = - linkedMapOf( - "schema" to - "blue-bex-dependency-resolution-evidence/1.0", - "status" to "resolved", - "mode" to blueLanguageDependencyMode, - "declared.coordinate" to - blueLanguageDeclaredCoordinate, - "effective.component" to component.displayName, - "effective.group" to artifact.moduleVersion.id.group, - "effective.name" to artifact.name, - "effective.version" to - artifact.moduleVersion.id.version, - "artifact.path" to artifact.file.canonicalPath, - "artifact.bytes" to artifact.file.length().toString(), - "artifact.sha256" to artifactHash, - "aggregate.compatibilityOnly" to "true", - "focused.declaredCoordinates" to - blueLanguageFocusedCoordinates.joinToString(","), - "focused.graphSha256" to - focusedResolution.graphSha256, - "focused.componentCount" to - focusedResolution.components.size.toString(), - "focused.edgeCount" to - focusedResolution.edges.size.toString(), - "focused.artifactCount" to - focusedResolution.artifacts.size.toString(), - "composite.path" to - (compositeDirectory?.path ?: ""), - "repository.policy" to "maven-central-only", - "provenance.status" to provenanceStatus, - "provenance.recorded.repository" to - publishedBlueLanguageRepository, - "provenance.recorded.coordinate" to - publishedBlueLanguageCoordinate, - "provenance.recorded.sha256" to - publishedBlueLanguageSha256, - "provenance.networkFetchObservation" to - "not-exposed-by-gradle-resolution-api", - "cache.blueLanguageModuleVersionPath" to - blueLanguageModuleVersionCache.canonicalPath, - "cache.blueLanguageModuleVersionInitiallyAbsent" to - blueLanguageModuleVersionCacheInitiallyAbsent.toString(), - "cache.freshProofRequired" to - blueLanguageRequireFreshModuleCache.get().toString(), - "cache.acceptance" to moduleVersionCacheAcceptance, - "cache.acceptanceScope" to - "standalone-published-focused-and-aggregate-language-module-version-caches" - ) - blueLanguageFocusedModuleVersionCaches - .toSortedMap() - .forEach { (moduleName, cache) -> - values["cache.focused.$moduleName.path"] = - cache.canonicalPath - values["cache.focused.$moduleName.initiallyAbsent"] = - blueLanguageFocusedModuleVersionCachesInitiallyAbsent - .getValue(moduleName) - .toString() - } - focusedResolution.artifacts.forEachIndexed { index, focused -> - val prefix = "focused.artifact.%03d".format(index) - values["$prefix.coordinate"] = focused.coordinate - values["$prefix.component"] = focused.component - values["$prefix.projectPath"] = - focused.projectPath.orEmpty() - values["$prefix.bytes"] = focused.bytes.toString() - values["$prefix.sha256"] = focused.sha256 - } - writeEvidence( - dependencyResolutionEvidence.get().asFile, - values - ) - } -} - -val writeBexConformanceReport by tasks.registering(JavaExec::class) { - group = "verification" - description = "Writes truthful BEX 2.0 test, coverage, identity, and artifact evidence." - dependsOn( - tasks.testClasses, - mainJar, - sourcesJarTask, - verifyDeterministicArchives, - binaryApiCheck, - java8BytecodeCheck, - benchmarkCompilationCheck, - writeDependencyResolutionEvidence - ) - classpath = sourceSets.test.get().runtimeClasspath - mainClass.set("blue.bex.conformance.BexConformanceReportMain") - javaLauncher.set( - javaToolchains.launcherFor { - languageVersion.set(JavaLanguageVersion.of(8)) - } - ) - doFirst { - args( - project.layout.projectDirectory.asFile.absolutePath, - project.layout.buildDirectory.get().asFile.absolutePath, - gradle.gradleVersion, - project.version.toString(), - blueLanguageDependencyMode, - blueLanguageDeclaredCoordinate, - project.layout.projectDirectory - .dir(".gradle/bex-hosted-release") - .asFile - .absolutePath, - blueLanguageCompositePath - ?.let { file(it).canonicalPath } - .orEmpty() - ) - } - outputs.file( - layout.buildDirectory.file( - "reports/bex-conformance/report.json" - ) - ) - outputs.file( - layout.buildDirectory.file( - "reports/bex-conformance/report.md" - ) - ) - outputs.file( - layout.buildDirectory.file( - "reports/bex-conformance/release-readiness.properties" - ) - ) - outputs.upToDateWhen { false } -} -writeBexConformanceReport { - mustRunAfter(tasks.test) -} - -tasks.register("bexConformanceReport") { - group = "verification" - description = "Runs all tests and produces the machine-readable BEX 2.0 conformance report." - dependsOn(tasks.test, writeBexConformanceReport) -} - -val bexReleaseEvidence by tasks.registering { - group = "verification" - description = - "Runs tests, conformance, same-tree and independent-clean archive gates, binary API, Java 8 bytecode, benchmark compilation, and writes release evidence." - dependsOn(tasks.test, writeBexConformanceReport) - doLast { - val readiness = - layout.buildDirectory.file( - "reports/bex-conformance/release-readiness.properties" - ).get().asFile - check(readiness.isFile) { - "Hosted release readiness evidence was not generated" - } - val values = - readiness.readLines() - .filter { it.contains("=") } - .associate { - val separator = it.indexOf('=') - it.substring(0, separator) to - it.substring(separator + 1) - } - check(values["releaseReady"] == "true") { - "Hosted release evidence is incomplete: " + - (values["reason"] - ?: "see build/reports/bex-conformance/report.md") - } - } -} - -val bexWorkingVerificationReport = - layout.buildDirectory.file( - "reports/latest-language-migration/final.json" - ) -val publicApiClassificationLedger = - layout.projectDirectory.file("docs/public-api-classification.json") -val writeProvisionalBexWorkingReceipt = { - blueLanguageFocusedResolutionEvidence.get().asFile.delete() - blueLanguageAggregateCompatibilityEvidence.get().asFile.delete() - latestLanguageMigrationBaselineEvidence.get().asFile.delete() - val outputFile = bexWorkingVerificationReport.get().asFile - outputFile.parentFile.mkdirs() - outputFile.writeText( - JsonOutput.prettyPrint( - JsonOutput.toJson( - linkedMapOf( - "schema" to - "blue-bex-working-verification/2.0", - "status" to "in-progress-or-failed", - "workingReady" to false, - "workingFailures" to - listOf("verification-did-not-complete"), - "recommendedCommand" to - "./gradlew bexWorkingVerification " + - "-PblueLanguageCompositePath=" + - (blueLanguageCompositePath ?: ""), - "recommendedCommandExecuted" to false - ) - ) - ) + "\n" - ) -} -gradle.taskGraph.whenReady { - val workingReportRequested = - allTasks.any { - it.path == ":bexWorkingVerification" || - it.path == ":writeBexWorkingVerificationReport" - } - if (workingReportRequested && !gradle.startParameter.isDryRun) { - writeProvisionalBexWorkingReceipt() - } -} -val initializeBexWorkingVerificationReceipt by tasks.registering { - group = "verification" - description = - "Invalidates migration evidence and writes a provisional red " + - "receipt before compilation or dependency resolution starts." - outputs.upToDateWhen { false } - doLast { - writeProvisionalBexWorkingReceipt() - } -} -val writeBexWorkingVerificationReport by tasks.registering { - group = "verification" - description = - "Writes the publication-independent BEX working-verification " + - "report for the exact local modular Language checkout." - dependsOn( - initializeBexWorkingVerificationReceipt, - tasks.test, - writeBexConformanceReport, - sourceReleaseArchive, - verifyBlueLanguageAggregateCompatibility, - writeFocusedLanguageResolutionEvidence, - writeLatestLanguageMigrationBaseline - ) - val conformanceReport = - layout.buildDirectory.file( - "reports/bex-conformance/report.json" - ) - inputs.file(conformanceReport) - inputs.file(blueLanguageAggregateCompatibilityEvidence) - inputs.file(blueLanguageFocusedResolutionEvidence) - inputs.file(latestLanguageMigrationBaselineEvidence) - inputs.file(latestLanguageMigrationLock) - inputs.file(publicApiClassificationLedger) - inputs.property("dependency.mode", blueLanguageDependencyMode) - inputs.property( - "focused.coordinates", - blueLanguageFocusedCoordinates.joinToString(",") - ) - outputs.file(bexWorkingVerificationReport) - outputs.upToDateWhen { false } - doLast { - val failures = mutableListOf() - fun requireWorking(value: Boolean, failure: String) { - if (!value) failures.add(failure) - } - fun mapValue(value: Any?): Map<*, *> = - value as? Map<*, *> ?: emptyMap() - fun child(parent: Map<*, *>, name: String): Map<*, *> = - mapValue(parent[name]) - fun intValue(parent: Map<*, *>, name: String): Int = - (parent[name] as? Number)?.toInt() ?: -1 - fun passed(parent: Map<*, *>, name: String): Boolean = - child(parent, name)["status"] == "passed" - - val conformanceFile = conformanceReport.get().asFile - check(conformanceFile.isFile) { - "Conformance report is missing: $conformanceFile" - } - @Suppress("UNCHECKED_CAST") - val report = - JsonSlurper().parse(conformanceFile) - as Map - @Suppress("UNCHECKED_CAST") - val focusedResolution = - JsonSlurper().parse( - blueLanguageFocusedResolutionEvidence.get().asFile - ) as Map - @Suppress("UNCHECKED_CAST") - val migrationBaseline = - JsonSlurper().parse( - latestLanguageMigrationBaselineEvidence.get().asFile - ) as Map - @Suppress("UNCHECKED_CAST") - val publicApiClassification = - JsonSlurper().parse(publicApiClassificationLedger.asFile) - as Map - val baselineLanguage = child(migrationBaseline, "language") - val baselineBex = child(migrationBaseline, "bex") - val languageCzToml = child(baselineLanguage, "czToml") - val bexCzToml = child(baselineBex, "czToml") - requireWorking( - migrationBaseline["status"] == "passed", - "migration-baseline-lock-validation-not-passing" - ) - requireWorking( - baselineLanguage["codeEquivalent"] == true, - "language-not-code-equivalent-to-verified-implementation" - ) - requireWorking( - languageCzToml["matches"] == true, - "language-cz-toml-differs-from-lock" - ) - requireWorking( - bexCzToml["matches"] == true, - "bex-cz-toml-differs-from-lock" - ) - val apiInventory = - child(publicApiClassification, "inventory") - val apiClassifications = - child(publicApiClassification, "classifications") - val classifiedApiTypes = - apiClassifications.values.flatMap { value -> - (value as? List<*>)?.map(Any?::toString) - ?: emptyList() - } - val requiredApiInventory = - file(apiInventory["path"].toString()) - val requiredApiTypes = - if (requiredApiInventory.isFile) { - requiredApiInventory.readLines().mapNotNull { line -> - if (line.startsWith("class public ")) { - line.substringBefore(" extends ") - .substringBefore(" implements ") - .removePrefix("class public ") - .trim() - .substringAfterLast(' ') - } else { - null - } - }.toSet() - } else { - emptySet() - } - requireWorking( - publicApiClassification["schema"] == - "blue-bex-public-api-classification/1.0" && - requiredApiInventory.isFile && - sha256(requiredApiInventory) == - apiInventory["sha256"] && - classifiedApiTypes.size == - (apiInventory["publicTypeCount"] as? Number) - ?.toInt() && - classifiedApiTypes.toSet().size == - classifiedApiTypes.size && - classifiedApiTypes.toSet() == requiredApiTypes, - "public-api-classification-ledger-not-current" - ) - val productionClasspaths = - child(focusedResolution, "productionClasspaths") - requireWorking( - focusedResolution["status"] == "passed" && - focusedResolution["mode"] == "local-composite" && - focusedResolution["graphSha256"] - ?.toString() - ?.matches(Regex("[0-9a-f]{64}")) == true && - child(productionClasspaths, "compile") - ["aggregatePresent"] == false && - child(productionClasspaths, "runtime") - ["aggregatePresent"] == false, - "focused-language-resolution-or-production-classpath-gate-not-passing" - ) - val totals = child(report, "finalTotals") - val tests = child(totals, "tests") - val behavior = child(totals, "behaviorFixtures") - val gas = child(totals, "gasMicrofixtures") - val vectors = child(totals, "normativeVectors") - val operators = child(totals, "operators") - - val testsExecuted = intValue(tests, "executed") - val testsPassed = intValue(tests, "passed") - val testsFailed = intValue(tests, "failed") - val testsSkipped = intValue(tests, "skipped") - val testsUnclassified = - if ( - testsExecuted >= 0 && testsPassed >= 0 && - testsFailed >= 0 && testsSkipped >= 0 - ) { - testsExecuted - testsPassed - - testsFailed - testsSkipped - } else { - -1 - } - requireWorking( - testsExecuted > 0 && testsFailed == 0 && - testsSkipped == 0 && testsUnclassified == 0 && - tests["zeroFailures"] == true && - tests["zeroSkips"] == true, - "ordinary-tests-not-passing-with-zero-skips-and-zero-unclassified" - ) - requireWorking( - intValue(behavior, "required") == 105 && - intValue(behavior, "executedAndPassing") == 105, - "behavior-fixtures-not-105-of-105" - ) - requireWorking( - intValue(gas, "required") == 30 && - intValue(gas, "executedAndPassing") == 30, - "gas-microfixtures-not-30-of-30" - ) - requireWorking( - intValue(vectors, "required") == 60 && - intValue(vectors, "executedAndPassing") == 60 && - vectors["allPassing"] == true, - "normative-vectors-not-60-of-60" - ) - requireWorking( - intValue(operators, "required") == 86 && - intValue(operators, "executedAndPassing") == 86, - "operator-coverage-not-86-of-86" - ) - - val identities = child(report, "identities") - val exactIdentitiesPassed = - identities["bexRegistry"] == requiredBexRegistryIdentity && - identities["fixtureBindsRegistry"] == - requiredBexRegistryIdentity && - identities["gasManifest"] == - requiredBexGasManifestIdentity && - identities["fixtureBindsGas"] == - requiredBexGasManifestIdentity && - identities["fixturePackage"] == - requiredBexFixturePackageIdentity - requireWorking( - exactIdentitiesPassed, - "normative-registry-gas-or-fixture-identity-mismatch" - ) - - val releaseGates = child(report, "releaseGates") - requireWorking( - passed(releaseGates, "deterministicArchives"), - "bex-owned-reproducibility-check-not-passing" - ) - requireWorking( - passed(releaseGates, "binaryApi"), - "binary-source-api-report-not-passing" - ) - requireWorking( - passed(releaseGates, "java8Bytecode"), - "java8-bytecode-check-not-passing" - ) - requireWorking( - child(report, "semanticBoundaryInvocationEvidence") - ["status"] == "passed" && - child(report, "ledgerLifecycleEvidence") - ["status"] == "passed", - "hosted-contracts-boundary-evidence-not-passing" - ) - val semanticParityPassed = - child(report, "representationMatrixResult")["status"] == - "passed" && - child(report, "intrinsicEvidence")["status"] == - "passed" && - intValue(behavior, "required") == 105 && - intValue(behavior, "executedAndPassing") == 105 && - vectors["allPassing"] == true && - intValue(operators, "executedAndPassing") == 86 && - exactIdentitiesPassed - requireWorking( - semanticParityPassed, - "semantic-parity-evidence-not-passing" - ) - val counterCoverage = child(report, "counterCoverage") - val gasParityPassed = - counterCoverage["allMicrofixturesPassing"] == true && - counterCoverage["vocabularyComplete"] == true && - intValue(counterCoverage, "declaredCounterCount") == 30 && - intValue(counterCoverage, "executedMicrofixtureCount") == 30 && - intValue(counterCoverage, "passingMicrofixtureCount") == 30 && - child(report, "gasExhaustionEvidence")["status"] == - "passed" && - child(report, "finiteLoopEvidence")["status"] == - "passed" && - intValue(gas, "executedAndPassing") == 30 && - exactIdentitiesPassed - requireWorking( - gasParityPassed, - "gas-parity-evidence-not-passing" - ) - requireWorking( - child(child(report, "dependency"), "resolution") - ["status"] == "passed", - "aggregate-compatibility-resolution-report-not-passing" - ) - requireWorking( - (report["artifacts"] as? List<*>)?.size == 4, - "working-artifacts-not-all-present" - ) - - fun legacyImportCount(sourceRoot: File): Int = - if (!sourceRoot.isDirectory) { - 0 - } else { - sourceRoot.walkTopDown() - .filter { it.isFile && it.extension == "java" } - .sumOf { source -> - source.useLines { lines -> - lines.count { line -> - forbiddenLegacyImportPatterns.any { - pattern -> pattern.containsMatchIn(line) - } - } - } - } - } - val productionLegacyImports = - legacyImportCount(file("src/main/java")) - val testLegacyImports = - legacyImportCount(file("src/test/java")) - requireWorking( - productionLegacyImports == 0, - "production-legacy-language-imports-present" - ) - requireWorking( - testLegacyImports == 0, - "test-legacy-language-imports-present" - ) - - val compositeDirectory = - blueLanguageCompositePath - ?.let { file(it).canonicalFile } - requireWorking( - blueLanguageDependencyMode == "local-composite" && - compositeDirectory?.isDirectory == true, - "bex-working-verification-requires-blueLanguageCompositePath" - ) - val languageFingerprint = - compositeDirectory - ?.takeIf { it.isDirectory } - ?.let(::gitWorkspaceFingerprint) - requireWorking( - languageFingerprint != null && - !languageFingerprint.dirty, - "local-language-checkout-is-dirty-or-unavailable" - ) - - val aggregateEvidence = readEvidence( - blueLanguageAggregateCompatibilityEvidence - .get().asFile - ) - requireWorking( - aggregateEvidence["status"] == "passed" && - aggregateEvidence["mode"] == "local-composite", - "aggregate-language-compatibility-smoke-not-passing" - ) - - val workingReady = failures.isEmpty() - val output = LinkedHashMap(report) - output["schema"] = - "blue-bex-working-verification/2.0" - output["status"] = - if (workingReady) "passed" else "failed" - output["workingReady"] = workingReady - output["workingFailures"] = failures - val workingTests = linkedMapOf() - tests.forEach { (key, value) -> - workingTests[key.toString()] = value - } - workingTests["unclassified"] = testsUnclassified - workingTests["zeroUnclassified"] = testsUnclassified == 0 - val workingFinalTotals = linkedMapOf() - totals.forEach { (key, value) -> - workingFinalTotals[key.toString()] = value - } - workingFinalTotals["tests"] = workingTests - output["tests"] = workingTests - output["finalTotals"] = workingFinalTotals - val hostedStandaloneMatrix = - child(report, "hostedStandaloneMatrix") - val standalonePublished = - child( - hostedStandaloneMatrix, - "standalonePublished" - ) - val strictReleaseFailures = - ((report["currentModeFailures"] as? List<*>) - ?.map(Any?::toString) - ?: emptyList()).toMutableList() - if (hostedStandaloneMatrix["allRequiredModesPassed"] != true) { - strictReleaseFailures += - "published-local-mode-matrix-not-passing" - } - output["strictRelease"] = - linkedMapOf( - "releaseReady" to report["releaseReady"], - "failures" to strictReleaseFailures.distinct(), - "allRequiredModesPassed" to - hostedStandaloneMatrix["allRequiredModesPassed"], - "publishedModeStatus" to - (standalonePublished["status"] ?: "not-executed"), - "evidence" to hostedStandaloneMatrix - ) - output["publishedModeStatus"] = - standalonePublished["status"] ?: "not-executed" - output["recommendedCommand"] = - "./gradlew bexWorkingVerification " + - "-PblueLanguageCompositePath=" + - (compositeDirectory?.path ?: "") - output["recommendedCommandExecuted"] = - gradle.startParameter.taskNames.any { requestedTask -> - requestedTask.substringAfterLast(':') == - "bexWorkingVerification" - } - output["reportProducerTask"] = - ":writeBexWorkingVerificationReport" - output["migrationBaseline"] = migrationBaseline - output["languageCodeEquivalence"] = baselineLanguage - output["sourceApiInventory"] = - migrationBaseline["sourceApiInventory"] - output["publicApiClassification"] = - publicApiClassification - output["migrationLedger"] = - migrationBaseline["migrationLedger"] - output["focusedLanguageResolution"] = focusedResolution - output["semanticParity"] = - linkedMapOf( - "status" to - if (semanticParityPassed) "passed" else "failed", - "normativeVectors" to vectors, - "behaviorFixtures" to behavior, - "operators" to operators, - "identities" to report["identities"], - "representationMatrixResult" to - report["representationMatrixResult"], - "intrinsicEvidence" to report["intrinsicEvidence"] - ) - output["gasParity"] = - linkedMapOf( - "status" to - if (gasParityPassed) "passed" else "failed", - "scope" to - "same-run-local-composite-exact-gas-evidence", - "gasMicrofixtures" to gas, - "counterCoverage" to report["counterCoverage"], - "gasExhaustionEvidence" to - report["gasExhaustionEvidence"], - "finiteLoopEvidence" to report["finiteLoopEvidence"], - "ledgerLifecycleEvidence" to - report["ledgerLifecycleEvidence"] - ) - output["hostedBoundaryResults"] = - linkedMapOf( - "semanticBoundaryInvocationEvidence" to - report["semanticBoundaryInvocationEvidence"], - "ledgerLifecycleEvidence" to - report["ledgerLifecycleEvidence"], - "hostedLocalLimitCapability" to - report["hostedLocalLimitCapability"], - "cyclicProofUnavailabilityCapability" to - report["cyclicProofUnavailabilityCapability"], - "hostedOutcomes" to report["hostedOutcomes"] - ) - output["workingDependency"] = - linkedMapOf( - "focusedCoordinates" to - blueLanguageFocusedCoordinates, - "languageCommit" to - languageFingerprint?.commit, - "languageWorkspaceSha256" to - languageFingerprint?.workspaceSha256, - "focusedResolution" to focusedResolution, - "aggregateCompatibility" to - aggregateEvidence.toSortedMap(), - "aggregateCompatibilityOnly" to true, - "productionLegacyImports" to - productionLegacyImports, - "testLegacyImports" to testLegacyImports - ) - val outputFile = - bexWorkingVerificationReport.get().asFile - outputFile.parentFile.mkdirs() - outputFile.writeText( - JsonOutput.prettyPrint(JsonOutput.toJson(output)) + "\n" - ) - check(workingReady) { - "BEX working verification is not ready: " + - failures.joinToString("; ") + - ". See " + outputFile - } - } -} - -listOf( - tasks.test, - writeBexConformanceReport, - sourceReleaseArchive, - verifyBlueLanguageAggregateCompatibility, - writeFocusedLanguageResolutionEvidence, - writeLatestLanguageMigrationBaseline -).forEach { verificationTask -> - verificationTask.configure { - mustRunAfter(initializeBexWorkingVerificationReceipt) - } -} - -val bexWorkingVerification by tasks.registering { - group = "verification" - description = - "Runs the complete local-composite BEX working gate without " + - "requiring a published Language release." - dependsOn(writeBexWorkingVerificationReport) -} - -val bexReleaseVerify by tasks.registering { - group = "verification" - description = - "Runs the strict published/local release matrix and fails closed " + - "when compatible published Language evidence is unavailable." - dependsOn(bexReleaseEvidence) -} - -tasks.check { - dependsOn( - verifyDeterministicArchives, - binaryApiCheck, - java8BytecodeCheck, - benchmarkCompilationCheck - ) -} - -val genResourcesDir = layout.buildDirectory.dir("generated-resources") -val generateBuildProperties by tasks.registering { - val buildPropertiesFile = genResourcesDir.map { it.file("blue/bex/build.properties") } - val sourceDateEpoch = - System.getenv("SOURCE_DATE_EPOCH")?.toLongOrNull() ?: 0L - val reproducibleBuildTimestamp = - Instant.ofEpochSecond(sourceDateEpoch).toString() - inputs.property("buildTimestamp", reproducibleBuildTimestamp) - outputs.file(buildPropertiesFile) - doLast { - val file = buildPropertiesFile.get().asFile - file.parentFile.mkdirs() - file.writeText( - """ - blue-bex-java.build.version=${project.version} - blue-bex-java.build.timestamp=$reproducibleBuildTimestamp - """.trimIndent() - ) - } -} - -sourceSets.main { - output.dir(genResourcesDir, "builtBy" to generateBuildProperties) -} - -tasks.withType().configureEach { - enabled = false -} - -publishing { - publications { - create("maven") { - groupId = "blue.bex" - artifactId = "blue-bex-java" - from(components["java"]) - - pom { - name.set("Blue BEX Java") - description.set("Compiled Java engine for Blue Expression Objects.") - url.set("https://timeline.blue") - licenses { - license { - name.set("MIT License") - url.set("https://github.com/bluecontract/blue-bex-java/blob/main/LICENSE") - } - } - developers { - developer { - name.set("Blue") - email.set("devsupport@timeline.blue") - } - } - scm { - url.set("https://github.com/bluecontract/blue-bex-java.git") - connection.set("scm:git:git@github.com:bluecontract/blue-bex-java.git") - developerConnection.set("scm:git:git@github.com:bluecontract/blue-bex-java.git") - } - } - } - } - - repositories { - maven { - url = layout.buildDirectory.dir("staging-deploy").get().asFile.toURI() - } - if (System.getenv("CI") == null) { - maven { - name = "local" - url = uri("file:///" + File(System.getProperty("user.home"), ".m2/repository").absolutePath) - } - } - } -} - -tasks.withType< - org.gradle.api.publish.maven.tasks.PublishToMavenRepository ->().configureEach { - dependsOn(bexReleaseVerify) -} -tasks.withType< - org.gradle.api.publish.maven.tasks.PublishToMavenLocal ->().configureEach { - dependsOn(bexReleaseVerify) -} tasks.matching { it.name in setOf( "jreleaserAnnounce", @@ -3987,7 +21,7 @@ tasks.matching { "jreleaserUpload" ) }.configureEach { - dependsOn(bexReleaseVerify) + dependsOn("bexReleaseVerify") } if (System.getenv("CI") != null) { @@ -3998,7 +32,8 @@ if (System.getenv("CI") != null) { } project { description.set("Compiled Java engine for Blue Expression Objects.") - copyright.set("Copyright 2026 Blue Company. Licensed under the MIT License") + copyright.set( + "Copyright 2026 Blue Company. Licensed under the MIT License") } deploy { maven { @@ -4016,43 +51,11 @@ if (System.getenv("CI") != null) { } } -fun determineProjectVersion(): String { - val tomlFile = file(".cz.toml") - val baseVersion = if (tomlFile.exists()) { - Regex("""version\s*=\s*"([^"]+)"""") - .find(tomlFile.readText()) - ?.groupValues - ?.get(1) - ?: "1.0.0" - } else { - "1.0.0" - } - return baseVersion + if (System.getenv("CI") == null) "-SNAPSHOT" else "" -} - -fun sourceReleaseExecutableModes( - archive: File, - rootDirectory: String -): Map { - val executablePaths = - listOf( - "gradlew", - ".github/scripts/run-final-publication-gates.sh" - ) - CommonsZipFile.builder().setFile(archive).get().use { zip -> - return executablePaths.associateWith { relativePath -> - val entry = - zip.getEntry("$rootDirectory/$relativePath") - ?: throw GradleException( - "Source release is missing executable $relativePath" - ) - val permissionBits = entry.unixMode and 0x1ff - check(permissionBits == 0x1ed) { - "Source-release executable $relativePath has mode " + - permissionBits.toString(8) + - ", expected 755" - } - permissionBits.toString(8).padStart(4, '0') - } - } +fun configuredVersion(): String { + val configured = Regex("""version\s*=\s*"([^"]+)"""") + .find(file(".cz.toml").readText()) + ?.groupValues + ?.get(1) + ?: "1.0.0" + return configured + if (System.getenv("CI") == null) "-SNAPSHOT" else "" } diff --git a/docs/BEX_CONFORMANCE.md b/docs/BEX_CONFORMANCE.md index 1035e2e..b89d4ff 100644 --- a/docs/BEX_CONFORMANCE.md +++ b/docs/BEX_CONFORMANCE.md @@ -1,158 +1,102 @@ -# Blue BEX 2.0 conformance +# Blue BEX 2.0 conformance evidence -The executable BEX 2.0 release package is copied unchanged under: +The executable BEX 2.0 package remains source controlled under +`src/test/resources/conformance/bex`. The conformance module consumes it but +does not package fixture implementation into the minimal runtime JAR. -```text -src/test/resources/conformance/bex/ -``` +## Normative inventory -`BexConformancePackageIntegrityTest` verifies the closed fixture schema, the -complete 147-file inventory, every LF-normalized byte length and SHA-256 -digest, all three package identities, the 60-vector reverse map, direct -coverage for all 86 operators, all 30 gas counters, and the exact runtime -registry files and BlueIds. - -`BexConformanceFixtureTest` executes all 105 manifest-declared behavior -fixtures. Explicit variants and complete `expected.cases` run independently. -The harness has no disabled-test, assumption, or skip path. Fixture -`additionalCase` metadata is not executable, as required by `HARNESS.md`. - -`BexGasMicrofixtureTest` executes all 30 named-counter microfixtures directly -against the public meter. Each test verifies the namespace, counter, sequence, -quantity, manifest weight, subtotal, reason, and trace-derived total. - -The JSON and Markdown reports also publish concrete exhaustion traces from -`BexPrimitiveExhaustionEvidenceTest`. Each example includes the exact -namespace, rejected counter, quantity, weight, admitted gas, effective budget, -rejected-charge absence, and zero later work. The numeric expectations are -source-controlled in -`src/test/resources/hosted-release/gas-exhaustion-trace-examples.properties`; -an example is marked passing only when its exact dynamic JUnit selector ran -and passed. - -The published baseline reconciliations are explicit: - -- the manifest count of 60 vectors and 105 behavior fixtures is authoritative; -- BEX-S-07 is a runtime uninitialized-binding failure; -- BEX-C-09 rejects recursion with `recursive-call-graph` before runtime; -- BEX-E-14's `result.identityB` expected value is a projection reference; -- `$findEntry` requires the canonical `index` in addition to the fixture's - `key`/`val` subset; -- BEX-G-09's `canonical-merge-sort` value is algorithm evidence backed by the - exact comparison trace. - -Run the complete test and evidence workflow with: +`BexConformancePackageIntegrityTest` verifies the closed package inventory, +LF-normalized byte lengths and SHA-256 values, reverse vector coverage, direct +coverage for every operator, and all gas counters. The executable totals are: ```text -./gradlew bexConformanceReport \ - -PblueLanguageCompositePath=../blue-language-java +60 normative vectors +105 behavior fixtures +30 gas microfixtures +86 operators ``` -The composite path is explicit. Omitting it selects -`standalone-published`, which resolves the declared Blue Language coordinate -from Maven Central only. Dependency resolution never consults `mavenLocal`; -the resolved standalone JAR must match the coordinate, repository provenance, -and SHA-256 recorded in -`src/test/resources/hosted-release/published-api-inspection.properties`. +`BexConformanceFixtureTest` executes every manifest-declared fixture and case. +`BexGasMicrofixtureTest` checks namespace, counter, sequence, quantity, weight, +subtotal, reason, and trace-derived total. There is no assumption, disabled, or +skip path in the harness. + +The report also derives representation, provider/cyclic evidence, semantic +identity admission, hosted ledger lifecycle, intrinsic dispatch, and gas +exhaustion from exact executed JUnit selectors. A passing inventory check is +never converted into an invented execution count. -The test task always finalizes by writing: +## Run locally + +Use the explicit verified Language composite: + +```bash +./gradlew --no-daemon clean bexWorkingVerification \ + -PblueLanguageCompositePath=/absolute/path/to/blue-language-java +``` + +The working gate creates: ```text -build/reports/bex-conformance/report.json -build/reports/bex-conformance/report.md +blue-bex-conformance/build/reports/bex-conformance/report.json +blue-bex-conformance/build/reports/bex-conformance/report.md +blue-bex-conformance/build/reports/bex-release/public-api.txt +blue-bex-conformance/build/reports/bex-release/public-api-classification.json +build/reports/latest-language-migration/baseline.json +build/reports/latest-language-migration/final.json +build/reports/bex-modernization/architecture.json ``` -The report is deterministic for a fixed source state, test result set, and -artifacts. It reports the current commit and dirty-worktree flag, Java and -Gradle versions, fixture/registry/gas identities, actual JUnit XML counts, -operator and counter matrices, cache and representation matrices, recursion -and finite-loop evidence, and SHA-256 hashes only for current-version -artifacts. Failed, skipped, or unexecuted evidence remains visibly so; a -declaration is never reported as an execution. - -Normative-vector passing totals are derived from the status of every mapped -behavior or gas fixture in `vector-coverage.yaml`. The report does not turn a -passing inventory-integrity test into a hardcoded `60/60` execution claim. - -`verifyDeterministicArchives` is an archive-packaging determinism gate. It -repackages the same compiled main output and source inputs and independently -regenerates Javadoc content before byte comparison. It does not claim a -second clean compilation. - -`writeCleanBuildArtifactHashes` is intentionally stricter: it runs only from a -completely clean checkout and records the commit, dependency mode, version, -and main, sources, Javadoc, and source-release hashes. Run it in two clean -checkouts of the same commit with the non-snapshot CI version and one source -epoch. Every `GRADLE_USER_HOME` below must be a distinct fresh empty directory. -Then compare the two property files with: +The final working report requires zero failed, skipped, and unclassified tests; +exact 60/105/30/86 execution totals; all critical semantic sections; hosted +boundary selectors; Java 8 classfiles; exact reviewed API descriptors; all +module and source artifacts; and byte-identical BEX-owned archive replicas. -```bash -export CI=true -export SOURCE_DATE_EPOCH="$(git show -s --format=%ct HEAD)" +Run the longer modernization gate separately: -(cd /first/clean/blue-bex-java && \ - GRADLE_USER_HOME=/tmp/blue-bex-gradle-one \ - ./gradlew --no-daemon clean test writeCleanBuildArtifactHashes) +```bash +./gradlew --no-daemon bexModernizationVerification \ + -PblueLanguageCompositePath=/absolute/path/to/blue-language-java +``` -(cd /second/clean/blue-bex-java && \ - GRADLE_USER_HOME=/tmp/blue-bex-gradle-two \ - ./gradlew --no-daemon clean test writeCleanBuildArtifactHashes) +It adds architecture/source metrics, concurrency and property tests, fourteen +developer guides, and the serious two-fork JMH campaign with GC allocation +profiling. Its reports are: -./gradlew verifyIndependentCleanBuildReproducibility \ - -PcleanBuildEvidenceOne=/first/clean/blue-bex-java/build/reports/bex-release/clean-build-artifacts.properties \ - -PcleanBuildEvidenceTwo=/second/clean/blue-bex-java/build/reports/bex-release/clean-build-artifacts.properties +```text +build/reports/bex-modernization/final.json +build/reports/bex-modernization/final.md +blue-bex-conformance/build/reports/jmh/results.json +blue-bex-conformance/build/reports/jmh/environment.json ``` -Both evidence producers must use the same dependency mode. The publication -pair uses standalone-published mode. To prove local-composite packaging -separately, run another two-clean-checkout pair in two additional BEX roots -with the same explicit -`-PblueLanguageCompositePath=/absolute/path/to/clean/blue-language-java` -argument on both builds. Keep all four roots until the final report has -re-hashed their outputs and receipt-owned Language JAR copies; never compare -one build from each mode. The local-composite receipt is accepted only when -its live source checkout is the exact published Language commit and carries -the recorded `v` tag. - -The combined evidence is commit-bound and stale evidence fails closed. The -conformance report also requires its own four artifacts to match the hashes -from both clean builds. After this comparison exists, record both modes and -make the final decision in the reporting checkout: +## Reproducibility claims -```bash -export CI=true -export SOURCE_DATE_EPOCH="$(git show -s --format=%ct HEAD)" - -GRADLE_USER_HOME=/tmp/blue-bex-standalone-mode \ - ./gradlew --no-daemon clean test \ - -PblueLanguageRequireFreshModuleCache=true -GRADLE_USER_HOME=/tmp/blue-bex-local-mode \ - ./gradlew --no-daemon clean test \ - -PblueLanguageCompositePath=/absolute/path/to/clean/blue-language-java -GRADLE_USER_HOME=/tmp/blue-bex-final-standalone \ - ./gradlew --no-daemon clean bexReleaseEvidence +`verifyReproducibleArchives` and +`verifySourceReleaseArchiveReproducibility` compare independently packaged +module/source/Javadoc/source-release archives from the same compiled inputs. +That is the BEX-owned working claim; it is not mislabeled as two clean builds. + +Public release additionally uses four isolated BEX checkouts and four isolated +Gradle homes: two standalone-published builds and two local-composite builds. +`.github/scripts/run-final-publication-gates.sh` records exact artifact +manifests for each pair and compares local versus published conformance fields +for semantic and exact-gas equality. + +## Fail-closed publication + +`bexPublishedLanguageVerification` authenticates resolved artifact bytes +against `published-api-inspection.properties`, requires every reviewed API +claim and a same-run local/published differential, and cannot pass from a CLI +coordinate/hash alone. `bexReleaseVerify` then requires modernization, both +independent clean-build pairs, clean exact-tagged BEX source, and writes: + +```text +build/reports/bex-release/final.json +build/reports/bex-release/final.md ``` -The release and RC workflows perform that complete sequence before any -publication command and archive `build/reports`, `build/distributions`, test -results, and persistent mode evidence. - -`binaryApiCheck` writes -`build/reports/bex-release/public-api.txt`, a deterministic -public/protected descriptor manifest of the packaged JAR, then fails closed -unless it exactly equals the source-controlled first-public BEX 2.0 baseline -in `src/test/resources/hosted-release/required-public-api.txt`. Missing, -changed, reordered, or unexpected public/protected signatures all fail. The -generated manifest hash is identity evidence; exact line equality is the API -compatibility claim. - -A module-specific cache acceptance is reported separately for the exact -`blue.language:blue-language-java:3.1.0-rc.19` Gradle module-version path. -Standalone acceptance passes only when that exact path was absent at project -configuration and the subsequently resolved JAR matches the recorded Maven -Central hash in the dedicated run that explicitly requires fresh-cache proof. -That authenticated mode receipt is reused by later publication invocations; -they do not overwrite it or require a populated cache to become absent again. -It does not claim that the entire Gradle cache was empty or that a network -fetch was directly observed. Local-composite runs remain `not-executed` for -this acceptance. +Unavailable or incompatible published Language modules remain visibly +`not-executed` or `incompatible`; they never make local-composite evidence red +and never become a public-release pass. diff --git a/docs/LATEST_LANGUAGE_API_MIGRATION.md b/docs/LATEST_LANGUAGE_API_MIGRATION.md index 1131041..747e699 100644 --- a/docs/LATEST_LANGUAGE_API_MIGRATION.md +++ b/docs/LATEST_LANGUAGE_API_MIGRATION.md @@ -10,12 +10,14 @@ It is the human-readable companion to | Input | Exact state | |---|---| | BEX baseline | `395c484111f8c4e9e0e98d2db7f1c5b0777bd5a8` | -| BEX migration | Uncommitted working-tree delta rooted at the exact baseline above | +| Working compatibility checkpoint | `169e589` | +| BEX migration | Modernization delta rooted at the working checkpoint; the commit containing this ledger is the final target revision | | Language target | `9a607e584ff5dd973684d35d71eb4022d946b760` | | Language verified implementation | `63a9ed6a1a66d47119a80d16ed2ab0beda0d2453` | | Language target delta | `LICENSE`, one migration report, and one modernization report only | | Previous API manifest SHA-256 | `830caa187023079ba53fa76d2932e6e12cb8c93be3f90ac887ad374d6642b315` | -| Migrated API manifest SHA-256 | `43aea6ae9de6f39729f93c146ff5453887da9a2303f6f4f33573ba47f37d5be0` | +| Working-checkpoint API manifest SHA-256 | `43aea6ae9de6f39729f93c146ff5453887da9a2303f6f4f33573ba47f37d5be0` | +| Final modular API manifest SHA-256 | `df602fa6b14afc285053fc8e34e349d7fa5ce26810a6c79ba14ac6361de463ee` | The migration target cannot truthfully name its eventual BEX commit while that commit is being assembled. The Git commit containing this ledger is the target @@ -24,8 +26,14 @@ entry was audited. ## Exact descriptor changes -There are six removals and eleven additions. No other generated production API -descriptor changed. +The subsequent modular modernization contains 254 removed and 483 added exact +owner-qualified descriptors relative to commit `169e589`. Both compared manifests, both +classifications, and the complete sorted addition/removal sets are +source-controlled under `gradle/verification/api/`. The `binaryApiCheck` task +recomputes the set difference, compares every line, authenticates every file +hash recorded in the JSON ledger, and compares the final classification with +same-run generation. The tables below are reviewed highlights; they are not +presented as the exhaustive machine delta. | Change | Classification | Exact signature | Replacement or purpose | Compatibility impact | |---|---|---|---|---| @@ -47,19 +55,87 @@ descriptor changed. | Removed | host SPI | `method public static referenceBacked(blue.bex.value.BexValue,blue.language.Blue):blue.bex.value.BexValue` | Replaced by the overload using the modular graph-capable runtime. | Binary and source breaking for direct host callers. | | Added | host SPI | `method public static referenceBacked(blue.bex.value.BexValue,blue.language.runtime.BlueLanguage):blue.bex.value.BexValue` | Verified, demand-driven reference materialization through `BlueLanguage`. | Additive alone; migration target for host integrations. | +The compiler-package acyclicity pass contributes these reviewed descriptors: + +| Change | Classification | Exact signature | Replacement or purpose | Compatibility impact | +|---|---|---|---|---| +| Removed | internal implementation | `class public final blue.bex.compile.BexCompiler` and its three public members | Compilation is now reached only through `BexEngine`; the package-private compiler and internal bridge bind the resulting opaque program to the complete compile environment. | Intentional pre-release removal of an implementation type that allowed callers to construct unbound executable IR. | +| Removed | stable API | `constructor public (blue.bex.api.BexProgramSource$Kind,java.lang.String,java.lang.String,java.lang.String)` on `blue.bex.compile.BexCompiledProgramKey` | Replaced by the compile-owned kind. | Binary and direct-constructor source breaking. | +| Added | stable API | `constructor public (blue.bex.compile.BexCompilationInput$Kind,java.lang.String,java.lang.String,java.lang.String)` on `blue.bex.compile.BexCompiledProgramKey` | Compile-owned cache-key kind. | Migration target for direct constructor callers. | +| Removed | stable API | `constructor public (blue.bex.api.BexProgramSource$Kind,java.lang.String,java.lang.String,java.lang.String,java.lang.String)` on `blue.bex.compile.BexCompiledProgramKey` | Replaced by the compile-owned kind. | Binary and direct-constructor source breaking. | +| Added | stable API | `constructor public (blue.bex.compile.BexCompilationInput$Kind,java.lang.String,java.lang.String,java.lang.String,java.lang.String)` on `blue.bex.compile.BexCompiledProgramKey` | Compile-owned environment-aware cache-key kind. | Migration target for direct constructor callers. | +| Removed | stable API | `method public kind():blue.bex.api.BexProgramSource$Kind` on `blue.bex.compile.BexCompiledProgramKey` | Replaced by the compile-owned kind result. | Binary and typed-read source breaking. | +| Added | stable API | `method public kind():blue.bex.compile.BexCompilationInput$Kind` on `blue.bex.compile.BexCompiledProgramKey` | Compile-owned kind result. | Use `BexCompilationInput.Kind` for typed reads. | +| Removed | stable API | `method public static from(blue.bex.api.BexProgramSource):blue.bex.compile.BexCompiledProgramKey` | Replaced by the compile-owned input factory. | Binary breaking; source compatible on recompilation. | +| Added | stable API | `method public static from(blue.bex.compile.BexCompilationInput):blue.bex.compile.BexCompiledProgramKey` | Compile-owned cache-key input. | Existing `BexProgramSource` calls remain source compatible. | +| Removed | stable API | `method public static from(blue.bex.api.BexProgramSource,java.lang.String):blue.bex.compile.BexCompiledProgramKey` | Replaced by the compile-owned input factory. | Binary breaking; source compatible on recompilation. | +| Added | stable API | `method public static from(blue.bex.compile.BexCompilationInput,java.lang.String):blue.bex.compile.BexCompiledProgramKey` | Compile-owned environment-aware cache-key input. | Existing `BexProgramSource` calls remain source compatible. | +| Added | stable API | `class public abstract interface blue.bex.compile.BexCompilationInput` | Compile-owned immutable source view. | Compatible additive type. | +| Added | stable API | `method public abstract isExpression():boolean` on `blue.bex.compile.BexCompilationInput` | Source-shape discriminator. | Member of a new type; already implemented by `BexProgramSource`. | +| Added | stable API | `method public abstract programNode():blue.language.snapshot.FrozenNode` on `blue.bex.compile.BexCompilationInput` | Selected frozen program root. | Member of a new type; already implemented by `BexProgramSource`. | +| Added | stable API | `method public abstract definitionNode():java.util.Optional` on `blue.bex.compile.BexCompilationInput` | Optional frozen definition root. | Member of a new type; already implemented by `BexProgramSource`. | +| Added | stable API | `method public abstract entry():java.util.Optional` on `blue.bex.compile.BexCompilationInput` | Optional selected entry. | Member of a new type; already implemented by `BexProgramSource`. | +| Added | stable API | `class public static final blue.bex.compile.BexCompilationInput$Kind extends java.lang.Enum` | Compile-owned cache source kind. | Compatible additive type. | +| Added | stable API | `field public static final FULL_PROGRAM:blue.bex.compile.BexCompilationInput$Kind` | Full-program kind. | Member of a new type. | +| Added | stable API | `field public static final EXPRESSION:blue.bex.compile.BexCompilationInput$Kind` | Expression kind. | Member of a new type. | +| Added | stable API | `method public static valueOf(java.lang.String):blue.bex.compile.BexCompilationInput$Kind` | Compiler-generated enum lookup. | Member of a new type. | +| Added | stable API | `method public static values():blue.bex.compile.BexCompilationInput$Kind[]` | Compiler-generated enum values. | Member of a new type. | +| Added | intrinsic SPI | `class public abstract interface blue.bex.compile.BexIntrinsicCatalog` | Compile-time intrinsic-membership view. | Compatible additive type. | +| Added | intrinsic SPI | `method public abstract supports(java.lang.String):boolean` on `blue.bex.compile.BexIntrinsicCatalog` | Exact BlueId membership. | Member of a new type; already implemented by `BexIntrinsicRegistry`. | +| Removed | stable API | `class public final blue.bex.api.BexProgramSource` | Declaration now records the compile-input role. | Compatible interface addition. | +| Added | stable API | `class public final blue.bex.api.BexProgramSource implements blue.bex.compile.BexCompilationInput` | Immutable host-to-compiler adapter. | Binary and source compatible interface addition. | +| Removed | intrinsic SPI | `class public final blue.bex.api.BexIntrinsicRegistry` | Declaration now records the compile-catalog role. | Compatible interface addition. | +| Added | intrinsic SPI | `class public final blue.bex.api.BexIntrinsicRegistry implements blue.bex.compile.BexIntrinsicCatalog` | Immutable runtime registry adapting to compilation. | Binary and source compatible interface addition. | + The exact string-BlueId intrinsic methods remain authoritative. The retained class convenience does not infer identity from a class name: it uses either an explicit `BexTypeBlueIdResolver` or the focused annotated-type mapping boundary. +## Modular host and IR package moves + +The following pre-release moves are intentional and have no deprecated +compatibility aliases: + +| Previous type | Final type | Reason | +|---|---|---| +| `blue.bex.api.ProcessorExecutionContextBexDocumentView` | `blue.bex.contracts.ProcessorExecutionContextBexDocumentView` | Contracts-host adapters no longer live in the pure API package. | +| `blue.bex.api.ProcessorExecutionContextBexGasLedgerHost` | `blue.bex.contracts.ProcessorExecutionContextBexGasLedgerHost` | Processor gas translation is Contracts-owned. | +| `blue.bex.output.ProcessorExecutionContextBexSemanticIdentityBoundary` | `blue.bex.contracts.ProcessorExecutionContextBexSemanticIdentityBoundary` | Hosted semantic-output admission is isolated from pure output code. | +| `blue.bex.runtime.CompileScope` | `blue.bex.compile.CompileScope` | Compiler scope moved with the immutable IR. | +| `blue.bex.runtime.CompiledExpression` | `blue.bex.compile.CompiledExpression` | Compiled expressions are compiler-owned IR. | +| `blue.bex.runtime.CompiledFrame` | `blue.bex.compile.CompiledFrame` | The frame uses the narrow `BexExecutionMachine` port. | +| `blue.bex.runtime.CompiledStatement` | `blue.bex.compile.CompiledStatement` | Compiled statements are compiler-owned IR. | +| `blue.bex.runtime.Control` | `blue.bex.compile.Control` | IR control flow moved with compiled statements. | + +`BexCompiledProgram` now executes through the compile-owned +`BexExecutionMachine` interface. `BexRuntime` implements that interface and +uses `BexRuntimeContext` and `BexRuntimeIntrinsics`, which removes the final +compile/runtime package cycle without changing execution or gas semantics. + +The final opacity and metrics pass removes public construction of executable +IR, binds every compiler-produced program to its exact compiler, Language, +gas-manifest, runtime-registry, and intrinsic-registry identity, and rejects a +program at execution when that identity differs. Runtime execution crosses an +internal `BexCompiledProgramRuntimeAccess` bridge; the stable program handle no +longer exposes `CompiledExpression`, `CompiledStatement`, or +`BexExecutionMachine` in its public methods. + +`BexMetrics` is now an immutable compatibility view with no mutation methods. +The supported sink receives `BexMetricsSnapshot`; invocation-owned mutation is +confined to the API-classified internal `BexMetricsRecorder`. Sink failures are +isolated because diagnostics cannot alter compile/cache/execution/gas success. + ## Public API classification and deterministic inventory [`public-api-classification.json`](public-api-classification.json) classifies -all 72 public production types as stable API, host SPI, intrinsic SPI, internal -implementation, or conformance-only. The exact 798 class/member descriptors are +all 101 public production types as stable API, host SPI, intrinsic SPI, internal +implementation, or conformance-only. The exact 1,028 class/member descriptors are source-controlled in `src/test/resources/hosted-release/required-public-api.txt`; that file is the machine-comparable inventory, while the JSON file supplies intent metadata. At this audited state the required inventory is byte-for-byte identical to -`build/reports/bex-release/public-api.txt`. Build wiring should continue to -generate the latter from compiled classes and fail on any diff from the former. +`blue-bex-conformance/build/reports/bex-release/public-api.txt`, and both have +SHA-256 `df602fa6b14afc285053fc8e34e349d7fa5ce26810a6c79ba14ac6361de463ee`. +Build wiring generates the latter from compiled classes and fails on any diff +from the reviewed source-controlled baseline. diff --git a/docs/adding-an-operator.md b/docs/adding-an-operator.md new file mode 100644 index 0000000..494a310 --- /dev/null +++ b/docs/adding-an-operator.md @@ -0,0 +1,62 @@ +# Adding a portable operator + +Portable operators are part of the BEX language, not application plugins. Add +one only as an intentional versioned language change. For application-specific +host work, use an [intrinsic](intrinsics.md). + +This repository's current modernization preserves the existing 86 operators and +does not authorize new semantics. + +## Required change set + +A future operator change is incomplete unless one review covers all of these: + +1. **Specification** — define source shape, operands, evaluation order, + laziness, value kinds, failures, pointer behavior, output behavior, and worked + examples in the BEX specification. +2. **Catalog** — add one machine-readable/operator-descriptor entry with its + canonical name, expression-or-statement kind, family, and documentation link. +3. **Compiler and IR** — validate static operands, compile dynamic operands, + retain source diagnostics, and represent the instruction immutably. +4. **Runtime** — implement deterministic semantics without importing compiler + implementation details back into lower layers. +5. **Gas** — use the existing closed counter vocabulary when it exactly models + the work. If it cannot, treat a vocabulary/weight change as a versioned gas + manifest identity change; never hide work in estimates or `gasConsumed`. +6. **Normative fixtures** — add success, edge, lazy/evaluation-order, failure, + identity/representation, and exact gas cases as appropriate. +7. **Coverage** — update operator and vector coverage so no catalog entry is + unclassified or unexecuted. +8. **Documentation** — update the relevant concept guide and operator reference. +9. **API review** — classify any new public/protected signature and update the + exact migration ledger/baseline intentionally. +10. **Evidence** — run focused tests, then the complete ordinary/conformance, + Java 8, archive, API, dependency, and reproducibility gates required for the + release mode. + +## Semantic review questions + +- Which operands are static, eager, lazy, or conditionally evaluated? +- What is the exact left-to-right/canonical traversal order? +- Which reads/constructions/comparisons occur, and which counters precede them? +- What happens for undefined, null, exact references, transient values, and + provider evidence failure? +- Does the operator preserve representation blindness? +- Does it cross the strict Blue output boundary, and exactly once? +- Can exhaustion happen before each unit of work with the rejected charge absent? +- Are patch/event order and failure atomicity preserved? +- Does the operator accidentally add host authority or Contracts semantics? + +## Things not to add as BEX operators + +Timeline, Mandate, Coordination, `Process Embedded`, feeder, persistence, +collection activation, and Contracts `collectionPaths` are orchestration or host +semantics. BEX may compute an ordinary value or patch that a host later uses; +that does not move those decisions into the expression language. + +## Commit discipline + +Keep the catalog/specification/implementation/fixture/gas changes reviewable as +one semantic unit. Do not make fixtures pass by changing expected behavior after +the implementation. Exact result and gas-trace parity must remain visible in the +machine-readable report, and skipped/unclassified cases fail closed. diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..c5ef423 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,91 @@ +# Architecture + +The design separates the portable BEX runtime from Contracts hosting, +conformance machinery, examples, and build tooling. Dependencies point inward; +host-specific code never leaks into the portable runtime. + +## Projects + +```text +blue-bex-core portable public API, compiler, IR, runtime, values, + pointers, results, gas, output and intrinsic SPIs +blue-bex-contracts ProcessorExecutionContext adapters and Contracts failure, + evidence, exact-value, semantic-output and gas bridges +blue-bex-conformance fixture runners, integrity checks and evidence production +blue-bex-java one-coordinate aggregate with no duplicate implementation +examples compile-tested standalone and hosted integrations +build-logic typed conventions, verification and publication tasks +``` + +`blue-bex-core` depends only on the focused Language model/core facilities it +uses. `blue-bex-contracts` depends on core plus `blue-contracts-core`. +Conformance and examples consume public modules; neither is packaged in the +minimal runtime JAR. The aggregate re-exports the intended runtime artifacts. + +There must be no project cycle, package cycle larger than one, or Java split +package. Contracts adapters therefore live only in `blue.bex.contracts`. + +## Runtime flow + +```text +selected FrozenNode + | +BexProgramSource + | +BexCompiler -- validates static shape and creates immutable compiled program + | +BexEngine cache -- keyed by source plus compiler/runtime/gas/intrinsic identity + | +BexRuntime -- one run-local frame/context/accumulator/gas session + | +BexOutputAdmission -- exact pass-through or strict transient Blue admission + | +BexExecutionResult -- value + patches + events + gas + metrics + output metadata +``` + +Compilation never performs host actions. Runtime reads are mediated by +`BexDocumentView` and explicit bindings. Output admission is the only route from +transient BEX data to exact Blue output. Intrinsics are statically identified by +BlueId and receive only their declared payload and named gas capability. + +## Ownership and mutability + +- An engine owns its immutable configuration and thread-safe compiled-program + cache. It may be shared by concurrent callers. +- A compiled program is immutable and shareable. +- An execution context, runtime frame, accumulator, gas session, and result + belong to one run. Do not reuse a context to communicate between runs. +- Exact host nodes are immutable snapshots/cursors. Mutable `Node` input must be + cloned/frozen at the boundary unless the host explicitly guarantees immutable + ownership. +- Returned collections and ledgers are immutable views or defensive copies. + +Public inputs are non-null unless a method explicitly documents `null` as +`undefined`, absence, or a default. Builder defaults are intentional; passing +`null` to bypass a required boundary is not supported. + +## Failure boundary + +Portable compiler/runtime/output errors use BEX-owned failures. The Contracts +adapter translates evidence unavailable, invalid evidence, portable limits, +processor failures, and host gas exhaustion without erasing their host +classification. Unexpected exceptions do not become successful undefined +values. + +No patch or event is committed by the engine. On failure or exhaustion, the +host receives no successful BEX result and must apply its own whole-invocation +rollback rules. + +## Gas ownership + +Core owns the closed BEX counter vocabulary, schedule, trace model, and the rule +that a charge precedes its work. Standalone execution owns a local ledger. +Hosted execution opens a parent-bounded child capability and merges its trace +exactly once. Intrinsic namespaces remain separate and can use only their +registered named counters. + +Metrics such as wall-clock time are diagnostic only. They never affect +semantics, cache keys (except declared configuration identity), BlueId, or gas. + +See [Compiler and IR](compiler-and-ir.md), [Runtime and context](runtime-and-context.md), +and [Contracts hosting](contracts-hosting.md). diff --git a/docs/blue-output-boundary.md b/docs/blue-output-boundary.md new file mode 100644 index 0000000..f17aaef --- /dev/null +++ b/docs/blue-output-boundary.md @@ -0,0 +1,73 @@ +# Blue output boundary + +BEX values are not automatically valid Blue nodes. Every value that leaves a +runtime position requiring Blue content passes through one strict, atomic output +admission boundary. + +## Exact values + +An existing exact value already owns an ordinary BlueId and verified semantic +cursor. Admission preserves that identity. It does not recursively clone, +serialize, size, expand, or rehash the value or its exact descendants. + +A pure reference has exactly one `blueId` and no sibling payload. Its identity is +enough for pass-through; semantic materialization is demanded only by an +operation that actually inspects content. + +## Transient values + +A transient value is converted recursively to runtime Blue content: + +- undefined root fails; +- undefined object members are omitted; +- an undefined list member fails; +- null becomes the Blue null/empty-node form; +- scalar kinds use deterministic Blue scalar rules; +- objects and lists traverse in the order required by BEX; +- exact descendants remain exact rather than being reconstructed; +- the final node is validated before direct identity establishment. + +The host's `BexSemanticIdentityBoundary` establishes the ordinary BlueId exactly +once. The admitted result retains both exact identity and the run-local semantic +cursor, so a later `$resultValue`, variable, event, or changeset read does not +repeat conversion or hashing. + +## Runtime content, not Source content + +Output is runtime Blue content. It is not a Source document waiting for +preprocessing. A root or nested `blue` directive is invalid, and admission never +runs Source preprocessing, complete resolution, canonicalization, or +minimization. + +Other fail-closed rules include: + +- scalar `value`, list `items`, and object payload cannot be mixed; +- `blueId` with sibling payload is invalid; +- computed `type`, `itemType`, `keyType`, `valueType`, `schema`, `mergePolicy`, + and `contracts` retain Blue Language meaning and must validate; +- unsupported schema/compatibility keys are rejected; +- `$previous`, `$pos`, and `$replace` list-control source forms are rejected; +- `$empty: true` is accepted only in the exact valid Blue placeholder shape; +- a transient cyclic-set member identity cannot be established without the + host's complete cyclic-set proof. + +## `$nodeBlueId` + +`$nodeBlueId` first evaluates its operand and charges the identity request. +For exact input it returns the already established ordinary BlueId. For transient +input it invokes this same output boundary and direct identity establishment. +There is no alternate BEX identity algorithm and no semantic-ID shortcut. + +## Gas and atomicity + +Admission charges `blueOutputBoundary` before conversion/validation. Transient +construction and direct identity work are charged only when performed. Exact +pass-through does not incur recursive member-production gas. + +If conversion, validation, identity evidence, or gas admission fails, the value +is not admitted. No later work occurs and buffered BEX patches/events do not +commit. Hosted execution preserves the host failure classification and merges a +successful child identity/gas effect exactly once. + +See [Values and identity](values-and-identity.md) and +[Gas and exhaustion](gas-and-exhaustion.md). diff --git a/docs/compiler-and-ir.md b/docs/compiler-and-ir.md new file mode 100644 index 0000000..79c6641 --- /dev/null +++ b/docs/compiler-and-ir.md @@ -0,0 +1,83 @@ +# Compiler and immutable IR + +The BEX compiler turns one selected immutable source tree into an immutable +compiled program. Compilation is deterministic, side-effect free, and separate +from execution. + +## Compilation pipeline + +Conceptually the compiler performs these stages: + +1. classify the root as a full program or expression; +2. validate declarations, reserved names, static fields, and operator shapes; +3. collect constants, functions, argument patterns, and intrinsic requirements; +4. validate function calls and reject recursive call graphs; +5. compile expressions/statements into immutable typed instruction objects; +6. retain source paths and operator/function metadata for diagnostics and gas; +7. produce one immutable `BexCompiledProgram` plus its exact cache key inputs. + +The operator catalog is the single recognition/metadata inventory for all 86 +portable operators. It records operator kind/family and supports compiler, +coverage, diagnostics, and documentation consistency. Portable operators are +not dynamically registered; `$intrinsic` remains the only host extension point. + +## Static and dynamic operands + +Each operator defines which fields are static syntax and which are expressions. +Static names, pointers, patterns, and control fields are validated once. Dynamic +operands remain compiled expressions and are evaluated only when their operator +semantics require them. + +This distinction prevents accidental execution inside Blue type/pattern fields +and preserves lazy semantics. A compiler refactor must never replace a lazy +operand with an eager Java evaluation. + +## Immutable compiled form + +Compiled programs, function definitions, instruction objects, declared patterns, +and required-intrinsic sets are immutable. Executable IR constructors and +instruction types are implementation-owned; consumers receive an opaque +`BexCompiledProgram` handle and cannot inject arbitrary mutable executable +nodes. A compiled object must not retain a +mutable source `Node`, execution context, frame, accumulator, or host session. +Run-local state lives only in runtime objects. + +Instruction objects expose behavior through narrow runtime interfaces. Internal +IR classes are not a portable serialization format or public source syntax; the +selected Blue program remains the identity-bearing source. + +## Cache identity + +Compilation caching is keyed by the selected source identity/fingerprint plus +every configuration element that can change compiled meaning, including compiler +identity, BEX registry identity, gas schedule identity/weights, relevant Blue +Language registry identity, and intrinsic registry identity. + +The same exact environment identity is retained in the compiled handle and +checked before execution. A program compiled with another intrinsic registry, +even one supporting the same BlueIds, is rejected rather than run under a +different implementation or gas catalog. + +A hit must be observationally identical to a cold compile. Cache state cannot +change BEX gas, results, failure ordering, or provider behavior. Cache entries do +not capture bindings or other execution state. + +## Compile-time failures + +Compilation fails closed for unknown operators, invalid operator shape, illegal +name containers, duplicate/unknown declarations, missing/extra function +arguments, unknown constants/functions, recursive calls, invalid static +pointers/patterns, unsupported intrinsic BlueIds, and other statically knowable +defects. + +Diagnostics retain the closest source path and operator/function context. The +compiler does not turn invalid source into runtime `undefined`. + +## Evolving the compiler + +Large operator families should remain cohesive and acyclic; shared parsing, +operand, pointer, and diagnostic utilities belong below family compilers rather +than importing the runtime back into compilation. A new portable operator must +update the specification, catalog, immutable IR/compiler, runtime semantics, +named gas, fixtures, coverage, docs, and API review together. See +[Adding an operator](adding-an-operator.md). diff --git a/docs/conformance.md b/docs/conformance.md new file mode 100644 index 0000000..0b5e8a4 --- /dev/null +++ b/docs/conformance.md @@ -0,0 +1,82 @@ +# Conformance + +The normative BEX 2.0 behavior is defined by the specification plus the +source-controlled machine-readable packages. Prose summaries do not override a +fixture manifest. + +## Exact package baseline + +```text +normative vectors 60 +behavior fixtures 105 +gas microfixtures 30 +normative operators 86 + +runtime registry +sha256:23d282ec1c0bb016263922b1b49c369fdd537efdcf23e005eceeb888d7763fe1 + +gas manifest +sha256:41247c820d91a12fdfc17fd9e787a5d8d668d8acc5954fdcb131715bf9e6147d + +fixture package +sha256:a1b7bb2b3687389409bc9d0aa450c734f7856d2bcb818c95f4d7ecb19095d20e +``` + +Documentation-only corrections do not change these identities. If +identity-bearing registry, manifest, or fixture bytes change in a future +version, the identity must change and be reviewed explicitly. + +## What conformance covers + +The package covers compiler validation; expression and statement semantics; +functions, constants, pointers, overlays, patches, and events; exact/transient +output; direct identity; intrinsic dispatch; representation invariance; provider +evidence; recursion/termination; all operator coverage; and exact named gas. + +The strengthened representation matrix requires equivalent exact values to be +indistinguishable across inline/reference, eager/lazy, warm/cold, and provider +segmentation for kind, existence, keys, entries, size, truthiness, equality, +matching, iteration, pointer access, explicit identity, and portable gas. + +Each gas counter has a microfixture. Exhaustion evidence checks the admitted +prefix, absent rejected charge, zero later work, discarded buffered effects, +local/parent limits, hosted shared budget, and exactly-once child merge. + +No new BEX fixture is required merely because Contracts supports +`collectionPaths`: activation of embedded scopes is outside BEX semantics. + +## Run the working suite + +```bash +./gradlew --no-daemon clean bexWorkingVerification \ + -PblueLanguageCompositePath=/absolute/path/to/blue-language-java +``` + +The gate includes all ordinary tests, all fixture/vector/gas/operator coverage, +local composite compile/runtime smoke, hosted adapters, Java 8 bytecode, public +API descriptors, artifacts, BEX-owned archive determinism, and a clean +machine-readable report. + +A green working result requires: + +```text +failed = 0 +skipped = 0 +unclassified = 0 +workingReady = true +``` + +Generated evidence lives under `build/reports`. Counts are derived from actual +JUnit/fixture mappings, not hardcoded as passing because an inventory test ran. +Missing, stale, failed, skipped, and unexecuted evidence stays visible. + +## Reports are evidence, not declarations + +Archive determinism from repackaging one compiled input is not the same claim as +two independent clean builds. Local-composite success is not published-artifact +success. A source inventory is not execution. Reports bind their claims to the +source commit/dirty state, dependency mode, artifact hashes, and exact test +results. + +See [`BEX_CONFORMANCE.md`](BEX_CONFORMANCE.md) for lower-level report fields and +[Release](release.md) for the stricter public gate. diff --git a/docs/contracts-hosting.md b/docs/contracts-hosting.md new file mode 100644 index 0000000..e79c872 --- /dev/null +++ b/docs/contracts-hosting.md @@ -0,0 +1,100 @@ +# Contracts hosting + +`blue-bex-core` is independently executable and has no +`ProcessorExecutionContext` dependency. All Contracts-specific composition lives +in `blue-bex-contracts`, package `blue.bex.contracts`. + +## What the adapter configures + +Given the active `ProcessorExecutionContext`, the adapter configures one +`BexExecutionContext.Builder` with: + +- invocation-owned canonical and resolved document views and current scope; +- current event, original processing event, current contract, and optional step + result bindings; +- a live parent-bounded BEX gas capability; +- the processor-owned semantic-output/identity boundary; +- evidence and failure translation that preserves Contracts classification. + +The host still selects the BEX source and engine/intrinsic registry. BEX neither +discovers programs nor gains general access to the processor context. + +```java +public BexExecutionResult run( + ProcessorExecutionContext processor, + FrozenNode selectedExpression) { + BexExecutionContext.Builder context = BexExecutionContext.builder(); + BexContractsExecutionContext.configure(context, processor, "bex:policy"); + + return BexEngine.builder().build().compileAndExecute( + BexProgramSource.expression(selectedExpression), + context.build()); +} +``` + +See the compile-tested hosted example for the exact current adapter signatures. +Use a stable deterministic runtime namespace. When one processor work session +runs several BEX programs, give each execution a distinct namespace. + +## Document and exact-value provenance + +The document adapter must preserve the invocation's canonical/resolved views, +scope, reference provider evidence, and immutable ownership. Exact values enter +BEX with their established ordinary BlueId and semantic cursor. They are not +cloned through Java maps or independently rehashed. + +Transient output is admitted through the processor-owned semantic-output +boundary. The host returns one established exact value/cursor capability; BEX +retains it without repeating identity work. Root and nested Source `blue` +directives remain invalid runtime output. + +## Shared gas + +Hosted BEX consumes a live child ledger of the current `RuntimeWorkSession`. +The effective BEX-local limit can reduce but never replenish the parent's +remaining budget. Core BEX counters remain under the execution's physical +namespace; intrinsic namespaces remain separately registered. + +Charges occur before work. The rejected entry is absent. The child trace merges +into Contracts exactly once, and no later BEX work or buffered effect commits +after exhaustion. Do not create a detached meter and reconcile an opaque total +after execution. + +## Failure translation + +The adapter preserves the exact host categories for: + +```text +execution evidence unavailable +invalid execution evidence +portable limit exceeded +processor failure +host gas limit exceeded +``` + +Portable compiler/runtime/output defects remain BEX failures. An unexpected +runtime exception is not swallowed as undefined. If provider evidence is +temporarily unavailable, the host may suspend/retry outside a completed BEX run; +the failed attempt does not commit a child ledger or BEX effects. Invalid +evidence is deterministic and retains the admitted trace prefix required by the +host contract. + +## Result ownership + +The BEX engine returns value, ordered patches, ordered events, and evidence. It +does not apply or dispatch them. Contracts validates and decides the larger +transaction's commit/rollback behavior. + +`collectionPaths`, Timeline, Mandate, Coordination, `Process Embedded`, and +collection activation remain Contracts/Coordination concerns. A BEX program can +construct an ordinary object or patch that adds it to a collection; only +Contracts decides whether that object becomes an embedded scope. + +## Testing a host integration + +Cover canonical/resolved/scope reads, exact inline/reference provenance, +unavailable and invalid provider evidence, direct transient identity, nested +exact descendants, multiple child namespaces, parent/local exhaustion, +rejected-charge omission, exactly-once trace merge, and failure rollback. Run +the generic hosted-consumer smoke against public APIs rather than internal test +helpers. diff --git a/docs/gas-and-exhaustion.md b/docs/gas-and-exhaustion.md new file mode 100644 index 0000000..744afde --- /dev/null +++ b/docs/gas-and-exhaustion.md @@ -0,0 +1,90 @@ +# Gas and exhaustion + +BEX 2.0 accounts deterministic logical work with a canonical named ledger. It +does not estimate serialized payload size or use machine-dependent time/memory +as portable gas. + +## Normative schedule + +```text +schedule: blue-bex/gas/2.0 +counters: 30 +identity: sha256:41247c820d91a12fdfc17fd9e787a5d8d668d8acc5954fdcb131715bf9e6147d +``` + +The closed vocabulary covers expression/statement/function/intrinsic work; +document and binding reads; pointer/object/list work; collection production; +text/numeric/comparison/sort work; patch/event append; transient construction; +Blue output admission; and identity requests. The exact names and weights live +in `src/main/resources/blue/bex/gas/blue-bex-gas-2.0.yaml` and are explained in +[`GAS.md`](GAS.md). + +Changing a counter name, order, or weight is a semantic versioned change. This +modernization does not do so. + +## Charge before work + +Every logical operation asks the active ledger to admit its named charge before +performing that work. If the charge would exceed the effective budget: + +1. the attempted charge is absent from the trace; +2. the associated work is not performed; +3. no later operand, callback, output admission, or intrinsic runs; +4. buffered patches and events do not commit. + +The trace contains ordered sequence, counter, quantity, weight, subtotal, and +available source/operator/reason metadata. `gasUsed` is derived from admitted +trace entries; an opaque externally supplied total is never authoritative. + +## Laziness and exact work + +Short-circuited or unselected work has no charge. Lazy bindings charge only when +read. A warm cache cannot earn gas credit. Equivalent inline/reference, +eager/lazy, warm/cold, and provider-segmented representations have identical +portable gas traces for the same logical work. + +Text work uses deterministic Unicode scalar-value blocks, not UTF-16 length. +Integer/decimal gas follows exact numeric work. Collections charge actual visits, +comparisons, and produced transient members. BEX never charges recursive +`estimatedSize` or serialized bytes. + +Existing exact values cross by identity and are not recursively sized or +rebuilt. Transient values pay for actual construction, traversal, conversion, +and identity establishment. + +## Standalone budgets + +Standalone execution has an effective parent remaining budget and an optional +BEX-local limit. `NO_LOCAL_LIMIT` means the local layer does not further reduce +the parent. A local limit may reduce the effective budget but can never replenish +or exceed the parent budget. + +`BexGasLimitExceededException` exposes the rejected counter and budget evidence +for portable standalone exhaustion. Callers must not resume the failed runtime. + +## Contracts-hosted budgets + +The Contracts adapter opens a live child capability against the invocation's +shared parent budget. All BEX work and registered intrinsic namespaces consume +that same live parent capacity. If one processor invocation runs several BEX +programs, each uses a distinct deterministic physical namespace while sharing +the parent. + +The child trace merges into Contracts exactly once. BEX must not independently +replay entries, return a magic `gasConsumed`, or let a local limit replenish the +parent. Host exhaustion/failure classification remains owned by Contracts. + +## Intrinsic gas + +Each intrinsic registration declares an immutable namespace-local counter +vocabulary and weights. An invocation may charge only those names. Intrinsic +work cannot be hidden in a portable integer or charged under the BEX core +namespace. See [Intrinsics](intrinsics.md). + +## Evidence + +The 30 gas microfixtures cover every closed BEX counter. Additional tests cover +exact charge order, lazy branches, representation parity, parent/shared limits, +rejected-charge omission, no work after rejection, and hosted merge behavior. +Passing claims come only from current generated reports; this guide does not +claim that an unexecuted release gate has passed. diff --git a/docs/intrinsics.md b/docs/intrinsics.md new file mode 100644 index 0000000..9ed1f8d --- /dev/null +++ b/docs/intrinsics.md @@ -0,0 +1,81 @@ +# Intrinsics + +`$intrinsic` is BEX's only host extension point. It binds a statically declared +Blue type identity to one deterministic processor and an exact named gas +catalog. Portable operators themselves are not dynamically pluggable. + +## Register by BlueId + +Prefer the explicit string identity: + +```java +BexIntrinsicRegistry registry = BexIntrinsicRegistry.builder() + .register( + operationBlueId, + operationRegistryIdentity, + Collections.singletonMap("signatureVerification", 500L), + invocation -> { + invocation.charge( + "signatureVerification", 1L, "verify-signature"); + return BexValues.scalar(verify(invocation)); + }) + .build(); + +BexEngine engine = BexEngine.builder().intrinsics(registry).build(); +``` + +A class convenience is valid only when an explicit `BexTypeBlueIdResolver` or +the supported annotated-type mapping boundary resolves that class. A Java class +name is never a BlueId, and reflective fallback identity is forbidden. + +The registry is immutable. Its identity includes sorted BlueIds, registry +identities, namespaces, counter names, and weights, so compilation caching cannot +confuse engines with different intrinsic semantics. + +## Program shape + +The exact `$intrinsic` source shape is defined in the BEX specification. The +operation type/BlueId is static: compilation records it in the program's +required-intrinsic set and fails when the engine does not support it. Payload +fields are ordinary compiled operands and evaluate in their normative order. + +## Invocation capability + +`BexIntrinsicInvocation` exposes: + +- the exact operation BlueId and type value; +- an immutable evaluated field map and undefined-for-missing lookup; +- the registration's physical gas namespace and named weights; +- `charge(name, quantity, reason)` for declared work only; +- `exactField(name)` when the operation explicitly requires admitted exact Blue + input; +- a diagnostic current ledger total. + +`exactField` uses the same strict output/identity boundary as other Blue output. +Do not serialize and reparse the value or calculate a second identity. + +## Processor contract + +An intrinsic processor must be deterministic for its declared inputs and host +capability. It must charge before performing each declared unit of work and may +not access undeclared global authority through BEX. Return a `BexValue`; a Java +`null` result is treated as undefined only where the API explicitly documents +that behavior. + +Exceptions fail the run. They are not converted into a successful false/null +result. Hosted adapters preserve recognized evidence, processor, portable-limit, +and gas classifications. Buffered BEX effects remain uncommitted. + +## Checklist for a new intrinsic + +1. Define and publish the operation's ordinary Blue type/BlueId. +2. Pin the registry identity that defines that operation. +3. Define a closed, collision-free namespace and counter/weight catalog. +4. Implement deterministic validation and charge-before-work processing. +5. Admit only fields whose semantics require exact Blue content. +6. Add compile, success, failure, gas-order, exhaustion, and hosted tests. +7. Document the authority and data exposed to the processor. +8. Include the registration in reproducibility/dependency evidence where it is + part of a released integration. + +Adding an intrinsic does not change the 86 portable BEX operators. diff --git a/docs/latest-language-api-migration.json b/docs/latest-language-api-migration.json index 2b86949..4405c06 100644 --- a/docs/latest-language-api-migration.json +++ b/docs/latest-language-api-migration.json @@ -3,7 +3,8 @@ "title": "Blue Language modular API migration", "sourceState": { "bexBaselineCommit": "395c484111f8c4e9e0e98d2db7f1c5b0777bd5a8", - "bexMigrationState": "working-tree delta rooted at bexBaselineCommit", + "bexWorkingCheckpointCommit": "169e589", + "bexMigrationState": "modernization delta rooted at bexWorkingCheckpointCommit; the containing commit is the final target revision", "languageExactCommit": "9a607e584ff5dd973684d35d71eb4022d946b760", "languageVerifiedImplementationCommit": "63a9ed6a1a66d47119a80d16ed2ab0beda0d2453", "languageDeltaClassification": "documentation-and-license-only", @@ -15,11 +16,86 @@ }, "manifests": { "beforeSha256": "830caa187023079ba53fa76d2932e6e12cb8c93be3f90ac887ad374d6642b315", - "afterSha256": "43aea6ae9de6f39729f93c146ff5453887da9a2303f6f4f33573ba47f37d5be0", + "workingCheckpointSha256": "43aea6ae9de6f39729f93c146ff5453887da9a2303f6f4f33573ba47f37d5be0", + "afterSha256": "df602fa6b14afc285053fc8e34e349d7fa5ce26810a6c79ba14ac6361de463ee", + "workingCheckpointPath": "gradle/verification/api/working-checkpoint-public-api.txt", "requiredPath": "src/test/resources/hosted-release/required-public-api.txt", - "generatedPath": "build/reports/bex-release/public-api.txt" + "generatedPath": "blue-bex-conformance/build/reports/bex-release/public-api.txt", + "workingCheckpointClassificationPath": "gradle/verification/api/working-checkpoint-public-api-classification.json", + "workingCheckpointClassificationSha256": "4e0738794bcf042f399ac0a15f153aa9ac5d07fd7eae95bc410ce3bf780f343e", + "afterClassificationPath": "docs/public-api-classification.json", + "afterClassificationSha256": "e4bb7931f6cbf07a3597348eb5de482376c2752b26265195060f9f19717be5b8", + "publicTypeCount": 101, + "publicDescriptorCount": 1028 }, - "changes": [ + "modernizationDelta": { + "baselineCommit": "169e589", + "baselineManifestSha256": "43aea6ae9de6f39729f93c146ff5453887da9a2303f6f4f33573ba47f37d5be0", + "removedDescriptorLines": 254, + "addedDescriptorLines": 483, + "removedDescriptorsPath": "gradle/verification/api/modernization-removed-descriptors.txt", + "removedDescriptorsSha256": "7d3ba5a5e69ddf8e2fee0d92c924cae3c4dd36d42e465dc9931c550a99db2d99", + "addedDescriptorsPath": "gradle/verification/api/modernization-added-descriptors.txt", + "addedDescriptorsSha256": "0d94702570948e6f4c3f5dd88b1670f57db60ec11e8bcaff5220f02599ce56fe", + "comparison": "complete bytewise set difference after qualifying every member descriptor with its owning class; :blue-bex-conformance:binaryApiCheck recomputes and compares every line", + "completeMachineAuditableDelta": true, + "packageMoves": [ + { + "from": "blue.bex.api.ProcessorExecutionContextBexDocumentView", + "to": "blue.bex.contracts.ProcessorExecutionContextBexDocumentView", + "classification": "host SPI", + "rationale": "Processor-specific adapters belong to the Contracts host module, not the pure API package." + }, + { + "from": "blue.bex.api.ProcessorExecutionContextBexGasLedgerHost", + "to": "blue.bex.contracts.ProcessorExecutionContextBexGasLedgerHost", + "classification": "host SPI", + "rationale": "Processor gas adaptation is owned by the Contracts host module." + }, + { + "from": "blue.bex.output.ProcessorExecutionContextBexSemanticIdentityBoundary", + "to": "blue.bex.contracts.ProcessorExecutionContextBexSemanticIdentityBoundary", + "classification": "host SPI", + "rationale": "Processor semantic-output admission is isolated from the pure output package." + }, + { + "from": "blue.bex.runtime.CompileScope", + "to": "blue.bex.compile.CompileScope", + "classification": "internal implementation", + "rationale": "Compiler-owned scope state moved with the immutable IR to remove the compile/runtime package cycle." + }, + { + "from": "blue.bex.runtime.CompiledExpression", + "to": "blue.bex.compile.CompiledExpression", + "classification": "internal implementation", + "rationale": "Compiled expressions are compiler-owned IR." + }, + { + "from": "blue.bex.runtime.CompiledFrame", + "to": "blue.bex.compile.CompiledFrame", + "classification": "internal implementation", + "rationale": "The IR frame now depends on a narrow compile-owned execution-machine port." + }, + { + "from": "blue.bex.runtime.CompiledStatement", + "to": "blue.bex.compile.CompiledStatement", + "classification": "internal implementation", + "rationale": "Compiled statements are compiler-owned IR." + }, + { + "from": "blue.bex.runtime.Control", + "to": "blue.bex.compile.Control", + "classification": "internal implementation", + "rationale": "IR control flow moved with compiled statements." + } + ], + "boundaryChanges": [ + "BexCompiledProgram executes through blue.bex.compile.BexExecutionMachine rather than concrete BexRuntime.", + "BexRuntime implements BexExecutionMachine and consumes BexRuntimeContext/BexRuntimeIntrinsics rather than API-owned concrete host types.", + "Contracts evidence and failure translation are exposed only from blue-bex-contracts." + ] + }, + "reviewedHighlights": [ { "id": "builder-blue-remove", "kind": "removed", @@ -197,6 +273,295 @@ "replacement": "replaces referenceBacked(BexValue, blue.language.Blue)", "binaryCompatibility": "additive by itself; paired removal is binary-breaking", "sourceCompatibility": "migration target for host integrations" + }, + { + "id": "compiler-implementation-surface-remove", + "kind": "removed", + "signature": "class public final blue.bex.compile.BexCompiler", + "classification": "internal implementation", + "rationale": "Only BexEngine may create compiler-produced programs so every opaque handle is bound to the exact compile environment.", + "replacement": "blue.bex.api.BexEngine compile and execute methods", + "binaryCompatibility": "intentional pre-release removal of a public implementation type and its three public members", + "sourceCompatibility": "migrate direct compiler callers to BexEngine" + }, + { + "id": "compiled-key-kind-constructor-remove", + "kind": "removed", + "signature": "constructor public (blue.bex.api.BexProgramSource$Kind,java.lang.String,java.lang.String,java.lang.String)", + "owner": "blue.bex.compile.BexCompiledProgramKey", + "classification": "stable API", + "rationale": "The compile-owned cache key cannot expose an API-owned enum without recreating the package cycle.", + "replacement": "constructor public (blue.bex.compile.BexCompilationInput$Kind,java.lang.String,java.lang.String,java.lang.String)", + "binaryCompatibility": "breaking: the constructor descriptor changed", + "sourceCompatibility": "breaking for direct constructor callers: use BexCompilationInput.Kind" + }, + { + "id": "compiled-key-input-kind-constructor-add", + "kind": "added", + "signature": "constructor public (blue.bex.compile.BexCompilationInput$Kind,java.lang.String,java.lang.String,java.lang.String)", + "owner": "blue.bex.compile.BexCompiledProgramKey", + "classification": "stable API", + "rationale": "Cache identity now uses the compile-owned source-kind enum.", + "replacement": "replaces the constructor accepting BexProgramSource.Kind", + "binaryCompatibility": "additive by itself; paired removal is binary-breaking", + "sourceCompatibility": "migration target for direct cache-key constructor callers" + }, + { + "id": "compiled-key-kind-environment-constructor-remove", + "kind": "removed", + "signature": "constructor public (blue.bex.api.BexProgramSource$Kind,java.lang.String,java.lang.String,java.lang.String,java.lang.String)", + "owner": "blue.bex.compile.BexCompiledProgramKey", + "classification": "stable API", + "rationale": "The environment-aware cache-key constructor must also avoid the API-owned source-kind enum.", + "replacement": "constructor public (blue.bex.compile.BexCompilationInput$Kind,java.lang.String,java.lang.String,java.lang.String,java.lang.String)", + "binaryCompatibility": "breaking: the constructor descriptor changed", + "sourceCompatibility": "breaking for direct constructor callers: use BexCompilationInput.Kind" + }, + { + "id": "compiled-key-input-kind-environment-constructor-add", + "kind": "added", + "signature": "constructor public (blue.bex.compile.BexCompilationInput$Kind,java.lang.String,java.lang.String,java.lang.String,java.lang.String)", + "owner": "blue.bex.compile.BexCompiledProgramKey", + "classification": "stable API", + "rationale": "Environment-aware cache identity is wholly compile-owned.", + "replacement": "replaces the environment-aware constructor accepting BexProgramSource.Kind", + "binaryCompatibility": "additive by itself; paired removal is binary-breaking", + "sourceCompatibility": "migration target for direct cache-key constructor callers" + }, + { + "id": "compiled-key-api-kind-method-remove", + "kind": "removed", + "signature": "method public kind():blue.bex.api.BexProgramSource$Kind", + "owner": "blue.bex.compile.BexCompiledProgramKey", + "classification": "stable API", + "rationale": "A compile-owned value cannot return an API-owned enum without a reverse package edge.", + "replacement": "method public kind():blue.bex.compile.BexCompilationInput$Kind", + "binaryCompatibility": "breaking: the return descriptor changed", + "sourceCompatibility": "breaking where callers name BexProgramSource.Kind" + }, + { + "id": "compiled-key-input-kind-method-add", + "kind": "added", + "signature": "method public kind():blue.bex.compile.BexCompilationInput$Kind", + "owner": "blue.bex.compile.BexCompiledProgramKey", + "classification": "stable API", + "rationale": "The key exposes the compile-owned source-kind identity.", + "replacement": "replaces kind():blue.bex.api.BexProgramSource$Kind", + "binaryCompatibility": "additive by itself; paired removal is binary-breaking", + "sourceCompatibility": "use BexCompilationInput.Kind for typed reads" + }, + { + "id": "compiled-key-api-source-factory-remove", + "kind": "removed", + "signature": "method public static from(blue.bex.api.BexProgramSource):blue.bex.compile.BexCompiledProgramKey", + "owner": "blue.bex.compile.BexCompiledProgramKey", + "classification": "stable API", + "rationale": "Cache-key construction now consumes the same compile-owned input boundary as compilation.", + "replacement": "method public static from(blue.bex.compile.BexCompilationInput):blue.bex.compile.BexCompiledProgramKey", + "binaryCompatibility": "breaking: the factory descriptor changed", + "sourceCompatibility": "compatible on recompilation because BexProgramSource implements BexCompilationInput" + }, + { + "id": "compiled-key-input-factory-add", + "kind": "added", + "signature": "method public static from(blue.bex.compile.BexCompilationInput):blue.bex.compile.BexCompiledProgramKey", + "owner": "blue.bex.compile.BexCompiledProgramKey", + "classification": "stable API", + "rationale": "Cache-key construction accepts the narrow immutable compiler input.", + "replacement": "replaces from(blue.bex.api.BexProgramSource)", + "binaryCompatibility": "additive by itself; paired removal is binary-breaking", + "sourceCompatibility": "existing BexProgramSource calls remain valid after recompilation" + }, + { + "id": "compiled-key-api-source-environment-factory-remove", + "kind": "removed", + "signature": "method public static from(blue.bex.api.BexProgramSource,java.lang.String):blue.bex.compile.BexCompiledProgramKey", + "owner": "blue.bex.compile.BexCompiledProgramKey", + "classification": "stable API", + "rationale": "Environment-aware key construction must not introduce a compile-to-API edge.", + "replacement": "method public static from(blue.bex.compile.BexCompilationInput,java.lang.String):blue.bex.compile.BexCompiledProgramKey", + "binaryCompatibility": "breaking: the factory descriptor changed", + "sourceCompatibility": "compatible on recompilation because BexProgramSource implements BexCompilationInput" + }, + { + "id": "compiled-key-input-environment-factory-add", + "kind": "added", + "signature": "method public static from(blue.bex.compile.BexCompilationInput,java.lang.String):blue.bex.compile.BexCompiledProgramKey", + "owner": "blue.bex.compile.BexCompiledProgramKey", + "classification": "stable API", + "rationale": "Environment-aware key construction accepts the compile-owned input view.", + "replacement": "replaces from(blue.bex.api.BexProgramSource,java.lang.String)", + "binaryCompatibility": "additive by itself; paired removal is binary-breaking", + "sourceCompatibility": "existing BexProgramSource calls remain valid after recompilation" + }, + { + "id": "compilation-input-type-add", + "kind": "added", + "signature": "class public abstract interface blue.bex.compile.BexCompilationInput", + "classification": "stable API", + "rationale": "The compiler owns a narrow immutable source view independent of host API policy.", + "replacement": "new compile boundary implemented by BexProgramSource", + "binaryCompatibility": "compatible additive type", + "sourceCompatibility": "compatible additive type" + }, + { + "id": "compilation-input-expression-method-add", + "kind": "added", + "signature": "method public abstract isExpression():boolean", + "owner": "blue.bex.compile.BexCompilationInput", + "classification": "stable API", + "rationale": "Distinguishes expression roots from full BEX programs without exposing API-owned enums.", + "replacement": "new member of BexCompilationInput", + "binaryCompatibility": "compatible as part of a new type", + "sourceCompatibility": "already implemented by BexProgramSource" + }, + { + "id": "compilation-input-program-method-add", + "kind": "added", + "signature": "method public abstract programNode():blue.language.snapshot.FrozenNode", + "owner": "blue.bex.compile.BexCompilationInput", + "classification": "stable API", + "rationale": "Supplies the frozen selected program root.", + "replacement": "new member of BexCompilationInput", + "binaryCompatibility": "compatible as part of a new type", + "sourceCompatibility": "already implemented by BexProgramSource" + }, + { + "id": "compilation-input-definition-method-add", + "kind": "added", + "signature": "method public abstract definitionNode():java.util.Optional", + "owner": "blue.bex.compile.BexCompilationInput", + "classification": "stable API", + "rationale": "Supplies the optional frozen shared definition root.", + "replacement": "new member of BexCompilationInput", + "binaryCompatibility": "compatible as part of a new type", + "sourceCompatibility": "already implemented by BexProgramSource" + }, + { + "id": "compilation-input-entry-method-add", + "kind": "added", + "signature": "method public abstract entry():java.util.Optional", + "owner": "blue.bex.compile.BexCompilationInput", + "classification": "stable API", + "rationale": "Supplies the optional selected entry function.", + "replacement": "new member of BexCompilationInput", + "binaryCompatibility": "compatible as part of a new type", + "sourceCompatibility": "already implemented by BexProgramSource" + }, + { + "id": "compilation-input-kind-type-add", + "kind": "added", + "signature": "class public static final blue.bex.compile.BexCompilationInput$Kind extends java.lang.Enum", + "classification": "stable API", + "rationale": "Cache identity uses a compile-owned source-kind enum.", + "replacement": "compile-owned counterpart to the retained BexProgramSource.Kind host enum", + "binaryCompatibility": "compatible additive type", + "sourceCompatibility": "migration target for direct BexCompiledProgramKey callers" + }, + { + "id": "compilation-input-kind-full-field-add", + "kind": "added", + "signature": "field public static final FULL_PROGRAM:blue.bex.compile.BexCompilationInput$Kind", + "owner": "blue.bex.compile.BexCompilationInput$Kind", + "classification": "stable API", + "rationale": "Identifies full-program compiler inputs.", + "replacement": "new enum constant", + "binaryCompatibility": "compatible as part of a new type", + "sourceCompatibility": "compatible additive constant" + }, + { + "id": "compilation-input-kind-expression-field-add", + "kind": "added", + "signature": "field public static final EXPRESSION:blue.bex.compile.BexCompilationInput$Kind", + "owner": "blue.bex.compile.BexCompilationInput$Kind", + "classification": "stable API", + "rationale": "Identifies expression compiler inputs.", + "replacement": "new enum constant", + "binaryCompatibility": "compatible as part of a new type", + "sourceCompatibility": "compatible additive constant" + }, + { + "id": "compilation-input-kind-valueof-method-add", + "kind": "added", + "signature": "method public static valueOf(java.lang.String):blue.bex.compile.BexCompilationInput$Kind", + "owner": "blue.bex.compile.BexCompilationInput$Kind", + "classification": "stable API", + "rationale": "Compiler-generated enum lookup descriptor.", + "replacement": "new enum member", + "binaryCompatibility": "compatible as part of a new type", + "sourceCompatibility": "compatible as part of a new type" + }, + { + "id": "compilation-input-kind-values-method-add", + "kind": "added", + "signature": "method public static values():blue.bex.compile.BexCompilationInput$Kind[]", + "owner": "blue.bex.compile.BexCompilationInput$Kind", + "classification": "stable API", + "rationale": "Compiler-generated enum values descriptor.", + "replacement": "new enum member", + "binaryCompatibility": "compatible as part of a new type", + "sourceCompatibility": "compatible as part of a new type" + }, + { + "id": "intrinsic-catalog-type-add", + "kind": "added", + "signature": "class public abstract interface blue.bex.compile.BexIntrinsicCatalog", + "classification": "intrinsic SPI", + "rationale": "Compilation depends only on stable intrinsic membership, not runtime processors or gas capabilities.", + "replacement": "new compile-owned view implemented by BexIntrinsicRegistry", + "binaryCompatibility": "compatible additive type", + "sourceCompatibility": "compatible additive type" + }, + { + "id": "intrinsic-catalog-supports-method-add", + "kind": "added", + "signature": "method public abstract supports(java.lang.String):boolean", + "owner": "blue.bex.compile.BexIntrinsicCatalog", + "classification": "intrinsic SPI", + "rationale": "The compiler needs only exact BlueId membership.", + "replacement": "new single abstract method of BexIntrinsicCatalog", + "binaryCompatibility": "compatible as part of a new type", + "sourceCompatibility": "already implemented by BexIntrinsicRegistry" + }, + { + "id": "program-source-interface-declaration-remove", + "kind": "removed", + "signature": "class public final blue.bex.api.BexProgramSource", + "classification": "stable API", + "rationale": "The class declaration now records its compile-owned immutable input role.", + "replacement": "class public final blue.bex.api.BexProgramSource implements blue.bex.compile.BexCompilationInput", + "binaryCompatibility": "compatible interface addition", + "sourceCompatibility": "compatible interface addition" + }, + { + "id": "program-source-interface-declaration-add", + "kind": "added", + "signature": "class public final blue.bex.api.BexProgramSource implements blue.bex.compile.BexCompilationInput", + "classification": "stable API", + "rationale": "The existing immutable source is the host-to-compiler adapter.", + "replacement": "replaces the same class declaration without BexCompilationInput", + "binaryCompatibility": "compatible interface addition", + "sourceCompatibility": "compatible interface addition" + }, + { + "id": "intrinsic-registry-interface-declaration-remove", + "kind": "removed", + "signature": "class public final blue.bex.api.BexIntrinsicRegistry", + "classification": "intrinsic SPI", + "rationale": "The class declaration now records its narrow compile-time catalog role.", + "replacement": "class public final blue.bex.api.BexIntrinsicRegistry implements blue.bex.compile.BexIntrinsicCatalog", + "binaryCompatibility": "compatible interface addition", + "sourceCompatibility": "compatible interface addition" + }, + { + "id": "intrinsic-registry-interface-declaration-add", + "kind": "added", + "signature": "class public final blue.bex.api.BexIntrinsicRegistry implements blue.bex.compile.BexIntrinsicCatalog", + "classification": "intrinsic SPI", + "rationale": "The immutable registry adapts to compilation without exposing runtime capabilities to the compiler.", + "replacement": "replaces the same class declaration without BexIntrinsicCatalog", + "binaryCompatibility": "compatible interface addition", + "sourceCompatibility": "compatible interface addition" } ] } diff --git a/docs/migrating-to-modular-blue-language.md b/docs/migrating-to-modular-blue-language.md new file mode 100644 index 0000000..81b5fc4 --- /dev/null +++ b/docs/migrating-to-modular-blue-language.md @@ -0,0 +1,119 @@ +# Migrating to modular Blue Language + +BEX no longer compiles against the removed monolithic `blue.language.Blue` +facade or the old `blue.language.utils` surface. It consumes focused modules and +the public modular runtime APIs. + +## Dependency shape + +Portable BEX uses the minimum applicable modules from: + +```text +blue.language:blue-language-model +blue.language:blue-language-core +blue.language:blue-language-mapping only for supported Java type mapping +``` + +Contracts adapters additionally use: + +```text +blue.language:blue-contracts-core +``` + +`blue.language:blue-language-java` is the aggregate compatibility artifact. It +must not appear on focused production compile/runtime classpaths. The aggregate +is an explicit conformance-only runtime dependency so clean local and published +runs resolve and hash the compatibility artifact; the strict release gate also +downloads and authenticates it directly in isolation. + +## Local composite mode + +Use the explicit checkout path: + +```bash +./gradlew --no-daemon clean bexWorkingVerification \ + -PblueLanguageCompositePath=/absolute/path/to/blue-language-java +``` + +Composite substitution maps every coordinate to its matching Language +subproject: + +```text +blue-language-model -> :blue-language-model +blue-language-core -> :blue-language-core +blue-language-mapping -> :blue-language-mapping +blue-contracts-core -> :blue-contracts-core +blue-language-java -> :blue-language-java (isolated compatibility only) +``` + +Mapping the aggregate coordinate to `project(":")` is wrong: the Language root +is an orchestration project and has no library classes. + +The working gate records the exact Language HEAD, code-equivalent implementation +commit when different, dirty state, module graph, project origins, resolved JAR +hashes, registry/Contracts identities, and BEX source state. The Language +checkout is read-only; never edit it to manufacture evidence. + +## Source migration map + +Important replacements include: + +```text +blue.language.Blue -> blue.language.runtime.BlueLanguage +blue.language.utils.JsonPointer -> blue.language.model.wire.JsonPointer +blue.language.snapshot.ResolvedSnapshot -> blue.language.merge.ResolvedSnapshot +root NodeProvider -> focused provider package +root BlueOperation* outcomes -> focused public API packages +old Properties core IDs -> BlueCoreTypeRegistry +old BlueId calculators/resolvers -> public direct identity and explicit + BEX-owned type resolver boundaries +``` + +Do not depend on `blue-language-mapping` merely to reach an internal resolver. +For intrinsic classes, prefer an explicit ordinary BlueId or +`BexTypeBlueIdResolver`. The exact public descriptor delta is recorded in +[`latest-language-api-migration.json`](latest-language-api-migration.json), with +both manifests, both classifications, and the complete additions/removals under +`gradle/verification/api/`. + +## Published mode + +Published mode resolves exact module coordinates from the controlled public +repository configuration. `mavenLocal()` or an uncontrolled same-GAV repository +must not masquerade as release evidence. The strict gate inspects coordinates, +origin, hashes, API, and a local/published differential run. + +If matching modular Language artifacts are unavailable, local composite work can +still be complete and committed, but `bexReleaseVerify` must remain red or +`not-executed`. See [Release](release.md). + +## Consumer migration + +Replace engine builder `.blue(oldFacade)` calls with `.language(BlueLanguage)`. +Move processor-specific setup to the `blue.bex.contracts` adapter rather than +passing `ProcessorExecutionContext` into core APIs. Preserve exact values with +the focused immutable snapshot/identity APIs instead of round-tripping through +maps or YAML. + +The Contracts boundary moves are intentional pre-release source and binary API +changes; there is no compatibility shim in core: + +| Previous API | Current API | +|---|---| +| `blue.bex.api.ProcessorExecutionContextBexDocumentView` | `blue.bex.contracts.ProcessorExecutionContextBexDocumentView` | +| `blue.bex.api.ProcessorExecutionContextBexGasLedgerHost` | `blue.bex.contracts.ProcessorExecutionContextBexGasLedgerHost` | +| `blue.bex.output.ProcessorExecutionContextBexSemanticIdentityBoundary` | `blue.bex.contracts.ProcessorExecutionContextBexSemanticIdentityBoundary` | +| `BexExecutionContext.Builder.processorExecutionContext(context)` | `BexContractsExecutionContext.builder(context)` or `configure(builder, context)` | +| `BexExecutionContext.Builder.processorExecutionContext(context, namespace)` | `BexContractsExecutionContext.builder(context, namespace)` or `configure(builder, context, namespace)` | + +`BexGasLedgerHost` is now host-neutral: its signatures use BEX-owned +`BexGasLedgerCapability`, `BexSharedGasBudget`, `BexGasChargeContext`, and +`BexHostGasExhaustion` instead of Contracts `GasMeter.ChildGasLedger`, +`RuntimeWorkBudget`, `GasChargeContext`, and `GasLimitExceededException` types. +Core failure lifecycle decisions go through the BEX-owned `BexFailureBoundary`; +`BexContractsFailureBoundary` performs the Contracts classification/translation +at the adapter edge. + +After migration, run an import scan for the removed packages, the full ordinary +and conformance suites, Java 8 bytecode verification, API comparison, and both +focused classpath and isolated aggregate checks. diff --git a/docs/program-model.md b/docs/program-model.md new file mode 100644 index 0000000..116cfaf --- /dev/null +++ b/docs/program-model.md @@ -0,0 +1,107 @@ +# Program model + +A BEX program is a deterministic Blue object tree selected by its host. BEX does +not search a document for executable content and does not execute arbitrary +objects merely because a key begins with `$`. + +## Source forms + +`BexProgramSource.expression(node)` compiles one expression. +`BexProgramSource.inline(node)` compiles a full program. A definition-backed +source combines a selected program node, shared definition node, and entry +function. + +A full program may contain: + +```text +constants statically named literal/expression-independent values +functions statically named functions with declared argument patterns +do ordered statement list +expr root expression +``` + +Name containers are Blue object properties, so Blue-reserved language keys are +not valid user names. Functions have a fixed argument ABI: missing and extra +arguments are compile errors. Declared Blue patterns are static and are checked +after call operands evaluate. + +## Operators and literals + +An operator is an object with exactly one recognized operator key in an +operator position: + +```yaml +$concat: + - "order-" + - $document: /orderNumber +``` + +Normal objects, lists, scalars, null, and undefined form the value model. +`$literal` prevents an operator-looking object from being compiled. Static Blue +fields such as `type` and `schema` retain Blue meaning; BEX expressions are not +evaluated inside static patterns. + +The normative list of 86 operators and each operand shape is in the +[BEX 2.0 specification](../specifications/blue-bex-specification-2.0.md). +The machine-readable operator coverage catalog prevents documentation or +compiler recognition from silently drifting. + +## Evaluation order and laziness + +Operand evaluation order is defined by each operator, not by Java map iteration. +Where multiple operands are eager, they evaluate in normative order. Lazy +operators evaluate only required operands: + +- `$and` and `$or` short-circuit; +- `$coalesce` stops at its selected value; +- `$if`/`$choose` execute only the selected branch; +- collection operators execute their body in source collection order; +- `$return`, `$returnIf`, failure, and gas exhaustion stop later work. + +Skipped work performs no reads, construction, intrinsic calls, output admission, +or gas charges. Object keys use deterministic Unicode ordering where the +specification requires canonical traversal/sorting. + +## Variables, constants, functions, and scopes + +`$const` names a declared constant and is resolved at compile time. `$var` reads +a run-local variable or function argument. Names are atomic: a slash in a name +is not a pointer. `$let` initializes a name once in its scope; `$set` updates an +existing initialized variable under the language rules. + +Function call graphs are compiled before execution. Recursive cycles are +rejected. Each call has an isolated frame; collection bodies add their specified +item/key/index bindings without leaking them after the iteration. + +## Reads and effects + +Document, event, processing-event, current-contract, steps, binding, variable, +constant, and result-overlay reads are distinct operations with distinct gas. +JSON Pointers have explicit canonical/resolved/scope-relative meanings. + +Changes and events are append-only run-local data. Patch order, event order, and +duplicates are significant. `$resultValue` reads a lazy overlay of the original +document and accumulated patches; reading it does not commit the patch. + +## Compile errors versus runtime errors + +Compilation rejects defects knowable from the selected source: unknown or +malformed operators, invalid declaration names, missing constants/functions, +call ABI mismatches, invalid static operands/patterns, recursive call graphs, +and multiple operators where one is required. + +Runtime errors depend on actual data or selected control flow: missing bindings, +dynamic pointer/key/type failures, exact arithmetic failures, division by zero, +explicit failure, invalid patch/event payloads, failed intrinsics, output +admission, provider evidence, and exhaustion. + +Diagnostics should preserve error class, source path, operator, function/frame, +and safe pointer details when known. Exact message text is normative only where a +fixture says so. + +## Deliberate non-features + +BEX does not define Timeline, Mandate, Coordination, `Process Embedded`, feeder, +persistence, or contract lifecycle semantics. Contracts `collectionPaths` and +collection activation decide whether an ordinary object-producing patch creates +an embedded scope. That decision does not add a BEX operator or change BEX gas. diff --git a/docs/public-api-classification.json b/docs/public-api-classification.json index b793ab2..976ddd2 100644 --- a/docs/public-api-classification.json +++ b/docs/public-api-classification.json @@ -1,17 +1,11 @@ { - "schema": "blue-bex-public-api-classification/1.0", + "schema": "blue-bex-public-api-classification/2.0", "inventory": { "path": "src/test/resources/hosted-release/required-public-api.txt", - "sha256": "43aea6ae9de6f39729f93c146ff5453887da9a2303f6f4f33573ba47f37d5be0", + "sha256": "df602fa6b14afc285053fc8e34e349d7fa5ce26810a6c79ba14ac6361de463ee", "manifestSchema": "blue-bex-binary-api-manifest/1.0", - "publicTypeCount": 72, - "publicDescriptorCount": 798 - }, - "sourceState": { - "bexBaselineCommit": "395c484111f8c4e9e0e98d2db7f1c5b0777bd5a8", - "bexMigrationState": "working-tree delta rooted at bexBaselineCommit", - "languageExactCommit": "9a607e584ff5dd973684d35d71eb4022d946b760", - "languageVerifiedImplementationCommit": "63a9ed6a1a66d47119a80d16ed2ab0beda0d2453" + "publicTypeCount": 101, + "publicDescriptorCount": 1027 }, "classifications": { "stable API": [ @@ -26,10 +20,11 @@ "blue.bex.api.BexProgramSource$Kind", "blue.bex.api.BexStepResults", "blue.bex.api.BexStepResults$Builder", + "blue.bex.compile.BexCompilationInput", + "blue.bex.compile.BexCompilationInput$Kind", "blue.bex.compile.BexCompiledProgram", "blue.bex.compile.BexCompiledProgramCache", "blue.bex.compile.BexCompiledProgramKey", - "blue.bex.compile.BexCompiler", "blue.bex.compile.LruBexCompiledProgramCache", "blue.bex.gas.BexGasCharge", "blue.bex.gas.BexGasCounter", @@ -42,53 +37,81 @@ "blue.bex.result.BexEvents", "blue.bex.result.BexExecutionResult", "blue.bex.result.BexMetrics", + "blue.bex.result.BexMetricsSnapshot", "blue.bex.result.BexPatchEntry", "blue.bex.value.BexUnicodeOrder", "blue.bex.value.BexUnicodeOrder$Comparison", "blue.bex.value.BexValue", + "blue.bex.value.BexValueKind", "blue.bex.value.BexValues" ], "host SPI": [ "blue.bex.api.BexDocumentView", + "blue.bex.api.BexFailureBoundary", + "blue.bex.api.BexFailureBoundary$Classification", "blue.bex.api.BexGasLedgerHost", "blue.bex.api.FrozenBexDocumentView", - "blue.bex.api.ProcessorExecutionContextBexDocumentView", - "blue.bex.api.ProcessorExecutionContextBexGasLedgerHost", + "blue.bex.contracts.BexContractsExecutionContext", + "blue.bex.contracts.BexContractsFailureBoundary", + "blue.bex.contracts.ProcessorExecutionContextBexDocumentView", + "blue.bex.contracts.ProcessorExecutionContextBexGasLedgerHost", + "blue.bex.contracts.ProcessorExecutionContextBexSemanticIdentityBoundary", + "blue.bex.gas.BexGasLedgerCapability", + "blue.bex.gas.BexGasLedgerLifecycle", + "blue.bex.gas.BexHostGasExhaustion", + "blue.bex.gas.BexSharedGasBudget", "blue.bex.output.BexAdmittedValue", "blue.bex.output.BexEstablishedIdentity", + "blue.bex.output.BexFailurePolicy", "blue.bex.output.BexOutputAdmission", "blue.bex.output.BexOutputKind", "blue.bex.output.BexSemanticIdentityBoundary", - "blue.bex.output.ProcessorExecutionContextBexSemanticIdentityBoundary" + "blue.bex.runtime.BexRuntimeContext", + "blue.bex.runtime.BexStepResultView", + "blue.bex.spi.BexDocumentAccess" ], "intrinsic SPI": [ "blue.bex.api.BexIntrinsicInvocation", "blue.bex.api.BexIntrinsicProcessor", "blue.bex.api.BexIntrinsicRegistry", "blue.bex.api.BexIntrinsicRegistry$Builder", - "blue.bex.api.BexTypeBlueIdResolver" + "blue.bex.api.BexTypeBlueIdResolver", + "blue.bex.compile.BexIntrinsicCatalog", + "blue.bex.runtime.BexRuntimeIntrinsics" ], "internal implementation": [ - "blue.bex.compile.BexCompiledProgram$ArgSpec", - "blue.bex.compile.BexCompiledProgram$CompiledFunction", + "blue.bex.BexExecutionEvidenceUnavailableException", + "blue.bex.BexInvalidExecutionEvidenceException", + "blue.bex.compile.BexCompiledProgramRuntimeAccess", + "blue.bex.compile.BexCompilerRuntimeAccess", "blue.bex.compile.BexContainsCache", + "blue.bex.compile.BexExecutionMachine", "blue.bex.compile.BexNodeIdentity", + "blue.bex.compile.CompileScope", + "blue.bex.compile.CompileScope$Visibility", + "blue.bex.compile.CompiledExpression", + "blue.bex.compile.CompiledFrame", + "blue.bex.compile.CompiledStatement", + "blue.bex.compile.Control", + "blue.bex.gas.BexGasChargeContext", "blue.bex.pointer.BexPointer", "blue.bex.pointer.BexPointerCache", + "blue.bex.result.BexMetricsRecorder", "blue.bex.result.BexResultOverlay", "blue.bex.runtime.BexExecutionAccumulator", "blue.bex.runtime.BexRuntime", - "blue.bex.runtime.CompileScope", - "blue.bex.runtime.CompileScope$Visibility", - "blue.bex.runtime.CompiledExpression", - "blue.bex.runtime.CompiledFrame", - "blue.bex.runtime.CompiledStatement", - "blue.bex.runtime.Control", "blue.bex.type.BexBlueTypeMatcher", + "blue.bex.type.BexPatternValidator", + "blue.bex.type.BexTypeMatchWorkRecorder", + "blue.bex.type.BexTypeMatcher", "blue.bex.value.BexBlueNodeWriter", + "blue.bex.value.BexChangesetValueView", + "blue.bex.value.BexEventsValueView", "blue.bex.value.BexFrozenWriter", "blue.bex.value.BexNodeWriter", + "blue.bex.value.BexPatchValueView", "blue.bex.value.BexSimpleWriter", + "blue.bex.value.BexValueMetrics", "blue.bex.value.ChangesetBexValue", "blue.bex.value.EventsBexValue", "blue.bex.value.OverlayListBexValue", @@ -97,8 +120,8 @@ "conformance-only": [] }, "notes": [ - "Classification is intent metadata; required-public-api.txt remains the exact descriptor inventory.", - "Public visibility alone does not make an internal implementation type stable.", - "Conformance-only types are test-source artifacts and therefore absent from the production binary manifest." + "Every public/protected binary type is classified exactly once from same-run manifest evidence.", + "Public visibility does not promote an internal implementation type to stable API.", + "Conformance-only types are absent from runtime module JARs." ] } diff --git a/docs/release.md b/docs/release.md new file mode 100644 index 0000000..f69bdd0 --- /dev/null +++ b/docs/release.md @@ -0,0 +1,86 @@ +# Release + +BEX has separate working and public-release gates. Keeping them separate allows +downstream local integration to proceed without pretending that unpublished +Language artifacts have public provenance. + +## Working local gate + +```bash +./gradlew --no-daemon clean bexWorkingVerification \ + -PblueLanguageCompositePath=/absolute/path/to/blue-language-java +``` + +This mandatory gate uses the exact local modular Language checkout and requires +all ordinary/conformance/hosted tests, 60 vectors, 105 behavior fixtures, 30 gas +microfixtures, 86 operator checks, Java 8 bytecode, API reports, runtime smoke, +artifact construction, BEX-owned archive determinism, and current dependency and +source evidence. + +Success requires zero failed, skipped, or unclassified evidence and +`workingReady = true`. A green, reviewable working commit is a usable local +artifact checkpoint even when published Language modules do not yet exist. + +## Strict public gate + +```bash +bash .github/scripts/run-final-publication-gates.sh +``` + +The strict gate additionally requires: + +- exact compatible published Language module coordinates from controlled + repositories (never an ambiguous `mavenLocal` substitute); +- published artifact API and SHA-256 inspection; +- semantic and exact gas differential proof between authenticated local and + published dependency modes; +- two independent clean builds with isolated Gradle homes and matching artifact + hashes; +- a clean tagged BEX source state and commit-bound evidence; +- `releaseReady = true` in the final report. + +The script is the supported publication entry point. It checks out the exact +reviewed Language tag, runs the local working and modernization gates, builds +two clean standalone and two clean local-composite BEX checkouts with isolated +Gradle homes, compares their artifact manifests, derives a local/published +semantic and exact-gas differential, authenticates the resolved Language JARs, +and finally invokes `bexReleaseVerify`. + +If matching published modules or any independent evidence is absent, the gate is +truthfully red or `not-executed`. It must not be described as passing and must +not be weakened to unblock local work. + +## Artifacts and evidence + +The release surface includes the aggregate JAR, intended module JARs, sources +JAR, Javadoc JAR, source archive, API descriptors and migration ledger, +dependency lock/evidence, working report, and strict release report. All +BEX-owned archives must be reproducible. + +Reports record source commit and dirty state, Language exact/code-equivalent +commits, dependency graph and JAR hashes, registry/gas/fixture identities, +test/fixture/operator totals, semantic and gas parity, hosted boundaries, Java 8 +verification, artifact hashes, and the exact local/published mode status. +The strict decision is written to `build/reports/bex-release/final.json` and +`final.md` even when the Gradle task fails closed. + +Do not publish from a dirty tree, change `.cz.toml` as part of this work, embed a +local checkout path in published metadata, or stage downloaded archives. Version +automation and tags remain the repository's existing release process. + +## Benchmark reporting + +JMH sources belong to the benchmark suite, but a benchmark claim is valid only +when a report records JVM/CPU details, forks, warmups, measurements, allocation +data, confidence intervals, and result/gas identity checks. Merely compiling a +benchmark is not a performance result. Long benchmark campaigns do not block the +first local compatibility checkpoint, but release/modernization reports must +label unexecuted campaigns honestly. `bexModernizationVerification` consumes +the serious `results.json` plus the recorded JVM/OS/CPU/campaign environment; +the bounded `jmhSmoke` result belongs only to the working compatibility gate. + +## Reading current state + +This guide defines gates; it does not assert their latest outcome. Inspect the +current generated JSON/Markdown under `build/reports` and require the relevant +ready flag. Working success and public-release eligibility are distinct fields. diff --git a/docs/runtime-and-context.md b/docs/runtime-and-context.md new file mode 100644 index 0000000..605b57e --- /dev/null +++ b/docs/runtime-and-context.md @@ -0,0 +1,83 @@ +# Runtime and execution context + +One BEX execution combines a shareable engine and compiled program with a +run-owned execution context. The runtime evaluates deterministic instructions, +buffers result effects, accounts named gas, and admits Blue output. + +## Engine and run ownership + +`BexEngine` owns immutable configuration, intrinsic registry, gas schedule, +metrics sink, pointer cache, and compiled-program cache. It is designed for +reuse, including concurrent independent compile/execute calls. The engine is +`AutoCloseable`: it closes only a default `BlueLanguage` runtime it created; +an explicitly supplied runtime remains caller-owned. + +`BexExecutionContext` belongs to one run. Its document view, bindings, lazy +binding slots, parent/local gas configuration, host gas capability, and semantic +identity boundary must not be used as cross-run mutable state. Compiled frames, +variables, accumulator, result overlay, gas session, and metrics are likewise +run-local. + +## Document views + +`BexDocumentView` exposes canonical and resolved reads plus the current scope +path. `$document` uses the canonical view unless the operand explicitly asks for +the resolved view. Scope-relative pointers are resolved by the view; callers +should not maintain a second competing scope value. + +Standalone integrations usually use `FrozenBexDocumentView`. Hosted integrations +must use the Contracts adapter so canonical/resolved/evidence behavior comes from +the active processor invocation. + +## Bindings + +Hosts provide named `BexValue` bindings. Standard bindings include event, +processing event, current contract, and prior step results. A missing binding +reads as defined by the operator/context contract; invalid standard-binding +configuration fails during context construction. + +Lazy bindings are evaluated at most once per built context and only on demand. +Their value or failure is memoized. Cyclic lazy reads fail deterministically. +Materializing `bindings()` deliberately resolves all slots in declaration order. +Suppliers must not rely on being called, because lazy control flow may skip them. + +Inputs are non-null unless the API explicitly maps `null` to undefined or a +documented default. Prefer explicit `BexValues.undefined()` when absence is part +of the program model. + +## Frames and control flow + +The root invocation and each function call have isolated frames. Variable +initialization, update, and collection iteration follow compiled lexical rules. +Control values for return/failure never escape as ordinary BEX values. Operand, +statement, patch, and event order are normative. + +## Buffered effects and overlay + +`$appendChange` and `$appendEvent` append to run-local ordered buffers. +`$resultValue` reads a lazy overlay of the original document plus current +patches. The engine does not apply those patches and does not dispatch events. +On runtime failure or exhaustion, no successful result is returned for the host +to commit. + +## Results and metrics + +`BexExecutionResult` contains the root value, ordered changeset/events, canonical +gas ledger/trace, admitted output metadata when applicable, and defensive metric +snapshots. `gasUsed()` is derived from the trace rather than accepted as an +opaque host number. + +Wall-clock and cache metrics are diagnostic. Internal mutation is confined to a +run-owned recorder; results and `BexMetricsSink` expose immutable +`BexMetricsSnapshot` values. A throwing sink is isolated and cannot change a +successful compilation, cache update, execution, or gas-ledger completion into +an observable failure. A metrics sink should still be thread-safe if its engine +is shared. + +## Failures + +Runtime failures stop later work. The charge for already admitted work remains; +the rejected gas charge and all later work are absent. Portable failures remain +BEX-owned, while the Contracts adapter preserves host evidence, processor, and +gas classifications. See [Gas and exhaustion](gas-and-exhaustion.md) and +[Contracts hosting](contracts-hosting.md). diff --git a/docs/start-here.md b/docs/start-here.md new file mode 100644 index 0000000..626338e --- /dev/null +++ b/docs/start-here.md @@ -0,0 +1,98 @@ +# Start here + +BEX is a small deterministic language whose programs are Blue data. The Java +library compiles a selected program tree into immutable executable form and runs +it against one immutable host context. The result is data for the host to +inspect; the engine does not mutate the host document or perform external work. + +## The four objects to know + +1. `BexProgramSource` identifies either a full program, a single expression, or + a program combined with a shared definition and entry function. +2. `BexExecutionContext` supplies the document view, bindings, scope, gas, and + host boundaries for one run. +3. `BexEngine` compiles, caches, and executes. Reuse it across independent runs. +4. `BexExecutionResult` contains the returned value, ordered changeset, ordered + events, canonical gas ledger, admitted output metadata, and diagnostic + metrics. + +## Run a standalone expression + +```java +FrozenNode expression = FrozenNode.fromResolvedNode( + new Node().properties("$add", new Node().items( + new Node().value(40L), + new Node().value(2L)))); +FrozenNode emptyDocument = FrozenNode.fromResolvedNode(new Node()); + +BexExecutionContext context = BexExecutionContext.builder() + .document(new FrozenBexDocumentView(emptyDocument)) + .binding("policy", BexValues.fromSimple( + Collections.singletonMap("limit", 100L))) + .gasLimit(10_000L) + .build(); + +BexExecutionResult result = BexEngine.builder().build() + .compileAndExecute(BexProgramSource.expression(expression), context); +``` + +The complete runnable source is in the `examples` module. + +## A full program + +A full program can define constants, functions, a statement body, and/or a root +expression: + +```yaml +constants: + threshold: 400 +expr: + $gte: + - $document: /amount + - $const: threshold +``` + +An object with exactly one recognized `$...` key is an operator. Ordinary +objects and lists are literal values unless they occur in an operator-defined +operand position. `$literal` is the explicit escape when a value would otherwise +look executable. + +BEX source is still Blue source. Parse and resolve it with the supported Blue +Language authoring boundary before wrapping the selected immutable program in +`BexProgramSource`; do not ask the BEX runtime to preprocess source documents. + +## Choose the right value boundary + +- Use `BexValues.frozen(...)` for an existing immutable exact Blue value. +- Use `BexValues.exact(...)` when a host already owns the canonical identity and + resolved semantic cursor. +- Use `BexValues.fromSimple(...)` for transient maps, lists, and scalar data. +- Use a lazy context binding only when constructing the value is expensive and + the program may not read it. + +Never convert an exact value through a Java map simply to give it to BEX. That +would discard identity/provenance and add work. + +## Understand failures + +Compilation rejects unknown operators, malformed operands, unknown constants or +functions, recursive call graphs, invalid declarations, and other static +defects. Runtime failures cover dynamic pointer/type errors, arithmetic errors, +explicit `$fail`, unavailable/invalid host evidence, output admission, intrinsic +failure, and gas exhaustion. + +Execution is fail-closed. A failed run does not commit its buffered changes or +events. The host owns the larger transaction. + +## Pick an integration mode + +Standalone mode uses a `FrozenBexDocumentView`, a local/parent gas budget, and +direct ordinary Blue identity establishment. Contracts-hosted mode uses the +`blue.bex.contracts` adapter so the invocation's document views, evidence, +shared budget, semantic identity boundary, and failure classification remain +host-owned. + +Continue with [Program model](program-model.md), [Values and identity](values-and-identity.md), +and [Gas and exhaustion](gas-and-exhaustion.md). Integrators should also read +[Blue output boundary](blue-output-boundary.md) and +[Contracts hosting](contracts-hosting.md). diff --git a/docs/values-and-identity.md b/docs/values-and-identity.md new file mode 100644 index 0000000..812d9eb --- /dev/null +++ b/docs/values-and-identity.md @@ -0,0 +1,105 @@ +# Values and identity + +BEX has one semantic value model and one ordinary BlueId model. Storage or +provider representation is not part of program-visible semantics. + +## Value kinds + +Portable operations work with: + +```text +undefined +null +Boolean +Text +Integer +Decimal +object +list +exact Blue value +changeset/event helper views +``` + +`undefined` means absence and is distinct from null. Undefined object members +are omitted during Blue output conversion; undefined list members and an +undefined root cannot cross the Blue output boundary. Integers are exact and +decimals use deterministic decimal arithmetic. Non-finite floating-point input +is rejected. + +## Exact and transient values + +An exact value already has ordinary Blue identity/provenance. It may be backed +by an immutable inline node, a pure reference, or a verified materialized +cursor. Exact values cross BEX boundaries by identity: the runtime must not +recursively clone, serialize, size, or hash them just because they are passed, +returned, stored, or emitted. + +A transient value was computed in BEX. It pays only for work actually performed: +construction, traversal, comparison, sorting, strict Blue conversion, and direct +identity establishment when requested or emitted. + +Use the most truthful host boundary: + +- `BexValues.frozen` for an immutable exact node; +- `BexValues.exact` for host-established canonical/resolved identity; +- `BexValues.referenceBacked` when semantic reads may demand verified provider + materialization; +- `BexValues.fromSimple` for transient Java scalar/list/map input; +- `BexValues.nodeSnapshot` for mutable `Node` input that must be cloned/frozen. + +## Representation blindness + +A verified pure reference and its verified materialization are the same exact +BEX value. Portable observations must not reveal whether equivalent content is: + +```text +inline or referenced +eager or lazy +warm or cold +held in one provider segment or several +``` + +This applies to kind, existence, keys, entries, size, truthiness, equality, +matching, iteration, pointer access, and explicit identity. Caching may reduce +host cost or diagnostics, but it cannot alter BEX gas or results. + +A semantic read of a reference may require provider evidence. Unavailable +evidence is not silently converted to `undefined`; invalid evidence is a +deterministic failure. Merely carrying or returning the exact reference does not +require materialization. + +## One BlueId + +There is no BEX semantic ID, canonicalized-source ID, or alternate hash format. +`$nodeBlueId` follows exactly two paths: + +1. For an exact value, return its already established ordinary BlueId without + transitive expansion. +2. For a transient value, perform strict transient-to-Blue output admission and + then direct ordinary BlueId establishment through the configured boundary. + +It never runs Source preprocessing, complete Source resolution, +canonicalization, minimization, or a second identity algorithm. A transient +cyclic-set member identity cannot be invented in isolation. + +## Equality is not identity + +BEX equality follows the language's exact semantic rules, including numeric and +structural behavior. It is not a comparison of serialized Java objects and is +not interchangeable with BlueId equality. Matching uses Blue's focused matcher +boundary and preserves failures from unresolved/invalid evidence. + +Object ordering is deterministic Unicode code-point ordering where canonical +order is required. It does not depend on locale, UTF-16 code-unit quirks, or host +map iteration order. + +## Output identity + +Existing exact output crosses by identity and charges only the output boundary +work defined by the gas model. Transient output is recursively validated as +runtime Blue content, established exactly once, and retained as an admitted +exact value alongside its semantic cursor. Nested exact descendants retain their +identity; the host boundary must not reopen them for redundant hashing. + +Read [Blue output boundary](blue-output-boundary.md) for validation rules and +[Gas and exhaustion](gas-and-exhaustion.md) for the exact charge boundary. diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..c550542 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,14 @@ +# BEX examples + +`StandaloneBexExample` has a `main` method and asserts both its result and the +presence of a canonical gas trace. + +`HostedBexExample` is the runnable integration method used from an active +Contracts processor invocation. The caller supplies the selected immutable BEX +expression and a deterministic runtime namespace; the adapter binds document, +event, contract, gas, semantic-output, evidence, and failure boundaries. + +The examples intentionally construct a small immutable `Node` tree directly. +Production applications may parse authored YAML through the supported Blue +Language source boundary before selecting/finalizing a `FrozenNode`; BEX itself +does not preprocess Source documents. diff --git a/examples/build.gradle.kts b/examples/build.gradle.kts new file mode 100644 index 0000000..186c080 --- /dev/null +++ b/examples/build.gradle.kts @@ -0,0 +1,27 @@ +plugins { + application + id("blue.bex.java8-library") + id("blue.bex.language-dependencies") +} + +description = "Compile-tested standalone and Contracts-hosted BEX examples" + +dependencies { + implementation(project(":blue-bex-java")) +} + +application { + mainClass.set("blue.bex.examples.StandaloneBexExample") +} + +tasks.register("hostedConsumerSmoke") { + group = "verification" + description = "Compiles and runs a generic Contracts-hosted BEX consumer." + dependsOn(tasks.classes) + classpath = sourceSets.main.get().runtimeClasspath + mainClass.set("blue.bex.examples.HostedConsumerSmoke") +} + +tasks.check { + dependsOn("hostedConsumerSmoke") +} diff --git a/examples/src/main/java/blue/bex/examples/HostedBexExample.java b/examples/src/main/java/blue/bex/examples/HostedBexExample.java new file mode 100644 index 0000000..d5a6592 --- /dev/null +++ b/examples/src/main/java/blue/bex/examples/HostedBexExample.java @@ -0,0 +1,58 @@ +package blue.bex.examples; + +import blue.bex.api.BexEngine; +import blue.bex.api.BexExecutionContext; +import blue.bex.api.BexGasLedgerHost; +import blue.bex.api.BexProgramSource; +import blue.bex.contracts.BexContractsExecutionContext; +import blue.bex.contracts.ProcessorExecutionContextBexGasLedgerHost; +import blue.bex.contracts.ProcessorExecutionContextBexSemanticIdentityBoundary; +import blue.bex.output.BexSemanticIdentityBoundary; +import blue.bex.result.BexExecutionResult; +import blue.language.processor.ProcessorExecutionContext; +import blue.language.snapshot.FrozenNode; + +import java.util.Objects; + +/** + * Contracts-hosted integration called from an active processor invocation. + * The surrounding Contracts processor owns commit/rollback of returned effects. + */ +public final class HostedBexExample { + private final BexEngine engine; + + public HostedBexExample(BexEngine engine) { + this.engine = Objects.requireNonNull(engine, "engine"); + } + + public BexExecutionResult run( + ProcessorExecutionContext processor, + FrozenNode selectedExpression, + String runtimeNamespace) { + BexExecutionContext context = BexContractsExecutionContext + .builder( + Objects.requireNonNull(processor, "processor"), + Objects.requireNonNull( + runtimeNamespace, "runtimeNamespace")) + .build(); + BexGasLedgerHost gasHost = context.gasLedgerHost(); + BexSemanticIdentityBoundary identityBoundary = + context.semanticIdentityBoundary(); + if (!(gasHost + instanceof ProcessorExecutionContextBexGasLedgerHost)) { + throw new IllegalStateException( + "Contracts execution did not install its BEX gas adapter"); + } + if (!(identityBoundary + instanceof ProcessorExecutionContextBexSemanticIdentityBoundary)) { + throw new IllegalStateException( + "Contracts execution did not install its BEX identity adapter"); + } + + return engine.compileAndExecute( + BexProgramSource.expression( + Objects.requireNonNull( + selectedExpression, "selectedExpression")), + context); + } +} diff --git a/examples/src/main/java/blue/bex/examples/HostedConsumerSmoke.java b/examples/src/main/java/blue/bex/examples/HostedConsumerSmoke.java new file mode 100644 index 0000000..ff8da9b --- /dev/null +++ b/examples/src/main/java/blue/bex/examples/HostedConsumerSmoke.java @@ -0,0 +1,306 @@ +package blue.bex.examples; + +import blue.bex.api.BexEngine; +import blue.bex.api.BexIntrinsicRegistry; +import blue.bex.result.BexExecutionResult; +import blue.bex.value.BexValue; +import blue.bex.value.BexValues; +import blue.language.identity.DirectBlueIdCalculator; +import blue.language.model.Node; +import blue.language.processor.ContractMatchingService; +import blue.language.processor.ContractProcessorRegistry; +import blue.language.processor.ContractProcessorRegistryBuilder; +import blue.language.processor.DocumentProcessingResult; +import blue.language.processor.DocumentProcessor; +import blue.language.processor.HandlerProcessor; +import blue.language.processor.ProcessorExecutionContext; +import blue.language.processor.ProcessorStatus; +import blue.language.processor.model.HandlerContract; +import blue.language.processor.model.JsonPatch; +import blue.language.processor.registry.BlueRuntimeTypeRegistry; +import blue.language.processor.registry.RuntimeBlueIds; +import blue.language.provider.NodeProvider; +import blue.language.provider.SequentialNodeProvider; +import blue.language.runtime.BlueLanguage; + +import java.math.BigInteger; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Runnable public-API smoke for a BEX engine hosted by generic Contracts. + * + *

A real lifecycle Handler receives a {@link ProcessorExecutionContext}, + * runs BEX through the Contracts gas and semantic-identity adapters, and + * buffers the computed answer as a Contracts patch. Successful initialization + * proves that the hosted gas trace was submitted and the patch was committed + * by the processor rather than by standalone BEX code.

+ */ +public final class HostedConsumerSmoke { + private static final String HANDLER_KEY = "runBex"; + private static final String LIFECYCLE_KEY = "lifecycle"; + private static final String ANSWER_KEY = "hostedAnswer"; + private static final String RUNTIME_NAMESPACE = "example-bex"; + private static final String INTRINSIC_BLUE_ID = + "blue.example/intrinsic/add/1"; + private static final String INTRINSIC_REGISTRY_IDENTITY = + "blue.example/intrinsics/1"; + private static final String INTRINSIC_NAMESPACE = + "example-add"; + private static final String INTRINSIC_COUNTER = "addition"; + + private HostedConsumerSmoke() { + } + + /** Runs the hosted smoke and returns its deterministic evidence. */ + public static SmokeResult runSmoke() { + BexIntrinsicRegistry intrinsics = BexIntrinsicRegistry.builder() + .register( + INTRINSIC_BLUE_ID, + INTRINSIC_REGISTRY_IDENTITY, + INTRINSIC_NAMESPACE, + Collections.singletonMap( + INTRINSIC_COUNTER, 3L), + invocation -> { + invocation.charge( + INTRINSIC_COUNTER, + 1L, + "example-addition"); + BigInteger answer = invocation.field("left") + .asInteger() + .add(invocation.field("right") + .asInteger()); + Map value = + new LinkedHashMap<>(); + value.put("answer", BexValues.scalar(answer)); + value.put( + "hosted", + BexValues.scalar(true)); + return BexValues.map(value); + }) + .build(); + BexEngine engine = BexEngine.builder() + .intrinsics(intrinsics) + .build(); + HostedHandlerProcessor handler = + new HostedHandlerProcessor(engine); + + Node handlerType = new Node().name("Hosted BEX Handler"); + String handlerBlueId = + DirectBlueIdCalculator.calculateBlueId(handlerType); + ContractProcessorRegistry registry = + ContractProcessorRegistryBuilder.create() + .register( + handlerBlueId, + handlerType, + handler) + .build(); + NodeProvider provider = new SequentialNodeProvider( + BlueRuntimeTypeRegistry.getDefault() + .asProcessorSnapshotProvider(), + registry.exactTypeProvider()); + + DocumentProcessingResult processed; + try (BlueLanguage language = BlueLanguage.builder() + .nodeProvider(provider) + .build(); + DocumentProcessor contracts = DocumentProcessor.builder() + .runtimeRegistry(registry) + .nodeProvider(provider) + .matchingService(new ContractMatchingService( + language.processing().runtimeAccess())) + .build()) { + processed = contracts.initializeDocument( + rootDocument(handlerBlueId)); + } + + BexExecutionResult bexResult = handler.lastResult.get(); + require( + processed.status() == ProcessorStatus.SUCCESS, + "Contracts initialization failed: " + diagnostic(processed)); + require( + handler.invocations.get() == 1, + "expected one hosted Handler invocation"); + require(bexResult != null, "hosted Handler did not return a BEX result"); + require( + bexResult.output() != null + && bexResult.output().reconstructed() + && bexResult.output().value().isExact(), + "expected transient BEX output to cross the hosted identity boundary" + + " (output=" + bexResult.output() + + ", admittedExact=" + + (bexResult.output() != null + && bexResult.output().value().isExact()) + + ")"); + require( + bexResult.gasLedger().quantity( + INTRINSIC_NAMESPACE, + INTRINSIC_COUNTER) == 1L, + "expected the intrinsic named-gas charge"); + require( + !bexResult.gasLedger().trace().isEmpty(), + "expected a non-empty canonical BEX gas trace"); + require( + processed.totalGas() >= bexResult.gasUsed(), + "Contracts gas must include the submitted BEX child trace"); + + Node answerNode = processed.document() + .getProperties() + .get(ANSWER_KEY); + BigInteger answer = answerNode != null + && answerNode.getValue() instanceof BigInteger + ? (BigInteger) answerNode.getValue() + : null; + require( + BigInteger.valueOf(42L).equals(answer), + "expected committed hosted answer 42, got " + answer); + + return new SmokeResult( + answer, + bexResult.gasUsed(), + processed.totalGas(), + bexResult.output().nodeBlueId()); + } + + /** Runs the smoke as an executable consumer. */ + public static void main(String[] args) { + SmokeResult result = runSmoke(); + System.out.println("hostedResult=" + result.answer()); + System.out.println("bexGas=" + result.bexGas()); + System.out.println("contractsGas=" + result.contractsGas()); + System.out.println("outputBlueId=" + result.outputBlueId()); + } + + private static Node rootDocument(String handlerBlueId) { + Node lifecycle = typed(RuntimeBlueIds.LIFECYCLE_EVENT_CHANNEL); + Node handler = typed(handlerBlueId) + .properties("channel", text(LIFECYCLE_KEY)) + .properties( + "event", + typed(RuntimeBlueIds.DOCUMENT_PROCESSING_INITIATED)); + return new Node() + .name("Hosted BEX smoke") + .properties(ANSWER_KEY, integer(0L)) + .contracts(new Node() + .properties(LIFECYCLE_KEY, lifecycle) + .properties(HANDLER_KEY, handler)); + } + + private static Node typed(String blueId) { + return new Node().type(new Node().blueId(blueId)); + } + + private static Node text(String value) { + return new Node().value(value); + } + + private static Node integer(long value) { + return new Node().value(BigInteger.valueOf(value)); + } + + private static String diagnostic(DocumentProcessingResult result) { + return result.diagnostic() == null + ? result.status().name() + : result.status().name() + + ": " + result.diagnostic().message(); + } + + private static void require(boolean condition, String message) { + if (!condition) { + throw new AssertionError(message); + } + } + + /** Exact Contracts model for the lifecycle Handler used by the smoke. */ + public static final class HostedHandler extends HandlerContract { + /** Creates an empty model for Contracts mapping. */ + public HostedHandler() { + } + } + + private static final class HostedHandlerProcessor + implements HandlerProcessor { + private final HostedBexExample hosted; + private final AtomicInteger invocations = new AtomicInteger(); + private final AtomicReference lastResult = + new AtomicReference<>(); + + private HostedHandlerProcessor(BexEngine engine) { + this.hosted = new HostedBexExample(engine); + } + + @Override + public Class contractType() { + return HostedHandler.class; + } + + @Override + public void execute( + HostedHandler contract, + ProcessorExecutionContext context) { + BexExecutionResult result = hosted.run( + context, + expression(), + RUNTIME_NAMESPACE); + BigInteger answer = result.value() + .get("answer") + .asInteger(); + context.applyPatch(JsonPatch.replace( + "/" + ANSWER_KEY, + new Node().value(answer))); + lastResult.set(result); + invocations.incrementAndGet(); + } + } + + private static blue.language.snapshot.FrozenNode expression() { + Node intrinsic = new Node() + .properties( + "type", + new Node().properties( + "blueId", + text(INTRINSIC_BLUE_ID))) + .properties("left", integer(40L)) + .properties("right", integer(2L)); + return blue.language.snapshot.FrozenNode.fromResolvedNode( + new Node().properties("$intrinsic", intrinsic)); + } + + /** Immutable summary printed by the executable and asserted by tests. */ + public static final class SmokeResult { + private final BigInteger answer; + private final long bexGas; + private final long contractsGas; + private final String outputBlueId; + + private SmokeResult( + BigInteger answer, + long bexGas, + long contractsGas, + String outputBlueId) { + this.answer = answer; + this.bexGas = bexGas; + this.contractsGas = contractsGas; + this.outputBlueId = outputBlueId; + } + + public BigInteger answer() { + return answer; + } + + public long bexGas() { + return bexGas; + } + + public long contractsGas() { + return contractsGas; + } + + public String outputBlueId() { + return outputBlueId; + } + } +} diff --git a/examples/src/main/java/blue/bex/examples/StandaloneBexExample.java b/examples/src/main/java/blue/bex/examples/StandaloneBexExample.java new file mode 100644 index 0000000..4ef1a24 --- /dev/null +++ b/examples/src/main/java/blue/bex/examples/StandaloneBexExample.java @@ -0,0 +1,47 @@ +package blue.bex.examples; + +import blue.bex.api.BexEngine; +import blue.bex.api.BexExecutionContext; +import blue.bex.api.BexProgramSource; +import blue.bex.api.FrozenBexDocumentView; +import blue.bex.result.BexExecutionResult; +import blue.language.model.Node; +import blue.language.snapshot.FrozenNode; + +import java.math.BigInteger; + +/** Minimal standalone BEX program with an exact result and gas assertion. */ +public final class StandaloneBexExample { + private StandaloneBexExample() { + } + + public static void main(String[] args) { + FrozenNode expression = FrozenNode.fromResolvedNode( + new Node().properties( + "$add", + new Node().items( + new Node().value(40L), + new Node().value(2L)))); + FrozenNode document = FrozenNode.fromResolvedNode(new Node()); + + BexExecutionResult result = BexEngine.builder() + .build() + .compileAndExecute( + BexProgramSource.expression(expression), + BexExecutionContext.builder() + .document(new FrozenBexDocumentView(document)) + .gasLimit(10_000L) + .build()); + + Object value = result.value().toSimple(); + if (!BigInteger.valueOf(42L).equals(value)) { + throw new AssertionError("expected 42, got " + value); + } + if (result.gasLedger().trace().isEmpty()) { + throw new AssertionError("expected a non-empty canonical gas trace"); + } + + System.out.println("result=" + value); + System.out.println("gas=" + result.gasUsed()); + } +} diff --git a/examples/src/main/java/blue/bex/examples/package-info.java b/examples/src/main/java/blue/bex/examples/package-info.java new file mode 100644 index 0000000..c966db9 --- /dev/null +++ b/examples/src/main/java/blue/bex/examples/package-info.java @@ -0,0 +1,10 @@ +/** + * Compile-tested standalone and Contracts-hosted BEX integrations. + * + *

Example engines may be shared, while each context/result is owned by one + * run. Required inputs are non-null and examples fail fast on unexpected output. + * Hosted failures remain owned by the active processor invocation. Examples use + * real gas boundaries and never substitute benchmark timing or opaque totals + * for the canonical named trace.

+ */ +package blue.bex.examples; diff --git a/examples/src/test/java/blue/bex/examples/HostedConsumerSmokeTest.java b/examples/src/test/java/blue/bex/examples/HostedConsumerSmokeTest.java new file mode 100644 index 0000000..169dbfc --- /dev/null +++ b/examples/src/test/java/blue/bex/examples/HostedConsumerSmokeTest.java @@ -0,0 +1,22 @@ +package blue.bex.examples; + +import org.junit.jupiter.api.Test; + +import java.math.BigInteger; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class HostedConsumerSmokeTest { + @Test + void executesInsideARealContractsLifecycleInvocation() { + HostedConsumerSmoke.SmokeResult result = + HostedConsumerSmoke.runSmoke(); + + assertEquals(BigInteger.valueOf(42L), result.answer()); + assertTrue(result.bexGas() > 0L); + assertTrue(result.contractsGas() >= result.bexGas()); + assertFalse(result.outputBlueId().isEmpty()); + } +} diff --git a/gradle/verification/api/modernization-added-descriptors.txt b/gradle/verification/api/modernization-added-descriptors.txt new file mode 100644 index 0000000..caebf91 --- /dev/null +++ b/gradle/verification/api/modernization-added-descriptors.txt @@ -0,0 +1,483 @@ +class public abstract interface blue.bex.api.BexDocumentView implements blue.bex.spi.BexDocumentAccess +class public abstract interface blue.bex.api.BexDocumentView implements blue.bex.spi.BexDocumentAccess :: method public abstract canonicalAt(java.lang.String):blue.bex.value.BexValue +class public abstract interface blue.bex.api.BexDocumentView implements blue.bex.spi.BexDocumentAccess :: method public abstract currentScopePath():java.lang.String +class public abstract interface blue.bex.api.BexDocumentView implements blue.bex.spi.BexDocumentAccess :: method public abstract resolvePointer(java.lang.String):java.lang.String +class public abstract interface blue.bex.api.BexDocumentView implements blue.bex.spi.BexDocumentAccess :: method public abstract resolvedAt(java.lang.String):blue.bex.value.BexValue +class public abstract interface blue.bex.api.BexFailureBoundary implements blue.bex.output.BexFailurePolicy +class public abstract interface blue.bex.api.BexFailureBoundary implements blue.bex.output.BexFailurePolicy :: field public static final STANDALONE:blue.bex.api.BexFailureBoundary +class public abstract interface blue.bex.api.BexFailureBoundary implements blue.bex.output.BexFailurePolicy :: method public abstract classify(java.lang.Throwable):blue.bex.api.BexFailureBoundary$Classification +class public abstract interface blue.bex.api.BexFailureBoundary implements blue.bex.output.BexFailurePolicy :: method public evidenceUnavailable(java.lang.Throwable):boolean +class public abstract interface blue.bex.api.BexFailureBoundary implements blue.bex.output.BexFailurePolicy :: method public preserveOrWrap(java.lang.String,java.lang.RuntimeException):java.lang.RuntimeException +class public abstract interface blue.bex.api.BexFailureBoundary implements blue.bex.output.BexFailurePolicy :: method public translate(java.lang.RuntimeException):java.lang.RuntimeException +class public abstract interface blue.bex.api.BexGasLedgerHost implements blue.bex.gas.BexGasLedgerLifecycle +class public abstract interface blue.bex.api.BexGasLedgerHost implements blue.bex.gas.BexGasLedgerLifecycle :: method public abstract evidenceUnavailable(blue.bex.gas.BexGasLedgerCapability):void +class public abstract interface blue.bex.api.BexGasLedgerHost implements blue.bex.gas.BexGasLedgerLifecycle :: method public abstract failedDeterministically(blue.bex.gas.BexGasLedgerCapability):void +class public abstract interface blue.bex.api.BexGasLedgerHost implements blue.bex.gas.BexGasLedgerLifecycle :: method public abstract open(java.lang.String,java.util.Map):blue.bex.gas.BexGasLedgerCapability +class public abstract interface blue.bex.api.BexGasLedgerHost implements blue.bex.gas.BexGasLedgerLifecycle :: method public abstract submit(blue.bex.gas.BexGasLedgerCapability):void +class public abstract interface blue.bex.api.BexGasLedgerHost implements blue.bex.gas.BexGasLedgerLifecycle :: method public localGasLimitExceeded(blue.bex.gas.BexGasLimitExceededException,java.lang.RuntimeException):java.lang.RuntimeException +class public abstract interface blue.bex.api.BexGasLedgerHost implements blue.bex.gas.BexGasLedgerLifecycle :: method public open(java.lang.String,java.util.Map,blue.bex.gas.BexSharedGasBudget):blue.bex.gas.BexGasLedgerCapability +class public abstract interface blue.bex.api.BexGasLedgerHost implements blue.bex.gas.BexGasLedgerLifecycle :: method public openSharedBudget(long):blue.bex.gas.BexSharedGasBudget +class public abstract interface blue.bex.api.BexGasLedgerHost implements blue.bex.gas.BexGasLedgerLifecycle :: method public propagateGasExhaustion(blue.bex.gas.BexGasLedgerCapability,blue.bex.gas.BexHostGasExhaustion):void +class public abstract interface blue.bex.api.BexGasLedgerHost implements blue.bex.gas.BexGasLedgerLifecycle :: method public separatesRuntimeNamespaces():boolean +class public abstract interface blue.bex.api.BexMetricsSink :: method public abstract accept(blue.bex.result.BexMetricsSnapshot):void +class public abstract interface blue.bex.compile.BexCompilationInput +class public abstract interface blue.bex.compile.BexCompilationInput :: method public abstract definitionNode():java.util.Optional +class public abstract interface blue.bex.compile.BexCompilationInput :: method public abstract entry():java.util.Optional +class public abstract interface blue.bex.compile.BexCompilationInput :: method public abstract isExpression():boolean +class public abstract interface blue.bex.compile.BexCompilationInput :: method public abstract programNode():blue.language.snapshot.FrozenNode +class public abstract interface blue.bex.compile.BexExecutionMachine +class public abstract interface blue.bex.compile.BexExecutionMachine :: method public abstract appendChange(blue.bex.result.BexPatchEntry):void +class public abstract interface blue.bex.compile.BexExecutionMachine :: method public abstract appendEvent(blue.bex.value.BexValue):void +class public abstract interface blue.bex.compile.BexExecutionMachine :: method public abstract canonicalPointer(java.lang.String):java.lang.String +class public abstract interface blue.bex.compile.BexExecutionMachine :: method public abstract changesetValue():blue.bex.value.BexValue +class public abstract interface blue.bex.compile.BexExecutionMachine :: method public abstract defaultResultValue():blue.bex.value.BexValue +class public abstract interface blue.bex.compile.BexExecutionMachine :: method public abstract eventsValue():blue.bex.value.BexValue +class public abstract interface blue.bex.compile.BexExecutionMachine :: method public abstract gas():blue.bex.gas.BexGasMeter +class public abstract interface blue.bex.compile.BexExecutionMachine :: method public abstract invokeIntrinsic(java.lang.String,blue.bex.value.BexValue,java.util.Map):blue.bex.value.BexValue +class public abstract interface blue.bex.compile.BexExecutionMachine :: method public abstract matchesType(blue.bex.value.BexValue,blue.language.snapshot.FrozenNode,blue.bex.BexSourcePath):boolean +class public abstract interface blue.bex.compile.BexExecutionMachine :: method public abstract metrics():blue.bex.result.BexMetricsRecorder +class public abstract interface blue.bex.compile.BexExecutionMachine :: method public abstract nodeBlueId(blue.bex.value.BexValue):blue.bex.value.BexValue +class public abstract interface blue.bex.compile.BexExecutionMachine :: method public abstract parseDynamicPointer(java.lang.String):java.util.List +class public abstract interface blue.bex.compile.BexExecutionMachine :: method public abstract program():blue.bex.compile.BexCompiledProgram +class public abstract interface blue.bex.compile.BexExecutionMachine :: method public abstract readBinding(java.lang.String,java.util.List):blue.bex.value.BexValue +class public abstract interface blue.bex.compile.BexExecutionMachine :: method public abstract readCurrentContract(java.util.List):blue.bex.value.BexValue +class public abstract interface blue.bex.compile.BexExecutionMachine :: method public abstract readDocument(java.lang.String,java.util.List,boolean):blue.bex.value.BexValue +class public abstract interface blue.bex.compile.BexExecutionMachine :: method public abstract readEvent(java.util.List):blue.bex.value.BexValue +class public abstract interface blue.bex.compile.BexExecutionMachine :: method public abstract readProcessingEvent(java.util.List):blue.bex.value.BexValue +class public abstract interface blue.bex.compile.BexExecutionMachine :: method public abstract readResultValue(java.lang.String,java.util.List):blue.bex.value.BexValue +class public abstract interface blue.bex.compile.BexExecutionMachine :: method public abstract readSteps(java.lang.String,java.util.List):blue.bex.value.BexValue +class public abstract interface blue.bex.compile.BexExecutionMachine :: method public abstract readValuePointer(blue.bex.value.BexValue,java.util.List):blue.bex.value.BexValue +class public abstract interface blue.bex.compile.BexExecutionMachine :: method public abstract resolvePointer(java.lang.String):java.lang.String +class public abstract interface blue.bex.compile.BexIntrinsicCatalog +class public abstract interface blue.bex.compile.BexIntrinsicCatalog :: method public abstract supports(java.lang.String):boolean +class public abstract interface blue.bex.compile.CompiledExpression +class public abstract interface blue.bex.compile.CompiledExpression :: method public abstract eval(blue.bex.compile.CompiledFrame):blue.bex.value.BexValue +class public abstract interface blue.bex.compile.CompiledStatement +class public abstract interface blue.bex.compile.CompiledStatement :: method public abstract exec(blue.bex.compile.CompiledFrame):blue.bex.compile.Control +class public abstract interface blue.bex.gas.BexGasLedgerCapability +class public abstract interface blue.bex.gas.BexGasLedgerCapability :: method public abstract charge(java.lang.String,long,blue.bex.gas.BexGasChargeContext):void +class public abstract interface blue.bex.gas.BexGasLedgerCapability :: method public abstract counterWeights():java.util.Map +class public abstract interface blue.bex.gas.BexGasLedgerCapability :: method public abstract effectiveBudget():long +class public abstract interface blue.bex.gas.BexGasLedgerCapability :: method public abstract namespace():java.lang.String +class public abstract interface blue.bex.gas.BexGasLedgerCapability :: method public abstract remainingGas():long +class public abstract interface blue.bex.gas.BexGasLedgerCapability :: method public abstract totalGas():long +class public abstract interface blue.bex.gas.BexGasLedgerCapability :: method public charge(java.lang.String,long):void +class public abstract interface blue.bex.gas.BexGasLedgerLifecycle +class public abstract interface blue.bex.gas.BexGasLedgerLifecycle :: method public abstract evidenceUnavailable(blue.bex.gas.BexGasLedgerCapability):void +class public abstract interface blue.bex.gas.BexGasLedgerLifecycle :: method public abstract failedDeterministically(blue.bex.gas.BexGasLedgerCapability):void +class public abstract interface blue.bex.gas.BexGasLedgerLifecycle :: method public abstract open(java.lang.String,java.util.Map):blue.bex.gas.BexGasLedgerCapability +class public abstract interface blue.bex.gas.BexGasLedgerLifecycle :: method public abstract submit(blue.bex.gas.BexGasLedgerCapability):void +class public abstract interface blue.bex.gas.BexGasLedgerLifecycle :: method public localGasLimitExceeded(blue.bex.gas.BexGasLimitExceededException,java.lang.RuntimeException):java.lang.RuntimeException +class public abstract interface blue.bex.gas.BexGasLedgerLifecycle :: method public open(java.lang.String,java.util.Map,blue.bex.gas.BexSharedGasBudget):blue.bex.gas.BexGasLedgerCapability +class public abstract interface blue.bex.gas.BexGasLedgerLifecycle :: method public openSharedBudget(long):blue.bex.gas.BexSharedGasBudget +class public abstract interface blue.bex.gas.BexGasLedgerLifecycle :: method public propagateGasExhaustion(blue.bex.gas.BexGasLedgerCapability,blue.bex.gas.BexHostGasExhaustion):void +class public abstract interface blue.bex.gas.BexGasLedgerLifecycle :: method public separatesRuntimeNamespaces():boolean +class public abstract interface blue.bex.gas.BexSharedGasBudget +class public abstract interface blue.bex.gas.BexSharedGasBudget :: method public abstract admittedGas():long +class public abstract interface blue.bex.gas.BexSharedGasBudget :: method public abstract maximumGas():long +class public abstract interface blue.bex.gas.BexSharedGasBudget :: method public abstract remainingGas():long +class public abstract interface blue.bex.output.BexFailurePolicy +class public abstract interface blue.bex.output.BexFailurePolicy :: field public static final STANDALONE:blue.bex.output.BexFailurePolicy +class public abstract interface blue.bex.output.BexFailurePolicy :: method public abstract evidenceUnavailable(java.lang.Throwable):boolean +class public abstract interface blue.bex.output.BexFailurePolicy :: method public preserveOrWrap(java.lang.String,java.lang.RuntimeException):java.lang.RuntimeException +class public abstract interface blue.bex.output.BexFailurePolicy :: method public translate(java.lang.RuntimeException):java.lang.RuntimeException +class public abstract interface blue.bex.runtime.BexRuntimeContext +class public abstract interface blue.bex.runtime.BexRuntimeContext :: method public abstract binding(java.lang.String):blue.bex.value.BexValue +class public abstract interface blue.bex.runtime.BexRuntimeContext :: method public abstract currentContract():blue.bex.value.BexValue +class public abstract interface blue.bex.runtime.BexRuntimeContext :: method public abstract currentScopePath():java.lang.String +class public abstract interface blue.bex.runtime.BexRuntimeContext :: method public abstract document():blue.bex.spi.BexDocumentAccess +class public abstract interface blue.bex.runtime.BexRuntimeContext :: method public abstract event():blue.bex.value.BexValue +class public abstract interface blue.bex.runtime.BexRuntimeContext :: method public abstract failureBoundary():blue.bex.output.BexFailurePolicy +class public abstract interface blue.bex.runtime.BexRuntimeContext :: method public abstract gasLedgerHost():blue.bex.gas.BexGasLedgerLifecycle +class public abstract interface blue.bex.runtime.BexRuntimeContext :: method public abstract gasLimit():long +class public abstract interface blue.bex.runtime.BexRuntimeContext :: method public abstract parentRemainingGas():long +class public abstract interface blue.bex.runtime.BexRuntimeContext :: method public abstract processingEvent():blue.bex.value.BexValue +class public abstract interface blue.bex.runtime.BexRuntimeContext :: method public abstract semanticIdentityBoundary():blue.bex.output.BexSemanticIdentityBoundary +class public abstract interface blue.bex.runtime.BexRuntimeContext :: method public abstract steps():blue.bex.runtime.BexStepResultView +class public abstract interface blue.bex.runtime.BexRuntimeIntrinsics +class public abstract interface blue.bex.runtime.BexRuntimeIntrinsics :: field public static final EMPTY:blue.bex.runtime.BexRuntimeIntrinsics +class public abstract interface blue.bex.runtime.BexRuntimeIntrinsics :: method public abstract invoke(java.lang.String,blue.bex.value.BexValue,java.util.Map,blue.bex.gas.BexGasMeter,blue.bex.output.BexOutputAdmission):blue.bex.value.BexValue +class public abstract interface blue.bex.runtime.BexRuntimeIntrinsics :: method public abstract registeredNamedWeights(java.util.Set):java.util.Map +class public abstract interface blue.bex.runtime.BexRuntimeIntrinsics :: method public abstract registeredNamespaceWeights(java.util.Set):java.util.Map +class public abstract interface blue.bex.runtime.BexStepResultView +class public abstract interface blue.bex.runtime.BexStepResultView :: method public abstract asValue():blue.bex.value.BexValue +class public abstract interface blue.bex.runtime.BexStepResultView :: method public abstract step(java.lang.String):blue.bex.value.BexValue +class public abstract interface blue.bex.spi.BexDocumentAccess +class public abstract interface blue.bex.spi.BexDocumentAccess :: method public abstract canonicalAt(java.lang.String):blue.bex.value.BexValue +class public abstract interface blue.bex.spi.BexDocumentAccess :: method public abstract currentScopePath():java.lang.String +class public abstract interface blue.bex.spi.BexDocumentAccess :: method public abstract resolvePointer(java.lang.String):java.lang.String +class public abstract interface blue.bex.spi.BexDocumentAccess :: method public abstract resolvedAt(java.lang.String):blue.bex.value.BexValue +class public abstract interface blue.bex.value.BexChangesetValueView +class public abstract interface blue.bex.value.BexChangesetValueView :: method public abstract entries():java.util.List +class public abstract interface blue.bex.value.BexEventsValueView +class public abstract interface blue.bex.value.BexEventsValueView :: method public abstract events():java.util.List +class public abstract interface blue.bex.value.BexPatchValueView +class public abstract interface blue.bex.value.BexPatchValueView :: method public abstract absolutePath():java.lang.String +class public abstract interface blue.bex.value.BexPatchValueView :: method public abstract op():java.lang.String +class public abstract interface blue.bex.value.BexPatchValueView :: method public abstract val():blue.bex.value.BexValue +class public abstract interface blue.bex.value.BexValue :: method public semanticKind():blue.bex.value.BexValueKind +class public abstract interface blue.bex.value.BexValueMetrics +class public abstract interface blue.bex.value.BexValueMetrics :: method public abstract incrementFrozenOutputConversions():void +class public abstract interface blue.bex.value.BexValueMetrics :: method public abstract incrementFrozenWriterNodeFallbacks():void +class public final blue.bex.BexExecutionEvidenceUnavailableException extends blue.bex.BexException +class public final blue.bex.BexExecutionEvidenceUnavailableException extends blue.bex.BexException :: constructor public (java.lang.String) +class public final blue.bex.BexExecutionEvidenceUnavailableException extends blue.bex.BexException :: constructor public (java.lang.String,java.util.Collection) +class public final blue.bex.BexExecutionEvidenceUnavailableException extends blue.bex.BexException :: method public requiredExactBlueIds():java.util.List +class public final blue.bex.BexInvalidExecutionEvidenceException extends blue.bex.BexException +class public final blue.bex.BexInvalidExecutionEvidenceException extends blue.bex.BexException :: constructor public (java.lang.String) +class public final blue.bex.api.BexEngine implements java.lang.AutoCloseable +class public final blue.bex.api.BexEngine implements java.lang.AutoCloseable :: method public close():void +class public final blue.bex.api.BexEngine implements java.lang.AutoCloseable :: method public compile(blue.bex.api.BexProgramSource):blue.bex.compile.BexCompiledProgram +class public final blue.bex.api.BexEngine implements java.lang.AutoCloseable :: method public compileAndExecute(blue.bex.api.BexProgramSource,blue.bex.api.BexExecutionContext):blue.bex.result.BexExecutionResult +class public final blue.bex.api.BexEngine implements java.lang.AutoCloseable :: method public execute(blue.bex.compile.BexCompiledProgram,blue.bex.api.BexExecutionContext):blue.bex.result.BexExecutionResult +class public final blue.bex.api.BexEngine implements java.lang.AutoCloseable :: method public static builder():blue.bex.api.BexEngine$Builder +class public final blue.bex.api.BexExecutionContext implements blue.bex.runtime.BexRuntimeContext +class public final blue.bex.api.BexExecutionContext implements blue.bex.runtime.BexRuntimeContext :: method public binding(java.lang.String):blue.bex.value.BexValue +class public final blue.bex.api.BexExecutionContext implements blue.bex.runtime.BexRuntimeContext :: method public bindings():java.util.Map +class public final blue.bex.api.BexExecutionContext implements blue.bex.runtime.BexRuntimeContext :: method public currentContract():blue.bex.value.BexValue +class public final blue.bex.api.BexExecutionContext implements blue.bex.runtime.BexRuntimeContext :: method public currentScopePath():java.lang.String +class public final blue.bex.api.BexExecutionContext implements blue.bex.runtime.BexRuntimeContext :: method public document():blue.bex.api.BexDocumentView +class public final blue.bex.api.BexExecutionContext implements blue.bex.runtime.BexRuntimeContext :: method public event():blue.bex.value.BexValue +class public final blue.bex.api.BexExecutionContext implements blue.bex.runtime.BexRuntimeContext :: method public failureBoundary():blue.bex.api.BexFailureBoundary +class public final blue.bex.api.BexExecutionContext implements blue.bex.runtime.BexRuntimeContext :: method public gasLedgerHost():blue.bex.api.BexGasLedgerHost +class public final blue.bex.api.BexExecutionContext implements blue.bex.runtime.BexRuntimeContext :: method public gasLimit():long +class public final blue.bex.api.BexExecutionContext implements blue.bex.runtime.BexRuntimeContext :: method public parentRemainingGas():long +class public final blue.bex.api.BexExecutionContext implements blue.bex.runtime.BexRuntimeContext :: method public processingEvent():blue.bex.value.BexValue +class public final blue.bex.api.BexExecutionContext implements blue.bex.runtime.BexRuntimeContext :: method public semanticIdentityBoundary():blue.bex.output.BexSemanticIdentityBoundary +class public final blue.bex.api.BexExecutionContext implements blue.bex.runtime.BexRuntimeContext :: method public static builder():blue.bex.api.BexExecutionContext$Builder +class public final blue.bex.api.BexExecutionContext implements blue.bex.runtime.BexRuntimeContext :: method public steps():blue.bex.api.BexStepResults +class public final blue.bex.api.BexExecutionContext implements blue.bex.runtime.BexRuntimeContext :: method public volatile document():blue.bex.spi.BexDocumentAccess synthetic bridge +class public final blue.bex.api.BexExecutionContext implements blue.bex.runtime.BexRuntimeContext :: method public volatile failureBoundary():blue.bex.output.BexFailurePolicy synthetic bridge +class public final blue.bex.api.BexExecutionContext implements blue.bex.runtime.BexRuntimeContext :: method public volatile gasLedgerHost():blue.bex.gas.BexGasLedgerLifecycle synthetic bridge +class public final blue.bex.api.BexExecutionContext implements blue.bex.runtime.BexRuntimeContext :: method public volatile steps():blue.bex.runtime.BexStepResultView synthetic bridge +class public final blue.bex.api.BexIntrinsicRegistry implements blue.bex.compile.BexIntrinsicCatalog,blue.bex.runtime.BexRuntimeIntrinsics +class public final blue.bex.api.BexIntrinsicRegistry implements blue.bex.compile.BexIntrinsicCatalog,blue.bex.runtime.BexRuntimeIntrinsics :: method public identity():java.lang.String +class public final blue.bex.api.BexIntrinsicRegistry implements blue.bex.compile.BexIntrinsicCatalog,blue.bex.runtime.BexRuntimeIntrinsics :: method public invoke(java.lang.String,blue.bex.value.BexValue,java.util.Map,blue.bex.gas.BexGasMeter,blue.bex.output.BexOutputAdmission):blue.bex.value.BexValue +class public final blue.bex.api.BexIntrinsicRegistry implements blue.bex.compile.BexIntrinsicCatalog,blue.bex.runtime.BexRuntimeIntrinsics :: method public registeredNamedWeights():java.util.Map +class public final blue.bex.api.BexIntrinsicRegistry implements blue.bex.compile.BexIntrinsicCatalog,blue.bex.runtime.BexRuntimeIntrinsics :: method public registeredNamedWeights(java.util.Set):java.util.Map +class public final blue.bex.api.BexIntrinsicRegistry implements blue.bex.compile.BexIntrinsicCatalog,blue.bex.runtime.BexRuntimeIntrinsics :: method public registeredNamespaceWeights():java.util.Map +class public final blue.bex.api.BexIntrinsicRegistry implements blue.bex.compile.BexIntrinsicCatalog,blue.bex.runtime.BexRuntimeIntrinsics :: method public registeredNamespaceWeights(java.util.Set):java.util.Map +class public final blue.bex.api.BexIntrinsicRegistry implements blue.bex.compile.BexIntrinsicCatalog,blue.bex.runtime.BexRuntimeIntrinsics :: method public static builder():blue.bex.api.BexIntrinsicRegistry$Builder +class public final blue.bex.api.BexIntrinsicRegistry implements blue.bex.compile.BexIntrinsicCatalog,blue.bex.runtime.BexRuntimeIntrinsics :: method public static empty():blue.bex.api.BexIntrinsicRegistry +class public final blue.bex.api.BexIntrinsicRegistry implements blue.bex.compile.BexIntrinsicCatalog,blue.bex.runtime.BexRuntimeIntrinsics :: method public supportedBlueIds():java.util.Set +class public final blue.bex.api.BexIntrinsicRegistry implements blue.bex.compile.BexIntrinsicCatalog,blue.bex.runtime.BexRuntimeIntrinsics :: method public supports(java.lang.String):boolean +class public final blue.bex.api.BexIntrinsicRegistry implements blue.bex.compile.BexIntrinsicCatalog,blue.bex.runtime.BexRuntimeIntrinsics :: method public with(java.lang.Class,blue.bex.api.BexTypeBlueIdResolver,java.lang.String,java.util.Map,blue.bex.api.BexIntrinsicProcessor):blue.bex.api.BexIntrinsicRegistry +class public final blue.bex.api.BexIntrinsicRegistry implements blue.bex.compile.BexIntrinsicCatalog,blue.bex.runtime.BexRuntimeIntrinsics :: method public with(java.lang.Class,java.lang.String,java.util.Map,blue.bex.api.BexIntrinsicProcessor):blue.bex.api.BexIntrinsicRegistry +class public final blue.bex.api.BexIntrinsicRegistry implements blue.bex.compile.BexIntrinsicCatalog,blue.bex.runtime.BexRuntimeIntrinsics :: method public with(java.lang.String,java.lang.String,java.util.Map,blue.bex.api.BexIntrinsicProcessor):blue.bex.api.BexIntrinsicRegistry +class public final blue.bex.api.BexProgramSource implements blue.bex.compile.BexCompilationInput +class public final blue.bex.api.BexProgramSource implements blue.bex.compile.BexCompilationInput :: method public definitionNode():java.util.Optional +class public final blue.bex.api.BexProgramSource implements blue.bex.compile.BexCompilationInput :: method public entry():java.util.Optional +class public final blue.bex.api.BexProgramSource implements blue.bex.compile.BexCompilationInput :: method public isExpression():boolean +class public final blue.bex.api.BexProgramSource implements blue.bex.compile.BexCompilationInput :: method public kind():blue.bex.api.BexProgramSource$Kind +class public final blue.bex.api.BexProgramSource implements blue.bex.compile.BexCompilationInput :: method public programNode():blue.language.snapshot.FrozenNode +class public final blue.bex.api.BexProgramSource implements blue.bex.compile.BexCompilationInput :: method public static expression(blue.language.snapshot.FrozenNode):blue.bex.api.BexProgramSource +class public final blue.bex.api.BexProgramSource implements blue.bex.compile.BexCompilationInput :: method public static inline(blue.language.snapshot.FrozenNode):blue.bex.api.BexProgramSource +class public final blue.bex.api.BexProgramSource implements blue.bex.compile.BexCompilationInput :: method public static withDefinition(blue.language.snapshot.FrozenNode,blue.language.snapshot.FrozenNode,java.lang.String):blue.bex.api.BexProgramSource +class public final blue.bex.api.BexStepResults implements blue.bex.runtime.BexStepResultView +class public final blue.bex.api.BexStepResults implements blue.bex.runtime.BexStepResultView :: method public asValue():blue.bex.value.BexValue +class public final blue.bex.api.BexStepResults implements blue.bex.runtime.BexStepResultView :: method public static builder():blue.bex.api.BexStepResults$Builder +class public final blue.bex.api.BexStepResults implements blue.bex.runtime.BexStepResultView :: method public static empty():blue.bex.api.BexStepResults +class public final blue.bex.api.BexStepResults implements blue.bex.runtime.BexStepResultView :: method public step(java.lang.String):blue.bex.value.BexValue +class public final blue.bex.compile.BexCompiledProgram :: method public compilationEnvironmentIdentity():java.lang.String +class public final blue.bex.compile.BexCompiledProgramKey :: constructor public (blue.bex.compile.BexCompilationInput$Kind,java.lang.String,java.lang.String,java.lang.String) +class public final blue.bex.compile.BexCompiledProgramKey :: constructor public (blue.bex.compile.BexCompilationInput$Kind,java.lang.String,java.lang.String,java.lang.String,java.lang.String) +class public final blue.bex.compile.BexCompiledProgramKey :: method public kind():blue.bex.compile.BexCompilationInput$Kind +class public final blue.bex.compile.BexCompiledProgramKey :: method public static from(blue.bex.compile.BexCompilationInput):blue.bex.compile.BexCompiledProgramKey +class public final blue.bex.compile.BexCompiledProgramKey :: method public static from(blue.bex.compile.BexCompilationInput,java.lang.String):blue.bex.compile.BexCompiledProgramKey +class public final blue.bex.compile.BexCompiledProgramRuntimeAccess +class public final blue.bex.compile.BexCompiledProgramRuntimeAccess :: method public static execute(blue.bex.compile.BexCompiledProgram,blue.bex.compile.BexExecutionMachine):blue.bex.value.BexValue +class public final blue.bex.compile.BexCompilerRuntimeAccess +class public final blue.bex.compile.BexCompilerRuntimeAccess :: method public static compile(blue.bex.compile.BexCompilationInput,blue.bex.result.BexMetricsRecorder,blue.bex.compile.BexIntrinsicCatalog,java.lang.String):blue.bex.compile.BexCompiledProgram +class public final blue.bex.compile.BexContainsCache :: method public synchronized containsBex(blue.language.snapshot.FrozenNode,blue.bex.result.BexMetricsRecorder):boolean +class public final blue.bex.compile.CompileScope +class public final blue.bex.compile.CompileScope :: constructor public () +class public final blue.bex.compile.CompileScope :: constructor public (blue.bex.compile.CompileScope) +class public final blue.bex.compile.CompileScope :: method public captureVisibility():blue.bex.compile.CompileScope$Visibility +class public final blue.bex.compile.CompileScope :: method public declareOrGetSlot(java.lang.String):int +class public final blue.bex.compile.CompileScope :: method public frameSize():int +class public final blue.bex.compile.CompileScope :: method public hasSlot(java.lang.String):boolean +class public final blue.bex.compile.CompileScope :: method public resolveSlot(java.lang.String):int +class public final blue.bex.compile.CompileScope :: method public restoreVisibility(blue.bex.compile.CompileScope$Visibility):void +class public final blue.bex.compile.CompiledFrame +class public final blue.bex.compile.CompiledFrame :: constructor public (blue.bex.compile.BexExecutionMachine,int,blue.bex.compile.CompiledFrame) +class public final blue.bex.compile.CompiledFrame :: method public appendChange(blue.bex.result.BexPatchEntry):void +class public final blue.bex.compile.CompiledFrame :: method public appendEvent(blue.bex.value.BexValue):void +class public final blue.bex.compile.CompiledFrame :: method public changesetValue():blue.bex.value.BexValue +class public final blue.bex.compile.CompiledFrame :: method public clear(int):void +class public final blue.bex.compile.CompiledFrame :: method public enter(blue.bex.BexSourcePath):blue.bex.BexSourcePath +class public final blue.bex.compile.CompiledFrame :: method public eventsValue():blue.bex.value.BexValue +class public final blue.bex.compile.CompiledFrame :: method public get(int):blue.bex.value.BexValue +class public final blue.bex.compile.CompiledFrame :: method public getRequired(int):blue.bex.value.BexValue +class public final blue.bex.compile.CompiledFrame :: method public isInitialized(int):boolean +class public final blue.bex.compile.CompiledFrame :: method public machine():blue.bex.compile.BexExecutionMachine +class public final blue.bex.compile.CompiledFrame :: method public parent():blue.bex.compile.CompiledFrame +class public final blue.bex.compile.CompiledFrame :: method public readBinding(java.lang.String,java.util.List):blue.bex.value.BexValue +class public final blue.bex.compile.CompiledFrame :: method public readCurrentContract(java.util.List):blue.bex.value.BexValue +class public final blue.bex.compile.CompiledFrame :: method public readDocument(java.lang.String,java.util.List,boolean):blue.bex.value.BexValue +class public final blue.bex.compile.CompiledFrame :: method public readEvent(java.util.List):blue.bex.value.BexValue +class public final blue.bex.compile.CompiledFrame :: method public readProcessingEvent(java.util.List):blue.bex.value.BexValue +class public final blue.bex.compile.CompiledFrame :: method public restore(blue.bex.BexSourcePath):void +class public final blue.bex.compile.CompiledFrame :: method public returnValue():blue.bex.value.BexValue +class public final blue.bex.compile.CompiledFrame :: method public returnValue(blue.bex.value.BexValue):void +class public final blue.bex.compile.CompiledFrame :: method public set(int,blue.bex.value.BexValue):void +class public final blue.bex.compile.CompiledFrame :: method public sourcePath():blue.bex.BexSourcePath +class public final blue.bex.compile.Control extends java.lang.Enum +class public final blue.bex.compile.Control extends java.lang.Enum :: field public static final CONTINUE:blue.bex.compile.Control +class public final blue.bex.compile.Control extends java.lang.Enum :: field public static final RETURN:blue.bex.compile.Control +class public final blue.bex.compile.Control extends java.lang.Enum :: method public static valueOf(java.lang.String):blue.bex.compile.Control +class public final blue.bex.compile.Control extends java.lang.Enum :: method public static values():blue.bex.compile.Control[] +class public final blue.bex.contracts.BexContractsExecutionContext +class public final blue.bex.contracts.BexContractsExecutionContext :: method public static builder(blue.language.processor.ProcessorExecutionContext):blue.bex.api.BexExecutionContext$Builder +class public final blue.bex.contracts.BexContractsExecutionContext :: method public static builder(blue.language.processor.ProcessorExecutionContext,java.lang.String):blue.bex.api.BexExecutionContext$Builder +class public final blue.bex.contracts.BexContractsExecutionContext :: method public static configure(blue.bex.api.BexExecutionContext$Builder,blue.language.processor.ProcessorExecutionContext):blue.bex.api.BexExecutionContext$Builder +class public final blue.bex.contracts.BexContractsExecutionContext :: method public static configure(blue.bex.api.BexExecutionContext$Builder,blue.language.processor.ProcessorExecutionContext,java.lang.String):blue.bex.api.BexExecutionContext$Builder +class public final blue.bex.contracts.BexContractsFailureBoundary implements blue.bex.api.BexFailureBoundary +class public final blue.bex.contracts.BexContractsFailureBoundary implements blue.bex.api.BexFailureBoundary :: field public static final INSTANCE:blue.bex.contracts.BexContractsFailureBoundary +class public final blue.bex.contracts.BexContractsFailureBoundary implements blue.bex.api.BexFailureBoundary :: method public classify(java.lang.Throwable):blue.bex.api.BexFailureBoundary$Classification +class public final blue.bex.contracts.BexContractsFailureBoundary implements blue.bex.api.BexFailureBoundary :: method public translate(java.lang.RuntimeException):java.lang.RuntimeException +class public final blue.bex.contracts.ProcessorExecutionContextBexDocumentView implements blue.bex.api.BexDocumentView +class public final blue.bex.contracts.ProcessorExecutionContextBexDocumentView implements blue.bex.api.BexDocumentView :: constructor public (blue.language.processor.ProcessorExecutionContext) +class public final blue.bex.contracts.ProcessorExecutionContextBexDocumentView implements blue.bex.api.BexDocumentView :: method public canonicalAt(java.lang.String):blue.bex.value.BexValue +class public final blue.bex.contracts.ProcessorExecutionContextBexDocumentView implements blue.bex.api.BexDocumentView :: method public currentScopePath():java.lang.String +class public final blue.bex.contracts.ProcessorExecutionContextBexDocumentView implements blue.bex.api.BexDocumentView :: method public resolvePointer(java.lang.String):java.lang.String +class public final blue.bex.contracts.ProcessorExecutionContextBexDocumentView implements blue.bex.api.BexDocumentView :: method public resolvedAt(java.lang.String):blue.bex.value.BexValue +class public final blue.bex.contracts.ProcessorExecutionContextBexGasLedgerHost implements blue.bex.api.BexGasLedgerHost +class public final blue.bex.contracts.ProcessorExecutionContextBexGasLedgerHost implements blue.bex.api.BexGasLedgerHost :: constructor public (blue.language.processor.ProcessorExecutionContext) +class public final blue.bex.contracts.ProcessorExecutionContextBexGasLedgerHost implements blue.bex.api.BexGasLedgerHost :: constructor public (blue.language.processor.ProcessorExecutionContext,java.lang.String) +class public final blue.bex.contracts.ProcessorExecutionContextBexGasLedgerHost implements blue.bex.api.BexGasLedgerHost :: constructor public (blue.language.processor.RuntimeWorkSession,java.lang.String) +class public final blue.bex.contracts.ProcessorExecutionContextBexGasLedgerHost implements blue.bex.api.BexGasLedgerHost :: method public evidenceUnavailable(blue.bex.gas.BexGasLedgerCapability):void +class public final blue.bex.contracts.ProcessorExecutionContextBexGasLedgerHost implements blue.bex.api.BexGasLedgerHost :: method public failedDeterministically(blue.bex.gas.BexGasLedgerCapability):void +class public final blue.bex.contracts.ProcessorExecutionContextBexGasLedgerHost implements blue.bex.api.BexGasLedgerHost :: method public localGasLimitExceeded(blue.bex.gas.BexGasLimitExceededException,java.lang.RuntimeException):java.lang.RuntimeException +class public final blue.bex.contracts.ProcessorExecutionContextBexGasLedgerHost implements blue.bex.api.BexGasLedgerHost :: method public open(java.lang.String,java.util.Map):blue.bex.gas.BexGasLedgerCapability +class public final blue.bex.contracts.ProcessorExecutionContextBexGasLedgerHost implements blue.bex.api.BexGasLedgerHost :: method public open(java.lang.String,java.util.Map,blue.bex.gas.BexSharedGasBudget):blue.bex.gas.BexGasLedgerCapability +class public final blue.bex.contracts.ProcessorExecutionContextBexGasLedgerHost implements blue.bex.api.BexGasLedgerHost :: method public openSharedBudget(long):blue.bex.gas.BexSharedGasBudget +class public final blue.bex.contracts.ProcessorExecutionContextBexGasLedgerHost implements blue.bex.api.BexGasLedgerHost :: method public physicalNamespace(java.lang.String):java.lang.String +class public final blue.bex.contracts.ProcessorExecutionContextBexGasLedgerHost implements blue.bex.api.BexGasLedgerHost :: method public propagateGasExhaustion(blue.bex.gas.BexGasLedgerCapability,blue.bex.gas.BexHostGasExhaustion):void +class public final blue.bex.contracts.ProcessorExecutionContextBexGasLedgerHost implements blue.bex.api.BexGasLedgerHost :: method public runtimeNamespace():java.lang.String +class public final blue.bex.contracts.ProcessorExecutionContextBexGasLedgerHost implements blue.bex.api.BexGasLedgerHost :: method public separatesRuntimeNamespaces():boolean +class public final blue.bex.contracts.ProcessorExecutionContextBexGasLedgerHost implements blue.bex.api.BexGasLedgerHost :: method public submit(blue.bex.gas.BexGasLedgerCapability):void +class public final blue.bex.contracts.ProcessorExecutionContextBexSemanticIdentityBoundary implements blue.bex.output.BexSemanticIdentityBoundary +class public final blue.bex.contracts.ProcessorExecutionContextBexSemanticIdentityBoundary implements blue.bex.output.BexSemanticIdentityBoundary :: constructor public (blue.language.processor.ProcessorExecutionContext) +class public final blue.bex.contracts.ProcessorExecutionContextBexSemanticIdentityBoundary implements blue.bex.output.BexSemanticIdentityBoundary :: method public establishIdentity(blue.language.model.Node):blue.bex.output.BexEstablishedIdentity +class public final blue.bex.gas.BexGasChargeContext +class public final blue.bex.gas.BexGasChargeContext :: method public contractKey():java.lang.String +class public final blue.bex.gas.BexGasChargeContext :: method public logicalPath():java.lang.String +class public final blue.bex.gas.BexGasChargeContext :: method public reason():java.lang.String +class public final blue.bex.gas.BexGasChargeContext :: method public scopePath():java.lang.String +class public final blue.bex.gas.BexGasChargeContext :: method public static empty():blue.bex.gas.BexGasChargeContext +class public final blue.bex.gas.BexGasChargeContext :: method public static of(java.lang.String,java.lang.String,java.lang.String,java.lang.String):blue.bex.gas.BexGasChargeContext +class public final blue.bex.gas.BexGasLedger :: constructor public (java.util.List,java.lang.String,java.lang.String) +class public final blue.bex.gas.BexGasLimitExceededException extends blue.bex.BexException :: method public hostGasExhaustion():blue.bex.gas.BexHostGasExhaustion +class public final blue.bex.gas.BexGasMeter :: constructor public (blue.bex.gas.BexGasSchedule,blue.bex.gas.BexGasLedgerCapability) +class public final blue.bex.gas.BexGasMeter :: constructor public (blue.bex.gas.BexGasSchedule,blue.bex.gas.BexGasLedgerCapability,long) +class public final blue.bex.gas.BexGasMeter :: method public propagateHostGasExhaustion(blue.bex.gas.BexHostGasExhaustion,java.util.function.Consumer,java.util.function.BiConsumer):void +class public final blue.bex.gas.BexHostGasExhaustion extends java.lang.RuntimeException +class public final blue.bex.gas.BexHostGasExhaustion extends java.lang.RuntimeException :: constructor public (java.lang.String,java.lang.String,long,long,long,long,java.lang.RuntimeException) +class public final blue.bex.gas.BexHostGasExhaustion extends java.lang.RuntimeException :: method public admittedGas():long +class public final blue.bex.gas.BexHostGasExhaustion extends java.lang.RuntimeException :: method public counterName():java.lang.String +class public final blue.bex.gas.BexHostGasExhaustion extends java.lang.RuntimeException :: method public effectiveBudget():long +class public final blue.bex.gas.BexHostGasExhaustion extends java.lang.RuntimeException :: method public hostFailure():java.lang.RuntimeException +class public final blue.bex.gas.BexHostGasExhaustion extends java.lang.RuntimeException :: method public namespace():java.lang.String +class public final blue.bex.gas.BexHostGasExhaustion extends java.lang.RuntimeException :: method public quantity():long +class public final blue.bex.gas.BexHostGasExhaustion extends java.lang.RuntimeException :: method public weight():long +class public final blue.bex.output.BexOutputAdmission :: constructor public (blue.bex.gas.BexGasMeter,blue.bex.output.BexSemanticIdentityBoundary,blue.bex.output.BexFailurePolicy) +class public final blue.bex.pointer.BexPointerCache :: method public synchronized get(java.lang.String,blue.bex.result.BexMetricsRecorder):blue.bex.pointer.BexPointer +class public final blue.bex.result.BexChangeset implements blue.bex.value.BexChangesetValueView +class public final blue.bex.result.BexChangeset implements blue.bex.value.BexChangesetValueView :: constructor public (java.util.List) +class public final blue.bex.result.BexChangeset implements blue.bex.value.BexChangesetValueView :: method public asValue():blue.bex.value.BexValue +class public final blue.bex.result.BexChangeset implements blue.bex.value.BexChangesetValueView :: method public entries():java.util.List +class public final blue.bex.result.BexChangeset implements blue.bex.value.BexChangesetValueView :: method public static patchEntryValue(blue.bex.result.BexPatchEntry):blue.bex.value.BexValue +class public final blue.bex.result.BexEvents implements blue.bex.value.BexEventsValueView +class public final blue.bex.result.BexEvents implements blue.bex.value.BexEventsValueView :: constructor public (java.util.List) +class public final blue.bex.result.BexEvents implements blue.bex.value.BexEventsValueView :: constructor public (java.util.List,java.util.List) +class public final blue.bex.result.BexEvents implements blue.bex.value.BexEventsValueView :: method public admittedEvents():java.util.List +class public final blue.bex.result.BexEvents implements blue.bex.value.BexEventsValueView :: method public asValue():blue.bex.value.BexValue +class public final blue.bex.result.BexEvents implements blue.bex.value.BexEventsValueView :: method public events():java.util.List +class public final blue.bex.result.BexExecutionResult :: constructor public (blue.bex.value.BexValue,blue.bex.result.BexChangeset,blue.bex.result.BexEvents,blue.bex.gas.BexGasLedger,blue.bex.result.BexMetricsSnapshot) +class public final blue.bex.result.BexExecutionResult :: constructor public (blue.bex.value.BexValue,blue.bex.result.BexChangeset,blue.bex.result.BexEvents,blue.bex.gas.BexGasLedger,blue.bex.result.BexMetricsSnapshot,blue.bex.output.BexAdmittedValue) +class public final blue.bex.result.BexExecutionResult :: constructor public (blue.bex.value.BexValue,blue.bex.result.BexChangeset,blue.bex.result.BexEvents,java.util.List,blue.bex.result.BexMetricsSnapshot) +class public final blue.bex.result.BexExecutionResult :: method public metricsSnapshot():blue.bex.result.BexMetricsSnapshot +class public final blue.bex.result.BexMetrics :: method public snapshot():blue.bex.result.BexMetricsSnapshot +class public final blue.bex.result.BexMetrics :: method public static fromSnapshot(blue.bex.result.BexMetricsSnapshot):blue.bex.result.BexMetrics +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: constructor public () +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public addCompileNanos(long):void +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public addExecuteNanos(long):void +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public compileCacheHits():long +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public compileCacheMisses():long +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public compileNanos():long +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public compiledExecutions():long +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public containsBexCacheHits():long +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public containsBexCacheMisses():long +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public containsBexScans():long +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public currentContractReads():long +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public eventReads():long +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public executeNanos():long +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public expressionEvaluations():long +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public frozenDocumentReads():long +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public frozenOutputConversions():long +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public frozenWriterNodeFallbacks():long +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public functionArgMapAllocations():long +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public functionCalls():long +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public incrementCompileCacheHits():void +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public incrementCompileCacheMisses():void +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public incrementCompiledExecutions():void +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public incrementContainsBexCacheHits():void +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public incrementContainsBexCacheMisses():void +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public incrementContainsBexScans():void +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public incrementCurrentContractReads():void +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public incrementEventReads():void +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public incrementExpressionEvaluations():void +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public incrementFrozenDocumentReads():void +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public incrementFrozenOutputConversions():void +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public incrementFrozenWriterNodeFallbacks():void +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public incrementFunctionArgMapAllocations():void +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public incrementFunctionCalls():void +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public incrementInterpretedFallbacks():void +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public incrementLoopIterations():void +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public incrementNodeMaterializations():void +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public incrementNodeOutputConversions():void +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public incrementPointerCacheHits():void +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public incrementPointerCacheMisses():void +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public incrementPointerParses():void +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public incrementResolvedDocumentReads():void +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public incrementResultOverlayAncestorHits():void +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public incrementResultOverlayDocumentFallbacks():void +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public incrementResultOverlayExactHits():void +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public incrementResultValueReads():void +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public incrementSimpleMaterializations():void +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public incrementStatementExecutions():void +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public incrementStepsReads():void +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public interpretedFallbacks():long +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public loopIterations():long +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public nodeMaterializations():long +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public nodeOutputConversions():long +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public pointerCacheHits():long +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public pointerCacheMisses():long +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public pointerParses():long +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public resolvedDocumentReads():long +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public resultOverlayAncestorHits():long +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public resultOverlayDocumentFallbacks():long +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public resultOverlayExactHits():long +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public resultValueReads():long +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public simpleMaterializations():long +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public snapshot():blue.bex.result.BexMetricsSnapshot +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public statementExecutions():long +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics :: method public stepsReads():long +class public final blue.bex.result.BexMetricsSnapshot +class public final blue.bex.result.BexMetricsSnapshot :: method public compileCacheHits():long +class public final blue.bex.result.BexMetricsSnapshot :: method public compileCacheMisses():long +class public final blue.bex.result.BexMetricsSnapshot :: method public compileNanos():long +class public final blue.bex.result.BexMetricsSnapshot :: method public compiledExecutions():long +class public final blue.bex.result.BexMetricsSnapshot :: method public containsBexCacheHits():long +class public final blue.bex.result.BexMetricsSnapshot :: method public containsBexCacheMisses():long +class public final blue.bex.result.BexMetricsSnapshot :: method public containsBexScans():long +class public final blue.bex.result.BexMetricsSnapshot :: method public currentContractReads():long +class public final blue.bex.result.BexMetricsSnapshot :: method public eventReads():long +class public final blue.bex.result.BexMetricsSnapshot :: method public executeNanos():long +class public final blue.bex.result.BexMetricsSnapshot :: method public expressionEvaluations():long +class public final blue.bex.result.BexMetricsSnapshot :: method public frozenDocumentReads():long +class public final blue.bex.result.BexMetricsSnapshot :: method public frozenOutputConversions():long +class public final blue.bex.result.BexMetricsSnapshot :: method public frozenWriterNodeFallbacks():long +class public final blue.bex.result.BexMetricsSnapshot :: method public functionArgMapAllocations():long +class public final blue.bex.result.BexMetricsSnapshot :: method public functionCalls():long +class public final blue.bex.result.BexMetricsSnapshot :: method public interpretedFallbacks():long +class public final blue.bex.result.BexMetricsSnapshot :: method public loopIterations():long +class public final blue.bex.result.BexMetricsSnapshot :: method public nodeMaterializations():long +class public final blue.bex.result.BexMetricsSnapshot :: method public nodeOutputConversions():long +class public final blue.bex.result.BexMetricsSnapshot :: method public pointerCacheHits():long +class public final blue.bex.result.BexMetricsSnapshot :: method public pointerCacheMisses():long +class public final blue.bex.result.BexMetricsSnapshot :: method public pointerParses():long +class public final blue.bex.result.BexMetricsSnapshot :: method public resolvedDocumentReads():long +class public final blue.bex.result.BexMetricsSnapshot :: method public resultOverlayAncestorHits():long +class public final blue.bex.result.BexMetricsSnapshot :: method public resultOverlayDocumentFallbacks():long +class public final blue.bex.result.BexMetricsSnapshot :: method public resultOverlayExactHits():long +class public final blue.bex.result.BexMetricsSnapshot :: method public resultValueReads():long +class public final blue.bex.result.BexMetricsSnapshot :: method public simpleMaterializations():long +class public final blue.bex.result.BexMetricsSnapshot :: method public statementExecutions():long +class public final blue.bex.result.BexMetricsSnapshot :: method public stepsReads():long +class public final blue.bex.result.BexPatchEntry implements blue.bex.value.BexPatchValueView +class public final blue.bex.result.BexPatchEntry implements blue.bex.value.BexPatchValueView :: constructor public (java.lang.String,java.lang.String,java.lang.String,blue.bex.value.BexValue) +class public final blue.bex.result.BexPatchEntry implements blue.bex.value.BexPatchValueView :: constructor public (java.lang.String,java.lang.String,java.lang.String,blue.bex.value.BexValue,blue.bex.output.BexAdmittedValue) +class public final blue.bex.result.BexPatchEntry implements blue.bex.value.BexPatchValueView :: method public absolutePath():java.lang.String +class public final blue.bex.result.BexPatchEntry implements blue.bex.value.BexPatchValueView :: method public absoluteSegments():java.util.List +class public final blue.bex.result.BexPatchEntry implements blue.bex.value.BexPatchValueView :: method public admittedValue():blue.bex.output.BexAdmittedValue +class public final blue.bex.result.BexPatchEntry implements blue.bex.value.BexPatchValueView :: method public authoredPath():java.lang.String +class public final blue.bex.result.BexPatchEntry implements blue.bex.value.BexPatchValueView :: method public op():java.lang.String +class public final blue.bex.result.BexPatchEntry implements blue.bex.value.BexPatchValueView :: method public val():blue.bex.value.BexValue +class public final blue.bex.result.BexResultOverlay :: constructor public (blue.bex.spi.BexDocumentAccess,blue.bex.result.BexMetricsRecorder) +class public final blue.bex.result.BexResultOverlay :: constructor public (blue.bex.spi.BexDocumentAccess,blue.bex.result.BexMetricsRecorder,blue.language.runtime.BlueLanguage) +class public final blue.bex.runtime.BexRuntime implements blue.bex.compile.BexExecutionMachine +class public final blue.bex.runtime.BexRuntime implements blue.bex.compile.BexExecutionMachine :: constructor public (blue.bex.compile.BexCompiledProgram,blue.bex.runtime.BexRuntimeContext,blue.language.runtime.BlueLanguage,blue.bex.gas.BexGasSchedule,blue.bex.result.BexMetricsRecorder,blue.bex.pointer.BexPointerCache) +class public final blue.bex.runtime.BexRuntime implements blue.bex.compile.BexExecutionMachine :: constructor public (blue.bex.compile.BexCompiledProgram,blue.bex.runtime.BexRuntimeContext,blue.language.runtime.BlueLanguage,blue.bex.gas.BexGasSchedule,blue.bex.result.BexMetricsRecorder,blue.bex.pointer.BexPointerCache,blue.bex.runtime.BexRuntimeIntrinsics) +class public final blue.bex.runtime.BexRuntime implements blue.bex.compile.BexExecutionMachine :: method public accumulator():blue.bex.runtime.BexExecutionAccumulator +class public final blue.bex.runtime.BexRuntime implements blue.bex.compile.BexExecutionMachine :: method public appendChange(blue.bex.result.BexPatchEntry):void +class public final blue.bex.runtime.BexRuntime implements blue.bex.compile.BexExecutionMachine :: method public appendEvent(blue.bex.value.BexValue):void +class public final blue.bex.runtime.BexRuntime implements blue.bex.compile.BexExecutionMachine :: method public canonicalPointer(java.lang.String):java.lang.String +class public final blue.bex.runtime.BexRuntime implements blue.bex.compile.BexExecutionMachine :: method public changesetValue():blue.bex.value.BexValue +class public final blue.bex.runtime.BexRuntime implements blue.bex.compile.BexExecutionMachine :: method public context():blue.bex.runtime.BexRuntimeContext +class public final blue.bex.runtime.BexRuntime implements blue.bex.compile.BexExecutionMachine :: method public defaultResultValue():blue.bex.value.BexValue +class public final blue.bex.runtime.BexRuntime implements blue.bex.compile.BexExecutionMachine :: method public eventsValue():blue.bex.value.BexValue +class public final blue.bex.runtime.BexRuntime implements blue.bex.compile.BexExecutionMachine :: method public execute():blue.bex.result.BexExecutionResult +class public final blue.bex.runtime.BexRuntime implements blue.bex.compile.BexExecutionMachine :: method public gas():blue.bex.gas.BexGasMeter +class public final blue.bex.runtime.BexRuntime implements blue.bex.compile.BexExecutionMachine :: method public intrinsics():blue.bex.runtime.BexRuntimeIntrinsics +class public final blue.bex.runtime.BexRuntime implements blue.bex.compile.BexExecutionMachine :: method public invokeIntrinsic(java.lang.String,blue.bex.value.BexValue,java.util.Map):blue.bex.value.BexValue +class public final blue.bex.runtime.BexRuntime implements blue.bex.compile.BexExecutionMachine :: method public matchesType(blue.bex.value.BexValue,blue.language.snapshot.FrozenNode,blue.bex.BexSourcePath):boolean +class public final blue.bex.runtime.BexRuntime implements blue.bex.compile.BexExecutionMachine :: method public metrics():blue.bex.result.BexMetricsRecorder +class public final blue.bex.runtime.BexRuntime implements blue.bex.compile.BexExecutionMachine :: method public nodeBlueId(blue.bex.value.BexValue):blue.bex.value.BexValue +class public final blue.bex.runtime.BexRuntime implements blue.bex.compile.BexExecutionMachine :: method public outputAdmission():blue.bex.output.BexOutputAdmission +class public final blue.bex.runtime.BexRuntime implements blue.bex.compile.BexExecutionMachine :: method public parseDynamicPointer(java.lang.String):java.util.List +class public final blue.bex.runtime.BexRuntime implements blue.bex.compile.BexExecutionMachine :: method public pointerCache():blue.bex.pointer.BexPointerCache +class public final blue.bex.runtime.BexRuntime implements blue.bex.compile.BexExecutionMachine :: method public program():blue.bex.compile.BexCompiledProgram +class public final blue.bex.runtime.BexRuntime implements blue.bex.compile.BexExecutionMachine :: method public readBinding(java.lang.String,java.util.List):blue.bex.value.BexValue +class public final blue.bex.runtime.BexRuntime implements blue.bex.compile.BexExecutionMachine :: method public readCurrentContract(java.util.List):blue.bex.value.BexValue +class public final blue.bex.runtime.BexRuntime implements blue.bex.compile.BexExecutionMachine :: method public readDocument(java.lang.String,java.util.List,boolean):blue.bex.value.BexValue +class public final blue.bex.runtime.BexRuntime implements blue.bex.compile.BexExecutionMachine :: method public readEvent(java.util.List):blue.bex.value.BexValue +class public final blue.bex.runtime.BexRuntime implements blue.bex.compile.BexExecutionMachine :: method public readProcessingEvent(java.util.List):blue.bex.value.BexValue +class public final blue.bex.runtime.BexRuntime implements blue.bex.compile.BexExecutionMachine :: method public readResultValue(java.lang.String,java.util.List):blue.bex.value.BexValue +class public final blue.bex.runtime.BexRuntime implements blue.bex.compile.BexExecutionMachine :: method public readSteps(java.lang.String,java.util.List):blue.bex.value.BexValue +class public final blue.bex.runtime.BexRuntime implements blue.bex.compile.BexExecutionMachine :: method public readValuePointer(blue.bex.value.BexValue,java.util.List):blue.bex.value.BexValue +class public final blue.bex.runtime.BexRuntime implements blue.bex.compile.BexExecutionMachine :: method public resolvePointer(java.lang.String):java.lang.String +class public final blue.bex.runtime.BexRuntime implements blue.bex.compile.BexExecutionMachine :: method public typeMatcher():blue.bex.type.BexBlueTypeMatcher +class public final blue.bex.type.BexPatternValidator +class public final blue.bex.type.BexPatternValidator :: constructor public () +class public final blue.bex.type.BexPatternValidator :: method public keyMatchesType(java.lang.String,blue.language.snapshot.FrozenNode):boolean +class public final blue.bex.type.BexPatternValidator :: method public requiresPresence(blue.language.snapshot.FrozenNode):boolean +class public final blue.bex.type.BexTypeMatchWorkRecorder +class public final blue.bex.type.BexTypeMatchWorkRecorder :: constructor public (blue.bex.gas.BexGasMeter,blue.bex.BexSourcePath) +class public final blue.bex.type.BexTypeMatchWorkRecorder :: method public compareText(java.lang.String,java.lang.String):int +class public final blue.bex.type.BexTypeMatchWorkRecorder :: method public comparisonNode():void +class public final blue.bex.type.BexTypeMatchWorkRecorder :: method public scalarMatches(java.lang.Object,java.lang.Object):boolean +class public final blue.bex.type.BexTypeMatcher +class public final blue.bex.type.BexTypeMatcher :: constructor public (blue.language.runtime.BlueLanguage) +class public final blue.bex.type.BexTypeMatcher :: method public matches(blue.bex.value.BexValue,blue.language.snapshot.FrozenNode,blue.bex.gas.BexGasMeter,blue.bex.BexSourcePath):boolean +class public final blue.bex.value.BexFrozenWriter :: method public static toFrozen(blue.bex.value.BexValue,blue.bex.value.BexValueMetrics):blue.language.snapshot.FrozenNode +class public final blue.bex.value.BexValueKind extends java.lang.Enum +class public final blue.bex.value.BexValueKind extends java.lang.Enum :: field public static final BOOLEAN:blue.bex.value.BexValueKind +class public final blue.bex.value.BexValueKind extends java.lang.Enum :: field public static final DECIMAL:blue.bex.value.BexValueKind +class public final blue.bex.value.BexValueKind extends java.lang.Enum :: field public static final INTEGER:blue.bex.value.BexValueKind +class public final blue.bex.value.BexValueKind extends java.lang.Enum :: field public static final LIST:blue.bex.value.BexValueKind +class public final blue.bex.value.BexValueKind extends java.lang.Enum :: field public static final NULL:blue.bex.value.BexValueKind +class public final blue.bex.value.BexValueKind extends java.lang.Enum :: field public static final OBJECT:blue.bex.value.BexValueKind +class public final blue.bex.value.BexValueKind extends java.lang.Enum :: field public static final TEXT:blue.bex.value.BexValueKind +class public final blue.bex.value.BexValueKind extends java.lang.Enum :: field public static final UNDEFINED:blue.bex.value.BexValueKind +class public final blue.bex.value.BexValueKind extends java.lang.Enum :: method public operatorName():java.lang.String +class public final blue.bex.value.BexValueKind extends java.lang.Enum :: method public static valueOf(java.lang.String):blue.bex.value.BexValueKind +class public final blue.bex.value.BexValueKind extends java.lang.Enum :: method public static values():blue.bex.value.BexValueKind[] +class public final blue.bex.value.BexValues :: method public static semanticKind(blue.bex.value.BexValue):blue.bex.value.BexValueKind +class public final blue.bex.value.ChangesetBexValue extends blue.bex.value.AbstractBexValue :: constructor public (blue.bex.value.BexChangesetValueView) +class public final blue.bex.value.EventsBexValue extends blue.bex.value.AbstractBexValue :: constructor public (blue.bex.value.BexEventsValueView) +class public final blue.bex.value.PatchEntryBexValue extends blue.bex.value.AbstractBexValue :: constructor public (blue.bex.value.BexPatchValueView) +class public static final blue.bex.api.BexExecutionContext$Builder :: method public failureBoundary(blue.bex.api.BexFailureBoundary):blue.bex.api.BexExecutionContext$Builder +class public static final blue.bex.api.BexFailureBoundary$Classification extends java.lang.Enum +class public static final blue.bex.api.BexFailureBoundary$Classification extends java.lang.Enum :: field public static final DETERMINISTIC:blue.bex.api.BexFailureBoundary$Classification +class public static final blue.bex.api.BexFailureBoundary$Classification extends java.lang.Enum :: field public static final EVIDENCE_UNAVAILABLE:blue.bex.api.BexFailureBoundary$Classification +class public static final blue.bex.api.BexFailureBoundary$Classification extends java.lang.Enum :: field public static final UNCLASSIFIED:blue.bex.api.BexFailureBoundary$Classification +class public static final blue.bex.api.BexFailureBoundary$Classification extends java.lang.Enum :: method public static valueOf(java.lang.String):blue.bex.api.BexFailureBoundary$Classification +class public static final blue.bex.api.BexFailureBoundary$Classification extends java.lang.Enum :: method public static values():blue.bex.api.BexFailureBoundary$Classification[] +class public static final blue.bex.compile.BexCompilationInput$Kind extends java.lang.Enum +class public static final blue.bex.compile.BexCompilationInput$Kind extends java.lang.Enum :: field public static final EXPRESSION:blue.bex.compile.BexCompilationInput$Kind +class public static final blue.bex.compile.BexCompilationInput$Kind extends java.lang.Enum :: field public static final FULL_PROGRAM:blue.bex.compile.BexCompilationInput$Kind +class public static final blue.bex.compile.BexCompilationInput$Kind extends java.lang.Enum :: method public static valueOf(java.lang.String):blue.bex.compile.BexCompilationInput$Kind +class public static final blue.bex.compile.BexCompilationInput$Kind extends java.lang.Enum :: method public static values():blue.bex.compile.BexCompilationInput$Kind[] +class public static final blue.bex.compile.CompileScope$Visibility diff --git a/gradle/verification/api/modernization-removed-descriptors.txt b/gradle/verification/api/modernization-removed-descriptors.txt new file mode 100644 index 0000000..808afda --- /dev/null +++ b/gradle/verification/api/modernization-removed-descriptors.txt @@ -0,0 +1,254 @@ +class public abstract interface blue.bex.api.BexDocumentView +class public abstract interface blue.bex.api.BexDocumentView :: method public abstract canonicalAt(java.lang.String):blue.bex.value.BexValue +class public abstract interface blue.bex.api.BexDocumentView :: method public abstract currentScopePath():java.lang.String +class public abstract interface blue.bex.api.BexDocumentView :: method public abstract resolvePointer(java.lang.String):java.lang.String +class public abstract interface blue.bex.api.BexDocumentView :: method public abstract resolvedAt(java.lang.String):blue.bex.value.BexValue +class public abstract interface blue.bex.api.BexGasLedgerHost +class public abstract interface blue.bex.api.BexGasLedgerHost :: method public abstract evidenceUnavailable(blue.language.processor.GasMeter$ChildGasLedger):void +class public abstract interface blue.bex.api.BexGasLedgerHost :: method public abstract failedDeterministically(blue.language.processor.GasMeter$ChildGasLedger):void +class public abstract interface blue.bex.api.BexGasLedgerHost :: method public abstract open(java.lang.String,java.util.Map):blue.language.processor.GasMeter$ChildGasLedger +class public abstract interface blue.bex.api.BexGasLedgerHost :: method public abstract submit(blue.language.processor.GasMeter$ChildGasLedger):void +class public abstract interface blue.bex.api.BexGasLedgerHost :: method public localGasLimitExceeded(blue.bex.gas.BexGasLimitExceededException,java.lang.RuntimeException):java.lang.RuntimeException +class public abstract interface blue.bex.api.BexGasLedgerHost :: method public open(java.lang.String,java.util.Map,blue.language.processor.RuntimeWorkBudget):blue.language.processor.GasMeter$ChildGasLedger +class public abstract interface blue.bex.api.BexGasLedgerHost :: method public openSharedBudget(long):blue.language.processor.RuntimeWorkBudget +class public abstract interface blue.bex.api.BexGasLedgerHost :: method public propagateGasExhaustion(blue.language.processor.GasMeter$ChildGasLedger,blue.language.processor.GasLimitExceededException):void +class public abstract interface blue.bex.api.BexGasLedgerHost :: method public separatesRuntimeNamespaces():boolean +class public abstract interface blue.bex.api.BexMetricsSink :: method public abstract accept(blue.bex.result.BexMetrics):void +class public abstract interface blue.bex.runtime.CompiledExpression +class public abstract interface blue.bex.runtime.CompiledExpression :: method public abstract eval(blue.bex.runtime.CompiledFrame):blue.bex.value.BexValue +class public abstract interface blue.bex.runtime.CompiledStatement +class public abstract interface blue.bex.runtime.CompiledStatement :: method public abstract exec(blue.bex.runtime.CompiledFrame):blue.bex.runtime.Control +class public final blue.bex.api.BexEngine +class public final blue.bex.api.BexEngine :: method public compile(blue.bex.api.BexProgramSource):blue.bex.compile.BexCompiledProgram +class public final blue.bex.api.BexEngine :: method public compileAndExecute(blue.bex.api.BexProgramSource,blue.bex.api.BexExecutionContext):blue.bex.result.BexExecutionResult +class public final blue.bex.api.BexEngine :: method public execute(blue.bex.compile.BexCompiledProgram,blue.bex.api.BexExecutionContext):blue.bex.result.BexExecutionResult +class public final blue.bex.api.BexEngine :: method public static builder():blue.bex.api.BexEngine$Builder +class public final blue.bex.api.BexExecutionContext +class public final blue.bex.api.BexExecutionContext :: method public binding(java.lang.String):blue.bex.value.BexValue +class public final blue.bex.api.BexExecutionContext :: method public bindings():java.util.Map +class public final blue.bex.api.BexExecutionContext :: method public currentContract():blue.bex.value.BexValue +class public final blue.bex.api.BexExecutionContext :: method public currentScopePath():java.lang.String +class public final blue.bex.api.BexExecutionContext :: method public document():blue.bex.api.BexDocumentView +class public final blue.bex.api.BexExecutionContext :: method public event():blue.bex.value.BexValue +class public final blue.bex.api.BexExecutionContext :: method public gasLedgerHost():blue.bex.api.BexGasLedgerHost +class public final blue.bex.api.BexExecutionContext :: method public gasLimit():long +class public final blue.bex.api.BexExecutionContext :: method public parentRemainingGas():long +class public final blue.bex.api.BexExecutionContext :: method public processingEvent():blue.bex.value.BexValue +class public final blue.bex.api.BexExecutionContext :: method public semanticIdentityBoundary():blue.bex.output.BexSemanticIdentityBoundary +class public final blue.bex.api.BexExecutionContext :: method public static builder():blue.bex.api.BexExecutionContext$Builder +class public final blue.bex.api.BexExecutionContext :: method public steps():blue.bex.api.BexStepResults +class public final blue.bex.api.BexIntrinsicRegistry +class public final blue.bex.api.BexIntrinsicRegistry :: method public identity():java.lang.String +class public final blue.bex.api.BexIntrinsicRegistry :: method public invoke(java.lang.String,blue.bex.value.BexValue,java.util.Map,blue.bex.gas.BexGasMeter,blue.bex.output.BexOutputAdmission):blue.bex.value.BexValue +class public final blue.bex.api.BexIntrinsicRegistry :: method public registeredNamedWeights():java.util.Map +class public final blue.bex.api.BexIntrinsicRegistry :: method public registeredNamedWeights(java.util.Set):java.util.Map +class public final blue.bex.api.BexIntrinsicRegistry :: method public registeredNamespaceWeights():java.util.Map +class public final blue.bex.api.BexIntrinsicRegistry :: method public registeredNamespaceWeights(java.util.Set):java.util.Map +class public final blue.bex.api.BexIntrinsicRegistry :: method public static builder():blue.bex.api.BexIntrinsicRegistry$Builder +class public final blue.bex.api.BexIntrinsicRegistry :: method public static empty():blue.bex.api.BexIntrinsicRegistry +class public final blue.bex.api.BexIntrinsicRegistry :: method public supportedBlueIds():java.util.Set +class public final blue.bex.api.BexIntrinsicRegistry :: method public supports(java.lang.String):boolean +class public final blue.bex.api.BexIntrinsicRegistry :: method public with(java.lang.Class,blue.bex.api.BexTypeBlueIdResolver,java.lang.String,java.util.Map,blue.bex.api.BexIntrinsicProcessor):blue.bex.api.BexIntrinsicRegistry +class public final blue.bex.api.BexIntrinsicRegistry :: method public with(java.lang.Class,java.lang.String,java.util.Map,blue.bex.api.BexIntrinsicProcessor):blue.bex.api.BexIntrinsicRegistry +class public final blue.bex.api.BexIntrinsicRegistry :: method public with(java.lang.String,java.lang.String,java.util.Map,blue.bex.api.BexIntrinsicProcessor):blue.bex.api.BexIntrinsicRegistry +class public final blue.bex.api.BexProgramSource +class public final blue.bex.api.BexProgramSource :: method public definitionNode():java.util.Optional +class public final blue.bex.api.BexProgramSource :: method public entry():java.util.Optional +class public final blue.bex.api.BexProgramSource :: method public isExpression():boolean +class public final blue.bex.api.BexProgramSource :: method public kind():blue.bex.api.BexProgramSource$Kind +class public final blue.bex.api.BexProgramSource :: method public programNode():blue.language.snapshot.FrozenNode +class public final blue.bex.api.BexProgramSource :: method public static expression(blue.language.snapshot.FrozenNode):blue.bex.api.BexProgramSource +class public final blue.bex.api.BexProgramSource :: method public static inline(blue.language.snapshot.FrozenNode):blue.bex.api.BexProgramSource +class public final blue.bex.api.BexProgramSource :: method public static withDefinition(blue.language.snapshot.FrozenNode,blue.language.snapshot.FrozenNode,java.lang.String):blue.bex.api.BexProgramSource +class public final blue.bex.api.BexStepResults +class public final blue.bex.api.BexStepResults :: method public asValue():blue.bex.value.BexValue +class public final blue.bex.api.BexStepResults :: method public static builder():blue.bex.api.BexStepResults$Builder +class public final blue.bex.api.BexStepResults :: method public static empty():blue.bex.api.BexStepResults +class public final blue.bex.api.BexStepResults :: method public step(java.lang.String):blue.bex.value.BexValue +class public final blue.bex.api.ProcessorExecutionContextBexDocumentView implements blue.bex.api.BexDocumentView +class public final blue.bex.api.ProcessorExecutionContextBexDocumentView implements blue.bex.api.BexDocumentView :: constructor public (blue.language.processor.ProcessorExecutionContext) +class public final blue.bex.api.ProcessorExecutionContextBexDocumentView implements blue.bex.api.BexDocumentView :: method public canonicalAt(java.lang.String):blue.bex.value.BexValue +class public final blue.bex.api.ProcessorExecutionContextBexDocumentView implements blue.bex.api.BexDocumentView :: method public currentScopePath():java.lang.String +class public final blue.bex.api.ProcessorExecutionContextBexDocumentView implements blue.bex.api.BexDocumentView :: method public resolvePointer(java.lang.String):java.lang.String +class public final blue.bex.api.ProcessorExecutionContextBexDocumentView implements blue.bex.api.BexDocumentView :: method public resolvedAt(java.lang.String):blue.bex.value.BexValue +class public final blue.bex.api.ProcessorExecutionContextBexGasLedgerHost implements blue.bex.api.BexGasLedgerHost +class public final blue.bex.api.ProcessorExecutionContextBexGasLedgerHost implements blue.bex.api.BexGasLedgerHost :: constructor public (blue.language.processor.ProcessorExecutionContext) +class public final blue.bex.api.ProcessorExecutionContextBexGasLedgerHost implements blue.bex.api.BexGasLedgerHost :: constructor public (blue.language.processor.ProcessorExecutionContext,java.lang.String) +class public final blue.bex.api.ProcessorExecutionContextBexGasLedgerHost implements blue.bex.api.BexGasLedgerHost :: constructor public (blue.language.processor.RuntimeWorkSession,java.lang.String) +class public final blue.bex.api.ProcessorExecutionContextBexGasLedgerHost implements blue.bex.api.BexGasLedgerHost :: method public evidenceUnavailable(blue.language.processor.GasMeter$ChildGasLedger):void +class public final blue.bex.api.ProcessorExecutionContextBexGasLedgerHost implements blue.bex.api.BexGasLedgerHost :: method public failedDeterministically(blue.language.processor.GasMeter$ChildGasLedger):void +class public final blue.bex.api.ProcessorExecutionContextBexGasLedgerHost implements blue.bex.api.BexGasLedgerHost :: method public localGasLimitExceeded(blue.bex.gas.BexGasLimitExceededException,java.lang.RuntimeException):java.lang.RuntimeException +class public final blue.bex.api.ProcessorExecutionContextBexGasLedgerHost implements blue.bex.api.BexGasLedgerHost :: method public open(java.lang.String,java.util.Map):blue.language.processor.GasMeter$ChildGasLedger +class public final blue.bex.api.ProcessorExecutionContextBexGasLedgerHost implements blue.bex.api.BexGasLedgerHost :: method public open(java.lang.String,java.util.Map,blue.language.processor.RuntimeWorkBudget):blue.language.processor.GasMeter$ChildGasLedger +class public final blue.bex.api.ProcessorExecutionContextBexGasLedgerHost implements blue.bex.api.BexGasLedgerHost :: method public openSharedBudget(long):blue.language.processor.RuntimeWorkBudget +class public final blue.bex.api.ProcessorExecutionContextBexGasLedgerHost implements blue.bex.api.BexGasLedgerHost :: method public physicalNamespace(java.lang.String):java.lang.String +class public final blue.bex.api.ProcessorExecutionContextBexGasLedgerHost implements blue.bex.api.BexGasLedgerHost :: method public propagateGasExhaustion(blue.language.processor.GasMeter$ChildGasLedger,blue.language.processor.GasLimitExceededException):void +class public final blue.bex.api.ProcessorExecutionContextBexGasLedgerHost implements blue.bex.api.BexGasLedgerHost :: method public runtimeNamespace():java.lang.String +class public final blue.bex.api.ProcessorExecutionContextBexGasLedgerHost implements blue.bex.api.BexGasLedgerHost :: method public separatesRuntimeNamespaces():boolean +class public final blue.bex.api.ProcessorExecutionContextBexGasLedgerHost implements blue.bex.api.BexGasLedgerHost :: method public submit(blue.language.processor.GasMeter$ChildGasLedger):void +class public final blue.bex.compile.BexCompiledProgram :: constructor public (blue.bex.compile.BexCompiledProgram$CompiledFunction,java.util.Map,java.util.Map,int,java.lang.String) +class public final blue.bex.compile.BexCompiledProgram :: constructor public (blue.bex.compile.BexCompiledProgram$CompiledFunction,java.util.Map,java.util.Map,int,java.lang.String,java.util.Set) +class public final blue.bex.compile.BexCompiledProgram :: method public constants():java.util.Map +class public final blue.bex.compile.BexCompiledProgram :: method public entry():blue.bex.compile.BexCompiledProgram$CompiledFunction +class public final blue.bex.compile.BexCompiledProgram :: method public execute(blue.bex.runtime.BexRuntime):blue.bex.value.BexValue +class public final blue.bex.compile.BexCompiledProgram :: method public functions():java.util.Map +class public final blue.bex.compile.BexCompiledProgram :: method public rootFrameSize():int +class public final blue.bex.compile.BexCompiledProgramKey :: constructor public (blue.bex.api.BexProgramSource$Kind,java.lang.String,java.lang.String,java.lang.String) +class public final blue.bex.compile.BexCompiledProgramKey :: constructor public (blue.bex.api.BexProgramSource$Kind,java.lang.String,java.lang.String,java.lang.String,java.lang.String) +class public final blue.bex.compile.BexCompiledProgramKey :: method public kind():blue.bex.api.BexProgramSource$Kind +class public final blue.bex.compile.BexCompiledProgramKey :: method public static from(blue.bex.api.BexProgramSource):blue.bex.compile.BexCompiledProgramKey +class public final blue.bex.compile.BexCompiledProgramKey :: method public static from(blue.bex.api.BexProgramSource,java.lang.String):blue.bex.compile.BexCompiledProgramKey +class public final blue.bex.compile.BexCompiler +class public final blue.bex.compile.BexCompiler :: constructor public (blue.bex.result.BexMetrics) +class public final blue.bex.compile.BexCompiler :: constructor public (blue.bex.result.BexMetrics,blue.bex.api.BexIntrinsicRegistry) +class public final blue.bex.compile.BexCompiler :: method public compile(blue.bex.api.BexProgramSource):blue.bex.compile.BexCompiledProgram +class public final blue.bex.compile.BexContainsCache :: method public synchronized containsBex(blue.language.snapshot.FrozenNode,blue.bex.result.BexMetrics):boolean +class public final blue.bex.gas.BexGasLimitExceededException extends blue.bex.BexException :: method public hostGasLimitExceeded():blue.language.processor.GasLimitExceededException +class public final blue.bex.gas.BexGasMeter :: constructor public (blue.bex.gas.BexGasSchedule,blue.language.processor.GasMeter$ChildGasLedger) +class public final blue.bex.gas.BexGasMeter :: constructor public (blue.bex.gas.BexGasSchedule,blue.language.processor.GasMeter$ChildGasLedger,long) +class public final blue.bex.gas.BexGasMeter :: method public propagateHostGasExhaustion(blue.language.processor.GasLimitExceededException,java.util.function.Consumer,java.util.function.BiConsumer):void +class public final blue.bex.output.ProcessorExecutionContextBexSemanticIdentityBoundary implements blue.bex.output.BexSemanticIdentityBoundary +class public final blue.bex.output.ProcessorExecutionContextBexSemanticIdentityBoundary implements blue.bex.output.BexSemanticIdentityBoundary :: constructor public (blue.language.processor.ProcessorExecutionContext) +class public final blue.bex.output.ProcessorExecutionContextBexSemanticIdentityBoundary implements blue.bex.output.BexSemanticIdentityBoundary :: method public establishIdentity(blue.language.model.Node):blue.bex.output.BexEstablishedIdentity +class public final blue.bex.pointer.BexPointerCache :: method public synchronized get(java.lang.String,blue.bex.result.BexMetrics):blue.bex.pointer.BexPointer +class public final blue.bex.result.BexChangeset +class public final blue.bex.result.BexChangeset :: constructor public (java.util.List) +class public final blue.bex.result.BexChangeset :: method public asValue():blue.bex.value.BexValue +class public final blue.bex.result.BexChangeset :: method public entries():java.util.List +class public final blue.bex.result.BexChangeset :: method public static patchEntryValue(blue.bex.result.BexPatchEntry):blue.bex.value.BexValue +class public final blue.bex.result.BexEvents +class public final blue.bex.result.BexEvents :: constructor public (java.util.List) +class public final blue.bex.result.BexEvents :: constructor public (java.util.List,java.util.List) +class public final blue.bex.result.BexEvents :: method public admittedEvents():java.util.List +class public final blue.bex.result.BexEvents :: method public asValue():blue.bex.value.BexValue +class public final blue.bex.result.BexEvents :: method public events():java.util.List +class public final blue.bex.result.BexExecutionResult :: constructor public (blue.bex.value.BexValue,blue.bex.result.BexChangeset,blue.bex.result.BexEvents,blue.bex.gas.BexGasLedger,blue.bex.result.BexMetrics) +class public final blue.bex.result.BexExecutionResult :: constructor public (blue.bex.value.BexValue,blue.bex.result.BexChangeset,blue.bex.result.BexEvents,blue.bex.gas.BexGasLedger,blue.bex.result.BexMetrics,blue.bex.output.BexAdmittedValue) +class public final blue.bex.result.BexExecutionResult :: constructor public (blue.bex.value.BexValue,blue.bex.result.BexChangeset,blue.bex.result.BexEvents,java.util.List,blue.bex.result.BexMetrics) +class public final blue.bex.result.BexMetrics :: method public addCompileNanos(long):void +class public final blue.bex.result.BexMetrics :: method public addExecuteNanos(long):void +class public final blue.bex.result.BexMetrics :: method public incrementCompileCacheHits():void +class public final blue.bex.result.BexMetrics :: method public incrementCompileCacheMisses():void +class public final blue.bex.result.BexMetrics :: method public incrementCompiledExecutions():void +class public final blue.bex.result.BexMetrics :: method public incrementContainsBexCacheHits():void +class public final blue.bex.result.BexMetrics :: method public incrementContainsBexCacheMisses():void +class public final blue.bex.result.BexMetrics :: method public incrementContainsBexScans():void +class public final blue.bex.result.BexMetrics :: method public incrementCurrentContractReads():void +class public final blue.bex.result.BexMetrics :: method public incrementEventReads():void +class public final blue.bex.result.BexMetrics :: method public incrementExpressionEvaluations():void +class public final blue.bex.result.BexMetrics :: method public incrementFrozenDocumentReads():void +class public final blue.bex.result.BexMetrics :: method public incrementFrozenOutputConversions():void +class public final blue.bex.result.BexMetrics :: method public incrementFrozenWriterNodeFallbacks():void +class public final blue.bex.result.BexMetrics :: method public incrementFunctionArgMapAllocations():void +class public final blue.bex.result.BexMetrics :: method public incrementFunctionCalls():void +class public final blue.bex.result.BexMetrics :: method public incrementInterpretedFallbacks():void +class public final blue.bex.result.BexMetrics :: method public incrementLoopIterations():void +class public final blue.bex.result.BexMetrics :: method public incrementNodeMaterializations():void +class public final blue.bex.result.BexMetrics :: method public incrementNodeOutputConversions():void +class public final blue.bex.result.BexMetrics :: method public incrementPointerCacheHits():void +class public final blue.bex.result.BexMetrics :: method public incrementPointerCacheMisses():void +class public final blue.bex.result.BexMetrics :: method public incrementPointerParses():void +class public final blue.bex.result.BexMetrics :: method public incrementResolvedDocumentReads():void +class public final blue.bex.result.BexMetrics :: method public incrementResultOverlayAncestorHits():void +class public final blue.bex.result.BexMetrics :: method public incrementResultOverlayDocumentFallbacks():void +class public final blue.bex.result.BexMetrics :: method public incrementResultOverlayExactHits():void +class public final blue.bex.result.BexMetrics :: method public incrementResultValueReads():void +class public final blue.bex.result.BexMetrics :: method public incrementSimpleMaterializations():void +class public final blue.bex.result.BexMetrics :: method public incrementStatementExecutions():void +class public final blue.bex.result.BexMetrics :: method public incrementStepsReads():void +class public final blue.bex.result.BexPatchEntry +class public final blue.bex.result.BexPatchEntry :: constructor public (java.lang.String,java.lang.String,java.lang.String,blue.bex.value.BexValue) +class public final blue.bex.result.BexPatchEntry :: constructor public (java.lang.String,java.lang.String,java.lang.String,blue.bex.value.BexValue,blue.bex.output.BexAdmittedValue) +class public final blue.bex.result.BexPatchEntry :: method public absolutePath():java.lang.String +class public final blue.bex.result.BexPatchEntry :: method public absoluteSegments():java.util.List +class public final blue.bex.result.BexPatchEntry :: method public admittedValue():blue.bex.output.BexAdmittedValue +class public final blue.bex.result.BexPatchEntry :: method public authoredPath():java.lang.String +class public final blue.bex.result.BexPatchEntry :: method public op():java.lang.String +class public final blue.bex.result.BexPatchEntry :: method public val():blue.bex.value.BexValue +class public final blue.bex.result.BexResultOverlay :: constructor public (blue.bex.api.BexDocumentView,blue.bex.result.BexMetrics) +class public final blue.bex.result.BexResultOverlay :: constructor public (blue.bex.api.BexDocumentView,blue.bex.result.BexMetrics,blue.language.runtime.BlueLanguage) +class public final blue.bex.runtime.BexRuntime +class public final blue.bex.runtime.BexRuntime :: constructor public (blue.bex.compile.BexCompiledProgram,blue.bex.api.BexExecutionContext,blue.language.runtime.BlueLanguage,blue.bex.gas.BexGasSchedule,blue.bex.result.BexMetrics,blue.bex.pointer.BexPointerCache) +class public final blue.bex.runtime.BexRuntime :: constructor public (blue.bex.compile.BexCompiledProgram,blue.bex.api.BexExecutionContext,blue.language.runtime.BlueLanguage,blue.bex.gas.BexGasSchedule,blue.bex.result.BexMetrics,blue.bex.pointer.BexPointerCache,blue.bex.api.BexIntrinsicRegistry) +class public final blue.bex.runtime.BexRuntime :: method public accumulator():blue.bex.runtime.BexExecutionAccumulator +class public final blue.bex.runtime.BexRuntime :: method public canonicalPointer(java.lang.String):java.lang.String +class public final blue.bex.runtime.BexRuntime :: method public context():blue.bex.api.BexExecutionContext +class public final blue.bex.runtime.BexRuntime :: method public defaultResultValue():blue.bex.value.BexValue +class public final blue.bex.runtime.BexRuntime :: method public execute():blue.bex.result.BexExecutionResult +class public final blue.bex.runtime.BexRuntime :: method public gas():blue.bex.gas.BexGasMeter +class public final blue.bex.runtime.BexRuntime :: method public intrinsics():blue.bex.api.BexIntrinsicRegistry +class public final blue.bex.runtime.BexRuntime :: method public invokeIntrinsic(java.lang.String,blue.bex.value.BexValue,java.util.Map):blue.bex.value.BexValue +class public final blue.bex.runtime.BexRuntime :: method public metrics():blue.bex.result.BexMetrics +class public final blue.bex.runtime.BexRuntime :: method public nodeBlueId(blue.bex.value.BexValue):blue.bex.value.BexValue +class public final blue.bex.runtime.BexRuntime :: method public outputAdmission():blue.bex.output.BexOutputAdmission +class public final blue.bex.runtime.BexRuntime :: method public parseDynamicPointer(java.lang.String):java.util.List +class public final blue.bex.runtime.BexRuntime :: method public pointerCache():blue.bex.pointer.BexPointerCache +class public final blue.bex.runtime.BexRuntime :: method public program():blue.bex.compile.BexCompiledProgram +class public final blue.bex.runtime.BexRuntime :: method public readBinding(java.lang.String,java.util.List):blue.bex.value.BexValue +class public final blue.bex.runtime.BexRuntime :: method public readCurrentContract(java.util.List):blue.bex.value.BexValue +class public final blue.bex.runtime.BexRuntime :: method public readDocument(java.lang.String,java.util.List,boolean):blue.bex.value.BexValue +class public final blue.bex.runtime.BexRuntime :: method public readEvent(java.util.List):blue.bex.value.BexValue +class public final blue.bex.runtime.BexRuntime :: method public readProcessingEvent(java.util.List):blue.bex.value.BexValue +class public final blue.bex.runtime.BexRuntime :: method public readResultValue(java.lang.String,java.util.List):blue.bex.value.BexValue +class public final blue.bex.runtime.BexRuntime :: method public readSteps(java.lang.String,java.util.List):blue.bex.value.BexValue +class public final blue.bex.runtime.BexRuntime :: method public readValuePointer(blue.bex.value.BexValue,java.util.List):blue.bex.value.BexValue +class public final blue.bex.runtime.BexRuntime :: method public resolvePointer(java.lang.String):java.lang.String +class public final blue.bex.runtime.BexRuntime :: method public typeMatcher():blue.bex.type.BexBlueTypeMatcher +class public final blue.bex.runtime.CompileScope +class public final blue.bex.runtime.CompileScope :: constructor public () +class public final blue.bex.runtime.CompileScope :: constructor public (blue.bex.runtime.CompileScope) +class public final blue.bex.runtime.CompileScope :: method public captureVisibility():blue.bex.runtime.CompileScope$Visibility +class public final blue.bex.runtime.CompileScope :: method public declareOrGetSlot(java.lang.String):int +class public final blue.bex.runtime.CompileScope :: method public frameSize():int +class public final blue.bex.runtime.CompileScope :: method public hasSlot(java.lang.String):boolean +class public final blue.bex.runtime.CompileScope :: method public resolveSlot(java.lang.String):int +class public final blue.bex.runtime.CompileScope :: method public restoreVisibility(blue.bex.runtime.CompileScope$Visibility):void +class public final blue.bex.runtime.CompiledFrame +class public final blue.bex.runtime.CompiledFrame :: constructor public (blue.bex.runtime.BexRuntime,int,blue.bex.runtime.CompiledFrame) +class public final blue.bex.runtime.CompiledFrame :: method public accumulator():blue.bex.runtime.BexExecutionAccumulator +class public final blue.bex.runtime.CompiledFrame :: method public clear(int):void +class public final blue.bex.runtime.CompiledFrame :: method public enter(blue.bex.BexSourcePath):blue.bex.BexSourcePath +class public final blue.bex.runtime.CompiledFrame :: method public get(int):blue.bex.value.BexValue +class public final blue.bex.runtime.CompiledFrame :: method public getRequired(int):blue.bex.value.BexValue +class public final blue.bex.runtime.CompiledFrame :: method public isInitialized(int):boolean +class public final blue.bex.runtime.CompiledFrame :: method public parent():blue.bex.runtime.CompiledFrame +class public final blue.bex.runtime.CompiledFrame :: method public readBinding(java.lang.String,java.util.List):blue.bex.value.BexValue +class public final blue.bex.runtime.CompiledFrame :: method public readCurrentContract(java.util.List):blue.bex.value.BexValue +class public final blue.bex.runtime.CompiledFrame :: method public readDocument(java.lang.String,java.util.List,boolean):blue.bex.value.BexValue +class public final blue.bex.runtime.CompiledFrame :: method public readEvent(java.util.List):blue.bex.value.BexValue +class public final blue.bex.runtime.CompiledFrame :: method public readProcessingEvent(java.util.List):blue.bex.value.BexValue +class public final blue.bex.runtime.CompiledFrame :: method public restore(blue.bex.BexSourcePath):void +class public final blue.bex.runtime.CompiledFrame :: method public returnValue():blue.bex.value.BexValue +class public final blue.bex.runtime.CompiledFrame :: method public returnValue(blue.bex.value.BexValue):void +class public final blue.bex.runtime.CompiledFrame :: method public runtime():blue.bex.runtime.BexRuntime +class public final blue.bex.runtime.CompiledFrame :: method public set(int,blue.bex.value.BexValue):void +class public final blue.bex.runtime.CompiledFrame :: method public sourcePath():blue.bex.BexSourcePath +class public final blue.bex.runtime.Control extends java.lang.Enum +class public final blue.bex.runtime.Control extends java.lang.Enum :: field public static final CONTINUE:blue.bex.runtime.Control +class public final blue.bex.runtime.Control extends java.lang.Enum :: field public static final RETURN:blue.bex.runtime.Control +class public final blue.bex.runtime.Control extends java.lang.Enum :: method public static valueOf(java.lang.String):blue.bex.runtime.Control +class public final blue.bex.runtime.Control extends java.lang.Enum :: method public static values():blue.bex.runtime.Control[] +class public final blue.bex.value.BexFrozenWriter :: method public static toFrozen(blue.bex.value.BexValue,blue.bex.result.BexMetrics):blue.language.snapshot.FrozenNode +class public final blue.bex.value.ChangesetBexValue extends blue.bex.value.AbstractBexValue :: constructor public (blue.bex.result.BexChangeset) +class public final blue.bex.value.EventsBexValue extends blue.bex.value.AbstractBexValue :: constructor public (blue.bex.result.BexEvents) +class public final blue.bex.value.PatchEntryBexValue extends blue.bex.value.AbstractBexValue :: constructor public (blue.bex.result.BexPatchEntry) +class public static final blue.bex.api.BexExecutionContext$Builder :: method public processorExecutionContext(blue.language.processor.ProcessorExecutionContext):blue.bex.api.BexExecutionContext$Builder +class public static final blue.bex.api.BexExecutionContext$Builder :: method public processorExecutionContext(blue.language.processor.ProcessorExecutionContext,java.lang.String):blue.bex.api.BexExecutionContext$Builder +class public static final blue.bex.compile.BexCompiledProgram$ArgSpec +class public static final blue.bex.compile.BexCompiledProgram$ArgSpec :: constructor public (java.lang.String,int,blue.language.snapshot.FrozenNode,java.lang.String) +class public static final blue.bex.compile.BexCompiledProgram$ArgSpec :: method public name():java.lang.String +class public static final blue.bex.compile.BexCompiledProgram$ArgSpec :: method public pattern():blue.language.snapshot.FrozenNode +class public static final blue.bex.compile.BexCompiledProgram$ArgSpec :: method public slot():int +class public static final blue.bex.compile.BexCompiledProgram$ArgSpec :: method public sourcePointer():java.lang.String +class public static final blue.bex.compile.BexCompiledProgram$ArgSpec :: method public typed():boolean +class public static final blue.bex.compile.BexCompiledProgram$CompiledFunction +class public static final blue.bex.compile.BexCompiledProgram$CompiledFunction :: constructor public (java.lang.String,java.util.List,java.util.List,blue.bex.runtime.CompiledExpression,int) +class public static final blue.bex.compile.BexCompiledProgram$CompiledFunction :: method public arg(java.lang.String):blue.bex.compile.BexCompiledProgram$ArgSpec +class public static final blue.bex.compile.BexCompiledProgram$CompiledFunction :: method public argSlot(java.lang.String):int +class public static final blue.bex.compile.BexCompiledProgram$CompiledFunction :: method public args():java.util.Collection +class public static final blue.bex.compile.BexCompiledProgram$CompiledFunction :: method public frameSize():int +class public static final blue.bex.compile.BexCompiledProgram$CompiledFunction :: method public hasArg(java.lang.String):boolean +class public static final blue.bex.compile.BexCompiledProgram$CompiledFunction :: method public invokePrepared(blue.bex.runtime.BexRuntime,blue.bex.runtime.CompiledFrame,int[],blue.bex.value.BexValue[]):blue.bex.value.BexValue +class public static final blue.bex.compile.BexCompiledProgram$CompiledFunction :: method public invokeRoot(blue.bex.runtime.BexRuntime):blue.bex.value.BexValue +class public static final blue.bex.compile.BexCompiledProgram$CompiledFunction :: method public name():java.lang.String +class public static final blue.bex.runtime.CompileScope$Visibility diff --git a/gradle/verification/api/working-checkpoint-public-api-classification.json b/gradle/verification/api/working-checkpoint-public-api-classification.json new file mode 100644 index 0000000..b793ab2 --- /dev/null +++ b/gradle/verification/api/working-checkpoint-public-api-classification.json @@ -0,0 +1,104 @@ +{ + "schema": "blue-bex-public-api-classification/1.0", + "inventory": { + "path": "src/test/resources/hosted-release/required-public-api.txt", + "sha256": "43aea6ae9de6f39729f93c146ff5453887da9a2303f6f4f33573ba47f37d5be0", + "manifestSchema": "blue-bex-binary-api-manifest/1.0", + "publicTypeCount": 72, + "publicDescriptorCount": 798 + }, + "sourceState": { + "bexBaselineCommit": "395c484111f8c4e9e0e98d2db7f1c5b0777bd5a8", + "bexMigrationState": "working-tree delta rooted at bexBaselineCommit", + "languageExactCommit": "9a607e584ff5dd973684d35d71eb4022d946b760", + "languageVerifiedImplementationCommit": "63a9ed6a1a66d47119a80d16ed2ab0beda0d2453" + }, + "classifications": { + "stable API": [ + "blue.bex.BexException", + "blue.bex.BexSourcePath", + "blue.bex.api.BexEngine", + "blue.bex.api.BexEngine$Builder", + "blue.bex.api.BexExecutionContext", + "blue.bex.api.BexExecutionContext$Builder", + "blue.bex.api.BexMetricsSink", + "blue.bex.api.BexProgramSource", + "blue.bex.api.BexProgramSource$Kind", + "blue.bex.api.BexStepResults", + "blue.bex.api.BexStepResults$Builder", + "blue.bex.compile.BexCompiledProgram", + "blue.bex.compile.BexCompiledProgramCache", + "blue.bex.compile.BexCompiledProgramKey", + "blue.bex.compile.BexCompiler", + "blue.bex.compile.LruBexCompiledProgramCache", + "blue.bex.gas.BexGasCharge", + "blue.bex.gas.BexGasCounter", + "blue.bex.gas.BexGasLedger", + "blue.bex.gas.BexGasLimitExceededException", + "blue.bex.gas.BexGasMeter", + "blue.bex.gas.BexGasSchedule", + "blue.bex.gas.BexGasSchedule$Builder", + "blue.bex.result.BexChangeset", + "blue.bex.result.BexEvents", + "blue.bex.result.BexExecutionResult", + "blue.bex.result.BexMetrics", + "blue.bex.result.BexPatchEntry", + "blue.bex.value.BexUnicodeOrder", + "blue.bex.value.BexUnicodeOrder$Comparison", + "blue.bex.value.BexValue", + "blue.bex.value.BexValues" + ], + "host SPI": [ + "blue.bex.api.BexDocumentView", + "blue.bex.api.BexGasLedgerHost", + "blue.bex.api.FrozenBexDocumentView", + "blue.bex.api.ProcessorExecutionContextBexDocumentView", + "blue.bex.api.ProcessorExecutionContextBexGasLedgerHost", + "blue.bex.output.BexAdmittedValue", + "blue.bex.output.BexEstablishedIdentity", + "blue.bex.output.BexOutputAdmission", + "blue.bex.output.BexOutputKind", + "blue.bex.output.BexSemanticIdentityBoundary", + "blue.bex.output.ProcessorExecutionContextBexSemanticIdentityBoundary" + ], + "intrinsic SPI": [ + "blue.bex.api.BexIntrinsicInvocation", + "blue.bex.api.BexIntrinsicProcessor", + "blue.bex.api.BexIntrinsicRegistry", + "blue.bex.api.BexIntrinsicRegistry$Builder", + "blue.bex.api.BexTypeBlueIdResolver" + ], + "internal implementation": [ + "blue.bex.compile.BexCompiledProgram$ArgSpec", + "blue.bex.compile.BexCompiledProgram$CompiledFunction", + "blue.bex.compile.BexContainsCache", + "blue.bex.compile.BexNodeIdentity", + "blue.bex.pointer.BexPointer", + "blue.bex.pointer.BexPointerCache", + "blue.bex.result.BexResultOverlay", + "blue.bex.runtime.BexExecutionAccumulator", + "blue.bex.runtime.BexRuntime", + "blue.bex.runtime.CompileScope", + "blue.bex.runtime.CompileScope$Visibility", + "blue.bex.runtime.CompiledExpression", + "blue.bex.runtime.CompiledFrame", + "blue.bex.runtime.CompiledStatement", + "blue.bex.runtime.Control", + "blue.bex.type.BexBlueTypeMatcher", + "blue.bex.value.BexBlueNodeWriter", + "blue.bex.value.BexFrozenWriter", + "blue.bex.value.BexNodeWriter", + "blue.bex.value.BexSimpleWriter", + "blue.bex.value.ChangesetBexValue", + "blue.bex.value.EventsBexValue", + "blue.bex.value.OverlayListBexValue", + "blue.bex.value.PatchEntryBexValue" + ], + "conformance-only": [] + }, + "notes": [ + "Classification is intent metadata; required-public-api.txt remains the exact descriptor inventory.", + "Public visibility alone does not make an internal implementation type stable.", + "Conformance-only types are test-source artifacts and therefore absent from the production binary manifest." + ] +} diff --git a/gradle/verification/api/working-checkpoint-public-api.txt b/gradle/verification/api/working-checkpoint-public-api.txt new file mode 100644 index 0000000..f05fe3c --- /dev/null +++ b/gradle/verification/api/working-checkpoint-public-api.txt @@ -0,0 +1,799 @@ +schema=blue-bex-binary-api-manifest/1.0 +class public blue.bex.BexException extends java.lang.RuntimeException + constructor public (java.lang.String) + constructor public (java.lang.String,java.lang.Throwable) + method public sourcePath():java.util.Optional + method public static at(blue.bex.BexSourcePath,java.lang.String):blue.bex.BexException + method public static at(blue.bex.BexSourcePath,java.lang.String,java.lang.Throwable):blue.bex.BexException + method public withSourcePath(blue.bex.BexSourcePath):blue.bex.BexException +class public final blue.bex.BexSourcePath + constructor public (java.lang.String,java.lang.String,java.lang.String) + method public equals(java.lang.Object):boolean + method public functionName():java.lang.String + method public hashCode():int + method public operator():java.lang.String + method public pointer():java.lang.String + method public static of(java.lang.String,java.lang.String,java.lang.String):blue.bex.BexSourcePath + method public toString():java.lang.String +class public abstract interface blue.bex.api.BexDocumentView + method public abstract canonicalAt(java.lang.String):blue.bex.value.BexValue + method public abstract currentScopePath():java.lang.String + method public abstract resolvePointer(java.lang.String):java.lang.String + method public abstract resolvedAt(java.lang.String):blue.bex.value.BexValue +class public final blue.bex.api.BexEngine + method public compile(blue.bex.api.BexProgramSource):blue.bex.compile.BexCompiledProgram + method public compileAndExecute(blue.bex.api.BexProgramSource,blue.bex.api.BexExecutionContext):blue.bex.result.BexExecutionResult + method public execute(blue.bex.compile.BexCompiledProgram,blue.bex.api.BexExecutionContext):blue.bex.result.BexExecutionResult + method public static builder():blue.bex.api.BexEngine$Builder +class public static final blue.bex.api.BexEngine$Builder + constructor public () + method public build():blue.bex.api.BexEngine + method public cache(blue.bex.compile.BexCompiledProgramCache):blue.bex.api.BexEngine$Builder + method public gasSchedule(blue.bex.gas.BexGasSchedule):blue.bex.api.BexEngine$Builder + method public intrinsic(java.lang.Class,blue.bex.api.BexTypeBlueIdResolver,java.lang.String,java.util.Map,blue.bex.api.BexIntrinsicProcessor):blue.bex.api.BexEngine$Builder + method public intrinsic(java.lang.Class,java.lang.String,java.util.Map,blue.bex.api.BexIntrinsicProcessor):blue.bex.api.BexEngine$Builder + method public intrinsic(java.lang.String,java.lang.String,java.util.Map,blue.bex.api.BexIntrinsicProcessor):blue.bex.api.BexEngine$Builder + method public intrinsics(blue.bex.api.BexIntrinsicRegistry):blue.bex.api.BexEngine$Builder + method public language(blue.language.runtime.BlueLanguage):blue.bex.api.BexEngine$Builder + method public metrics(blue.bex.api.BexMetricsSink):blue.bex.api.BexEngine$Builder +class public final blue.bex.api.BexExecutionContext + method public binding(java.lang.String):blue.bex.value.BexValue + method public bindings():java.util.Map + method public currentContract():blue.bex.value.BexValue + method public currentScopePath():java.lang.String + method public document():blue.bex.api.BexDocumentView + method public event():blue.bex.value.BexValue + method public gasLedgerHost():blue.bex.api.BexGasLedgerHost + method public gasLimit():long + method public parentRemainingGas():long + method public processingEvent():blue.bex.value.BexValue + method public semanticIdentityBoundary():blue.bex.output.BexSemanticIdentityBoundary + method public static builder():blue.bex.api.BexExecutionContext$Builder + method public steps():blue.bex.api.BexStepResults +class public static final blue.bex.api.BexExecutionContext$Builder + constructor public () + method public binding(java.lang.String,blue.bex.value.BexValue):blue.bex.api.BexExecutionContext$Builder + method public bindings(java.util.Map):blue.bex.api.BexExecutionContext$Builder + method public build():blue.bex.api.BexExecutionContext + method public currentContract(blue.bex.value.BexValue):blue.bex.api.BexExecutionContext$Builder + method public document(blue.bex.api.BexDocumentView):blue.bex.api.BexExecutionContext$Builder + method public event(blue.bex.value.BexValue):blue.bex.api.BexExecutionContext$Builder + method public gasLedgerHost(blue.bex.api.BexGasLedgerHost):blue.bex.api.BexExecutionContext$Builder + method public gasLimit(long):blue.bex.api.BexExecutionContext$Builder + method public lazyBinding(java.lang.String,java.util.function.Supplier):blue.bex.api.BexExecutionContext$Builder + method public parentRemainingGas(long):blue.bex.api.BexExecutionContext$Builder + method public processingEvent(blue.bex.value.BexValue):blue.bex.api.BexExecutionContext$Builder + method public processorExecutionContext(blue.language.processor.ProcessorExecutionContext):blue.bex.api.BexExecutionContext$Builder + method public processorExecutionContext(blue.language.processor.ProcessorExecutionContext,java.lang.String):blue.bex.api.BexExecutionContext$Builder + method public semanticIdentityBoundary(blue.bex.output.BexSemanticIdentityBoundary):blue.bex.api.BexExecutionContext$Builder + method public steps(blue.bex.api.BexStepResults):blue.bex.api.BexExecutionContext$Builder +class public abstract interface blue.bex.api.BexGasLedgerHost + method public abstract evidenceUnavailable(blue.language.processor.GasMeter$ChildGasLedger):void + method public abstract failedDeterministically(blue.language.processor.GasMeter$ChildGasLedger):void + method public abstract open(java.lang.String,java.util.Map):blue.language.processor.GasMeter$ChildGasLedger + method public abstract submit(blue.language.processor.GasMeter$ChildGasLedger):void + method public localGasLimitExceeded(blue.bex.gas.BexGasLimitExceededException,java.lang.RuntimeException):java.lang.RuntimeException + method public open(java.lang.String,java.util.Map,blue.language.processor.RuntimeWorkBudget):blue.language.processor.GasMeter$ChildGasLedger + method public openSharedBudget(long):blue.language.processor.RuntimeWorkBudget + method public propagateGasExhaustion(blue.language.processor.GasMeter$ChildGasLedger,blue.language.processor.GasLimitExceededException):void + method public separatesRuntimeNamespaces():boolean +class public final blue.bex.api.BexIntrinsicInvocation + method public blueId():java.lang.String + method public charge(java.lang.String,long):void + method public charge(java.lang.String,long,java.lang.String):void + method public exactField(java.lang.String):blue.bex.output.BexAdmittedValue + method public field(java.lang.String):blue.bex.value.BexValue + method public fields():java.util.Map + method public gasNamespace():java.lang.String + method public gasUsed():long + method public namedCounterWeights():java.util.Map + method public type():blue.bex.value.BexValue +class public abstract interface blue.bex.api.BexIntrinsicProcessor + method public abstract execute(blue.bex.api.BexIntrinsicInvocation):blue.bex.value.BexValue +class public final blue.bex.api.BexIntrinsicRegistry + method public identity():java.lang.String + method public invoke(java.lang.String,blue.bex.value.BexValue,java.util.Map,blue.bex.gas.BexGasMeter,blue.bex.output.BexOutputAdmission):blue.bex.value.BexValue + method public registeredNamedWeights():java.util.Map + method public registeredNamedWeights(java.util.Set):java.util.Map + method public registeredNamespaceWeights():java.util.Map + method public registeredNamespaceWeights(java.util.Set):java.util.Map + method public static builder():blue.bex.api.BexIntrinsicRegistry$Builder + method public static empty():blue.bex.api.BexIntrinsicRegistry + method public supportedBlueIds():java.util.Set + method public supports(java.lang.String):boolean + method public with(java.lang.Class,blue.bex.api.BexTypeBlueIdResolver,java.lang.String,java.util.Map,blue.bex.api.BexIntrinsicProcessor):blue.bex.api.BexIntrinsicRegistry + method public with(java.lang.Class,java.lang.String,java.util.Map,blue.bex.api.BexIntrinsicProcessor):blue.bex.api.BexIntrinsicRegistry + method public with(java.lang.String,java.lang.String,java.util.Map,blue.bex.api.BexIntrinsicProcessor):blue.bex.api.BexIntrinsicRegistry +class public static final blue.bex.api.BexIntrinsicRegistry$Builder + constructor public () + method public build():blue.bex.api.BexIntrinsicRegistry + method public register(java.lang.Class,blue.bex.api.BexTypeBlueIdResolver,java.lang.String,java.util.Map,blue.bex.api.BexIntrinsicProcessor):blue.bex.api.BexIntrinsicRegistry$Builder + method public register(java.lang.Class,java.lang.String,java.util.Map,blue.bex.api.BexIntrinsicProcessor):blue.bex.api.BexIntrinsicRegistry$Builder + method public register(java.lang.String,java.lang.String,java.lang.String,java.util.Map,blue.bex.api.BexIntrinsicProcessor):blue.bex.api.BexIntrinsicRegistry$Builder + method public register(java.lang.String,java.lang.String,java.util.Map,blue.bex.api.BexIntrinsicProcessor):blue.bex.api.BexIntrinsicRegistry$Builder +class public abstract interface blue.bex.api.BexMetricsSink + field public static final NOOP:blue.bex.api.BexMetricsSink + method public abstract accept(blue.bex.result.BexMetrics):void +class public final blue.bex.api.BexProgramSource + method public definitionNode():java.util.Optional + method public entry():java.util.Optional + method public isExpression():boolean + method public kind():blue.bex.api.BexProgramSource$Kind + method public programNode():blue.language.snapshot.FrozenNode + method public static expression(blue.language.snapshot.FrozenNode):blue.bex.api.BexProgramSource + method public static inline(blue.language.snapshot.FrozenNode):blue.bex.api.BexProgramSource + method public static withDefinition(blue.language.snapshot.FrozenNode,blue.language.snapshot.FrozenNode,java.lang.String):blue.bex.api.BexProgramSource +class public static final blue.bex.api.BexProgramSource$Kind extends java.lang.Enum + field public static final EXPRESSION:blue.bex.api.BexProgramSource$Kind + field public static final FULL_PROGRAM:blue.bex.api.BexProgramSource$Kind + method public static valueOf(java.lang.String):blue.bex.api.BexProgramSource$Kind + method public static values():blue.bex.api.BexProgramSource$Kind[] +class public final blue.bex.api.BexStepResults + method public asValue():blue.bex.value.BexValue + method public static builder():blue.bex.api.BexStepResults$Builder + method public static empty():blue.bex.api.BexStepResults + method public step(java.lang.String):blue.bex.value.BexValue +class public static final blue.bex.api.BexStepResults$Builder + constructor public () + method public build():blue.bex.api.BexStepResults + method public put(java.lang.String,blue.bex.result.BexExecutionResult):blue.bex.api.BexStepResults$Builder + method public put(java.lang.String,blue.bex.value.BexValue):blue.bex.api.BexStepResults$Builder +class public abstract interface blue.bex.api.BexTypeBlueIdResolver + method public abstract resolve(java.lang.Class):java.lang.String +class public final blue.bex.api.FrozenBexDocumentView implements blue.bex.api.BexDocumentView + constructor public (blue.language.snapshot.FrozenNode) + constructor public (blue.language.snapshot.FrozenNode,blue.language.snapshot.FrozenNode,java.lang.String) + method public canonicalAt(java.lang.String):blue.bex.value.BexValue + method public currentScopePath():java.lang.String + method public resolvePointer(java.lang.String):java.lang.String + method public resolvedAt(java.lang.String):blue.bex.value.BexValue +class public final blue.bex.api.ProcessorExecutionContextBexDocumentView implements blue.bex.api.BexDocumentView + constructor public (blue.language.processor.ProcessorExecutionContext) + method public canonicalAt(java.lang.String):blue.bex.value.BexValue + method public currentScopePath():java.lang.String + method public resolvePointer(java.lang.String):java.lang.String + method public resolvedAt(java.lang.String):blue.bex.value.BexValue +class public final blue.bex.api.ProcessorExecutionContextBexGasLedgerHost implements blue.bex.api.BexGasLedgerHost + constructor public (blue.language.processor.ProcessorExecutionContext) + constructor public (blue.language.processor.ProcessorExecutionContext,java.lang.String) + constructor public (blue.language.processor.RuntimeWorkSession,java.lang.String) + method public evidenceUnavailable(blue.language.processor.GasMeter$ChildGasLedger):void + method public failedDeterministically(blue.language.processor.GasMeter$ChildGasLedger):void + method public localGasLimitExceeded(blue.bex.gas.BexGasLimitExceededException,java.lang.RuntimeException):java.lang.RuntimeException + method public open(java.lang.String,java.util.Map):blue.language.processor.GasMeter$ChildGasLedger + method public open(java.lang.String,java.util.Map,blue.language.processor.RuntimeWorkBudget):blue.language.processor.GasMeter$ChildGasLedger + method public openSharedBudget(long):blue.language.processor.RuntimeWorkBudget + method public physicalNamespace(java.lang.String):java.lang.String + method public propagateGasExhaustion(blue.language.processor.GasMeter$ChildGasLedger,blue.language.processor.GasLimitExceededException):void + method public runtimeNamespace():java.lang.String + method public separatesRuntimeNamespaces():boolean + method public submit(blue.language.processor.GasMeter$ChildGasLedger):void +class public final blue.bex.compile.BexCompiledProgram + constructor public (blue.bex.compile.BexCompiledProgram$CompiledFunction,java.util.Map,java.util.Map,int,java.lang.String) + constructor public (blue.bex.compile.BexCompiledProgram$CompiledFunction,java.util.Map,java.util.Map,int,java.lang.String,java.util.Set) + method public constant(java.lang.String):blue.bex.value.BexValue + method public constants():java.util.Map + method public entry():blue.bex.compile.BexCompiledProgram$CompiledFunction + method public execute(blue.bex.runtime.BexRuntime):blue.bex.value.BexValue + method public functions():java.util.Map + method public programBlueId():java.lang.String + method public requiredIntrinsicBlueIds():java.util.Set + method public rootFrameSize():int +class public static final blue.bex.compile.BexCompiledProgram$ArgSpec + constructor public (java.lang.String,int,blue.language.snapshot.FrozenNode,java.lang.String) + method public name():java.lang.String + method public pattern():blue.language.snapshot.FrozenNode + method public slot():int + method public sourcePointer():java.lang.String + method public typed():boolean +class public static final blue.bex.compile.BexCompiledProgram$CompiledFunction + constructor public (java.lang.String,java.util.List,java.util.List,blue.bex.runtime.CompiledExpression,int) + method public arg(java.lang.String):blue.bex.compile.BexCompiledProgram$ArgSpec + method public argSlot(java.lang.String):int + method public args():java.util.Collection + method public frameSize():int + method public hasArg(java.lang.String):boolean + method public invokePrepared(blue.bex.runtime.BexRuntime,blue.bex.runtime.CompiledFrame,int[],blue.bex.value.BexValue[]):blue.bex.value.BexValue + method public invokeRoot(blue.bex.runtime.BexRuntime):blue.bex.value.BexValue + method public name():java.lang.String +class public abstract interface blue.bex.compile.BexCompiledProgramCache + method public abstract get(blue.bex.compile.BexCompiledProgramKey):blue.bex.compile.BexCompiledProgram + method public abstract put(blue.bex.compile.BexCompiledProgramKey,blue.bex.compile.BexCompiledProgram):void +class public final blue.bex.compile.BexCompiledProgramKey + constructor public (blue.bex.api.BexProgramSource$Kind,java.lang.String,java.lang.String,java.lang.String) + constructor public (blue.bex.api.BexProgramSource$Kind,java.lang.String,java.lang.String,java.lang.String,java.lang.String) + constructor public (java.lang.String,java.lang.String,java.lang.String) + field public static final BEX_RUNTIME_REGISTRY_IDENTITY:java.lang.String + field public static final COMPILER_IDENTITY:java.lang.String + method public compileEnvironmentIdentity():java.lang.String + method public definitionIdentity():java.lang.String + method public entryName():java.lang.String + method public equals(java.lang.Object):boolean + method public hashCode():int + method public kind():blue.bex.api.BexProgramSource$Kind + method public programIdentity():java.lang.String + method public static from(blue.bex.api.BexProgramSource):blue.bex.compile.BexCompiledProgramKey + method public static from(blue.bex.api.BexProgramSource,java.lang.String):blue.bex.compile.BexCompiledProgramKey +class public final blue.bex.compile.BexCompiler + constructor public (blue.bex.result.BexMetrics) + constructor public (blue.bex.result.BexMetrics,blue.bex.api.BexIntrinsicRegistry) + method public compile(blue.bex.api.BexProgramSource):blue.bex.compile.BexCompiledProgram +class public final blue.bex.compile.BexContainsCache + constructor public () + constructor public (int) + method public synchronized containsBex(blue.language.snapshot.FrozenNode,blue.bex.result.BexMetrics):boolean +class public final blue.bex.compile.BexNodeIdentity + method public static safeBlueId(blue.language.snapshot.FrozenNode):java.lang.String + method public static stable(blue.language.snapshot.FrozenNode):java.lang.String +class public final blue.bex.compile.LruBexCompiledProgramCache implements blue.bex.compile.BexCompiledProgramCache + constructor public () + constructor public (int) + method public synchronized get(blue.bex.compile.BexCompiledProgramKey):blue.bex.compile.BexCompiledProgram + method public synchronized put(blue.bex.compile.BexCompiledProgramKey,blue.bex.compile.BexCompiledProgram):void +class public final blue.bex.gas.BexGasCharge + constructor public (long,blue.bex.gas.BexGasCounter,long,long,java.lang.String,java.lang.String,java.lang.String) + constructor public (long,blue.bex.gas.BexGasCounter,long,long,long,java.lang.String,java.lang.String,java.lang.String) + constructor public (long,java.lang.String,java.lang.String,long,long,java.lang.String,java.lang.String,java.lang.String) + constructor public (long,java.lang.String,java.lang.String,long,long,long,java.lang.String,java.lang.String,java.lang.String) + method public counter():blue.bex.gas.BexGasCounter + method public counterName():java.lang.String + method public equals(java.lang.Object):boolean + method public gas():long + method public hashCode():int + method public namespace():java.lang.String + method public operator():java.lang.String + method public portableCounter():blue.bex.gas.BexGasCounter + method public qualifiedCounterName():java.lang.String + method public quantity():long + method public reason():java.lang.String + method public sequence():long + method public sourcePath():java.lang.String + method public toString():java.lang.String + method public weight():long +class public final blue.bex.gas.BexGasCounter extends java.lang.Enum + field public static final BINDING_READ:blue.bex.gas.BexGasCounter + field public static final BLUE_OUTPUT_BOUNDARY:blue.bex.gas.BexGasCounter + field public static final COLLECTION_ITEM_PRODUCED:blue.bex.gas.BexGasCounter + field public static final COLLECTION_ITEM_VISITED:blue.bex.gas.BexGasCounter + field public static final COMPARISON_NODE_VISITED:blue.bex.gas.BexGasCounter + field public static final CONSTANT_READ:blue.bex.gas.BexGasCounter + field public static final CURRENT_CONTRACT_READ:blue.bex.gas.BexGasCounter + field public static final DOCUMENT_READ:blue.bex.gas.BexGasCounter + field public static final EVENT_APPENDED:blue.bex.gas.BexGasCounter + field public static final EVENT_READ:blue.bex.gas.BexGasCounter + field public static final EXPRESSION_EVALUATED:blue.bex.gas.BexGasCounter + field public static final FUNCTION_CALLED:blue.bex.gas.BexGasCounter + field public static final INTEGER_LIMB_OPERATION:blue.bex.gas.BexGasCounter + field public static final INTRINSIC_CALLED:blue.bex.gas.BexGasCounter + field public static final LIST_ITEM_READ:blue.bex.gas.BexGasCounter + field public static final MANIFEST_IDENTITY:java.lang.String + field public static final NAMESPACE:java.lang.String + field public static final NODE_IDENTITY_REQUESTED:blue.bex.gas.BexGasCounter + field public static final OBJECT_MEMBER_READ:blue.bex.gas.BexGasCounter + field public static final PATCH_APPENDED:blue.bex.gas.BexGasCounter + field public static final POINTER_SEGMENT_READ:blue.bex.gas.BexGasCounter + field public static final POINTER_SEGMENT_WRITTEN:blue.bex.gas.BexGasCounter + field public static final PROCESSING_EVENT_READ:blue.bex.gas.BexGasCounter + field public static final RESULT_VALUE_READ:blue.bex.gas.BexGasCounter + field public static final SCHEDULE_ID:java.lang.String + field public static final SORT_COMPARISON:blue.bex.gas.BexGasCounter + field public static final STATEMENT_EXECUTED:blue.bex.gas.BexGasCounter + field public static final STEPS_READ:blue.bex.gas.BexGasCounter + field public static final TEXT_BLOCK_CONSTRUCTED:blue.bex.gas.BexGasCounter + field public static final TEXT_BLOCK_EXAMINED:blue.bex.gas.BexGasCounter + field public static final TRANSIENT_LIST_ITEM_PRODUCED:blue.bex.gas.BexGasCounter + field public static final TRANSIENT_OBJECT_MEMBER_PRODUCED:blue.bex.gas.BexGasCounter + field public static final VARIABLE_READ:blue.bex.gas.BexGasCounter + method public canonicalName():java.lang.String + method public counterName():java.lang.String + method public defaultWeight():long + method public static defaultWeights():java.util.Map + method public static fromCanonicalName(java.lang.String):blue.bex.gas.BexGasCounter + method public static fromName(java.lang.String):blue.bex.gas.BexGasCounter + method public static valueOf(java.lang.String):blue.bex.gas.BexGasCounter + method public static values():blue.bex.gas.BexGasCounter[] + method public toString():java.lang.String +class public final blue.bex.gas.BexGasLedger + constructor public (java.util.List) + method public equals(java.lang.Object):boolean + method public gasUsed():long + method public hashCode():int + method public manifestIdentity():java.lang.String + method public namedQuantities():java.util.Map + method public quantities():java.util.Map + method public quantity(blue.bex.gas.BexGasCounter):long + method public quantity(java.lang.String,java.lang.String):long + method public scheduleId():java.lang.String + method public static empty():blue.bex.gas.BexGasLedger + method public toString():java.lang.String + method public totalGas():long + method public trace():java.util.List +class public final blue.bex.gas.BexGasLimitExceededException extends blue.bex.BexException + method public admittedGas():long + method public counter():blue.bex.gas.BexGasCounter + method public counterName():java.lang.String + method public effectiveBudget():long + method public hostGasLimitExceeded():blue.language.processor.GasLimitExceededException + method public namespace():java.lang.String + method public quantity():long + method public weight():long +class public final blue.bex.gas.BexGasMeter + constructor public (blue.bex.gas.BexGasSchedule,blue.language.processor.GasMeter$ChildGasLedger) + constructor public (blue.bex.gas.BexGasSchedule,blue.language.processor.GasMeter$ChildGasLedger,long) + constructor public (blue.bex.gas.BexGasSchedule,java.util.Map,long,java.util.Map) + constructor public (blue.bex.gas.BexGasSchedule,long) + constructor public (blue.bex.gas.BexGasSchedule,long,long) + constructor public (blue.bex.gas.BexGasSchedule,long,long,java.util.Map) + field public static final NO_LOCAL_LIMIT:long + method public charge(blue.bex.gas.BexGasCounter):void + method public charge(blue.bex.gas.BexGasCounter,blue.bex.BexSourcePath,java.lang.String,java.lang.String):void + method public charge(blue.bex.gas.BexGasCounter,java.lang.String,java.lang.String,java.lang.String):void + method public charge(blue.bex.gas.BexGasCounter,long):void + method public charge(blue.bex.gas.BexGasCounter,long,blue.bex.BexSourcePath,java.lang.String,java.lang.String):void + method public charge(blue.bex.gas.BexGasCounter,long,java.lang.String):void + method public charge(blue.bex.gas.BexGasCounter,long,java.lang.String,java.lang.String,java.lang.String):void + method public chargeNamed(java.lang.String,java.lang.String,long):void + method public chargeNamed(java.lang.String,java.lang.String,long,blue.bex.BexSourcePath,java.lang.String,java.lang.String):void + method public chargeNamed(java.lang.String,java.lang.String,long,java.lang.String):void + method public chargeNamed(java.lang.String,java.lang.String,long,java.lang.String,java.lang.String,java.lang.String):void + method public chargeNamed(java.lang.String,java.lang.String,long,long,java.lang.String,java.lang.String,java.lang.String):void + method public childLedgerWeights():java.util.Map + method public effectiveBudget():long + method public failHostLedger(java.util.function.Consumer):void + method public hasHostLedger():boolean + method public hostLedgerFinalized():boolean + method public hostLedgerSubmitted():boolean + method public ledger():blue.bex.gas.BexGasLedger + method public localLimit():long + method public parentRemainingGas():long + method public propagateHostGasExhaustion(blue.language.processor.GasLimitExceededException,java.util.function.Consumer,java.util.function.BiConsumer):void + method public registeredNamedWeights():java.util.Map + method public remaining():long + method public remainingGas():long + method public schedule():blue.bex.gas.BexGasSchedule + method public static childLedgerWeights(blue.bex.gas.BexGasSchedule,java.util.Map):java.util.Map + method public static hostedWithSharedLocalLimit(blue.bex.gas.BexGasSchedule,java.util.Map,long,java.util.Map):blue.bex.gas.BexGasMeter + method public static qualifiedCounterName(java.lang.String,java.lang.String):java.lang.String + method public submitHostLedger(java.util.function.Consumer):void + method public totalGas():long + method public trace():java.util.List + method public unavailableHostLedger(java.util.function.Consumer):void + method public used():long +class public final blue.bex.gas.BexGasSchedule + field public final bindingRead:long + field public final blueOutputBoundary:long + field public final collectionItemProduced:long + field public final collectionItemVisited:long + field public final comparisonNodeVisited:long + field public final constantRead:long + field public final currentContractRead:long + field public final documentRead:long + field public final eventAppended:long + field public final eventRead:long + field public final expressionEvaluated:long + field public final functionCalled:long + field public final integerLimbOperation:long + field public final intrinsicCalled:long + field public final listItemRead:long + field public final nodeIdentityRequested:long + field public final objectMemberRead:long + field public final patchAppended:long + field public final pointerSegmentRead:long + field public final pointerSegmentWritten:long + field public final processingEventRead:long + field public final resultValueRead:long + field public final sortComparison:long + field public final statementExecuted:long + field public final stepsRead:long + field public final textBlockConstructed:long + field public final textBlockExamined:long + field public final transientListItemProduced:long + field public final transientObjectMemberProduced:long + field public final variableRead:long + field public static final MANIFEST_IDENTITY:java.lang.String + field public static final SCHEDULE_ID:java.lang.String + method public counterWeights():java.util.Map + method public manifestIdentity():java.lang.String + method public namedWeights():java.util.Map + method public scheduleId():java.lang.String + method public static builder():blue.bex.gas.BexGasSchedule$Builder + method public static defaults():blue.bex.gas.BexGasSchedule + method public toBuilder():blue.bex.gas.BexGasSchedule$Builder + method public weight(blue.bex.gas.BexGasCounter):long + method public weight(java.lang.String):long + method public weights():java.util.Map +class public static final blue.bex.gas.BexGasSchedule$Builder + method public bindingRead(long):blue.bex.gas.BexGasSchedule$Builder + method public blueOutputBoundary(long):blue.bex.gas.BexGasSchedule$Builder + method public build():blue.bex.gas.BexGasSchedule + method public collectionItemProduced(long):blue.bex.gas.BexGasSchedule$Builder + method public collectionItemVisited(long):blue.bex.gas.BexGasSchedule$Builder + method public comparisonNodeVisited(long):blue.bex.gas.BexGasSchedule$Builder + method public constantRead(long):blue.bex.gas.BexGasSchedule$Builder + method public currentContractRead(long):blue.bex.gas.BexGasSchedule$Builder + method public documentRead(long):blue.bex.gas.BexGasSchedule$Builder + method public eventAppended(long):blue.bex.gas.BexGasSchedule$Builder + method public eventRead(long):blue.bex.gas.BexGasSchedule$Builder + method public expressionEvaluated(long):blue.bex.gas.BexGasSchedule$Builder + method public functionCalled(long):blue.bex.gas.BexGasSchedule$Builder + method public integerLimbOperation(long):blue.bex.gas.BexGasSchedule$Builder + method public intrinsicCalled(long):blue.bex.gas.BexGasSchedule$Builder + method public listItemRead(long):blue.bex.gas.BexGasSchedule$Builder + method public nodeIdentityRequested(long):blue.bex.gas.BexGasSchedule$Builder + method public objectMemberRead(long):blue.bex.gas.BexGasSchedule$Builder + method public patchAppended(long):blue.bex.gas.BexGasSchedule$Builder + method public pointerSegmentRead(long):blue.bex.gas.BexGasSchedule$Builder + method public pointerSegmentWritten(long):blue.bex.gas.BexGasSchedule$Builder + method public processingEventRead(long):blue.bex.gas.BexGasSchedule$Builder + method public resultValueRead(long):blue.bex.gas.BexGasSchedule$Builder + method public sortComparison(long):blue.bex.gas.BexGasSchedule$Builder + method public statementExecuted(long):blue.bex.gas.BexGasSchedule$Builder + method public stepsRead(long):blue.bex.gas.BexGasSchedule$Builder + method public textBlockConstructed(long):blue.bex.gas.BexGasSchedule$Builder + method public textBlockExamined(long):blue.bex.gas.BexGasSchedule$Builder + method public transientListItemProduced(long):blue.bex.gas.BexGasSchedule$Builder + method public transientObjectMemberProduced(long):blue.bex.gas.BexGasSchedule$Builder + method public variableRead(long):blue.bex.gas.BexGasSchedule$Builder + method public weight(blue.bex.gas.BexGasCounter,long):blue.bex.gas.BexGasSchedule$Builder + method public weight(java.lang.String,long):blue.bex.gas.BexGasSchedule$Builder +class public final blue.bex.output.BexAdmittedValue + method public node():blue.language.model.Node + method public nodeBlueId():java.lang.String + method public reconstructed():boolean + method public semanticValue():blue.bex.value.BexValue + method public value():blue.bex.value.BexValue +class public final blue.bex.output.BexEstablishedIdentity + constructor public (java.lang.String,blue.language.snapshot.FrozenNode) + method public blueId():java.lang.String + method public frozenValue():blue.language.snapshot.FrozenNode +class public final blue.bex.output.BexOutputAdmission + constructor public (blue.bex.gas.BexGasMeter,blue.bex.output.BexSemanticIdentityBoundary) + method public admit(blue.bex.value.BexValue,blue.bex.output.BexOutputKind):blue.bex.output.BexAdmittedValue + method public semanticIdentityMergeCount():long +class public final blue.bex.output.BexOutputKind extends java.lang.Enum + field public static final EVENT:blue.bex.output.BexOutputKind + field public static final INTRINSIC_INPUT:blue.bex.output.BexOutputKind + field public static final NODE_IDENTITY:blue.bex.output.BexOutputKind + field public static final PATCH_VALUE:blue.bex.output.BexOutputKind + field public static final ROOT_RESULT:blue.bex.output.BexOutputKind + method public reason():java.lang.String + method public static valueOf(java.lang.String):blue.bex.output.BexOutputKind + method public static values():blue.bex.output.BexOutputKind[] +class public abstract interface blue.bex.output.BexSemanticIdentityBoundary + field public static final STANDALONE:blue.bex.output.BexSemanticIdentityBoundary + method public abstract establishIdentity(blue.language.model.Node):blue.bex.output.BexEstablishedIdentity +class public final blue.bex.output.ProcessorExecutionContextBexSemanticIdentityBoundary implements blue.bex.output.BexSemanticIdentityBoundary + constructor public (blue.language.processor.ProcessorExecutionContext) + method public establishIdentity(blue.language.model.Node):blue.bex.output.BexEstablishedIdentity +class public final blue.bex.pointer.BexPointer + method public descendant(java.util.List):blue.bex.pointer.BexPointer + method public equals(java.lang.Object):boolean + method public hashCode():int + method public root():boolean + method public segments():java.util.List + method public static parse(java.lang.String):blue.bex.pointer.BexPointer + method public text():java.lang.String + method public toString():java.lang.String +class public final blue.bex.pointer.BexPointerCache + constructor public () + constructor public (int) + method public capacity():int + method public synchronized get(java.lang.String,blue.bex.result.BexMetrics):blue.bex.pointer.BexPointer +class public final blue.bex.result.BexChangeset + constructor public (java.util.List) + method public asValue():blue.bex.value.BexValue + method public entries():java.util.List + method public static patchEntryValue(blue.bex.result.BexPatchEntry):blue.bex.value.BexValue +class public final blue.bex.result.BexEvents + constructor public (java.util.List) + constructor public (java.util.List,java.util.List) + method public admittedEvents():java.util.List + method public asValue():blue.bex.value.BexValue + method public events():java.util.List +class public final blue.bex.result.BexExecutionResult + constructor public (blue.bex.value.BexValue,blue.bex.result.BexChangeset,blue.bex.result.BexEvents,blue.bex.gas.BexGasLedger,blue.bex.result.BexMetrics) + constructor public (blue.bex.value.BexValue,blue.bex.result.BexChangeset,blue.bex.result.BexEvents,blue.bex.gas.BexGasLedger,blue.bex.result.BexMetrics,blue.bex.output.BexAdmittedValue) + constructor public (blue.bex.value.BexValue,blue.bex.result.BexChangeset,blue.bex.result.BexEvents,java.util.List,blue.bex.result.BexMetrics) + method public changeset():blue.bex.result.BexChangeset + method public events():blue.bex.result.BexEvents + method public gasLedger():blue.bex.gas.BexGasLedger + method public gasTrace():java.util.List + method public gasUsed():long + method public ledger():blue.bex.gas.BexGasLedger + method public metrics():blue.bex.result.BexMetrics + method public output():blue.bex.output.BexAdmittedValue + method public trace():java.util.List + method public value():blue.bex.value.BexValue +class public final blue.bex.result.BexMetrics + constructor public () + method public addCompileNanos(long):void + method public addExecuteNanos(long):void + method public compileCacheHits():long + method public compileCacheMisses():long + method public compileNanos():long + method public compiledExecutions():long + method public containsBexCacheHits():long + method public containsBexCacheMisses():long + method public containsBexScans():long + method public copy():blue.bex.result.BexMetrics + method public currentContractReads():long + method public eventReads():long + method public executeNanos():long + method public expressionEvaluations():long + method public frozenDocumentReads():long + method public frozenOutputConversions():long + method public frozenWriterNodeFallbacks():long + method public functionArgMapAllocations():long + method public functionCalls():long + method public incrementCompileCacheHits():void + method public incrementCompileCacheMisses():void + method public incrementCompiledExecutions():void + method public incrementContainsBexCacheHits():void + method public incrementContainsBexCacheMisses():void + method public incrementContainsBexScans():void + method public incrementCurrentContractReads():void + method public incrementEventReads():void + method public incrementExpressionEvaluations():void + method public incrementFrozenDocumentReads():void + method public incrementFrozenOutputConversions():void + method public incrementFrozenWriterNodeFallbacks():void + method public incrementFunctionArgMapAllocations():void + method public incrementFunctionCalls():void + method public incrementInterpretedFallbacks():void + method public incrementLoopIterations():void + method public incrementNodeMaterializations():void + method public incrementNodeOutputConversions():void + method public incrementPointerCacheHits():void + method public incrementPointerCacheMisses():void + method public incrementPointerParses():void + method public incrementResolvedDocumentReads():void + method public incrementResultOverlayAncestorHits():void + method public incrementResultOverlayDocumentFallbacks():void + method public incrementResultOverlayExactHits():void + method public incrementResultValueReads():void + method public incrementSimpleMaterializations():void + method public incrementStatementExecutions():void + method public incrementStepsReads():void + method public interpretedFallbacks():long + method public loopIterations():long + method public nodeMaterializations():long + method public nodeOutputConversions():long + method public pointerCacheHits():long + method public pointerCacheMisses():long + method public pointerParses():long + method public resolvedDocumentReads():long + method public resultOverlayAncestorHits():long + method public resultOverlayDocumentFallbacks():long + method public resultOverlayExactHits():long + method public resultValueReads():long + method public simpleMaterializations():long + method public statementExecutions():long + method public stepsReads():long +class public final blue.bex.result.BexPatchEntry + constructor public (java.lang.String,java.lang.String,java.lang.String,blue.bex.value.BexValue) + constructor public (java.lang.String,java.lang.String,java.lang.String,blue.bex.value.BexValue,blue.bex.output.BexAdmittedValue) + method public absolutePath():java.lang.String + method public absoluteSegments():java.util.List + method public admittedValue():blue.bex.output.BexAdmittedValue + method public authoredPath():java.lang.String + method public op():java.lang.String + method public val():blue.bex.value.BexValue +class public final blue.bex.result.BexResultOverlay + constructor public (blue.bex.api.BexDocumentView,blue.bex.result.BexMetrics) + constructor public (blue.bex.api.BexDocumentView,blue.bex.result.BexMetrics,blue.language.runtime.BlueLanguage) + method public append(blue.bex.result.BexPatchEntry):void + method public rootValue():blue.bex.value.BexValue + method public valueAt(java.lang.String,java.util.List):blue.bex.value.BexValue +class public final blue.bex.runtime.BexExecutionAccumulator + constructor public (blue.bex.result.BexResultOverlay) + constructor public (blue.bex.result.BexResultOverlay,blue.bex.output.BexOutputAdmission) + method public appendChange(blue.bex.result.BexPatchEntry):void + method public appendEvent(blue.bex.value.BexValue):void + method public changeset():blue.bex.result.BexChangeset + method public events():blue.bex.result.BexEvents + method public overlay():blue.bex.result.BexResultOverlay +class public final blue.bex.runtime.BexRuntime + constructor public (blue.bex.compile.BexCompiledProgram,blue.bex.api.BexExecutionContext,blue.language.runtime.BlueLanguage,blue.bex.gas.BexGasSchedule,blue.bex.result.BexMetrics,blue.bex.pointer.BexPointerCache) + constructor public (blue.bex.compile.BexCompiledProgram,blue.bex.api.BexExecutionContext,blue.language.runtime.BlueLanguage,blue.bex.gas.BexGasSchedule,blue.bex.result.BexMetrics,blue.bex.pointer.BexPointerCache,blue.bex.api.BexIntrinsicRegistry) + method public accumulator():blue.bex.runtime.BexExecutionAccumulator + method public canonicalPointer(java.lang.String):java.lang.String + method public context():blue.bex.api.BexExecutionContext + method public defaultResultValue():blue.bex.value.BexValue + method public execute():blue.bex.result.BexExecutionResult + method public gas():blue.bex.gas.BexGasMeter + method public intrinsics():blue.bex.api.BexIntrinsicRegistry + method public invokeIntrinsic(java.lang.String,blue.bex.value.BexValue,java.util.Map):blue.bex.value.BexValue + method public metrics():blue.bex.result.BexMetrics + method public nodeBlueId(blue.bex.value.BexValue):blue.bex.value.BexValue + method public outputAdmission():blue.bex.output.BexOutputAdmission + method public parseDynamicPointer(java.lang.String):java.util.List + method public pointerCache():blue.bex.pointer.BexPointerCache + method public program():blue.bex.compile.BexCompiledProgram + method public readBinding(java.lang.String,java.util.List):blue.bex.value.BexValue + method public readCurrentContract(java.util.List):blue.bex.value.BexValue + method public readDocument(java.lang.String,java.util.List,boolean):blue.bex.value.BexValue + method public readEvent(java.util.List):blue.bex.value.BexValue + method public readProcessingEvent(java.util.List):blue.bex.value.BexValue + method public readResultValue(java.lang.String,java.util.List):blue.bex.value.BexValue + method public readSteps(java.lang.String,java.util.List):blue.bex.value.BexValue + method public readValuePointer(blue.bex.value.BexValue,java.util.List):blue.bex.value.BexValue + method public resolvePointer(java.lang.String):java.lang.String + method public typeMatcher():blue.bex.type.BexBlueTypeMatcher +class public final blue.bex.runtime.CompileScope + constructor public () + constructor public (blue.bex.runtime.CompileScope) + method public captureVisibility():blue.bex.runtime.CompileScope$Visibility + method public declareOrGetSlot(java.lang.String):int + method public frameSize():int + method public hasSlot(java.lang.String):boolean + method public resolveSlot(java.lang.String):int + method public restoreVisibility(blue.bex.runtime.CompileScope$Visibility):void +class public static final blue.bex.runtime.CompileScope$Visibility +class public abstract interface blue.bex.runtime.CompiledExpression + method public abstract eval(blue.bex.runtime.CompiledFrame):blue.bex.value.BexValue +class public final blue.bex.runtime.CompiledFrame + constructor public (blue.bex.runtime.BexRuntime,int,blue.bex.runtime.CompiledFrame) + method public accumulator():blue.bex.runtime.BexExecutionAccumulator + method public clear(int):void + method public enter(blue.bex.BexSourcePath):blue.bex.BexSourcePath + method public get(int):blue.bex.value.BexValue + method public getRequired(int):blue.bex.value.BexValue + method public isInitialized(int):boolean + method public parent():blue.bex.runtime.CompiledFrame + method public readBinding(java.lang.String,java.util.List):blue.bex.value.BexValue + method public readCurrentContract(java.util.List):blue.bex.value.BexValue + method public readDocument(java.lang.String,java.util.List,boolean):blue.bex.value.BexValue + method public readEvent(java.util.List):blue.bex.value.BexValue + method public readProcessingEvent(java.util.List):blue.bex.value.BexValue + method public restore(blue.bex.BexSourcePath):void + method public returnValue():blue.bex.value.BexValue + method public returnValue(blue.bex.value.BexValue):void + method public runtime():blue.bex.runtime.BexRuntime + method public set(int,blue.bex.value.BexValue):void + method public sourcePath():blue.bex.BexSourcePath +class public abstract interface blue.bex.runtime.CompiledStatement + method public abstract exec(blue.bex.runtime.CompiledFrame):blue.bex.runtime.Control +class public final blue.bex.runtime.Control extends java.lang.Enum + field public static final CONTINUE:blue.bex.runtime.Control + field public static final RETURN:blue.bex.runtime.Control + method public static valueOf(java.lang.String):blue.bex.runtime.Control + method public static values():blue.bex.runtime.Control[] +class public final blue.bex.type.BexBlueTypeMatcher + constructor public (blue.language.runtime.BlueLanguage) + method public matches(blue.bex.value.BexValue,blue.language.snapshot.FrozenNode,blue.bex.gas.BexGasMeter,blue.bex.BexSourcePath):boolean +class public final blue.bex.value.BexBlueNodeWriter + method public static hasLanguageField(blue.bex.value.BexValue):boolean + method public static isLanguageField(java.lang.String):boolean + method public static toNode(blue.bex.value.BexValue):blue.language.model.Node + method public static toSemanticNode(blue.bex.value.BexValue):blue.language.model.Node +class public final blue.bex.value.BexFrozenWriter + method public static toFrozen(blue.bex.value.BexValue):blue.language.snapshot.FrozenNode + method public static toFrozen(blue.bex.value.BexValue,blue.bex.result.BexMetrics):blue.language.snapshot.FrozenNode + method public toFrozenValue(blue.bex.value.BexValue):blue.language.snapshot.FrozenNode +class public final blue.bex.value.BexNodeWriter + method public static toNode(blue.bex.value.BexValue):blue.language.model.Node +class public final blue.bex.value.BexSimpleWriter + method public static toSimple(blue.bex.value.BexValue):java.lang.Object +class public final blue.bex.value.BexUnicodeOrder + field public static final CODE_POINT_COMPARATOR:java.util.Comparator + method public static compareCodePoints(java.lang.String,java.lang.String):int + method public static sortedCopy(java.util.Collection):java.util.List + method public static sortedCopy(java.util.Collection,blue.bex.value.BexUnicodeOrder$Comparison):java.util.List +class public abstract static interface blue.bex.value.BexUnicodeOrder$Comparison + method public abstract compare(java.lang.String,java.lang.String):int +class public abstract interface blue.bex.value.BexValue + method public abstract asBoolean():boolean + method public abstract asInteger():java.math.BigInteger + method public abstract asNumber():java.math.BigDecimal + method public abstract asText():java.lang.String + method public abstract at(java.lang.String):blue.bex.value.BexValue + method public abstract at(java.util.List):blue.bex.value.BexValue + method public abstract get(java.lang.String):blue.bex.value.BexValue + method public abstract isList():boolean + method public abstract isNull():boolean + method public abstract isObject():boolean + method public abstract isScalar():boolean + method public abstract isUndefined():boolean + method public abstract keys():java.util.List + method public abstract size():int + method public abstract toNode():blue.language.model.Node + method public abstract toSimple():java.lang.Object + method public exactBlueId():java.lang.String + method public isExact():boolean +class public final blue.bex.value.BexValues + field public static final NULL:blue.bex.value.BexValue + field public static final UNDEFINED:blue.bex.value.BexValue + method public static admittedExact(blue.language.snapshot.FrozenNode,java.lang.String,blue.bex.value.BexValue):blue.bex.value.BexValue + method public static empty(blue.bex.value.BexValue):boolean + method public static equal(blue.bex.value.BexValue,blue.bex.value.BexValue):boolean + method public static exact(blue.language.snapshot.FrozenNode,blue.language.snapshot.FrozenNode):blue.bex.value.BexValue + method public static exact(blue.language.snapshot.FrozenNode,blue.language.snapshot.FrozenNode,java.lang.String):blue.bex.value.BexValue + method public static fromSimple(java.lang.Object):blue.bex.value.BexValue + method public static frozen(blue.language.snapshot.FrozenNode):blue.bex.value.BexValue + method public static frozenBlueId(blue.bex.value.BexValue):java.lang.String + method public static kind(blue.bex.value.BexValue):java.lang.String + method public static list(java.util.List):blue.bex.value.BexValue + method public static map(java.util.Map):blue.bex.value.BexValue + method public static nodeCursorTrustedImmutable(blue.language.model.Node):blue.bex.value.BexValue + method public static nodeSnapshot(blue.language.model.Node):blue.bex.value.BexValue + method public static nullValue():blue.bex.value.BexValue + method public static overlay(blue.bex.value.BexValue,java.lang.String,blue.bex.value.BexValue):blue.bex.value.BexValue + method public static pointerSet(blue.bex.value.BexValue,java.util.List,blue.bex.value.BexValue,java.lang.String):blue.bex.value.BexValue + method public static referenceBacked(blue.bex.value.BexValue,blue.language.runtime.BlueLanguage):blue.bex.value.BexValue + method public static resultOverlayPointerSet(blue.bex.value.BexValue,java.util.List,blue.bex.value.BexValue,java.lang.String):blue.bex.value.BexValue + method public static scalar(java.lang.Object):blue.bex.value.BexValue + method public static transientFrozen(blue.language.snapshot.FrozenNode):blue.bex.value.BexValue + method public static truthy(blue.bex.value.BexValue):boolean + method public static undefined():blue.bex.value.BexValue +class public final blue.bex.value.ChangesetBexValue extends blue.bex.value.AbstractBexValue + constructor public (blue.bex.result.BexChangeset) + method public get(java.lang.String):blue.bex.value.BexValue + method public isList():boolean + method public size():int + method public toNode():blue.language.model.Node + method public toSimple():java.lang.Object + method public volatile asBoolean():boolean synthetic bridge + method public volatile asInteger():java.math.BigInteger synthetic bridge + method public volatile asNumber():java.math.BigDecimal synthetic bridge + method public volatile asText():java.lang.String synthetic bridge + method public volatile at(java.lang.String):blue.bex.value.BexValue synthetic bridge + method public volatile at(java.util.List):blue.bex.value.BexValue synthetic bridge + method public volatile isNull():boolean synthetic bridge + method public volatile isObject():boolean synthetic bridge + method public volatile isScalar():boolean synthetic bridge + method public volatile isUndefined():boolean synthetic bridge + method public volatile keys():java.util.List synthetic bridge +class public final blue.bex.value.EventsBexValue extends blue.bex.value.AbstractBexValue + constructor public (blue.bex.result.BexEvents) + method public get(java.lang.String):blue.bex.value.BexValue + method public isList():boolean + method public size():int + method public toNode():blue.language.model.Node + method public toSimple():java.lang.Object + method public volatile asBoolean():boolean synthetic bridge + method public volatile asInteger():java.math.BigInteger synthetic bridge + method public volatile asNumber():java.math.BigDecimal synthetic bridge + method public volatile asText():java.lang.String synthetic bridge + method public volatile at(java.lang.String):blue.bex.value.BexValue synthetic bridge + method public volatile at(java.util.List):blue.bex.value.BexValue synthetic bridge + method public volatile isNull():boolean synthetic bridge + method public volatile isObject():boolean synthetic bridge + method public volatile isScalar():boolean synthetic bridge + method public volatile isUndefined():boolean synthetic bridge + method public volatile keys():java.util.List synthetic bridge +class public final blue.bex.value.OverlayListBexValue extends blue.bex.value.AbstractBexValue + constructor public (blue.bex.value.BexValue,java.util.Map) + method public get(java.lang.String):blue.bex.value.BexValue + method public isList():boolean + method public size():int + method public toNode():blue.language.model.Node + method public toSimple():java.lang.Object + method public volatile asBoolean():boolean synthetic bridge + method public volatile asInteger():java.math.BigInteger synthetic bridge + method public volatile asNumber():java.math.BigDecimal synthetic bridge + method public volatile asText():java.lang.String synthetic bridge + method public volatile at(java.lang.String):blue.bex.value.BexValue synthetic bridge + method public volatile at(java.util.List):blue.bex.value.BexValue synthetic bridge + method public volatile isNull():boolean synthetic bridge + method public volatile isObject():boolean synthetic bridge + method public volatile isScalar():boolean synthetic bridge + method public volatile isUndefined():boolean synthetic bridge + method public volatile keys():java.util.List synthetic bridge +class public final blue.bex.value.PatchEntryBexValue extends blue.bex.value.AbstractBexValue + constructor public (blue.bex.result.BexPatchEntry) + method public get(java.lang.String):blue.bex.value.BexValue + method public isObject():boolean + method public keys():java.util.List + method public size():int + method public toNode():blue.language.model.Node + method public toSimple():java.lang.Object + method public volatile asBoolean():boolean synthetic bridge + method public volatile asInteger():java.math.BigInteger synthetic bridge + method public volatile asNumber():java.math.BigDecimal synthetic bridge + method public volatile asText():java.lang.String synthetic bridge + method public volatile at(java.lang.String):blue.bex.value.BexValue synthetic bridge + method public volatile at(java.util.List):blue.bex.value.BexValue synthetic bridge + method public volatile isList():boolean synthetic bridge + method public volatile isNull():boolean synthetic bridge + method public volatile isScalar():boolean synthetic bridge + method public volatile isUndefined():boolean synthetic bridge diff --git a/gradle/verification/latest-language-baseline.json b/gradle/verification/latest-language-baseline.json index 6f381bb..ecd6c24 100644 --- a/gradle/verification/latest-language-baseline.json +++ b/gradle/verification/latest-language-baseline.json @@ -1,13 +1,89 @@ { "schema": "blue-bex-latest-language-baseline/1.0", + "captureProvenance": { + "status": "reconstructed-from-immutable-git-objects-and-supplied-current-state", + "baselineCommit": "395c484111f8c4e9e0e98d2db7f1c5b0777bd5a8", + "executionLogRetained": false, + "note": "Static source facts and hashes are exact at the baseline commit. The original failing Gradle stdout was not retained and is not represented as a same-run command log." + }, + "verificationEnvironment": { + "capturePhase": "post-checkpoint verification host", + "java": "OpenJDK 26.0.1+8-34", + "gradleWrapper": "9.6.0", + "os": "macOS 26.5.2 (Darwin 25.5.0)", + "architecture": "arm64", + "productionBytecodeTarget": "Java 8 / class major 52" + }, "bex": { "migrationBaselineCommit": "395c484111f8c4e9e0e98d2db7f1c5b0777bd5a8", - "czTomlSha256": "2dd317fbe362561f0e9827c705ff6260fd6df26c16518e0acce99ecba595a4b1" + "czTomlSha256": "2dd317fbe362561f0e9827c705ff6260fd6df26c16518e0acce99ecba595a4b1", + "workingTreeAtBaseline": "recorded-by-supplied-current-state; exact porcelain bytes unavailable", + "projectLayout": [ + ":" + ], + "sourceShape": { + "productionJavaFiles": 85, + "productionJavaLines": 16066, + "productionPackages": 10, + "publicTopLevelTypes": 61, + "publicMethodsApproximate": 583, + "testJavaFiles": 49, + "testJavaLines": 23652, + "testAnnotationOccurrences": 359, + "packageSccsLargerThanOne": 1, + "packagesInLargestScc": 8, + "rootBuildGradleKtsLines": 2233 + }, + "largestSources": { + "BexCompiler.java": 1628, + "BexExpressions.java": 1676, + "BexBlueTypeMatcher.java": 967, + "BexGasMeter.java": 905, + "BexExecutionContext.java": 639, + "BexRuntime.java": 511, + "README.md": 929 + }, + "packageGraph": { + "source": "supplied-current-state static analysis", + "stronglyConnectedPackages": 8 + }, + "publicApiInventory": { + "source": "supplied-current-state static analysis", + "publicTopLevelTypes": 61, + "publicMethodsApproximate": 583 + } + }, + "knownFailingComposite": { + "outcome": "failed-before-migration", + "reproducedSourceFact": "blue.language:blue-language-java was substituted with included-build project ':'", + "actualAggregateProject": ":blue-language-java", + "rootProjectRole": "empty orchestration project", + "exactStdoutRetained": false, + "reason": "The invalid substitution selected the empty Language root rather than the aggregate library." + }, + "normativePackageAtBaseline": { + "runtimeRegistryIdentity": "sha256:23d282ec1c0bb016263922b1b49c369fdd537efdcf23e005eceeb888d7763fe1", + "gasManifestIdentity": "sha256:41247c820d91a12fdfc17fd9e787a5d8d668d8acc5954fdcb131715bf9e6147d", + "fixturePackageIdentity": "sha256:a1b7bb2b3687389409bc9d0aa450c734f7856d2bcb818c95f4d7ecb19095d20e", + "sourceFileSha256": { + "specifications/blue-bex-specification-2.0.md": "b25d6d255f84c584ed7a484411430fab50c18142a1bb6c08cfb104acf09d6f69", + "src/test/resources/conformance/bex/fixtures/manifest.yaml": "b31e6f483bb8f16a6ce94db5aa99fa3db355b592ef669616ecb64189ea4d7f60", + "src/test/resources/conformance/bex/gas-manifest.yaml": "1f689e0cf51b0f9afa6b18a640e0c755470921a7b0d66f62bfc2206679de640d", + "src/test/resources/conformance/bex/registry/manifest.yaml": "1e6456aaa848a637107280de9135b4ddbdaa951436810f202be0b1cadd41ccfc", + "docs/FIXTURES.md": "1431a489a3fdad58ff3bdd001f8946672376fbd829a70f51943fb81e9ce93ce4" + }, + "retainedTestEvidence": { + "tests": 741, + "status": "older-Language-only; not accepted as latest-Language evidence" + } }, "language": { "exactHead": "9a607e584ff5dd973684d35d71eb4022d946b760", "verifiedImplementationCommit": "63a9ed6a1a66d47119a80d16ed2ab0beda0d2453", "verifiedReleaseVersion": "3.1.0-rc.18", + "localCompositeProjectVersion": "3.1.0-rc.18-SNAPSHOT", + "bexDeclaredPublishedCandidateVersion": "3.1.0-rc.19", + "publishedCandidateStatus": "not-executed", "documentationOnlyDiffPaths": [ "LICENSE", "docs/collection-paths-and-cohesion-migration-report.md", @@ -16,24 +92,28 @@ "czTomlSha256": "f6717f9a9e38df0dea4b5eeec5a0264c0490856ef4c261c053a06883ba3e69f2", "focusedModules": [ { - "coordinate": "blue.language:blue-language-model:3.1.0-rc.19", + "declaredPublishedCoordinate": "blue.language:blue-language-model:3.1.0-rc.19", "projectPath": ":blue-language-model", - "verifiedArtifactSha256": "ef55be8331147442b858474add4782489d993568effe30202a9c4a8b014d5bd8" + "localArtifact": "blue-language-model-3.1.0-rc.18-SNAPSHOT.jar", + "verifiedLocalArtifactSha256": "ef55be8331147442b858474add4782489d993568effe30202a9c4a8b014d5bd8" }, { - "coordinate": "blue.language:blue-language-core:3.1.0-rc.19", + "declaredPublishedCoordinate": "blue.language:blue-language-core:3.1.0-rc.19", "projectPath": ":blue-language-core", - "verifiedArtifactSha256": "a7d3c72640ab8ac5832feaad576cd1a56457cb87eaf07323fe04a88ae5730740" + "localArtifact": "blue-language-core-3.1.0-rc.18-SNAPSHOT.jar", + "verifiedLocalArtifactSha256": "a7d3c72640ab8ac5832feaad576cd1a56457cb87eaf07323fe04a88ae5730740" }, { - "coordinate": "blue.language:blue-language-mapping:3.1.0-rc.19", + "declaredPublishedCoordinate": "blue.language:blue-language-mapping:3.1.0-rc.19", "projectPath": ":blue-language-mapping", - "verifiedArtifactSha256": "d9141d5c611bde7eb6a21bce3dc4bc0df7d8167f013eeaef2a365dd0a6af329b" + "localArtifact": "blue-language-mapping-3.1.0-rc.18-SNAPSHOT.jar", + "verifiedLocalArtifactSha256": "d9141d5c611bde7eb6a21bce3dc4bc0df7d8167f013eeaef2a365dd0a6af329b" }, { - "coordinate": "blue.language:blue-contracts-core:3.1.0-rc.19", + "declaredPublishedCoordinate": "blue.language:blue-contracts-core:3.1.0-rc.19", "projectPath": ":blue-contracts-core", - "verifiedArtifactSha256": "ec45224ffee3e0c47246869d89c002657c9d1f348af8c553be3b6c0874bf7bae" + "localArtifact": "blue-contracts-core-3.1.0-rc.18-SNAPSHOT.jar", + "verifiedLocalArtifactSha256": "ec45224ffee3e0c47246869d89c002657c9d1f348af8c553be3b6c0874bf7bae" } ], "hostingPackageIdentities": { diff --git a/settings.gradle.kts b/settings.gradle.kts index dcc4fc2..4d52e79 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,51 +1,61 @@ +pluginManagement { + includeBuild("build-logic") + repositories { + gradlePluginPortal() + mavenCentral() + } +} + plugins { id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" } rootProject.name = "blue-bex-java" -val blueLanguageCompositePath = - providers.gradleProperty("blueLanguageCompositePath") - .orNull - ?.trim() - ?.takeIf { it.isNotEmpty() } +include( + ":blue-bex-core", + ":blue-bex-contracts", + ":blue-bex-conformance", + ":blue-bex-java", + ":examples" +) + +val compositePath = providers.gradleProperty("blueLanguageCompositePath") + .orNull + ?.trim() + ?.takeIf(String::isNotEmpty) -if (blueLanguageCompositePath != null) { - val compositeDirectory = file(blueLanguageCompositePath) - require(compositeDirectory.isDirectory) { - "blueLanguageCompositePath is not a directory: " + - compositeDirectory.absolutePath +if (compositePath != null) { + val checkout = file(compositePath) + require(checkout.isDirectory) { + "blueLanguageCompositePath is not a directory: ${checkout.absolutePath}" } require( - file("${compositeDirectory.path}/settings.gradle.kts").isFile || - file("${compositeDirectory.path}/settings.gradle").isFile + file("${checkout.path}/settings.gradle.kts").isFile || + file("${checkout.path}/settings.gradle").isFile ) { - "blueLanguageCompositePath is not a Gradle build: " + - compositeDirectory.absolutePath + "blueLanguageCompositePath is not a Gradle build: ${checkout.absolutePath}" } - val requiredLanguageProjects = - listOf( - "blue-language-model", - "blue-language-core", - "blue-language-mapping", - "blue-language-ipfs", - "blue-contracts-core", - "blue-conformance", - "blue-language-java" - ) - val missingLanguageProjects = - requiredLanguageProjects.filter { projectName -> - val projectDirectory = - file("${compositeDirectory.path}/$projectName") - !projectDirectory.isDirectory || - (!file("${projectDirectory.path}/build.gradle.kts").isFile && - !file("${projectDirectory.path}/build.gradle").isFile) - } - require(missingLanguageProjects.isEmpty()) { - "blueLanguageCompositePath does not contain the required Gradle " + - "subprojects: " + missingLanguageProjects.joinToString(", ") + + val required = listOf( + "blue-language-model", + "blue-language-core", + "blue-language-mapping", + "blue-contracts-core", + "blue-language-java" + ) + val missing = required.filter { name -> + val projectDirectory = file("${checkout.path}/$name") + !projectDirectory.isDirectory || + (!file("${projectDirectory.path}/build.gradle.kts").isFile && + !file("${projectDirectory.path}/build.gradle").isFile) } - includeBuild(compositeDirectory) { + require(missing.isEmpty()) { + "blueLanguageCompositePath is missing required projects: " + + missing.joinToString(", ") + } + + includeBuild(checkout) { dependencySubstitution { substitute(module("blue.language:blue-language-model")) .using(project(":blue-language-model")) @@ -53,12 +63,8 @@ if (blueLanguageCompositePath != null) { .using(project(":blue-language-core")) substitute(module("blue.language:blue-language-mapping")) .using(project(":blue-language-mapping")) - substitute(module("blue.language:blue-language-ipfs")) - .using(project(":blue-language-ipfs")) substitute(module("blue.language:blue-contracts-core")) .using(project(":blue-contracts-core")) - substitute(module("blue.language:blue-conformance")) - .using(project(":blue-conformance")) substitute(module("blue.language:blue-language-java")) .using(project(":blue-language-java")) } diff --git a/src/main/java/blue/bex/api/ProcessorExecutionContextBexGasLedgerHost.java b/src/main/java/blue/bex/api/ProcessorExecutionContextBexGasLedgerHost.java deleted file mode 100644 index 408a5e9..0000000 --- a/src/main/java/blue/bex/api/ProcessorExecutionContextBexGasLedgerHost.java +++ /dev/null @@ -1,176 +0,0 @@ -package blue.bex.api; - -import blue.bex.gas.BexGasCounter; -import blue.bex.gas.BexGasLimitExceededException; -import blue.language.processor.GasLimitExceededException; -import blue.language.processor.GasMeter; -import blue.language.processor.ProcessorExecutionContext; -import blue.language.processor.ProcessorErrorCategory; -import blue.language.processor.ProcessorFailureException; -import blue.language.processor.RuntimeWorkBudget; -import blue.language.processor.RuntimeWorkSession; - -import java.util.Map; -import java.util.Objects; - -/** - * Contracts 1.0 host adapter for BEX's parent-bounded runtime ledger. - * - *

The adapter opens the logical BEX ledger under one deterministic - * physical runtime namespace. Callers executing more than one BEX program in - * a host invocation must provide distinct physical namespaces; all such - * ledgers remain owned by the same {@link RuntimeWorkSession} and therefore - * share its live parent budget. When BEX declares a local cap, the adapter - * also opens one invocation-owned {@link RuntimeWorkBudget} and attaches the - * primary and intrinsic ledgers to that exact shared admission boundary.

- */ -public final class ProcessorExecutionContextBexGasLedgerHost implements BexGasLedgerHost { - private final ProcessorExecutionContext context; - private final RuntimeWorkSession session; - private final String runtimeNamespace; - - public ProcessorExecutionContextBexGasLedgerHost(ProcessorExecutionContext context) { - this(context, BexGasCounter.NAMESPACE); - } - - public ProcessorExecutionContextBexGasLedgerHost( - ProcessorExecutionContext context, - String runtimeNamespace) { - this.context = Objects.requireNonNull(context, "context"); - this.session = null; - this.runtimeNamespace = - requireRuntimeNamespace(runtimeNamespace); - } - - public ProcessorExecutionContextBexGasLedgerHost( - RuntimeWorkSession session, - String runtimeNamespace) { - this.context = null; - this.session = Objects.requireNonNull(session, "session"); - this.runtimeNamespace = - requireRuntimeNamespace(runtimeNamespace); - } - - @Override - public GasMeter.ChildGasLedger open(String namespace, - Map counterWeights) { - return open(namespace, counterWeights, null); - } - - @Override - public RuntimeWorkBudget openSharedBudget(long maximumGas) { - return session != null - ? session.openSharedBudget(maximumGas) - : BexGasLedgerHost.super.openSharedBudget(maximumGas); - } - - @Override - public GasMeter.ChildGasLedger open( - String namespace, - Map counterWeights, - RuntimeWorkBudget sharedBudget) { - String logicalNamespace = - requireRuntimeNamespace(namespace); - String physicalNamespace = physicalNamespace(logicalNamespace); - if (session != null) { - return session.openLedger( - physicalNamespace, - counterWeights, - sharedBudget); - } - if (sharedBudget != null) { - throw new IllegalArgumentException( - "ProcessorExecutionContext does not expose shared runtime budgets"); - } - return context.newRuntimeGasLedger( - physicalNamespace, counterWeights); - } - - @Override - public void submit(GasMeter.ChildGasLedger ledger) { - if (session != null) { - session.submit(ledger); - } else { - context.submitRuntimeGasLedger(ledger); - } - } - - @Override - public boolean separatesRuntimeNamespaces() { - return true; - } - - /** - * The enclosing processor failure owns prefix retention. BEX must leave - * this ledger staged and unsubmitted. - */ - @Override - public void failedDeterministically(GasMeter.ChildGasLedger ledger) { - Objects.requireNonNull(ledger, "ledger"); - } - - /** - * The enclosing processor suspension owns reservation discard. BEX must - * leave this ledger staged and unsubmitted. - */ - @Override - public void evidenceUnavailable(GasMeter.ChildGasLedger ledger) { - Objects.requireNonNull(ledger, "ledger"); - } - - @Override - public RuntimeException localGasLimitExceeded( - BexGasLimitExceededException exhaustion, - RuntimeException originalFailure) { - BexGasLimitExceededException exact = - Objects.requireNonNull(exhaustion, "exhaustion"); - Objects.requireNonNull( - originalFailure, "originalFailure"); - return new ProcessorFailureException( - ProcessorErrorCategory.GasLimitExceeded, - exact.getMessage(), - exact); - } - - @Override - public void propagateGasExhaustion( - GasMeter.ChildGasLedger ledger, - GasLimitExceededException exhaustion) { - Objects.requireNonNull(ledger, "ledger"); - GasLimitExceededException exact = - Objects.requireNonNull(exhaustion, "exhaustion"); - if (session != null) { - session.propagateGasExhaustion(exact); - } - throw exact; - } - - public String runtimeNamespace() { - return runtimeNamespace; - } - - /** - * Returns the deterministic physical session namespace for one logical - * portable ledger. The primary BEX ledger uses the configured namespace - * verbatim; intrinsic ledgers are separate children below it. - */ - public String physicalNamespace(String logicalNamespace) { - String exactLogical = - requireRuntimeNamespace(logicalNamespace); - return BexGasCounter.NAMESPACE.equals(exactLogical) - ? runtimeNamespace - : runtimeNamespace + "/" + exactLogical; - } - - private static String requireRuntimeNamespace(String value) { - if (value == null || value.trim().isEmpty()) { - throw new IllegalArgumentException( - "runtimeNamespace is required"); - } - if (value.indexOf('/') >= 0) { - throw new IllegalArgumentException( - "runtimeNamespace must not contain the reserved '/' separator"); - } - return value; - } -} diff --git a/src/main/java/blue/bex/compile/BexCompiler.java b/src/main/java/blue/bex/compile/BexCompiler.java deleted file mode 100644 index 6c7ce9c..0000000 --- a/src/main/java/blue/bex/compile/BexCompiler.java +++ /dev/null @@ -1,1632 +0,0 @@ -package blue.bex.compile; - -import blue.bex.BexException; -import blue.bex.BexSourcePath; -import blue.bex.api.BexIntrinsicRegistry; -import blue.bex.runtime.CompileScope; -import blue.bex.runtime.CompiledExpression; -import blue.bex.runtime.CompiledStatement; -import blue.bex.value.BexValue; -import blue.bex.value.BexUnicodeOrder; -import blue.bex.value.BexValues; -import blue.bex.result.BexMetrics; -import blue.language.model.Node; -import blue.language.model.Schema; -import blue.language.registry.BlueCoreTypeRegistry; -import blue.language.snapshot.FrozenNode; - -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * Compiler from frozen BEX Blue data to specialized runtime objects. - */ -public final class BexCompiler { - private static final String TEXT_TYPE_BLUE_ID = - BlueCoreTypeRegistry.INSTANCE.blueId("Text"); - private static final String INTEGER_TYPE_BLUE_ID = - BlueCoreTypeRegistry.INSTANCE.blueId("Integer"); - private static final String DOUBLE_TYPE_BLUE_ID = - BlueCoreTypeRegistry.INSTANCE.blueId("Double"); - private static final String BOOLEAN_TYPE_BLUE_ID = - BlueCoreTypeRegistry.INSTANCE.blueId("Boolean"); - private static final Set RESERVED_BLUE_KEYS = reservedBlueKeys(); - - private final BexContainsCache containsCache = new BexContainsCache(); - private final BexMetrics metrics; - private final BexIntrinsicRegistry intrinsics; - private final Set requiredIntrinsicBlueIds = new LinkedHashSet<>(); - private Map functionSignatures = Collections.emptyMap(); - private Map constants = Collections.emptyMap(); - private String currentFunction = "$root"; - - public BexCompiler(BexMetrics metrics) { - this(metrics, BexIntrinsicRegistry.empty()); - } - - public BexCompiler(BexMetrics metrics, BexIntrinsicRegistry intrinsics) { - this.metrics = metrics; - this.intrinsics = intrinsics != null ? intrinsics : BexIntrinsicRegistry.empty(); - } - - public BexCompiledProgram compile(blue.bex.api.BexProgramSource source) { - requiredIntrinsicBlueIds.clear(); - FrozenNode step = source.programNode(); - FrozenNode definition = source.definitionNode().orElse(null); - if (source.isExpression()) { - if (definition != null) { - throw new BexException("Expression BEX source cannot use a definition"); - } - if (source.entry().isPresent()) { - throw new BexException("Expression BEX source cannot use an entry"); - } - constants = Collections.emptyMap(); - functionSignatures = Collections.emptyMap(); - currentFunction = "$root"; - CompileScope scope = new CompileScope(); - BexCompiledProgram.CompiledFunction root = new BexCompiledProgram.CompiledFunction("$root", - Collections.emptyList(), - Collections.emptyList(), - compileExpr(step, scope, "/expr"), - scope.frameSize()); - return new BexCompiledProgram(root, Collections.emptyMap(), constants, scope.frameSize(), - BexNodeIdentity.safeBlueId(step), requiredIntrinsicBlueIds); - } - requireProgramNode(step, "program"); - if (definition != null) { - requireProgramNode(definition, "definition"); - } - - Map loadedConstants = new LinkedHashMap<>(); - loadConstants(loadedConstants, explicitProp(definition, "constants")); - loadConstants(loadedConstants, explicitProp(step, "constants")); - constants = Collections.unmodifiableMap(new LinkedHashMap<>(loadedConstants)); - - Map functionNodes = new LinkedHashMap<>(); - loadFunctions(functionNodes, explicitProp(definition, "functions")); - loadFunctions(functionNodes, explicitProp(step, "functions")); - rejectRecursion(functionNodes); - functionSignatures = compileFunctionSignatures(functionNodes); - - Map compiledFunctions = new LinkedHashMap<>(); - for (String name : BexUnicodeOrder.sortedCopy(functionNodes.keySet())) { - compiledFunctions.put(name, compileFunction(name, functionNodes.get(name), functionSignatures.get(name))); - } - - boolean hasStepEntry = hasExplicitProperty(step, "entry"); - boolean hasStepExpr = hasExplicitProperty(step, "expr"); - boolean hasStepDo = hasExplicitProperty(step, "do"); - String entryName = source.entry().isPresent() - ? requiredNonEmptyText(scalarNode(source.entry().get()), "entry") - : hasStepEntry - ? requiredNonEmptyText(explicitProp(step, "entry"), "entry") - : null; - BexCompiledProgram.CompiledFunction root; - int rootFrameSize = 0; - if (entryName != null) { - BexCompiledProgram.CompiledFunction entry = compiledFunctions.get(entryName); - if (entry == null) { - throw new BexException("Unknown entry function: " + entryName); - } - if (!entry.args().isEmpty()) { - throw new BexException("Entry function " + entryName + " declares arguments but entry invocation provides none"); - } - root = new BexCompiledProgram.CompiledFunction("$root", Collections.emptyList(), - Collections.singletonList(sourceStatement("$root", "/entry", "$return", - new ReturnStatement(sourceExpr("$root", "/entry/$call", "$call", - new CallExpr(entryName, new int[0], new CompiledExpression[0]))))), - null, 0); - validateUnselectedRoot(step, hasStepExpr, hasStepDo); - } else if (hasStepExpr) { - currentFunction = "$root"; - CompileScope scope = new CompileScope(); - CompiledExpression expression = compileExpr(explicitProp(step, "expr"), scope, "/expr"); - rootFrameSize = scope.frameSize(); - root = new BexCompiledProgram.CompiledFunction("$root", Collections.emptyList(), - Collections.emptyList(), expression, rootFrameSize); - if (hasStepDo) { - validateRootStatements(explicitProp(step, "do")); - } - } else { - CompileScope scope = new CompileScope(); - currentFunction = "$root"; - List statements = hasStepDo - ? compileStatements(explicitProp(step, "do"), scope, "/do") - : Collections.emptyList(); - rootFrameSize = scope.frameSize(); - root = new BexCompiledProgram.CompiledFunction("$root", Collections.emptyList(), statements, null, rootFrameSize); - } - - return new BexCompiledProgram(root, compiledFunctions, constants, rootFrameSize, - BexNodeIdentity.safeBlueId(step), requiredIntrinsicBlueIds); - } - - private BexCompiledProgram.CompiledFunction compileFunction(String name, FrozenNode functionNode, FunctionSignature signature) { - String previousFunction = currentFunction; - currentFunction = name; - try { - CompileScope scope = functionScope(name, signature); - String basePointer = "/functions/" + escape(name); - boolean hasExpression = hasExplicitProperty(functionNode, "expr"); - boolean hasStatements = hasExplicitProperty(functionNode, "do"); - CompiledExpression expression = hasExpression - ? compileExpr(explicitProp(functionNode, "expr"), scope, basePointer + "/expr") - : null; - List statements = !hasExpression && hasStatements - ? compileStatements(explicitProp(functionNode, "do"), scope, basePointer + "/do") - : Collections.emptyList(); - if (hasExpression && hasStatements) { - CompileScope validationScope = functionScope(name, signature); - compileStatements(explicitProp(functionNode, "do"), validationScope, basePointer + "/do"); - } - return new BexCompiledProgram.CompiledFunction(name, signature.args(), - statements, expression, scope.frameSize()); - } finally { - currentFunction = previousFunction; - } - } - - private CompileScope functionScope(String name, FunctionSignature signature) { - CompileScope scope = new CompileScope(); - for (BexCompiledProgram.ArgSpec arg : signature.args()) { - int slot = scope.declareOrGetSlot(arg.name()); - if (slot != arg.slot()) { - throw new BexException("Internal function arg slot mismatch for " + name + "." + arg.name()); - } - } - return scope; - } - - private void validateUnselectedRoot(FrozenNode step, boolean hasExpression, boolean hasStatements) { - if (hasExpression) { - currentFunction = "$root"; - compileExpr(explicitProp(step, "expr"), new CompileScope(), "/expr"); - } - if (hasStatements) { - validateRootStatements(explicitProp(step, "do")); - } - } - - private void validateRootStatements(FrozenNode statements) { - currentFunction = "$root"; - compileStatements(statements, new CompileScope(), "/do"); - } - - private Map compileFunctionSignatures(Map functionNodes) { - Map signatures = new LinkedHashMap<>(); - for (String name : BexUnicodeOrder.sortedCopy(functionNodes.keySet())) { - signatures.put(name, compileFunctionSignature(name, functionNodes.get(name))); - } - return signatures; - } - - private FunctionSignature compileFunctionSignature(String name, FrozenNode functionNode) { - requireObjectBody(functionNode, "Function " + name, "args", "expr", "do"); - List names = new ArrayList<>(); - FrozenNode argsNode = prop(functionNode, "args"); - if (argsNode != null) { - validatePlainObjectContainer(argsNode, "Function " + name + " args"); - if (argsNode.getProperties() == null) { - if (!argsNode.isEmptyNode()) { - throw new BexException("Function " + name + " args must be an object"); - } - } else { - names.addAll(argsNode.getProperties().keySet()); - Collections.sort(names, BexUnicodeOrder.CODE_POINT_COMPARATOR); - } - } - List args = new ArrayList<>(); - for (int i = 0; i < names.size(); i++) { - String arg = names.get(i); - FrozenNode pattern = argsNode.getProperties().get(arg); - String sourcePointer = "/functions/" + escape(name) + "/args/" + escape(arg); - rejectBexAnywhereInStaticPattern(pattern, sourcePointer); - args.add(new BexCompiledProgram.ArgSpec(arg, i, - pattern, - sourcePointer)); - } - return new FunctionSignature(Collections.unmodifiableList(args)); - } - - private void loadConstants(Map constants, FrozenNode node) { - validatePlainObjectContainer(node, "constants"); - if (node == null || node.getProperties() == null) { - return; - } - for (String name : BexUnicodeOrder.sortedCopy(node.getProperties().keySet())) { - FrozenNode value = node.getProperties().get(name); - rejectBexInStaticBlueDefinitionFields(value, "/constants/" + escape(name)); - constants.put(name, BexValues.frozen(value)); - } - } - - private void loadFunctions(Map functions, FrozenNode node) { - validatePlainObjectContainer(node, "functions"); - if (node == null || node.getProperties() == null) { - return; - } - for (String name : BexUnicodeOrder.sortedCopy(node.getProperties().keySet())) { - functions.put(name, node.getProperties().get(name)); - } - } - - private void rejectRecursion(Map functions) { - Map> calls = new LinkedHashMap<>(); - for (String name : BexUnicodeOrder.sortedCopy(functions.keySet())) { - List targets = new ArrayList<>(); - FrozenNode function = functions.get(name); - String base = "/functions/" + escape(name); - if (hasExplicitProperty(function, "expr")) { - collectCalls(explicitProp(function, "expr"), name, base + "/expr", targets); - } - if (hasExplicitProperty(function, "do")) { - collectCalls(explicitProp(function, "do"), name, base + "/do", targets); - } - calls.put(name, targets); - } - Map states = new LinkedHashMap<>(); - for (String name : calls.keySet()) { - states.put(name, VisitState.UNVISITED); - } - ArrayDeque stack = new ArrayDeque<>(); - for (String name : calls.keySet()) { - if (states.get(name) == VisitState.UNVISITED) { - detectCycle(name, calls, states, stack); - } - } - } - - private void detectCycle(String current, - Map> calls, - Map states, - ArrayDeque stack) { - states.put(current, VisitState.VISITING); - stack.addLast(current); - List edges = new ArrayList<>(calls.get(current)); - Collections.sort(edges, (left, right) -> { - int byTarget = BexUnicodeOrder.compareCodePoints(left.target, right.target); - return byTarget != 0 - ? byTarget - : BexUnicodeOrder.compareCodePoints(left.sourcePath.pointer(), right.sourcePath.pointer()); - }); - for (CallSite edge : edges) { - if (!calls.containsKey(edge.target)) { - continue; - } - VisitState state = states.get(edge.target); - if (state == VisitState.VISITING) { - throw BexException.at(edge.sourcePath, - "Compile error reason=recursive-call-graph: recursive BEX function cycle " - + cycleText(stack, edge.target)); - } - if (state == VisitState.UNVISITED) { - detectCycle(edge.target, calls, states, stack); - } - } - stack.removeLast(); - states.put(current, VisitState.VISITED); - } - - private String cycleText(ArrayDeque stack, String target) { - List cycle = new ArrayList<>(); - boolean append = false; - for (String name : stack) { - if (name.equals(target)) { - append = true; - } - if (append) { - cycle.add(name); - } - } - cycle.add(target); - return String.join(" -> ", cycle); - } - - private void collectCalls(FrozenNode node, String functionName, String pointer, List calls) { - if (node == null) { - return; - } - if (isOperator(node, "$literal")) { - return; - } - if (isOperator(node, "$is")) { - collectCalls(prop(onlyValue(node), "node"), functionName, - pointer + "/$is/node", calls); - return; - } - if (isOperator(node, "$fail")) { - FrozenNode body = onlyValue(node); - boolean messageWrapper = body != null - && body.getProperties() != null - && hasExplicitProperty(body, "message"); - collectCalls(messageWrapper ? explicitProp(body, "message") : body, - functionName, - messageWrapper ? pointer + "/$fail/message" : pointer + "/$fail", - calls); - return; - } - if (isOperator(node, "$call")) { - FrozenNode body = onlyValue(node); - String function = text(prop(body, "function")); - if (function != null) { - calls.add(new CallSite(function, - BexSourcePath.of(functionName, pointer + "/$call", "$call"))); - } - } - if (node.getProperties() != null) { - for (String key : BexUnicodeOrder.sortedCopy(node.getProperties().keySet())) { - collectCalls(node.getProperties().get(key), functionName, - pointer + "/" + escape(key), calls); - } - } - if (node.getItems() != null) { - for (int i = 0; i < node.getItems().size(); i++) { - collectCalls(node.getItems().get(i), functionName, - pointer + "/" + i, calls); - } - } - } - - private List compileStatements(FrozenNode node, CompileScope scope, String pointer) { - if (node == null) { - return Collections.emptyList(); - } - if (node.getItems() == null) { - throw BexException.at(BexSourcePath.of(currentFunction, pointer, null), - "Compile error: statement body must be a list"); - } - List statements = new ArrayList<>(); - for (int i = 0; i < node.getItems().size(); i++) { - FrozenNode item = node.getItems().get(i); - statements.add(compileStatement(item, scope, pointer + "/" + i)); - } - return statements; - } - - private CompiledStatement compileStatement(FrozenNode statement, CompileScope scope, String pointer) { - if (isEmptyStatement(statement, pointer)) { - throw BexException.at(BexSourcePath.of(currentFunction, pointer, null), - "Compile error: null or empty statement item"); - } - if (statement.getProperties() == null) { - throw BexException.at(BexSourcePath.of(currentFunction, pointer, null), - "Compile error: statement must be an operator object"); - } - int count = 0; - String op = null; - FrozenNode body = null; - for (Map.Entry entry : statement.getProperties().entrySet()) { - if (entry.getKey().startsWith("$")) { - count++; - op = entry.getKey(); - body = entry.getValue(); - } - } - if (count != 1 - || statement.getProperties().size() != 1 - || authoredFieldNames(statement).size() != 1) { - throw BexException.at(BexSourcePath.of(currentFunction, pointer, null), - "Compile error: statement must have exactly one $ operator"); - } - String bodyPointer = pointer + "/" + escape(op); - BexSourcePath sourcePath = BexSourcePath.of(currentFunction, bodyPointer, op); - try { - if ("$empty".equals(op)) { - throw new BexException("Compile error: $empty placeholder is not a BEX statement"); - } - validateStatementBody(op, body); - CompiledStatement compiled; - if ("$let".equals(op)) { - if (prop(body, "vars") != null) { - compiled = compileMultiLet(body, scope, bodyPointer); - } else { - String name = requiredText(prop(body, "name"), "$let.name"); - int slot = scope.declareOrGetSlot(name); - compiled = new LetStatement(slot, compileExpr(required(prop(body, "expr"), "$let.expr"), scope, bodyPointer + "/expr")); - } - } else if ("$set".equals(op)) { - String name = requiredText(prop(body, "name"), "$set.name"); - int slot = scope.resolveSlot(name); - compiled = new SetStatement(slot, compileExpr(required(prop(body, "expr"), "$set.expr"), scope, bodyPointer + "/expr")); - } else if ("$if".equals(op)) { - compiled = new IfStatement(compileExpr(required(prop(body, "cond"), "$if.cond"), scope, bodyPointer + "/cond"), - compileStatements(prop(body, "then"), scope, bodyPointer + "/then"), - compileStatements(prop(body, "else"), scope, bodyPointer + "/else")); - } else if ("$forEach".equals(op)) { - String itemName = requiredText(prop(body, "item"), "$forEach.item"); - String keyName = prop(body, "key") != null ? requiredText(prop(body, "key"), "$forEach.key") : null; - String indexName = prop(body, "index") != null ? requiredText(prop(body, "index"), "$forEach.index") : null; - validateDistinctForEachBindings(itemName, keyName, indexName); - CompiledExpression input = compileExpr(required(prop(body, "in"), "$forEach.in"), - scope, bodyPointer + "/in"); - int slot = scope.declareOrGetSlot(itemName); - int keySlot = keyName != null ? scope.declareOrGetSlot(keyName) : -1; - int indexSlot = indexName != null ? scope.declareOrGetSlot(indexName) : -1; - compiled = new ForEachStatement(input, - slot, keySlot, indexSlot, compileStatements(prop(body, "do"), scope, bodyPointer + "/do")); - } else if ("$appendChange".equals(op)) { - compiled = new AppendChangeStatement(textOrExpr(required(prop(body, "op"), "$appendChange.op"), scope, null, bodyPointer + "/op"), - pointerOperand(required(prop(body, "path"), "$appendChange.path"), scope, bodyPointer + "/path"), - prop(body, "val") != null ? compileExpr(prop(body, "val"), scope, bodyPointer + "/val") : null); - } else if ("$appendChanges".equals(op)) { - compiled = new AppendChangesStatement(compileExpr(body, scope, bodyPointer)); - } else if ("$appendEvent".equals(op)) { - compiled = new AppendEventStatement(compileExpr(body, scope, bodyPointer)); - } else if ("$appendEvents".equals(op)) { - compiled = new AppendEventsStatement(compileExpr(body, scope, bodyPointer)); - } else if ("$call".equals(op)) { - compiled = new CallStatement(compileCall(body, scope, bodyPointer)); - } else if ("$return".equals(op)) { - if (body == null || body.isEmptyNode() || (body.getProperties() != null && body.getProperties().isEmpty())) { - compiled = new ReturnStatement(null); - } else { - compiled = new ReturnStatement(compileExpr(body, scope, bodyPointer)); - } - } else if ("$returnIf".equals(op)) { - if (hasExplicitProperty(body, "value")) { - throw new BexException("$returnIf uses expr for its return payload; value is not supported"); - } - compiled = new ReturnIfStatement( - compileExpr(required(prop(body, "cond"), "$returnIf.cond"), scope, bodyPointer + "/cond"), - hasExplicitProperty(body, "expr") - ? compileExpr(explicitProp(body, "expr"), scope, bodyPointer + "/expr") - : null); - } else if ("$fail".equals(op)) { - compiled = new FailStatement(failMessageExpr(body, scope, bodyPointer)); - } else if ("$failIf".equals(op)) { - compiled = new FailIfStatement( - compileExpr(required(prop(body, "cond"), "$failIf.cond"), scope, bodyPointer + "/cond"), - compileExpr(required(prop(body, "message"), "$failIf.message"), scope, bodyPointer + "/message")); - } else { - throw new BexException("Unknown statement operator: " + op); - } - return new SourceStatement(sourcePath, compiled); - } catch (BexException ex) { - throw ex.withSourcePath(sourcePath); - } - } - - private CompiledStatement compileMultiLet(FrozenNode body, CompileScope scope, String pointer) { - FrozenNode varsNode = required(prop(body, "vars"), "$let.vars"); - validatePlainObjectContainer(varsNode, "$let.vars"); - if (varsNode.getProperties() == null) { - if (varsNode.isEmptyNode()) { - return new MultiLetStatement(new int[0], new CompiledExpression[0], false); - } - throw new BexException("$let.vars must be an object"); - } - List names = new ArrayList<>(varsNode.getProperties().keySet()); - boolean sequential = prop(body, "order") != null; - if (sequential) { - names = orderedLetNames(prop(body, "order"), varsNode, pointer + "/order"); - } else { - Collections.sort(names, BexUnicodeOrder.CODE_POINT_COMPARATOR); - } - - int[] slots = new int[names.size()]; - List expressions = new ArrayList<>(); - if (sequential) { - for (int i = 0; i < names.size(); i++) { - String name = names.get(i); - expressions.add(compileExpr(varsNode.getProperties().get(name), scope, - pointer + "/vars/" + escape(name))); - slots[i] = scope.declareOrGetSlot(name); - } - } else { - for (int i = 0; i < names.size(); i++) { - slots[i] = scope.declareOrGetSlot(names.get(i)); - } - for (String name : names) { - expressions.add(compileExpr(varsNode.getProperties().get(name), scope, - pointer + "/vars/" + escape(name))); - } - } - return new MultiLetStatement(slots, expressions.toArray(new CompiledExpression[0]), sequential); - } - - private List orderedLetNames(FrozenNode orderNode, FrozenNode varsNode, String pointer) { - if (orderNode == null || orderNode.getItems() == null) { - throw new BexException("$let.order must be a list"); - } - List names = new ArrayList<>(); - Set seen = new LinkedHashSet<>(); - for (int i = 0; i < orderNode.getItems().size(); i++) { - String name = requiredText(orderNode.getItems().get(i), "$let.order item"); - if (!seen.add(name)) { - throw new BexException("$let.order contains duplicate variable: " + name); - } - if (varsNode.getProperties() == null || !varsNode.getProperties().containsKey(name)) { - throw new BexException("$let.order references unknown variable: " + name); - } - names.add(name); - } - if (varsNode.getProperties() != null && seen.size() != varsNode.getProperties().size()) { - for (String name : varsNode.getProperties().keySet()) { - if (!seen.contains(name)) { - throw new BexException("$let.order missing variable: " + name); - } - } - } - return names; - } - - private boolean isEmptyStatement(FrozenNode statement, String pointer) { - return statement == null || statement.isEmptyNode(); - } - - private CompiledExpression compileExpr(FrozenNode node, CompileScope scope, String pointer) { - if (node == null) { - return sourceExpr(currentFunction, pointer, null, new LiteralExpr(BexValues.nullValue())); - } - rejectBexInStaticBlueDefinitionFields(node, pointer); - if (isExpressionOperatorShape(node)) { - String op = node.getProperties().keySet().iterator().next(); - FrozenNode body = node.getProperties().values().iterator().next(); - BexSourcePath sourcePath = BexSourcePath.of(currentFunction, pointer + "/" + escape(op), op); - try { - return new SourceExpr(sourcePath, compileOperator(op, body, scope, pointer + "/" + escape(op))); - } catch (BexException ex) { - throw ex.withSourcePath(sourcePath); - } - } - if (node.isEmptyNode()) { - return sourceExpr(currentFunction, pointer, null, new LiteralExpr(BexValues.nullValue())); - } - if (isScalarNode(node)) { - return sourceExpr(currentFunction, pointer, null, - new TransientLiteralExpr(node.getValue())); - } - if (node.getItems() != null && !hasLanguageFields(node)) { - List items = new ArrayList<>(); - for (int i = 0; i < node.getItems().size(); i++) { - items.add(compileExpr(node.getItems().get(i), scope, pointer + "/" + i)); - } - return sourceExpr(currentFunction, pointer, null, new ListExpr(items)); - } - if (node.getProperties() != null || hasLanguageFields(node)) { - Map fields = new LinkedHashMap<>(); - addMetadataFields(fields, node, scope, pointer); - if (node.getItems() != null) { - List items = new ArrayList<>(); - for (int i = 0; i < node.getItems().size(); i++) { - items.add(compileExpr(node.getItems().get(i), scope, pointer + "/" + i)); - } - fields.put("items", new ListExpr(items)); - } - if (node.getProperties() != null) { - for (String key : BexUnicodeOrder.sortedCopy(node.getProperties().keySet())) { - fields.put(key, compileExpr(node.getProperties().get(key), scope, - pointer + "/" + escape(key))); - } - } - return sourceExpr(currentFunction, pointer, null, new ObjectExpr(fields)); - } - return sourceExpr(currentFunction, pointer, null, - new TransientLiteralExpr(node.getValue())); - } - - private CompiledExpression compileOperator(String op, FrozenNode body, CompileScope scope, String pointer) { - validateExpressionBody(op, body); - if ("$literal".equals(op)) return new LiteralExpr(BexValues.frozen(body)); - if ("$null".equals(op)) return new LiteralExpr(BexValues.nullValue()); - if ("$emptyObject".equals(op)) return new LiteralExpr(BexValues.map(Collections.emptyMap())); - if ("$emptyList".equals(op)) return new LiteralExpr(BexValues.list(Collections.emptyList())); - if ("$document".equals(op)) return documentExpr(body, scope, pointer); - if ("$binding".equals(op)) return bindingExpr(body, scope, pointer); - if ("$event".equals(op)) return contextPointerExpr(body, scope, ContextKind.EVENT, pointer); - if ("$processingEvent".equals(op)) return contextPointerExpr(body, scope, ContextKind.PROCESSING_EVENT, pointer); - if ("$steps".equals(op)) return stepsExpr(body, scope, pointer); - if ("$currentContract".equals(op)) return contextPointerExpr(body, scope, ContextKind.CURRENT_CONTRACT, pointer); - if ("$var".equals(op)) return varExpr(body, scope, pointer); - if ("$const".equals(op)) { - String name = constName(body); - if (!constants.containsKey(name)) { - throw new BexException("Unknown constant: " + name); - } - return new ConstExpr(name, pathOperandOrNull(body, scope, pointer)); - } - if ("$get".equals(op)) return new GetExpr(compileExpr(required(prop(body, "object"), "$get.object"), scope, pointer + "/object"), textOrExpr(required(prop(body, "key"), "$get.key"), scope, null, pointer + "/key")); - if ("$changeset".equals(op)) return new ChangesetExpr(); - if ("$events".equals(op)) return new EventsExpr(); - if ("$resultValue".equals(op)) return new ResultValueExpr(pointerOperand(body, scope, pointer)); - if ("$unwrap".equals(op)) return new UnaryExpr(compileExpr(body, scope, pointer), UnaryOp.UNWRAP); - if ("$is".equals(op)) return isExpr(body, scope, pointer); - if ("$text".equals(op)) return new UnaryExpr(compileExpr(body, scope, pointer), UnaryOp.TEXT); - if ("$integer".equals(op)) return new UnaryExpr(compileExpr(body, scope, pointer), UnaryOp.INTEGER); - if ("$number".equals(op)) return new UnaryExpr(compileExpr(body, scope, pointer), UnaryOp.NUMBER); - if ("$boolean".equals(op)) return new UnaryExpr(compileExpr(body, scope, pointer), UnaryOp.BOOLEAN); - if ("$object".equals(op)) return new UnaryExpr(compileExpr(body, scope, pointer), UnaryOp.OBJECT); - if ("$list".equals(op)) return new UnaryExpr(compileExpr(body, scope, pointer), UnaryOp.LIST); - if ("$concat".equals(op)) return new VariadicExpr(compileExprList(body, scope, pointer), VariadicOp.CONCAT); - if ("$pointerJoin".equals(op)) return new PointerJoinExpr(compileExprList(body, scope, pointer)); - if ("$join".equals(op)) return new JoinExpr(compileExpr(required(prop(body, "list"), "$join.list"), scope, pointer + "/list"), compileExpr(required(prop(body, "separator"), "$join.separator"), scope, pointer + "/separator")); - if ("$split".equals(op)) return new SplitExpr(compileExpr(required(prop(body, "text"), "$split.text"), scope, pointer + "/text"), compileExpr(required(prop(body, "separator"), "$split.separator"), scope, pointer + "/separator"), prop(body, "limit") != null ? compileExpr(prop(body, "limit"), scope, pointer + "/limit") : null); - if ("$startsWith".equals(op)) return new BinaryTextExpr(compileExprList(body, scope, pointer), BinaryTextOp.STARTS_WITH); - if ("$sliceAfter".equals(op)) return new BinaryTextExpr(compileExprList(body, scope, pointer), BinaryTextOp.SLICE_AFTER); - if ("$eq".equals(op)) return new CompareExpr(compileExprList(body, scope, pointer), CompareOp.EQ); - if ("$ne".equals(op)) return new CompareExpr(compileExprList(body, scope, pointer), CompareOp.NE); - if ("$gt".equals(op)) return new CompareExpr(compileExprList(body, scope, pointer), CompareOp.GT); - if ("$gte".equals(op)) return new CompareExpr(compileExprList(body, scope, pointer), CompareOp.GTE); - if ("$lt".equals(op)) return new CompareExpr(compileExprList(body, scope, pointer), CompareOp.LT); - if ("$lte".equals(op)) return new CompareExpr(compileExprList(body, scope, pointer), CompareOp.LTE); - if ("$and".equals(op)) return new LogicalExpr(compileExprList(body, scope, pointer), true); - if ("$or".equals(op)) return new LogicalExpr(compileExprList(body, scope, pointer), false); - if ("$not".equals(op)) return new NotExpr(compileExpr(body, scope, pointer)); - if ("$truthy".equals(op)) return new UnaryExpr(compileExpr(body, scope, pointer), UnaryOp.TRUTHY); - if ("$empty".equals(op) || "$isEmpty".equals(op)) return new UnaryExpr(compileExpr(body, scope, pointer), UnaryOp.EMPTY); - if ("$exists".equals(op)) return new UnaryExpr(compileExpr(body, scope, pointer), UnaryOp.EXISTS); - if ("$kind".equals(op)) return new KindExpr(compileExpr(body, scope, pointer)); - if ("$isKind".equals(op)) return new IsKindExpr(compileExpr(required(prop(body, "val"), "$isKind.val"), scope, pointer + "/val"), kindSet(required(prop(body, "kind"), "$isKind.kind"))); - if ("$nodeBlueId".equals(op)) return new NodeBlueIdExpr(compileExpr(body, scope, pointer)); - if ("$coalesce".equals(op)) return new CoalesceExpr(compileExprList(body, scope, pointer)); - if ("$default".equals(op)) return new CoalesceExpr(compileExprList(body, scope, pointer)); - if ("$add".equals(op)) return new NumericExpr(compileExprList(body, scope, pointer), NumericOp.ADD); - if ("$subtract".equals(op)) return new NumericExpr(compileExprList(body, scope, pointer), NumericOp.SUBTRACT); - if ("$multiply".equals(op)) return new NumericExpr(compileExprList(body, scope, pointer), NumericOp.MULTIPLY); - if ("$divide".equals(op)) return new NumericExpr(compileExprList(body, scope, pointer), NumericOp.DIVIDE); - if ("$keys".equals(op)) return new UnaryExpr(compileExpr(body, scope, pointer), UnaryOp.KEYS); - if ("$entries".equals(op)) return new UnaryExpr(compileExpr(body, scope, pointer), UnaryOp.ENTRIES); - if ("$size".equals(op)) return new UnaryExpr(compileExpr(body, scope, pointer), UnaryOp.SIZE); - if ("$listGet".equals(op)) return new ListGetExpr(compileExpr(required(prop(body, "list"), "$listGet.list"), scope, pointer + "/list"), compileExpr(required(prop(body, "index"), "$listGet.index"), scope, pointer + "/index"), prop(body, "default") != null ? compileExpr(prop(body, "default"), scope, pointer + "/default") : null); - if ("$listConcat".equals(op)) return new VariadicExpr(compileExprList(body, scope, pointer), VariadicOp.LIST_CONCAT); - if ("$merge".equals(op)) return new VariadicExpr(compileExprList(body, scope, pointer), VariadicOp.MERGE); - if ("$objectSet".equals(op)) return new ObjectSetExpr(compileExpr(required(prop(body, "object"), "$objectSet.object"), scope, pointer + "/object"), textOrExpr(required(prop(body, "key"), "$objectSet.key"), scope, null, pointer + "/key"), compileExpr(required(prop(body, "val"), "$objectSet.val"), scope, pointer + "/val")); - if ("$pointerGet".equals(op)) return new PointerGetExpr(compileExpr(required(prop(body, "object"), "$pointerGet.object"), scope, pointer + "/object"), valuePointerOperand(required(prop(body, "path"), "$pointerGet.path"), scope, pointer + "/path"), prop(body, "default") != null ? compileExpr(prop(body, "default"), scope, pointer + "/default") : null); - if ("$pointerSet".equals(op)) return new PointerSetExpr(compileExpr(required(prop(body, "object"), "$pointerSet.object"), scope, pointer + "/object"), textOrExpr(prop(body, "op"), scope, "set", pointer + "/op"), valuePointerOperand(required(prop(body, "path"), "$pointerSet.path"), scope, pointer + "/path"), prop(body, "val") != null ? compileExpr(prop(body, "val"), scope, pointer + "/val") : null); - if ("$map".equals(op)) return collectionExpr(body, scope, pointer, CollectionOp.MAP, "expr"); - if ("$filter".equals(op)) return collectionExpr(body, scope, pointer, CollectionOp.FILTER, "where"); - if ("$flatMap".equals(op)) return collectionExpr(body, scope, pointer, CollectionOp.FLAT_MAP, "expr"); - if ("$reduce".equals(op)) return reduceExpr(body, scope, pointer); - if ("$some".equals(op)) return collectionExpr(body, scope, pointer, CollectionOp.SOME, "where"); - if ("$find".equals(op)) return collectionExpr(body, scope, pointer, CollectionOp.FIND, "where"); - if ("$findEntry".equals(op)) return collectionExpr(body, scope, pointer, CollectionOp.FIND_ENTRY, "where"); - if ("$includes".equals(op)) return new IncludesExpr(compileExpr(required(prop(body, "list"), "$includes.list"), scope, pointer + "/list"), - compileExpr(required(prop(body, "val"), "$includes.val"), scope, pointer + "/val")); - if ("$hasKey".equals(op)) return new HasKeyExpr(compileExpr(required(prop(body, "object"), "$hasKey.object"), scope, pointer + "/object"), - textOrExpr(required(prop(body, "key"), "$hasKey.key"), scope, null, pointer + "/key")); - if ("$objectFromEntries".equals(op)) return new ObjectFromEntriesExpr(compileExpr(body, scope, pointer)); - if ("$intrinsic".equals(op)) return intrinsicExpr(body, scope, pointer); - if ("$fail".equals(op)) return new FailExpr(failMessageExpr(body, scope, pointer)); - if ("$choose".equals(op)) return new ChooseExpr(compileExpr(required(prop(body, "cond"), "$choose.cond"), scope, pointer + "/cond"), compileExpr(required(prop(body, "then"), "$choose.then"), scope, pointer + "/then"), prop(body, "else") != null ? compileExpr(prop(body, "else"), scope, pointer + "/else") : new LiteralExpr(BexValues.undefined())); - if ("$call".equals(op)) return compileCall(body, scope, pointer); - throw new BexException("Unknown expression operator: " + op); - } - - private CompiledExpression failMessageExpr(FrozenNode body, - CompileScope scope, - String pointer) { - boolean messageWrapper = body != null - && body.getProperties() != null - && hasExplicitProperty(body, "message"); - return compileExpr(messageWrapper ? explicitProp(body, "message") : body, - scope, - messageWrapper ? pointer + "/message" : pointer); - } - - private void validateExpressionBody(String op, FrozenNode body) { - if ("$concat".equals(op) - || "$pointerJoin".equals(op) - || "$listConcat".equals(op) - || "$merge".equals(op) - || "$and".equals(op) - || "$or".equals(op) - || "$coalesce".equals(op) - || "$default".equals(op)) { - requireListBody(body, op); - return; - } - if ("$eq".equals(op) - || "$ne".equals(op) - || "$gt".equals(op) - || "$gte".equals(op) - || "$lt".equals(op) - || "$lte".equals(op) - || "$startsWith".equals(op) - || "$sliceAfter".equals(op)) { - requireListArity(body, op, 2); - return; - } - if ("$add".equals(op) - || "$subtract".equals(op) - || "$multiply".equals(op) - || "$divide".equals(op)) { - requireListBody(body, op); - if (body.getItems().isEmpty()) { - throw new BexException(op + " requires at least one operand"); - } - return; - } - if ("$get".equals(op)) { - requireObjectBody(body, op, "object", "key"); - } else if ("$is".equals(op)) { - requireObjectBody(body, op, "node", "pattern"); - } else if ("$isKind".equals(op)) { - requireObjectBody(body, op, "val", "kind"); - } else if ("$join".equals(op)) { - requireObjectBody(body, op, "list", "separator"); - } else if ("$split".equals(op)) { - requireObjectBody(body, op, "text", "separator", "limit"); - } else if ("$listGet".equals(op)) { - requireObjectBody(body, op, "list", "index", "default"); - } else if ("$objectSet".equals(op)) { - requireObjectBody(body, op, "object", "key", "val"); - } else if ("$pointerGet".equals(op)) { - requireObjectBody(body, op, "object", "path", "default"); - } else if ("$pointerSet".equals(op)) { - requireObjectBody(body, op, "object", "path", "op", "val"); - } else if ("$map".equals(op) || "$flatMap".equals(op)) { - requireObjectBody(body, op, "in", "item", "key", "index", "expr"); - } else if ("$filter".equals(op) - || "$some".equals(op) - || "$find".equals(op) - || "$findEntry".equals(op)) { - requireObjectBody(body, op, "in", "item", "key", "index", "where"); - } else if ("$reduce".equals(op)) { - requireObjectBody(body, op, "in", "acc", "init", "item", "key", "index", "expr"); - } else if ("$includes".equals(op)) { - requireObjectBody(body, op, "list", "val"); - } else if ("$hasKey".equals(op)) { - requireObjectBody(body, op, "object", "key"); - } else if ("$choose".equals(op)) { - requireObjectBody(body, op, "cond", "then", "else"); - } else if ("$call".equals(op)) { - requireObjectBody(body, op, "function", "args"); - } else if ("$intrinsic".equals(op)) { - requireObjectNode(body, op); - } else if ("$binding".equals(op)) { - if (!isScalarBody(body)) { - requireObjectBody(body, op, "name", "path"); - } - } else if ("$steps".equals(op)) { - if (!isScalarBody(body)) { - requireObjectBody(body, op, "step", "path"); - } - } else if ("$var".equals(op) || "$const".equals(op)) { - if (!isScalarBody(body)) { - requireObjectBody(body, op, "name", "path"); - } - } else if ("$document".equals(op) - && body != null - && hasAuthoredField(body, "path") - && !isExpressionOperatorShape(body)) { - requireObjectBody(body, op, "path", "view"); - } - } - - private void validateStatementBody(String op, FrozenNode body) { - if ("$let".equals(op)) { - requireObjectBody(body, op, "name", "expr", "vars", "order"); - boolean multi = hasAuthoredField(body, "vars"); - if (multi && (hasAuthoredField(body, "name") || hasAuthoredField(body, "expr"))) { - throw new BexException("$let must use either name/expr or vars/order form"); - } - if (!multi && (!hasAuthoredField(body, "name") || !hasAuthoredField(body, "expr"))) { - throw new BexException("$let single-binding form requires name and expr"); - } - } else if ("$set".equals(op)) { - requireObjectBody(body, op, "name", "expr"); - } else if ("$if".equals(op)) { - requireObjectBody(body, op, "cond", "then", "else"); - } else if ("$forEach".equals(op)) { - requireObjectBody(body, op, "in", "item", "key", "index", "do"); - if (!hasAuthoredField(body, "do")) { - throw new BexException("$forEach.do is required"); - } - } else if ("$appendChange".equals(op)) { - requireObjectBody(body, op, "op", "path", "val"); - } else if ("$call".equals(op)) { - requireObjectBody(body, op, "function", "args"); - } else if ("$returnIf".equals(op)) { - requireObjectBody(body, op, "cond", "expr"); - } else if ("$failIf".equals(op)) { - requireObjectBody(body, op, "cond", "message"); - } - } - - private void requireListBody(FrozenNode body, String op) { - if (body == null || body.getItems() == null || hasNonListPayload(body)) { - throw new BexException(op + " expects a list body"); - } - } - - private void requireListArity(FrozenNode body, String op, int expected) { - requireListBody(body, op); - if (body.getItems().size() != expected) { - throw new BexException(op + " expects exactly " + expected + " operands"); - } - } - - private void requireObjectBody(FrozenNode body, String op, String... allowedFields) { - requireObjectNode(body, op); - Set allowed = new LinkedHashSet<>(); - Collections.addAll(allowed, allowedFields); - for (String field : authoredFieldNames(body)) { - if (!allowed.contains(field)) { - throw new BexException(op + " has unknown body field: " + field); - } - } - } - - private void requireObjectNode(FrozenNode body, String op) { - if (body == null - || body.getItems() != null - || body.getValue() != null - || body.getReferenceBlueId() != null - || body.getPreviousBlueId() != null - || body.getPosition() != null) { - throw new BexException(op + " expects an object body"); - } - } - - private void requireProgramNode(FrozenNode node, String label) { - if (node == null - || node.getValue() != null - || node.getItems() != null - || node.getReferenceBlueId() != null - || node.getPreviousBlueId() != null - || node.getPosition() != null) { - throw new BexException("BEX " + label + " must be an object node"); - } - } - - private boolean hasNonListPayload(FrozenNode node) { - return node.getValue() != null - || node.getProperties() != null - || hasLanguageFields(node) - || node.getPreviousBlueId() != null - || node.getPosition() != null; - } - - private boolean isScalarBody(FrozenNode body) { - return isScalarNode(body); - } - - /** - * Blue preprocessing adds an exact core-type reference to an authored - * scalar. That inferred metadata is part of the scalar representation, not - * a BEX object literal field. Keep the exception deliberately narrow: - * computed or additional Blue metadata must still compile as an object. - */ - private boolean isScalarNode(FrozenNode node) { - if (node == null - || node.getValue() == null - || node.getItems() != null - || node.getProperties() != null - || node.getName() != null - || node.getDescription() != null - || node.getItemType() != null - || node.getKeyType() != null - || node.getValueType() != null - || node.getReferenceBlueId() != null - || node.getBlue() != null - || node.getContracts() != null - || node.getSchema() != null - || node.getMergePolicy() != null - || node.getPreviousBlueId() != null - || node.getPosition() != null) { - return false; - } - FrozenNode type = node.getType(); - if (type == null) { - return true; - } - String expectedTypeBlueId = scalarTypeBlueId(node.getValue()); - return expectedTypeBlueId != null - && type.isReferenceOnly() - && expectedTypeBlueId.equals(type.getReferenceBlueId()); - } - - private String scalarTypeBlueId(Object value) { - if (value instanceof String) { - return TEXT_TYPE_BLUE_ID; - } - if (value instanceof Boolean) { - return BOOLEAN_TYPE_BLUE_ID; - } - if (value instanceof java.math.BigDecimal - || value instanceof Float - || value instanceof Double) { - return DOUBLE_TYPE_BLUE_ID; - } - if (value instanceof java.math.BigInteger - || value instanceof Byte - || value instanceof Short - || value instanceof Integer - || value instanceof Long) { - return INTEGER_TYPE_BLUE_ID; - } - return null; - } - - private CompiledExpression intrinsicExpr(FrozenNode body, CompileScope scope, String pointer) { - if (body == null || body.getValue() != null || body.getItems() != null) { - throw new BexException("$intrinsic expects an object body"); - } - FrozenNode typeNode = required(prop(body, "type"), "$intrinsic.type"); - rejectBexAnywhereInStaticPattern(typeNode, pointer + "/type"); - String blueId = intrinsicTypeBlueId(typeNode); - BexValue typeValue = BexValues.frozen(typeNode); - if (blueId == null || blueId.isEmpty()) { - throw new BexException("$intrinsic.type must resolve to a BlueId"); - } - if (!intrinsics.supports(blueId)) { - throw new BexException("Unsupported intrinsic BlueId: " + blueId); - } - requiredIntrinsicBlueIds.add(blueId); - - Map fields = new LinkedHashMap<>(); - if (body.getProperties() != null) { - for (String fieldName : BexUnicodeOrder.sortedCopy(body.getProperties().keySet())) { - if ("type".equals(fieldName)) { - continue; - } - fields.put(fieldName, compileExpr(body.getProperties().get(fieldName), scope, - pointer + "/" + escape(fieldName))); - } - } - return new IntrinsicExpr(blueId, typeValue, fields); - } - - private String intrinsicTypeBlueId(FrozenNode typeNode) { - if (hasExplicitProperty(typeNode, "blueId")) { - return text(explicitProp(typeNode, "blueId")); - } - if (typeNode.getReferenceBlueId() != null && !typeNode.getReferenceBlueId().isEmpty()) { - return typeNode.getReferenceBlueId(); - } - String blueId = BexNodeIdentity.safeBlueId(typeNode); - if (blueId != null && !blueId.isEmpty()) { - return blueId; - } - return text(typeNode); - } - - private CompiledExpression varExpr(FrozenNode body, CompileScope scope, String pointer) { - if (body != null && body.getProperties() != null) { - String name = requiredText(prop(body, "name"), "$var.name"); - return new VarExpr(scope.resolveSlot(name), pathOperandOrNull(body, scope, pointer)); - } - return new VarExpr(scope.resolveSlot(requiredText(body, "$var"))); - } - - private String constName(FrozenNode body) { - if (body != null && body.getProperties() != null) { - return requiredText(prop(body, "name"), "$const.name"); - } - return requiredText(body, "$const"); - } - - private PointerOperand pathOperandOrNull(FrozenNode body, CompileScope scope, String pointer) { - if (body == null || body.getProperties() == null || prop(body, "path") == null) { - return null; - } - return valuePointerOperand(prop(body, "path"), scope, pointer + "/path"); - } - - private Set kindSet(FrozenNode node) { - Set kinds = new LinkedHashSet<>(); - if (node.getItems() != null) { - for (FrozenNode item : node.getItems()) { - kinds.add(validateKind(requiredText(item, "$isKind.kind item"))); - } - } else { - kinds.add(validateKind(requiredText(node, "$isKind.kind"))); - } - return Collections.unmodifiableSet(kinds); - } - - private String validateKind(String kind) { - if ("undefined".equals(kind) - || "null".equals(kind) - || "text".equals(kind) - || "integer".equals(kind) - || "double".equals(kind) - || "boolean".equals(kind) - || "object".equals(kind) - || "list".equals(kind)) { - return kind; - } - throw new BexException("Unknown BEX kind: " + kind); - } - - private CompiledExpression collectionExpr(FrozenNode body, CompileScope scope, String pointer, - CollectionOp op, String bodyField) { - String operator = collectionOperatorName(op); - CompiledExpression input = compileExpr(required(prop(body, "in"), operator + ".in"), scope, pointer + "/in"); - String itemName = requiredText(prop(body, "item"), operator + ".item"); - String keyName = prop(body, "key") != null ? requiredText(prop(body, "key"), operator + ".key") : null; - String indexName = prop(body, "index") != null ? requiredText(prop(body, "index"), operator + ".index") : null; - validateDistinctCollectionBindings(operator, itemName, keyName, indexName); - CompileScope.Visibility visibility = scope.captureVisibility(); - try { - int itemSlot = scope.declareOrGetSlot(itemName); - int keySlot = keyName != null ? scope.declareOrGetSlot(keyName) : -1; - int indexSlot = indexName != null ? scope.declareOrGetSlot(indexName) : -1; - CompiledExpression expr = compileExpr(required(prop(body, bodyField), operator + "." + bodyField), - scope, pointer + "/" + bodyField); - return new CollectionQueryExpr(input, itemSlot, keySlot, indexSlot, expr, op); - } finally { - scope.restoreVisibility(visibility); - } - } - - private CompiledExpression reduceExpr(FrozenNode body, CompileScope scope, String pointer) { - CompiledExpression input = compileExpr(required(prop(body, "in"), "$reduce.in"), scope, pointer + "/in"); - String accName = requiredText(prop(body, "acc"), "$reduce.acc"); - String itemName = requiredText(prop(body, "item"), "$reduce.item"); - String keyName = prop(body, "key") != null ? requiredText(prop(body, "key"), "$reduce.key") : null; - String indexName = prop(body, "index") != null ? requiredText(prop(body, "index"), "$reduce.index") : null; - validateDistinctCollectionBindings("$reduce", itemName, keyName, indexName); - if (accName.equals(itemName) || accName.equals(keyName) || accName.equals(indexName)) { - throw new BexException("$reduce.acc must use a different binding name"); - } - CompiledExpression init = compileExpr(required(prop(body, "init"), "$reduce.init"), scope, pointer + "/init"); - CompileScope.Visibility visibility = scope.captureVisibility(); - try { - int accSlot = scope.declareOrGetSlot(accName); - int itemSlot = scope.declareOrGetSlot(itemName); - int keySlot = keyName != null ? scope.declareOrGetSlot(keyName) : -1; - int indexSlot = indexName != null ? scope.declareOrGetSlot(indexName) : -1; - CompiledExpression expr = compileExpr(required(prop(body, "expr"), "$reduce.expr"), scope, pointer + "/expr"); - return new ReduceExpr(input, accSlot, init, itemSlot, keySlot, indexSlot, expr); - } finally { - scope.restoreVisibility(visibility); - } - } - - private String collectionOperatorName(CollectionOp op) { - switch (op) { - case MAP: - return "$map"; - case FILTER: - return "$filter"; - case FLAT_MAP: - return "$flatMap"; - case SOME: - return "$some"; - case FIND: - return "$find"; - case FIND_ENTRY: - return "$findEntry"; - default: - return "collection operator"; - } - } - - private void validateDistinctCollectionBindings(String operator, String itemName, String keyName, String indexName) { - if (keyName != null && keyName.equals(itemName)) { - throw new BexException(operator + ".key must use a different binding name than " + operator + ".item"); - } - if (indexName != null && indexName.equals(itemName)) { - throw new BexException(operator + ".index must use a different binding name than " + operator + ".item"); - } - if (keyName != null && indexName != null && keyName.equals(indexName)) { - throw new BexException(operator + ".key must use a different binding name than " + operator + ".index"); - } - } - - private CompiledExpression isExpr(FrozenNode body, CompileScope scope, String pointer) { - if (body == null || body.getProperties() == null) { - throw new BexException("$is expects an object body"); - } - FrozenNode pattern = required(prop(body, "pattern"), "$is.pattern"); - rejectBexAnywhereInStaticPattern(pattern, pointer + "/pattern"); - return new IsExpr( - compileExpr(required(prop(body, "node"), "$is.node"), scope, pointer + "/node"), - pattern); - } - - private CompiledExpression documentExpr(FrozenNode body, CompileScope scope, String pointer) { - boolean resolved = false; - FrozenNode pointerNode = body; - if (body != null && body.getProperties() != null && body.getProperties().containsKey("path")) { - pointerNode = prop(body, "path"); - String view = text(prop(body, "view")); - resolved = "resolved".equals(view); - } - return new DocumentExpr(pointerOperand(pointerNode, scope, pointer), resolved); - } - - private CompiledExpression contextPointerExpr(FrozenNode body, CompileScope scope, ContextKind kind, String pointer) { - return new ContextPointerExpr(valuePointerOperand(body, scope, pointer), kind); - } - - private CompiledExpression bindingExpr(FrozenNode body, CompileScope scope, String pointer) { - if (body != null && body.getValue() != null && body.getProperties() == null && body.getItems() == null) { - String selector = String.valueOf(body.getValue()); - int slash = selector.indexOf('/'); - String name = slash >= 0 ? selector.substring(0, slash) : selector; - if (name.isEmpty()) { - throw new BexException("$binding short form requires a binding name"); - } - String path = slash >= 0 ? selector.substring(slash) : "/"; - return new BindingExpr(new StaticTextExpr(name), StaticValuePointerOperand.of(path)); - } - if (body == null || body.getProperties() == null) { - throw new BexException("$binding expects a binding name or object form"); - } - TextOperand name = textOrExpr(required(prop(body, "name"), "$binding.name"), scope, null, pointer + "/name"); - FrozenNode path = prop(body, "path") != null ? prop(body, "path") : scalarNode("/"); - return new BindingExpr(name, valuePointerOperand(path, scope, pointer + "/path")); - } - - private CompiledExpression stepsExpr(FrozenNode body, CompileScope scope, String pointer) { - if (body.getValue() != null) { - String selector = String.valueOf(body.getValue()); - int dot = selector.indexOf('.'); - String step = dot >= 0 ? selector.substring(0, dot) : selector; - String path = dot >= 0 ? "/" + selector.substring(dot + 1) : "/"; - return new StepsExpr(new StaticTextExpr(step), StaticValuePointerOperand.of(path)); - } - return new StepsExpr(textOrExpr(required(prop(body, "step"), "$steps.step"), scope, null, pointer + "/step"), - valuePointerOperand(prop(body, "path") != null ? prop(body, "path") : scalarNode("/"), scope, pointer + "/path")); - } - - private CallExpr compileCall(FrozenNode body, CompileScope scope, String pointer) { - String function = requiredText(prop(body, "function"), "$call.function"); - FunctionSignature signature = functionSignatures.get(function); - if (signature == null) { - throw new BexException("Unknown function: " + function); - } - List argExpressions = new ArrayList<>(); - List targetSlots = new ArrayList<>(); - Set providedArgs = new LinkedHashSet<>(); - FrozenNode argsNode = prop(body, "args"); - if (argsNode != null) { - validatePlainObjectContainer(argsNode, "$call.args"); - if (argsNode.getProperties() == null) { - if (!argsNode.isEmptyNode()) { - throw new BexException("$call.args must be an object at " + pointer + "/args"); - } - } else { - for (String argName : BexUnicodeOrder.sortedCopy(argsNode.getProperties().keySet())) { - BexCompiledProgram.ArgSpec arg = signature.arg(argName); - if (arg == null) { - throw new BexException("Unknown argument " + argName + " for function " + function); - } - providedArgs.add(argName); - targetSlots.add(arg.slot()); - argExpressions.add(compileExpr(argsNode.getProperties().get(argName), scope, - pointer + "/args/" + escape(argName))); - } - } - } - for (BexCompiledProgram.ArgSpec arg : signature.args()) { - if (!providedArgs.contains(arg.name())) { - throw new BexException("Missing argument " + arg.name() + " for function " + function); - } - } - int[] slots = new int[targetSlots.size()]; - for (int i = 0; i < targetSlots.size(); i++) { - slots[i] = targetSlots.get(i); - } - return new CallExpr(function, - slots, - argExpressions.toArray(new CompiledExpression[0])); - } - - private List compileExprList(FrozenNode node, CompileScope scope, String pointer) { - if (node == null || node.getItems() == null) { - throw new BexException("Operator expects a list"); - } - List expressions = new ArrayList<>(); - for (int i = 0; i < node.getItems().size(); i++) { - expressions.add(compileExpr(node.getItems().get(i), scope, pointer + "/" + i)); - } - return expressions; - } - - private TextOperand textOrExpr(FrozenNode node, CompileScope scope, String defaultText, String pointer) { - if (node == null) { - return new StaticTextExpr(defaultText); - } - if (node.getValue() != null && node.getProperties() == null && node.getItems() == null) { - return new StaticTextExpr(String.valueOf(node.getValue())); - } - return new DynamicTextExpr(compileExpr(node, scope, pointer), pointer); - } - - private PointerOperand pointerOperand(FrozenNode node, CompileScope scope, String pointer) { - if (node != null && node.getProperties() != null && node.getProperties().containsKey("path")) { - node = prop(node, "path"); - } - if (node != null && node.getValue() != null && node.getProperties() == null && node.getItems() == null) { - return StaticPointerOperand.of(String.valueOf(node.getValue())); - } - return new DynamicPointerOperand(compileExpr(node, scope, pointer)); - } - - private PointerOperand valuePointerOperand(FrozenNode node, CompileScope scope, String pointer) { - if (node != null && node.getProperties() != null && node.getProperties().containsKey("path")) { - node = prop(node, "path"); - } - if (node != null && node.getValue() != null && node.getProperties() == null && node.getItems() == null) { - return StaticValuePointerOperand.of(String.valueOf(node.getValue())); - } - return new DynamicValuePointerOperand(compileExpr(node, scope, pointer)); - } - - private FrozenNode prop(FrozenNode node, String key) { - if (node == null) { - return null; - } - if (node.getProperties() != null && node.getProperties().containsKey(key)) { - return node.getProperties().get(key); - } - if ("name".equals(key) && node.getName() != null) { - return scalarNode(node.getName()); - } - if ("description".equals(key) && node.getDescription() != null) { - return scalarNode(node.getDescription()); - } - if ("type".equals(key) && node.getType() != null) { - return node.getType(); - } - if ("itemType".equals(key) && node.getItemType() != null) { - return node.getItemType(); - } - if ("keyType".equals(key) && node.getKeyType() != null) { - return node.getKeyType(); - } - if ("valueType".equals(key) && node.getValueType() != null) { - return node.getValueType(); - } - if ("value".equals(key) && node.getValue() != null) { - return scalarNode(node.getValue()); - } - if ("blueId".equals(key) && node.getReferenceBlueId() != null) { - return scalarNode(node.getReferenceBlueId()); - } - if ("blue".equals(key) && node.getBlue() != null) { - return node.getBlue(); - } - if ("contracts".equals(key) && node.getContracts() != null) { - return node.getContracts(); - } - return null; - } - - private FrozenNode explicitProp(FrozenNode node, String key) { - return node != null && node.getProperties() != null - ? node.getProperties().get(key) - : null; - } - - private boolean hasExplicitProperty(FrozenNode node, String key) { - return node != null && node.getProperties() != null && node.getProperties().containsKey(key); - } - - private boolean hasAuthoredField(FrozenNode node, String key) { - return authoredFieldNames(node).contains(key); - } - - private Set authoredFieldNames(FrozenNode node) { - Set fields = new LinkedHashSet<>(); - if (node == null) { - return fields; - } - if (node.getProperties() != null) { - fields.addAll(node.getProperties().keySet()); - } - if (node.getName() != null) fields.add("name"); - if (node.getDescription() != null) fields.add("description"); - if (node.getType() != null) fields.add("type"); - if (node.getItemType() != null) fields.add("itemType"); - if (node.getKeyType() != null) fields.add("keyType"); - if (node.getValueType() != null) fields.add("valueType"); - if (node.getValue() != null) fields.add("value"); - if (node.getItems() != null) fields.add("items"); - if (node.getReferenceBlueId() != null) fields.add("blueId"); - if (node.getBlue() != null) fields.add("blue"); - if (node.getSchema() != null) fields.add("schema"); - if (node.getMergePolicy() != null) fields.add("mergePolicy"); - if (node.getContracts() != null) fields.add("contracts"); - if (node.getPreviousBlueId() != null) fields.add("$previous"); - if (node.getPosition() != null) fields.add("$pos"); - return fields; - } - - private FrozenNode required(FrozenNode node, String label) { - if (node == null) { - throw new BexException("Missing required field: " + label); - } - return node; - } - - private String requiredText(FrozenNode node, String label) { - String value = text(node); - if (value == null) { - throw new BexException("Missing required text field: " + label); - } - return value; - } - - private String requiredNonEmptyText(FrozenNode node, String label) { - String value = requiredText(node, label); - if (value.isEmpty()) { - throw new BexException("Required text field is empty: " + label); - } - return value; - } - - private String text(FrozenNode node) { - return node != null && node.getValue() instanceof String - ? (String) node.getValue() - : null; - } - - private boolean isExpressionOperatorShape(FrozenNode node) { - if (node == null - || node.getProperties() == null - || node.getProperties().size() != 1 - || authoredFieldNames(node).size() != 1) { - return false; - } - String key = node.getProperties().keySet().iterator().next(); - return key.startsWith("$"); - } - - private boolean isOperator(FrozenNode node, String op) { - return isExpressionOperatorShape(node) && node.getProperties().containsKey(op); - } - - private FrozenNode onlyValue(FrozenNode node) { - return node.getProperties().values().iterator().next(); - } - - private void addMetadataFields(Map fields, FrozenNode node, CompileScope scope, String pointer) { - if (node.getName() != null) { - fields.put("name", new TransientLiteralExpr(node.getName())); - } - if (node.getDescription() != null) { - fields.put("description", new TransientLiteralExpr(node.getDescription())); - } - if (node.getType() != null) { - fields.put("type", compileExpr(node.getType(), scope, pointer + "/type")); - } - if (node.getItemType() != null) { - fields.put("itemType", compileExpr(node.getItemType(), scope, pointer + "/itemType")); - } - if (node.getKeyType() != null) { - fields.put("keyType", compileExpr(node.getKeyType(), scope, pointer + "/keyType")); - } - if (node.getValueType() != null) { - fields.put("valueType", compileExpr(node.getValueType(), scope, pointer + "/valueType")); - } - if (node.getValue() != null) { - fields.put("value", new TransientLiteralExpr(node.getValue())); - } - if (node.getReferenceBlueId() != null) { - fields.put("blueId", new TransientLiteralExpr(node.getReferenceBlueId())); - } - if (node.getBlue() != null) { - fields.put("blue", compileExpr(node.getBlue(), scope, pointer + "/blue")); - } - if (node.getContracts() != null) { - fields.put("contracts", compileExpr(node.getContracts(), scope, pointer + "/contracts")); - } - if (node.getSchema() != null) { - fields.put("schema", new LiteralExpr(BexValues.nodeSnapshot(new blue.language.model.Node().schema(node.getSchema())))); - } - if (node.getMergePolicy() != null) { - fields.put("mergePolicy", new TransientLiteralExpr(node.getMergePolicy())); - } - } - - private boolean hasLanguageFields(FrozenNode node) { - return node.getName() != null - || node.getDescription() != null - || node.getType() != null - || node.getItemType() != null - || node.getKeyType() != null - || node.getValueType() != null - || node.getReferenceBlueId() != null - || node.getBlue() != null - || node.getContracts() != null - || node.getSchema() != null - || node.getMergePolicy() != null; - } - - private FrozenNode scalarNode(Object value) { - return FrozenNode.fromResolvedNode(new blue.language.model.Node().value(value)); - } - - private CompiledExpression sourceExpr(String functionName, String pointer, String operator, CompiledExpression expression) { - return new SourceExpr(BexSourcePath.of(functionName, pointer, operator), expression); - } - - private CompiledStatement sourceStatement(String functionName, String pointer, String operator, CompiledStatement statement) { - return new SourceStatement(BexSourcePath.of(functionName, pointer, operator), statement); - } - - private void validateDistinctForEachBindings(String itemName, String keyName, String indexName) { - if (keyName != null && keyName.equals(itemName)) { - throw new BexException("$forEach.key must use a different binding name than $forEach.item"); - } - if (indexName != null && indexName.equals(itemName)) { - throw new BexException("$forEach.index must use a different binding name than $forEach.item"); - } - if (keyName != null && indexName != null && keyName.equals(indexName)) { - throw new BexException("$forEach.key must use a different binding name than $forEach.index"); - } - } - - private String escape(String segment) { - return segment.replace("~", "~0").replace("/", "~1"); - } - - private void validatePlainObjectContainer(FrozenNode node, String label) { - if (node == null) { - return; - } - if (node.getValue() != null || node.getItems() != null || node.getReferenceBlueId() != null) { - throw new BexException(label + " must be a plain object with non-reserved field names"); - } - if (node.getPreviousBlueId() != null || node.getPosition() != null) { - throw new BexException(label + " contains a Blue list-control key; use non-reserved names"); - } - if (node.getContracts() != null) { - throw new BexException(label + " contains reserved Blue key: contracts"); - } - if (node.getName() != null - || node.getDescription() != null - || node.getType() != null - || node.getItemType() != null - || node.getKeyType() != null - || node.getValueType() != null - || node.getBlue() != null - || node.getSchema() != null - || node.getMergePolicy() != null) { - throw new BexException(label + " contains a Blue language key; use non-reserved names"); - } - if (node.getProperties() != null) { - for (String key : node.getProperties().keySet()) { - if (RESERVED_BLUE_KEYS.contains(key)) { - throw new BexException(label + " contains reserved Blue key: " + key); - } - } - } - } - - private void rejectBexAnywhereInStaticPattern(FrozenNode pattern, String pointer) { - if (pattern != null && containsCache.containsBex(pattern, metrics)) { - throw new BexException("BEX expressions inside static Blue patterns are not supported at " + pointer); - } - rejectBexInStaticBlueDefinitionFields(pattern, pointer); - } - - private void rejectBexInStaticBlueDefinitionFields(FrozenNode node, String pointer) { - if (node == null) { - return; - } - rejectBexInStaticField(node.getType(), pointer + "/type", "type"); - rejectBexInStaticField(node.getItemType(), pointer + "/itemType", "itemType"); - rejectBexInStaticField(node.getKeyType(), pointer + "/keyType", "keyType"); - rejectBexInStaticField(node.getValueType(), pointer + "/valueType", "valueType"); - rejectBexInStaticField(node.getBlue(), pointer + "/blue", "blue"); - rejectBexInStaticField(node.getContracts(), pointer + "/contracts", "contracts"); - if (node.getSchema() != null) { - rejectBexInSchema(node.getSchema(), pointer + "/schema"); - } - if (node.getItems() != null) { - for (int i = 0; i < node.getItems().size(); i++) { - rejectBexInStaticBlueDefinitionFields(node.getItems().get(i), pointer + "/" + i); - } - } - if (node.getProperties() != null) { - for (Map.Entry entry : node.getProperties().entrySet()) { - rejectBexInStaticBlueDefinitionFields(entry.getValue(), pointer + "/" + escape(entry.getKey())); - } - } - } - - private void rejectBexInStaticField(FrozenNode field, String pointer, String fieldName) { - if (field != null && containsCache.containsBex(field, metrics)) { - throw new BexException("BEX expressions inside Blue " + fieldName - + " fields are not supported at " + pointer); - } - rejectBexInStaticBlueDefinitionFields(field, pointer); - } - - private void rejectBexInSchema(Schema schema, String pointer) { - rejectSchemaNode(schema.getRequired(), pointer); - rejectSchemaNode(schema.getMinLength(), pointer); - rejectSchemaNode(schema.getMaxLength(), pointer); - rejectSchemaNode(schema.getMinimum(), pointer); - rejectSchemaNode(schema.getMaximum(), pointer); - rejectSchemaNode(schema.getExclusiveMinimum(), pointer); - rejectSchemaNode(schema.getExclusiveMaximum(), pointer); - rejectSchemaNode(schema.getMultipleOf(), pointer); - rejectSchemaNode(schema.getMinItems(), pointer); - rejectSchemaNode(schema.getMaxItems(), pointer); - rejectSchemaNode(schema.getUniqueItems(), pointer); - rejectSchemaNode(schema.getMinFields(), pointer); - rejectSchemaNode(schema.getMaxFields(), pointer); - if (schema.getEnum() != null) { - for (Node node : schema.getEnum()) { - rejectSchemaNode(node, pointer); - } - } - } - - private void rejectSchemaNode(Node node, String pointer) { - if (node == null) { - return; - } - FrozenNode frozen = FrozenNode.fromResolvedNode(node); - if (containsCache.containsBex(frozen, metrics)) { - throw new BexException("BEX expressions inside schema are not supported at " + pointer); - } - rejectBexInStaticBlueDefinitionFields(frozen, pointer); - } - - private static Set reservedBlueKeys() { - Set keys = new LinkedHashSet<>(); - Collections.addAll(keys, - "name", - "description", - "type", - "itemType", - "keyType", - "valueType", - "value", - "items", - "blueId", - "blue", - "schema", - "constraints", - "mergePolicy", - "properties", - "contracts", - "$previous", - "$pos", - "$replace", - "$empty"); - return Collections.unmodifiableSet(keys); - } - - private static final class FunctionSignature { - private final List args; - private final Map argsByName; - - private FunctionSignature(List args) { - this.args = args; - Map byName = new LinkedHashMap<>(); - for (BexCompiledProgram.ArgSpec arg : args) { - byName.put(arg.name(), arg); - } - this.argsByName = Collections.unmodifiableMap(byName); - } - - private List args() { - return args; - } - - private BexCompiledProgram.ArgSpec arg(String name) { - return argsByName.get(name); - } - } - - private enum VisitState { - UNVISITED, - VISITING, - VISITED - } - - private static final class CallSite { - private final String target; - private final BexSourcePath sourcePath; - - private CallSite(String target, BexSourcePath sourcePath) { - this.target = target; - this.sourcePath = sourcePath; - } - } - -} diff --git a/src/main/java/blue/bex/compile/BexExpressions.java b/src/main/java/blue/bex/compile/BexExpressions.java deleted file mode 100644 index 79d8fed..0000000 --- a/src/main/java/blue/bex/compile/BexExpressions.java +++ /dev/null @@ -1,1676 +0,0 @@ -package blue.bex.compile; - -import blue.bex.BexException; -import blue.bex.BexSourcePath; -import blue.bex.gas.BexGasCounter; -import blue.bex.runtime.CompiledExpression; -import blue.bex.runtime.CompiledFrame; -import blue.bex.value.BexValue; -import blue.bex.value.BexUnicodeOrder; -import blue.bex.value.BexValues; - -import java.math.BigDecimal; -import java.math.BigInteger; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -final class BexGasWork { - private static final int TEXT_BLOCK_CODE_POINTS = 64; - private static final BigInteger TEN = BigInteger.TEN; - - private BexGasWork() { - } - - static void charge(CompiledFrame frame, BexGasCounter counter) { - charge(frame, counter, 1L); - } - - static void charge(CompiledFrame frame, BexGasCounter counter, long quantity) { - if (quantity <= 0L) { - return; - } - BexSourcePath source = frame.sourcePath(); - frame.runtime().gas().charge(counter, - quantity, - source, - source != null ? source.operator() : null, - counter.canonicalName()); - } - - static long textBlocks(String text) { - return textBlocksForCodePoints(textCodePoints(text)); - } - - static long textCodePoints(String text) { - return text == null || text.isEmpty() - ? 0L - : text.codePointCount(0, text.length()); - } - - static long textBlocksForCodePoints(long codePoints) { - if (codePoints < 0L) { - throw new IllegalArgumentException( - "Text code-point count must be non-negative"); - } - return codePoints == 0L - ? 0L - : 1L + (codePoints - 1L) / TEXT_BLOCK_CODE_POINTS; - } - - /** - * Converts one scalar to canonical text while admitting every full-scan - * block before the conversion or scan which consumes that block. - */ - static MeteredText fullText( - CompiledFrame frame, - BexValue value) { - return fullText(frame, value, false); - } - - /** - * `$text` additionally constructs a new BEX Text value. Each output - * block's examination and construction units are admitted before the - * scalar formatter computes that block. - */ - static MeteredText constructedText( - CompiledFrame frame, - BexValue value) { - return fullText(frame, value, true); - } - - private static MeteredText fullText( - CompiledFrame frame, - BexValue value, - boolean constructed) { - BexValue exactValue = - value != null ? value : BexValues.undefined(); - if (exactValue.isUndefined() || exactValue.isNull()) { - return new MeteredText("", 0L, 0L); - } - if (!exactValue.isScalar()) { - throw new BexException("Value cannot be converted to text"); - } - - Object raw = exactValue.toSimple(); - if (raw instanceof String) { - TextScan scan = chargeTextScan( - frame, - BexGasCounter.TEXT_BLOCK_EXAMINED, - (String) raw, - 0, - ((String) raw).length()); - if (constructed) { - charge( - frame, - BexGasCounter.TEXT_BLOCK_CONSTRUCTED, - textBlocksForCodePoints(scan.codePoints)); - } - return new MeteredText( - (String) raw, - scan.codePoints, - scan.pointerEscapeExpansions); - } - - ScalarTextCursor cursor = scalarTextCursor(raw); - StringBuilder text = null; - long codePoints = 0L; - while (cursor.hasNext()) { - charge( - frame, - BexGasCounter.TEXT_BLOCK_EXAMINED); - if (constructed) { - charge( - frame, - BexGasCounter.TEXT_BLOCK_CONSTRUCTED); - } - String block = cursor.nextBlock( - TEXT_BLOCK_CODE_POINTS); - if (block.isEmpty()) { - throw new BexException( - "Scalar text conversion made no progress"); - } - if (text == null) { - text = new StringBuilder(); - } - text.append(block); - codePoints += block.codePointCount( - 0, block.length()); - } - return new MeteredText( - text != null ? text.toString() : "", - codePoints, - 0L); - } - - /** - * Charges one construction unit before scanning each output block and - * creates the requested substring only after its final unit is admitted. - */ - static String constructSubstring( - CompiledFrame frame, - String text, - int start, - int end) { - chargeSubstringConstruction( - frame, text, start, end); - return text.substring(start, end); - } - - static void chargeSubstringConstruction( - CompiledFrame frame, - String text, - int start, - int end) { - chargeTextScan( - frame, - BexGasCounter.TEXT_BLOCK_CONSTRUCTED, - text, - start, - end); - } - - static void chargeTextConstruction( - CompiledFrame frame, - String text) { - chargeSubstringConstruction( - frame, text, 0, text.length()); - } - - /** - * Canonical Unicode code-point comparison with block-pair admission before - * each compared block is read. - */ - static int compareText( - CompiledFrame frame, - String left, - String right) { - int leftOffset = 0; - int rightOffset = 0; - while (leftOffset < left.length() && rightOffset < right.length()) { - charge( - frame, - BexGasCounter.TEXT_BLOCK_EXAMINED, - 2L); - int inBlock = 0; - while (inBlock < TEXT_BLOCK_CODE_POINTS - && leftOffset < left.length() - && rightOffset < right.length()) { - int leftCodePoint = codePointAt( - left, leftOffset, left.length()); - int rightCodePoint = codePointAt( - right, rightOffset, right.length()); - leftOffset += charCountAt( - left, leftOffset, left.length()); - rightOffset += charCountAt( - right, rightOffset, right.length()); - if (leftCodePoint != rightCodePoint) { - return Integer.compare( - leftCodePoint, rightCodePoint); - } - inBlock++; - } - } - return Integer.compare( - left.length() - leftOffset, - right.length() - rightOffset); - } - - /** - * Scalar Text equality with each pair of source blocks admitted before it - * is read. This deliberately avoids eagerly materializing formatted - * numeric scalars, even though equality currently calls it only for Text. - */ - static boolean equalText( - CompiledFrame frame, - BexValue left, - BexValue right) { - ScalarTextCursor leftCursor = - scalarTextCursor(left); - ScalarTextCursor rightCursor = - scalarTextCursor(right); - while (leftCursor.hasNext() - && rightCursor.hasNext()) { - charge( - frame, - BexGasCounter.TEXT_BLOCK_EXAMINED, - 2L); - String leftBlock = leftCursor.nextBlock( - TEXT_BLOCK_CODE_POINTS); - String rightBlock = rightCursor.nextBlock( - TEXT_BLOCK_CODE_POINTS); - if (!leftBlock.equals(rightBlock)) { - return false; - } - } - return !leftCursor.hasNext() - && !rightCursor.hasNext(); - } - - /** - * Lazily converts and compares prefix blocks. Non-Text scalar formatting - * is deferred until after the exact block-pair charge which consumes it. - */ - static PrefixResult comparePrefix( - CompiledFrame frame, - BexValue text, - BexValue prefix) { - ScalarTextCursor textCursor = - scalarTextCursor(text); - ScalarTextCursor prefixCursor = - scalarTextCursor(prefix); - while (prefixCursor.hasNext()) { - if (!textCursor.hasNext()) { - return new PrefixResult( - false, textCursor); - } - charge( - frame, - BexGasCounter.TEXT_BLOCK_EXAMINED, - 2L); - String prefixBlock = prefixCursor.nextBlock( - TEXT_BLOCK_CODE_POINTS); - int comparedCodePoints = prefixBlock.codePointCount( - 0, prefixBlock.length()); - String textBlock = textCursor.nextBlock( - comparedCodePoints); - if (!textBlock.equals(prefixBlock)) { - return new PrefixResult( - false, textCursor); - } - } - return new PrefixResult( - true, textCursor); - } - - static long integerLimbs(BigInteger value) { - if (value == null) { - return 1L; - } - int bits = value.abs().bitLength(); - return Math.max(1L, (bits + 31L) / 32L); - } - - /** - * Exact Integer conversion with admission before parsing or decimal - * conversion work. - */ - static BigInteger convertInteger( - CompiledFrame frame, - BexValue value) { - Object raw = numericScalar(value, "integer"); - if (raw instanceof BigInteger) { - BigInteger integer = (BigInteger) raw; - charge( - frame, - BexGasCounter.INTEGER_LIMB_OPERATION, - integerLimbs(integer)); - return integer; - } - if (isIntegralPrimitive(raw)) { - long primitive = ((Number) raw).longValue(); - long limbs = primitive == Long.MIN_VALUE - || primitive > 0xFFFFFFFFL - || primitive < -0xFFFFFFFFL - ? 2L - : 1L; - charge( - frame, - BexGasCounter.INTEGER_LIMB_OPERATION, - limbs); - return BigInteger.valueOf(primitive); - } - if (raw instanceof String) { - return parseIntegerText( - frame, (String) raw); - } - if (raw instanceof BigDecimal) { - return exactDecimalInteger( - frame, (BigDecimal) raw); - } - if (raw instanceof Float || raw instanceof Double) { - charge( - frame, - BexGasCounter.INTEGER_LIMB_OPERATION); - BigDecimal decimal; - try { - decimal = BigDecimal.valueOf( - ((Number) raw).doubleValue()); - } catch (NumberFormatException ex) { - throw new BexException( - "Value cannot be converted to integer"); - } - charge( - frame, - BexGasCounter.INTEGER_LIMB_OPERATION, - integerLimbs(decimal.unscaledValue())); - BigInteger integer; - try { - integer = decimal.toBigIntegerExact(); - } catch (ArithmeticException ex) { - throw new BexException( - "Value cannot be converted to integer"); - } - return integer; - } - throw new BexException( - "Value cannot be converted to integer"); - } - - /** - * Reads an already-integer operand without adding a conversion charge, - * while routing Text and decimal coercion through the metered conversion - * path before arithmetic begins. - */ - static BigInteger integerOperand( - CompiledFrame frame, - BexValue value) { - Object raw = numericScalar(value, "integer"); - if (raw instanceof BigInteger) { - return (BigInteger) raw; - } - if (isIntegralPrimitive(raw)) { - return BigInteger.valueOf( - ((Number) raw).longValue()); - } - return convertInteger(frame, value); - } - - /** - * Decimal conversion with one scale-alignment unit and every unscaled - * magnitude limb admitted before parsing or allocation. - */ - static BigDecimal convertNumber( - CompiledFrame frame, - BexValue value) { - Object raw = numericScalar(value, "number"); - if (raw instanceof String) { - charge( - frame, - BexGasCounter.INTEGER_LIMB_OPERATION, - 2L); - return parseDecimalText( - frame, (String) raw, 1L); - } - if (raw instanceof BigDecimal) { - BigDecimal decimal = (BigDecimal) raw; - charge( - frame, - BexGasCounter.INTEGER_LIMB_OPERATION, - integerLimbs(decimal.unscaledValue()) + 1L); - return decimal; - } - if (raw instanceof BigInteger) { - BigInteger integer = (BigInteger) raw; - charge( - frame, - BexGasCounter.INTEGER_LIMB_OPERATION, - integerLimbs(integer) + 1L); - return new BigDecimal(integer); - } - if (isIntegralPrimitive(raw)) { - BigInteger integer = BigInteger.valueOf( - ((Number) raw).longValue()); - charge( - frame, - BexGasCounter.INTEGER_LIMB_OPERATION, - integerLimbs(integer) + 1L); - return new BigDecimal(integer); - } - if (raw instanceof Float || raw instanceof Double) { - charge( - frame, - BexGasCounter.INTEGER_LIMB_OPERATION, - 2L); - BigDecimal decimal; - try { - decimal = BigDecimal.valueOf( - ((Number) raw).doubleValue()); - } catch (NumberFormatException ex) { - throw new BexException( - "Value cannot be converted to number"); - } - charge( - frame, - BexGasCounter.INTEGER_LIMB_OPERATION, - integerLimbs(decimal.unscaledValue()) - 1L); - return decimal; - } - throw new BexException( - "Value cannot be converted to number"); - } - - /** - * Numeric equality/ordering conversion and comparison. The guaranteed - * first limb of each operand (plus the canonical decimal alignment unit) - * is admitted before any Text parse or Integer-to-decimal allocation. - */ - static int compareNumbers( - CompiledFrame frame, - BexValue left, - BexValue right, - boolean decimalAlignment) { - Object leftRaw = numericScalar(left, "number"); - Object rightRaw = numericScalar(right, "number"); - long base = 2L; - if (decimalAlignment - && (isDecimalRaw(leftRaw) - || isDecimalRaw(rightRaw))) { - base++; - } - charge( - frame, - BexGasCounter.INTEGER_LIMB_OPERATION, - base); - BigDecimal leftNumber = comparisonNumber( - frame, leftRaw); - BigDecimal rightNumber = comparisonNumber( - frame, rightRaw); - return leftNumber.compareTo(rightNumber); - } - - private static BigDecimal comparisonNumber( - CompiledFrame frame, - Object raw) { - if (raw instanceof BigDecimal) { - BigDecimal decimal = (BigDecimal) raw; - charge( - frame, - BexGasCounter.INTEGER_LIMB_OPERATION, - integerLimbs(decimal.unscaledValue()) - 1L); - return decimal; - } - if (raw instanceof BigInteger) { - BigInteger integer = (BigInteger) raw; - charge( - frame, - BexGasCounter.INTEGER_LIMB_OPERATION, - integerLimbs(integer) - 1L); - return new BigDecimal(integer); - } - if (isIntegralPrimitive(raw)) { - BigInteger integer = BigInteger.valueOf( - ((Number) raw).longValue()); - charge( - frame, - BexGasCounter.INTEGER_LIMB_OPERATION, - integerLimbs(integer) - 1L); - return new BigDecimal(integer); - } - if (raw instanceof String) { - return parseDecimalText( - frame, (String) raw, 1L); - } - if (raw instanceof Float || raw instanceof Double) { - BigDecimal decimal; - try { - decimal = BigDecimal.valueOf( - ((Number) raw).doubleValue()); - } catch (NumberFormatException ex) { - throw new BexException( - "Value cannot be converted to number"); - } - charge( - frame, - BexGasCounter.INTEGER_LIMB_OPERATION, - integerLimbs(decimal.unscaledValue()) - 1L); - return decimal; - } - throw new BexException( - "Value cannot be converted to number"); - } - - private static BigInteger parseIntegerText( - CompiledFrame frame, - String text) { - charge( - frame, - BexGasCounter.INTEGER_LIMB_OPERATION); - if (text == null || text.isEmpty()) { - throw new BexException( - "Value cannot be converted to integer"); - } - int offset = 0; - boolean negative = false; - if (text.charAt(0) == '-') { - negative = true; - offset = 1; - } - if (offset == text.length()) { - throw new BexException( - "Value cannot be converted to integer"); - } - - IncrementalMagnitude magnitude = - new IncrementalMagnitude(frame, 1L); - while (offset < text.length()) { - char character = text.charAt(offset++); - if (character < '0' || character > '9') { - throw new BexException( - "Value cannot be converted to integer"); - } - magnitude.append(character - '0'); - } - BigInteger integer = magnitude.value(); - return negative ? integer.negate() : integer; - } - - private static BigInteger exactDecimalInteger( - CompiledFrame frame, - BigDecimal decimal) { - int scale = decimal.scale(); - BigInteger unscaled = decimal.unscaledValue(); - if (scale <= 0) { - long admittedMagnitudeLimbs = - integerLimbs(unscaled); - charge( - frame, - BexGasCounter.INTEGER_LIMB_OPERATION, - admittedMagnitudeLimbs + 1L); - boolean negative = unscaled.signum() < 0; - IncrementalMagnitude magnitude = - new IncrementalMagnitude( - frame, - unscaled.abs(), - admittedMagnitudeLimbs); - for (long remaining = -(long) scale; - remaining > 0L; - remaining--) { - magnitude.append(0); - } - BigInteger integer = magnitude.value(); - return negative ? integer.negate() : integer; - } - - /* - * Exact scale reduction must inspect the unscaled magnitude. Admit - * every such limb before BigDecimal performs that work; charging only - * the eventual result limbs would allow a large fractional magnitude - * to be inspected before a later admission. - */ - charge( - frame, - BexGasCounter.INTEGER_LIMB_OPERATION, - integerLimbs(unscaled) + 1L); - BigInteger integer; - try { - integer = decimal.toBigIntegerExact(); - } catch (ArithmeticException ex) { - throw new BexException( - "Value cannot be converted to integer"); - } - return integer; - } - - private static BigDecimal parseDecimalText( - CompiledFrame frame, - String text, - long admittedLimbs) { - if (text == null || text.isEmpty()) { - throw new BexException( - "Value cannot be converted to number"); - } - int offset = 0; - boolean negative = false; - char first = text.charAt(offset); - if (first == '+' || first == '-') { - negative = first == '-'; - offset++; - } - - IncrementalMagnitude magnitude = - new IncrementalMagnitude( - frame, admittedLimbs); - boolean decimalPoint = false; - boolean digitSeen = false; - long fractionalDigits = 0L; - while (offset < text.length()) { - char character = text.charAt(offset); - if (character == 'e' || character == 'E') { - break; - } - if (character == '.') { - if (decimalPoint) { - throw new BexException( - "Value cannot be converted to number"); - } - decimalPoint = true; - offset++; - continue; - } - int digit = Character.digit(character, 10); - if (digit < 0) { - throw new BexException( - "Value cannot be converted to number"); - } - digitSeen = true; - magnitude.append(digit); - if (decimalPoint) { - fractionalDigits++; - } - offset++; - } - if (!digitSeen) { - throw new BexException( - "Value cannot be converted to number"); - } - - long exponent = 0L; - if (offset < text.length()) { - offset++; - boolean exponentNegative = false; - if (offset < text.length() - && (text.charAt(offset) == '+' - || text.charAt(offset) == '-')) { - exponentNegative = text.charAt(offset) == '-'; - offset++; - } - if (offset == text.length()) { - throw new BexException( - "Value cannot be converted to number"); - } - while (offset < text.length()) { - int digit = Character.digit( - text.charAt(offset++), 10); - if (digit < 0 - || exponent > (Long.MAX_VALUE - digit) / 10L) { - throw new BexException( - "Value cannot be converted to number"); - } - exponent = exponent * 10L + digit; - } - if (exponentNegative) { - exponent = -exponent; - } - } - - long scale; - try { - scale = Math.subtractExact( - fractionalDigits, exponent); - } catch (ArithmeticException overflow) { - throw new BexException( - "Value cannot be converted to number"); - } - if (scale < Integer.MIN_VALUE - || scale > Integer.MAX_VALUE) { - throw new BexException( - "Value cannot be converted to number"); - } - BigInteger unscaled = magnitude.value(); - if (negative) { - unscaled = unscaled.negate(); - } - return new BigDecimal(unscaled, (int) scale); - } - - private static Object numericScalar( - BexValue value, - String target) { - BexValue exactValue = - value != null ? value : BexValues.undefined(); - if (!exactValue.isScalar()) { - throw new BexException( - "Value cannot be converted to " + target); - } - return exactValue.toSimple(); - } - - private static ScalarTextCursor scalarTextCursor( - BexValue value) { - BexValue exactValue = - value != null ? value : BexValues.undefined(); - if (exactValue.isUndefined() || exactValue.isNull()) { - return new RawStringCursor(""); - } - if (!exactValue.isScalar()) { - throw new BexException( - "Value cannot be converted to text"); - } - return scalarTextCursor(exactValue.toSimple()); - } - - private static ScalarTextCursor scalarTextCursor( - Object raw) { - if (raw instanceof String) { - return new RawStringCursor((String) raw); - } - if (raw instanceof BigInteger) { - return new IntegerTextCursor( - (BigInteger) raw); - } - if (raw instanceof BigDecimal) { - return new DecimalTextCursor( - (BigDecimal) raw); - } - if (isIntegralPrimitive(raw) - || raw instanceof Float - || raw instanceof Double - || raw instanceof Boolean) { - return new FixedScalarTextCursor(raw); - } - throw new BexException( - "Value cannot be converted to text"); - } - - private static boolean isIntegralPrimitive(Object raw) { - return raw instanceof Byte - || raw instanceof Short - || raw instanceof Integer - || raw instanceof Long; - } - - private static boolean isDecimalRaw(Object raw) { - return raw instanceof BigDecimal - || raw instanceof Float - || raw instanceof Double; - } - - private static int decimalDigits(BigInteger magnitude) { - if (magnitude.signum() == 0) { - return 1; - } - int bitLength = magnitude.bitLength(); - int estimate = Math.max( - 1, - (int) Math.floor( - (bitLength - 1) - * 0.3010299956639812d) - + 1); - BigInteger lower = TEN.pow(estimate - 1); - while (magnitude.compareTo(lower) < 0) { - estimate--; - lower = lower.divide(TEN); - } - BigInteger upper = lower.multiply(TEN); - while (magnitude.compareTo(upper) >= 0) { - estimate++; - lower = upper; - upper = upper.multiply(TEN); - } - return estimate; - } - - private static int decimalDigits(long value) { - long remaining = value < 0L ? -value : value; - int digits = 1; - while (remaining >= 10L) { - remaining /= 10L; - digits++; - } - return digits; - } - - private static TextScan chargeTextScan( - CompiledFrame frame, - BexGasCounter counter, - String text, - int start, - int end) { - int offset = start; - long codePoints = 0L; - long pointerEscapeExpansions = 0L; - while (offset < end) { - charge(frame, counter); - int inBlock = 0; - while (inBlock < TEXT_BLOCK_CODE_POINTS - && offset < end) { - char character = text.charAt(offset); - if (character == '~' || character == '/') { - pointerEscapeExpansions++; - } - offset += charCountAt(text, offset, end); - codePoints++; - inBlock++; - } - } - return new TextScan( - codePoints, - pointerEscapeExpansions); - } - - private static int codePointAt( - String text, - int offset, - int end) { - char first = text.charAt(offset); - if (Character.isHighSurrogate(first) - && offset + 1 < end) { - char second = text.charAt(offset + 1); - if (Character.isLowSurrogate(second)) { - return Character.toCodePoint( - first, second); - } - } - return first; - } - - private static int charCountAt( - String text, - int offset, - int end) { - char first = text.charAt(offset); - return Character.isHighSurrogate(first) - && offset + 1 < end - && Character.isLowSurrogate( - text.charAt(offset + 1)) - ? 2 - : 1; - } - - static final class MeteredText { - private final String text; - private final long codePoints; - private final long pointerEscapeExpansions; - - private MeteredText( - String text, - long codePoints, - long pointerEscapeExpansions) { - this.text = text; - this.codePoints = codePoints; - this.pointerEscapeExpansions = - pointerEscapeExpansions; - } - - String text() { - return text; - } - - long codePoints() { - return codePoints; - } - - long pointerEscapeExpansions() { - return pointerEscapeExpansions; - } - } - - private static final class TextScan { - private final long codePoints; - private final long pointerEscapeExpansions; - - private TextScan( - long codePoints, - long pointerEscapeExpansions) { - this.codePoints = codePoints; - this.pointerEscapeExpansions = - pointerEscapeExpansions; - } - } - - static final class PrefixResult { - private final boolean matched; - private final ScalarTextCursor text; - - private PrefixResult( - boolean matched, - ScalarTextCursor text) { - this.matched = matched; - this.text = text; - } - - boolean matched() { - return matched; - } - - String constructSuffix( - CompiledFrame frame) { - if (!matched) { - return ""; - } - StringBuilder suffix = null; - while (text.hasNext()) { - charge( - frame, - BexGasCounter.TEXT_BLOCK_CONSTRUCTED); - String block = text.nextBlock( - TEXT_BLOCK_CODE_POINTS); - if (suffix == null) { - suffix = new StringBuilder(); - } - suffix.append(block); - } - return suffix != null - ? suffix.toString() - : ""; - } - } - - private interface ScalarTextCursor { - boolean hasNext(); - - String nextBlock(int maxCodePoints); - } - - private static final class RawStringCursor - implements ScalarTextCursor { - private final String text; - private int offset; - - private RawStringCursor(String text) { - this.text = text; - } - - @Override - public boolean hasNext() { - return offset < text.length(); - } - - @Override - public String nextBlock(int maxCodePoints) { - int start = offset; - int consumed = 0; - while (consumed < maxCodePoints - && offset < text.length()) { - offset += charCountAt( - text, offset, text.length()); - consumed++; - } - return text.substring(start, offset); - } - } - - private static final class FixedScalarTextCursor - implements ScalarTextCursor { - private final Object raw; - private String text; - private int offset; - - private FixedScalarTextCursor(Object raw) { - this.raw = raw; - } - - @Override - public boolean hasNext() { - return text == null || offset < text.length(); - } - - @Override - public String nextBlock(int maxCodePoints) { - if (text == null) { - text = String.valueOf(raw); - } - int end = Math.min( - text.length(), - offset + maxCodePoints); - String block = text.substring(offset, end); - offset = end; - return block; - } - } - - private static final class IntegerTextCursor - implements ScalarTextCursor { - private final boolean negative; - private final DecimalDigitsCursor digits; - private boolean signPending; - - private IntegerTextCursor(BigInteger integer) { - this.negative = integer.signum() < 0; - this.signPending = negative; - this.digits = new DecimalDigitsCursor(integer); - } - - @Override - public boolean hasNext() { - return signPending || digits.hasNext(); - } - - @Override - public String nextBlock(int maxCodePoints) { - StringBuilder block = - new StringBuilder(maxCodePoints); - if (signPending && block.length() < maxCodePoints) { - block.append('-'); - signPending = false; - } - if (block.length() < maxCodePoints - && digits.hasNext()) { - block.append(digits.nextDigits( - maxCodePoints - block.length())); - } - return block.toString(); - } - } - - private static final class DecimalTextCursor - implements ScalarTextCursor { - private static final int LITERAL = 0; - private static final int DIGITS = 1; - private static final int ZEROS = 2; - private static final int EXPONENT = 3; - - private final BigDecimal decimal; - private final DecimalDigitsCursor digits; - private List parts; - private int partIndex; - - private DecimalTextCursor(BigDecimal decimal) { - this.decimal = decimal; - this.digits = new DecimalDigitsCursor( - decimal.unscaledValue()); - } - - @Override - public boolean hasNext() { - if (parts == null) { - return true; - } - skipEmptyParts(); - return partIndex < parts.size(); - } - - @Override - public String nextBlock(int maxCodePoints) { - if (parts == null) { - initialize(); - } - StringBuilder block = - new StringBuilder(maxCodePoints); - while (block.length() < maxCodePoints) { - skipEmptyParts(); - if (partIndex >= parts.size()) { - break; - } - TextPart part = parts.get(partIndex); - int capacity = - maxCodePoints - block.length(); - if (part.kind == DIGITS) { - int take = (int) Math.min( - part.remaining, - (long) capacity); - block.append( - digits.nextDigits(take)); - part.remaining -= take; - } else if (part.kind == ZEROS) { - int take = (int) Math.min( - part.remaining, - (long) capacity); - for (int index = 0; - index < take; - index++) { - block.append('0'); - } - part.remaining -= take; - } else { - if (part.text == null) { - part.text = part.kind == EXPONENT - ? Long.toString(part.exponent) - : ""; - } - int take = Math.min( - capacity, - part.text.length() - - part.textOffset); - block.append( - part.text, - part.textOffset, - part.textOffset + take); - part.textOffset += take; - part.remaining -= take; - } - } - return block.toString(); - } - - private void initialize() { - parts = new ArrayList<>(); - if (decimal.signum() < 0) { - parts.add(TextPart.literal("-")); - } - long precision = digits.digitCount(); - long scale = decimal.scale(); - long adjustedExponent = - -scale + precision - 1L; - if (scale >= 0L - && adjustedExponent >= -6L) { - if (scale == 0L) { - parts.add(TextPart.digits( - precision)); - } else if (scale < precision) { - parts.add(TextPart.digits( - precision - scale)); - parts.add(TextPart.literal(".")); - parts.add(TextPart.digits(scale)); - } else { - parts.add(TextPart.literal("0.")); - parts.add(TextPart.zeros( - scale - precision)); - parts.add(TextPart.digits( - precision)); - } - return; - } - - parts.add(TextPart.digits(1L)); - if (precision > 1L) { - parts.add(TextPart.literal(".")); - parts.add(TextPart.digits( - precision - 1L)); - } - parts.add(TextPart.literal( - adjustedExponent >= 0L - ? "E+" - : "E-")); - parts.add(TextPart.exponent( - Math.abs(adjustedExponent))); - } - - private void skipEmptyParts() { - while (partIndex < parts.size() - && parts.get(partIndex).remaining == 0L) { - partIndex++; - } - } - } - - private static final class DecimalDigitsCursor { - private final BigInteger signed; - private BigInteger magnitude; - private BigInteger emittedPrefix = BigInteger.ZERO; - private int totalDigits; - private int emittedDigits; - private boolean initialized; - - private DecimalDigitsCursor(BigInteger signed) { - this.signed = signed; - } - - private boolean hasNext() { - return !initialized || emittedDigits < totalDigits; - } - - private int digitCount() { - initialize(); - return totalDigits; - } - - private String nextDigits(int maximum) { - if (maximum <= 0) { - throw new IllegalArgumentException( - "Decimal digit block must be positive"); - } - initialize(); - int take = Math.min( - maximum, totalDigits - emittedDigits); - int after = totalDigits - - emittedDigits - - take; - - /* - * Extract only the prefix ending at this admitted output block. - * In particular, do not use divideAndRemainder here: its remainder - * materializes every lower digit even though a later text-block - * charge may still reject before those digits are examined. - * - * Recomputing the progressively longer quotient from the original - * magnitude is deliberate. Work for a lower block begins only - * when nextDigits is called for that block, after its caller has - * admitted the corresponding text counter. - */ - BigInteger divisor = TEN.pow(after); - BigInteger prefixThroughBlock = - magnitude.divide(divisor); - BigInteger blockMagnitude = - prefixThroughBlock.subtract( - emittedPrefix.multiply( - TEN.pow(take))); - String digits = blockMagnitude.toString(); - emittedPrefix = prefixThroughBlock; - emittedDigits += take; - if (digits.length() == take) { - return digits; - } - StringBuilder padded = - new StringBuilder(take); - for (int index = digits.length(); - index < take; - index++) { - padded.append('0'); - } - padded.append(digits); - return padded.toString(); - } - - private void initialize() { - if (initialized) { - return; - } - magnitude = signed.signum() < 0 - ? signed.negate() - : signed; - totalDigits = decimalDigits( - magnitude); - initialized = true; - } - } - - private static final class TextPart { - private final int kind; - private long remaining; - private String text; - private int textOffset; - private long exponent; - - private TextPart( - int kind, - long remaining, - String text, - long exponent) { - this.kind = kind; - this.remaining = remaining; - this.text = text; - this.exponent = exponent; - } - - private static TextPart literal(String value) { - return new TextPart( - DecimalTextCursor.LITERAL, - value.length(), - value, - 0L); - } - - private static TextPart digits(long count) { - return new TextPart( - DecimalTextCursor.DIGITS, - count, - null, - 0L); - } - - private static TextPart zeros(long count) { - return new TextPart( - DecimalTextCursor.ZEROS, - count, - null, - 0L); - } - - private static TextPart exponent(long value) { - return new TextPart( - DecimalTextCursor.EXPONENT, - decimalDigits(value), - null, - value); - } - } - - private static final class IncrementalMagnitude { - private final CompiledFrame frame; - private BigInteger value; - private long admittedLimbs; - - private IncrementalMagnitude( - CompiledFrame frame, - long admittedLimbs) { - this(frame, BigInteger.ZERO, admittedLimbs); - } - - private IncrementalMagnitude( - CompiledFrame frame, - BigInteger value, - long admittedLimbs) { - this.frame = frame; - this.value = value; - this.admittedLimbs = admittedLimbs; - } - - private void append(int digit) { - if (wouldGrow(digit)) { - charge( - frame, - BexGasCounter.INTEGER_LIMB_OPERATION); - admittedLimbs++; - } - value = value.multiply(TEN) - .add(BigInteger.valueOf(digit)); - } - - private boolean wouldGrow(int digit) { - long admittedBits = admittedLimbs * 32L; - if (admittedBits > Integer.MAX_VALUE) { - throw new BexException( - "Integer magnitude exceeds supported range"); - } - if ((long) value.bitLength() + 4L - <= admittedBits) { - return false; - } - BigInteger boundary = BigInteger.ONE.shiftLeft( - (int) admittedBits); - BigInteger largestWithoutGrowth = - boundary.subtract( - BigInteger.ONE - .add(BigInteger.valueOf(digit))) - .divide(TEN); - return value.compareTo( - largestWithoutGrowth) > 0; - } - - private BigInteger value() { - return value; - } - } -} - -abstract class Expr implements CompiledExpression { - @Override - public final BexValue eval(CompiledFrame frame) { - BexGasWork.charge(frame, BexGasCounter.EXPRESSION_EVALUATED); - frame.runtime().metrics().incrementExpressionEvaluations(); - try { - return doEval(frame); - } catch (BexException ex) { - BexSourcePath sourcePath = frame.sourcePath(); - if (sourcePath != null && !ex.sourcePath().isPresent()) { - throw ex.withSourcePath(sourcePath); - } - throw ex; - } - } - - protected abstract BexValue doEval(CompiledFrame frame); -} - -final class SourceExpr implements CompiledExpression { - private final BexSourcePath sourcePath; - private final CompiledExpression delegate; - - SourceExpr(BexSourcePath sourcePath, CompiledExpression delegate) { - this.sourcePath = sourcePath; - this.delegate = delegate; - } - - @Override - public BexValue eval(CompiledFrame frame) { - BexSourcePath previous = frame.enter(sourcePath); - try { - return delegate.eval(frame); - } catch (BexException ex) { - if (!ex.sourcePath().isPresent()) { - throw ex.withSourcePath(sourcePath); - } - throw ex; - } finally { - frame.restore(previous); - } - } -} - -final class LiteralExpr extends Expr { - private final BexValue value; - - LiteralExpr(BexValue value) { - this.value = value; - } - - @Override - protected BexValue doEval(CompiledFrame frame) { - return value; - } -} - -final class TransientLiteralExpr extends Expr { - private final Object scalar; - - TransientLiteralExpr(Object scalar) { - this.scalar = scalar; - } - - @Override - protected BexValue doEval(CompiledFrame frame) { - if (scalar instanceof String) { - BexGasWork.chargeTextConstruction( - frame, (String) scalar); - } - return BexValues.scalar(scalar); - } -} - -final class DocumentExpr extends Expr { - private final PointerOperand pointer; - private final boolean resolved; - - DocumentExpr(PointerOperand pointer, boolean resolved) { - this.pointer = pointer; - this.resolved = resolved; - } - - @Override - protected BexValue doEval(CompiledFrame frame) { - ResolvedPointer resolvedPointer = pointer.resolve(frame); - return frame.readDocument(resolvedPointer.absolute(), resolvedPointer.segments(), resolved); - } -} - -enum ContextKind { EVENT, PROCESSING_EVENT, CURRENT_CONTRACT } - -final class ContextPointerExpr extends Expr { - private final PointerOperand pointer; - private final ContextKind kind; - - ContextPointerExpr(PointerOperand pointer, ContextKind kind) { - this.pointer = pointer; - this.kind = kind; - } - - @Override - protected BexValue doEval(CompiledFrame frame) { - List segments = pointer.segments(frame); - if (kind == ContextKind.EVENT) { - return frame.readEvent(segments); - } - if (kind == ContextKind.PROCESSING_EVENT) { - return frame.readProcessingEvent(segments); - } - return frame.readCurrentContract(segments); - } -} - -final class StepsExpr extends Expr { - private final TextOperand step; - private final PointerOperand pointer; - - StepsExpr(TextOperand step, PointerOperand pointer) { - this.step = step; - this.pointer = pointer; - } - - @Override - protected BexValue doEval(CompiledFrame frame) { - String stepName = step.get(frame); - if (stepName.isEmpty()) { - throw new BexException("$steps.step cannot be empty"); - } - return frame.runtime().readSteps(stepName, pointer.segments(frame)); - } -} - -final class BindingExpr extends Expr { - private final TextOperand name; - private final PointerOperand pointer; - - BindingExpr(TextOperand name, PointerOperand pointer) { - this.name = name; - this.pointer = pointer; - } - - @Override - protected BexValue doEval(CompiledFrame frame) { - String bindingName = name.get(frame); - if (bindingName.isEmpty()) { - throw new BexException("$binding.name cannot be empty"); - } - return frame.readBinding(bindingName, pointer.segments(frame)); - } -} - -final class VarExpr extends Expr { - private final int slot; - private final PointerOperand pointer; - - VarExpr(int slot) { - this(slot, null); - } - - VarExpr(int slot, PointerOperand pointer) { - this.slot = slot; - this.pointer = pointer; - } - - @Override - protected BexValue doEval(CompiledFrame frame) { - BexGasWork.charge(frame, BexGasCounter.VARIABLE_READ); - BexValue value = frame.getRequired(slot); - return pointer != null - ? frame.runtime().readValuePointer(value, pointer.segments(frame)) - : value; - } -} - -final class ConstExpr extends Expr { - private final String name; - private final PointerOperand pointer; - - ConstExpr(String name) { - this(name, null); - } - - ConstExpr(String name, PointerOperand pointer) { - this.name = name; - this.pointer = pointer; - } - - @Override - protected BexValue doEval(CompiledFrame frame) { - BexGasWork.charge(frame, BexGasCounter.CONSTANT_READ); - BexValue value = frame.runtime().program().constant(name); - return pointer != null - ? frame.runtime().readValuePointer(value, pointer.segments(frame)) - : value; - } -} - -final class GetExpr extends Expr { - private final CompiledExpression object; - private final TextOperand key; - - GetExpr(CompiledExpression object, TextOperand key) { - this.object = object; - this.key = key; - } - - @Override - protected BexValue doEval(CompiledFrame frame) { - BexValue value = object.eval(frame); - String evaluatedKey = key.get(frame); - if (value.isObject()) { - BexGasWork.charge(frame, BexGasCounter.OBJECT_MEMBER_READ); - } - return value.get(evaluatedKey); - } -} - -final class ObjectExpr extends Expr { - private final Map fields; - - ObjectExpr(Map fields) { - this.fields = new LinkedHashMap<>(); - for (String key : BexUnicodeOrder.sortedCopy(fields.keySet())) { - this.fields.put(key, fields.get(key)); - } - } - - @Override - protected BexValue doEval(CompiledFrame frame) { - Map out = new LinkedHashMap<>(); - for (Map.Entry entry : fields.entrySet()) { - BexValue value = entry.getValue().eval(frame); - if (!value.isUndefined()) { - BexGasWork.charge(frame, BexGasCounter.TRANSIENT_OBJECT_MEMBER_PRODUCED); - out.put(entry.getKey(), value); - } - } - return BexValues.map(out); - } -} - -final class ListExpr extends Expr { - private final List items; - - ListExpr(List items) { - this.items = items; - } - - @Override - protected BexValue doEval(CompiledFrame frame) { - List out = new ArrayList<>(); - for (CompiledExpression item : items) { - BexValue value = item.eval(frame); - if (value.isUndefined()) { - throw new BexException( - "Undefined cannot appear in a Blue list"); - } - BexGasWork.charge(frame, BexGasCounter.TRANSIENT_LIST_ITEM_PRODUCED); - out.add(value); - } - return BexValues.list(out); - } -} - -final class IntrinsicExpr extends Expr { - private final String blueId; - private final BexValue type; - private final Map fields; - - IntrinsicExpr(String blueId, BexValue type, Map fields) { - this.blueId = blueId; - this.type = type; - this.fields = new LinkedHashMap<>(); - for (String key : BexUnicodeOrder.sortedCopy(fields.keySet())) { - this.fields.put(key, fields.get(key)); - } - } - - @Override - protected BexValue doEval(CompiledFrame frame) { - Map values = new LinkedHashMap<>(); - for (Map.Entry entry : fields.entrySet()) { - BexValue value = entry.getValue().eval(frame); - if (!value.isUndefined()) { - values.put(entry.getKey(), value); - } - } - BexGasWork.charge(frame, BexGasCounter.INTRINSIC_CALLED); - return frame.runtime().invokeIntrinsic(blueId, type, values); - } -} - -final class FailExpr extends Expr { - private final CompiledExpression message; - - FailExpr(CompiledExpression message) { - this.message = message; - } - - @Override - protected BexValue doEval(CompiledFrame frame) { - throw new BexException(message.eval(frame).asText()); - } -} - -final class NodeBlueIdExpr extends Expr { - private final CompiledExpression expression; - - NodeBlueIdExpr(CompiledExpression expression) { - this.expression = expression; - } - - @Override - protected BexValue doEval(CompiledFrame frame) { - return frame.runtime().nodeBlueId(expression.eval(frame)); - } -} diff --git a/src/main/java/blue/bex/gas/BexGasMeter.java b/src/main/java/blue/bex/gas/BexGasMeter.java deleted file mode 100644 index fc102c7..0000000 --- a/src/main/java/blue/bex/gas/BexGasMeter.java +++ /dev/null @@ -1,905 +0,0 @@ -package blue.bex.gas; - -import blue.bex.BexSourcePath; -import blue.language.processor.GasChargeContext; -import blue.language.processor.GasLimitExceededException; -import blue.language.processor.GasMeter; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.IdentityHashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.function.BiConsumer; -import java.util.function.Consumer; - -/** - * Deterministic, live-bounded BEX 2.0 named gas meter. - * - *

Every charge is checked and, when host-backed, admitted to the shared - * child ledger before the corresponding local trace entry is appended and - * before the caller performs its work. A charge which cannot fit is absent - * from both traces.

- */ -public final class BexGasMeter { - /** Sentinel accepted by local-limit constructors for no local sub-limit. */ - public static final long NO_LOCAL_LIMIT = -1L; - - private final BexGasSchedule schedule; - private final long parentRemainingGas; - private final long localLimit; - private final long effectiveBudget; - private final Map hostLedgers; - private final boolean qualifiedHostCounters; - private final boolean hostEnforcesLocalLimit; - private final Map registeredNamedWeights; - private final List trace = new ArrayList<>(); - private long totalGas; - private HostLedgerState hostLedgerState = HostLedgerState.OPEN; - - private enum HostLedgerState { - OPEN, - SUBMITTED, - FAILED, - UNAVAILABLE, - EXHAUSTED - } - - /** - * Creates a standalone meter bounded by the supplied parent budget. - */ - public BexGasMeter(BexGasSchedule schedule, long parentRemainingGas) { - this(schedule, - requireBudget(parentRemainingGas, "parentRemainingGas"), - NO_LOCAL_LIMIT, - Collections.emptyMap(), - false, - false, - Collections.emptyMap()); - } - - /** - * Creates a standalone meter whose local BEX limit can only reduce the - * supplied parent budget. - */ - public BexGasMeter(BexGasSchedule schedule, - long parentRemainingGas, - long localLimit) { - this(schedule, - requireBudget(parentRemainingGas, "parentRemainingGas"), - requireLocalLimit(localLimit), - Collections.emptyMap(), - false, - false, - Collections.emptyMap()); - } - - /** - * Creates a standalone meter with registry-bound named child counters. - */ - public BexGasMeter(BexGasSchedule schedule, - long parentRemainingGas, - long localLimit, - Map registeredNamedWeights) { - this(schedule, - requireBudget(parentRemainingGas, "parentRemainingGas"), - requireLocalLimit(localLimit), - Collections.emptyMap(), - false, - false, - registeredNamedWeights); - } - - /** - * Creates a meter over a live parent-bounded host child ledger. - */ - public BexGasMeter(BexGasSchedule schedule, - GasMeter.ChildGasLedger hostLedger) { - this(schedule, hostLedger, NO_LOCAL_LIMIT); - } - - /** - * Creates a meter over a live parent-bounded host child ledger. The local - * limit may only reduce the child ledger's initial remaining budget. - */ - public BexGasMeter(BexGasSchedule schedule, - GasMeter.ChildGasLedger hostLedger, - long localLimit) { - this(schedule, - Objects.requireNonNull(hostLedger, "hostLedger").remainingGas(), - requireLocalLimit(localLimit), - singletonHostLedger(hostLedger), - true, - false, - Collections.emptyMap()); - } - - /** - * Creates a hosted meter with one physical child ledger per logical - * runtime namespace. The map must contain {@code bex}; registered - * intrinsic namespaces use their own unqualified counter catalogs. - */ - public BexGasMeter( - BexGasSchedule schedule, - Map hostLedgers, - long localLimit, - Map registeredNamedWeights) { - this(schedule, - parentBudget(hostLedgers), - requireLocalLimit(localLimit), - hostLedgers, - false, - false, - registeredNamedWeights); - } - - /** - * Creates a hosted meter whose configured local limit is enforced by one - * invocation-owned budget shared by every supplied host ledger. - * - *

The meter retains the configured limit for portable diagnostics but - * does not race the canonical host admission path with a duplicate local - * precheck. The host therefore records the exact rejected charge before - * any corresponding BEX or intrinsic work occurs.

- * - * @param schedule exact BEX gas schedule - * @param hostLedgers one live physical ledger per logical namespace - * @param localLimit non-negative maximum enforced by the shared host - * budget - * @param registeredNamedWeights exact intrinsic counter registry - * @return live BEX meter using canonical host-side local admission - */ - public static BexGasMeter hostedWithSharedLocalLimit( - BexGasSchedule schedule, - Map hostLedgers, - long localLimit, - Map registeredNamedWeights) { - return new BexGasMeter( - schedule, - parentBudget(hostLedgers), - requireLocalLimit(localLimit), - hostLedgers, - false, - true, - registeredNamedWeights); - } - - private BexGasMeter(BexGasSchedule schedule, - long parentRemainingGas, - long localLimit, - Map hostLedgers, - boolean qualifiedHostCounters, - boolean hostEnforcesLocalLimit, - Map registeredNamedWeights) { - this.schedule = Objects.requireNonNull(schedule, "schedule"); - this.parentRemainingGas = parentRemainingGas; - this.localLimit = localLimit; - this.effectiveBudget = localLimit == NO_LOCAL_LIMIT - ? parentRemainingGas - : Math.min(parentRemainingGas, localLimit); - this.hostLedgers = immutableHostLedgers(hostLedgers); - this.qualifiedHostCounters = qualifiedHostCounters; - if (hostEnforcesLocalLimit - && (this.hostLedgers.isEmpty() - || localLimit == NO_LOCAL_LIMIT)) { - throw new IllegalArgumentException( - "Host-enforced local limits require hosted ledgers " - + "and a non-negative local limit"); - } - this.hostEnforcesLocalLimit = hostEnforcesLocalLimit; - this.registeredNamedWeights = - immutableRegisteredWeights(registeredNamedWeights); - } - - public BexGasSchedule schedule() { - return schedule; - } - - /** - * Produces the deterministic host-ledger key for a counter. Portable BEX - * counters remain unqualified; registry child counters are qualified by - * their exact namespace. - */ - public static String qualifiedCounterName(String namespace, - String counterName) { - String exactNamespace = requireName(namespace, "Gas namespace"); - String exactCounter = requireName(counterName, "Gas counter"); - return BexGasCounter.NAMESPACE.equals(exactNamespace) - ? exactCounter - : exactNamespace + "." + exactCounter; - } - - /** - * Builds a deterministic combined catalog for standalone inspection. - * Hosted intrinsic execution uses one physical ledger per namespace and - * never passes this combined map to a host. Registered map keys must - * already be produced by - * {@link #qualifiedCounterName(String, String)}. - */ - public static Map childLedgerWeights( - BexGasSchedule schedule, - Map registeredNamedWeights) { - LinkedHashMap combined = new LinkedHashMap<>( - Objects.requireNonNull(schedule, "schedule").counterWeights()); - Map registered = - immutableRegisteredWeights(registeredNamedWeights); - for (Map.Entry entry : registered.entrySet()) { - if (combined.containsKey(entry.getKey())) { - throw new IllegalArgumentException( - "Registered gas counter collides with BEX manifest counter: " - + entry.getKey()); - } - combined.put(entry.getKey(), entry.getValue()); - } - return Collections.unmodifiableMap(combined); - } - - public Map registeredNamedWeights() { - return registeredNamedWeights; - } - - public Map childLedgerWeights() { - return childLedgerWeights(schedule, registeredNamedWeights); - } - - /** - * Returns the exact parent budget observed when this child meter began. - */ - public long parentRemainingGas() { - return parentRemainingGas; - } - - /** - * Returns the configured local sub-limit, or {@link #NO_LOCAL_LIMIT}. - */ - public long localLimit() { - return localLimit; - } - - public long effectiveBudget() { - return effectiveBudget; - } - - public long totalGas() { - return totalGas; - } - - /** Compatibility alias for {@link #totalGas()}. */ - public long used() { - return totalGas; - } - - public long remainingGas() { - return effectiveBudget - totalGas; - } - - /** Alias for {@link #remainingGas()}. */ - public long remaining() { - return remainingGas(); - } - - /** - * Returns an immutable snapshot of all successfully admitted charges. - */ - public List trace() { - return Collections.unmodifiableList(new ArrayList<>(trace)); - } - - /** - * Returns an immutable snapshot of the current admitted ledger. - */ - public BexGasLedger ledger() { - return new BexGasLedger( - trace, - schedule.scheduleId(), - schedule.manifestIdentity()); - } - - public boolean hasHostLedger() { - return !hostLedgers.isEmpty(); - } - - public boolean hostLedgerSubmitted() { - return hostLedgerState == HostLedgerState.SUBMITTED; - } - - public boolean hostLedgerFinalized() { - return hostLedgerState != HostLedgerState.OPEN; - } - - public void charge(BexGasCounter counter) { - charge(counter, 1L); - } - - public void charge(BexGasCounter counter, long quantity) { - BexGasCounter exactCounter = - Objects.requireNonNull(counter, "counter"); - charge(exactCounter, - quantity, - (String) null, - null, - exactCounter.canonicalName()); - } - - public void charge(BexGasCounter counter, - long quantity, - String reason) { - charge(counter, quantity, (String) null, null, reason); - } - - public void charge(BexGasCounter counter, - String sourcePath, - String operator, - String reason) { - charge(counter, 1L, sourcePath, operator, reason); - } - - public void charge(BexGasCounter counter, - BexSourcePath sourcePath, - String operator, - String reason) { - charge(counter, - 1L, - sourcePath != null ? sourcePath.toString() : null, - operator, - reason); - } - - public void charge(BexGasCounter counter, - long quantity, - BexSourcePath sourcePath, - String operator, - String reason) { - charge(counter, - quantity, - sourcePath != null ? sourcePath.toString() : null, - operator, - reason); - } - - public void charge(BexGasCounter counter, - long quantity, - String sourcePath, - String operator, - String reason) { - BexGasCounter exactCounter = - Objects.requireNonNull(counter, "counter"); - long weight = schedule.weight(exactCounter); - chargeAdmitted( - BexGasCounter.NAMESPACE, - exactCounter.canonicalName(), - exactCounter, - quantity, - weight, - sourcePath, - operator, - reason); - } - - public void chargeNamed(String namespace, - String counterName, - long quantity) { - chargeNamed(namespace, - counterName, - quantity, - (String) null, - null, - qualifiedCounterName(namespace, counterName)); - } - - public void chargeNamed(String namespace, - String counterName, - long quantity, - String reason) { - chargeNamed(namespace, - counterName, - quantity, - (String) null, - null, - reason); - } - - public void chargeNamed(String namespace, - String counterName, - long quantity, - String sourcePath, - String operator, - String reason) { - NamedWeight named = registeredWeight(namespace, counterName); - chargeAdmitted( - named.namespace, - named.counterName, - named.portableCounter, - quantity, - named.weight, - sourcePath, - operator, - reason); - } - - public void chargeNamed(String namespace, - String counterName, - long quantity, - long declaredWeight, - String sourcePath, - String operator, - String reason) { - NamedWeight named = registeredWeight(namespace, counterName); - if (declaredWeight != named.weight) { - throw new IllegalArgumentException( - "Registered gas weight mismatch for " - + qualifiedCounterName(namespace, counterName) - + ": expected " + named.weight - + " but was " + declaredWeight); - } - chargeAdmitted( - named.namespace, - named.counterName, - named.portableCounter, - quantity, - named.weight, - sourcePath, - operator, - reason); - } - - public void chargeNamed(String namespace, - String counterName, - long quantity, - BexSourcePath sourcePath, - String operator, - String reason) { - chargeNamed(namespace, - counterName, - quantity, - sourcePath != null ? sourcePath.toString() : null, - operator, - reason); - } - - private void chargeAdmitted(String namespace, - String counterName, - BexGasCounter portableCounter, - long quantity, - long weight, - String sourcePath, - String operator, - String reason) { - ensureOpen(); - if (quantity < 0L) { - throw new IllegalArgumentException( - "Gas quantity must be non-negative"); - } - if (quantity == 0L || weight == 0L) { - return; - } - String exactReason = requireReason(reason); - long gas = multiplyExact(quantity, weight); - - /* - * A hosted meter prechecks only the optional BEX-local sub-limit. The - * processor-owned child ledger remains the sole authority for the live - * parent budget, including reservations consumed after this meter was - * opened. A standalone meter has no such owner and therefore checks - * the complete effective budget itself. - */ - long localAdmissionBudget = hostLedgers.isEmpty() - ? effectiveBudget - : hostEnforcesLocalLimit - ? NO_LOCAL_LIMIT - : localLimit; - if (localAdmissionBudget != NO_LOCAL_LIMIT - && gas > localAdmissionBudget - totalGas) { - throw exhausted( - namespace, - counterName, - portableCounter, - quantity, - weight); - } - - GasMeter.ChildGasLedger hostLedger = - qualifiedHostCounters - ? hostLedgers.get(BexGasCounter.NAMESPACE) - : hostLedgers.get(namespace); - if (!hostLedgers.isEmpty() && hostLedger == null) { - throw new IllegalStateException( - "No live host child ledger for logical namespace " - + namespace); - } - if (hostLedger != null) { - try { - hostLedger.charge( - qualifiedHostCounters - ? qualifiedCounterName( - namespace, counterName) - : counterName, - quantity, - GasChargeContext.of( - emptyToNull(sourcePath), - null, - emptyToNull(operator), - exactReason)); - } catch (GasLimitExceededException exhausted) { - /* - * Retain the exact host rejection so the owning runtime work - * session can validate and propagate that same object. The - * BEX wrapper still exposes the portable logical namespace and - * preserves the invariant that the rejected entry is absent - * locally. - */ - throw exhausted( - namespace, - counterName, - portableCounter, - quantity, - weight, - exhausted); - } - } - - trace.add(new BexGasCharge( - trace.size(), - namespace, - counterName, - quantity, - weight, - gas, - sourcePath, - operator, - exactReason)); - totalGas += gas; - } - - /** - * Submits a successfully completed wrapped host child ledger exactly once. - * The final state is set before invoking the callback, so a - * throwing callback cannot cause an accidental second merge attempt. - */ - public void submitHostLedger( - Consumer submitter) { - finalizeHostLedger( - HostLedgerState.SUBMITTED, - Objects.requireNonNull(submitter, "submitter")); - } - - /** - * Finalizes the BEX side of a deterministic-failure callback. The host - * decides whether its contract merges immediately or leaves the ledger - * staged for enclosing-session finalization. - */ - public void failHostLedger( - Consumer failureHandler) { - finalizeHostLedger( - HostLedgerState.FAILED, - Objects.requireNonNull(failureHandler, "failureHandler")); - } - - /** - * Finalizes the BEX side of a transient-unavailability callback. - */ - public void unavailableHostLedger( - Consumer unavailableHandler) { - finalizeHostLedger( - HostLedgerState.UNAVAILABLE, - Objects.requireNonNull( - unavailableHandler, "unavailableHandler")); - } - - /** - * Hands the exact recorded host rejection back to its owner. - */ - public void propagateHostGasExhaustion( - GasLimitExceededException exhaustion, - Consumer prefixHandler, - BiConsumer exhaustionHandler) { - requireOpenHostLedger(); - hostLedgerState = HostLedgerState.EXHAUSTED; - GasLimitExceededException exactExhaustion = - Objects.requireNonNull(exhaustion, "exhaustion"); - Consumer exactPrefixHandler = - Objects.requireNonNull( - prefixHandler, "prefixHandler"); - Throwable prefixFailure = null; - for (GasMeter.ChildGasLedger hostLedger - : hostLedgers.values()) { - try { - exactPrefixHandler.accept(hostLedger); - } catch (RuntimeException | Error failure) { - prefixFailure = retainFailure( - prefixFailure, failure); - } - } - try { - Objects.requireNonNull( - exhaustionHandler, "exhaustionHandler").accept( - rejectionLedger(exactExhaustion), - exactExhaustion); - } catch (RuntimeException | Error propagated) { - if (prefixFailure != null - && prefixFailure != propagated) { - propagated.addSuppressed(prefixFailure); - } - throw propagated; - } - rethrowFailure(prefixFailure); - } - - private void finalizeHostLedger( - HostLedgerState finalState, - Consumer callback) { - requireOpenHostLedger(); - hostLedgerState = finalState; - Throwable callbackFailure = null; - for (GasMeter.ChildGasLedger hostLedger - : hostLedgers.values()) { - try { - callback.accept(hostLedger); - } catch (RuntimeException | Error failure) { - callbackFailure = retainFailure( - callbackFailure, failure); - } - } - rethrowFailure(callbackFailure); - } - - private static Throwable retainFailure( - Throwable retained, - Throwable next) { - if (retained == null) { - return next; - } - if (retained != next) { - retained.addSuppressed(next); - } - return retained; - } - - private static void rethrowFailure(Throwable failure) { - if (failure instanceof RuntimeException) { - throw (RuntimeException) failure; - } - if (failure instanceof Error) { - throw (Error) failure; - } - } - - private void requireOpenHostLedger() { - if (hostLedgers.isEmpty()) { - throw new IllegalStateException( - "This BEX gas meter has no host child ledgers"); - } - if (hostLedgerState != HostLedgerState.OPEN) { - throw new IllegalStateException( - "BEX host child ledger was already finalized as " - + hostLedgerState); - } - } - - private GasMeter.ChildGasLedger rejectionLedger( - GasLimitExceededException exhaustion) { - for (GasMeter.ChildGasLedger ledger : hostLedgers.values()) { - if (ledger.namespace().equals(exhaustion.namespace())) { - return ledger; - } - } - /* - * Semantic admission shares the same work session but is not a BEX - * child ledger. The focused host callback receives the primary BEX - * ledger as an ownership token; the processor adapter validates the - * exact exception against the session itself. - */ - return hostLedgers.get(BexGasCounter.NAMESPACE); - } - - private void ensureOpen() { - if (hostLedgerState != HostLedgerState.OPEN) { - throw new IllegalStateException( - "Cannot charge a finalized BEX host child ledger"); - } - } - - private NamedWeight registeredWeight(String namespace, - String counterName) { - String exactNamespace = requireName(namespace, "Gas namespace"); - String exactCounterName = requireName(counterName, "Gas counter"); - if (BexGasCounter.NAMESPACE.equals(exactNamespace)) { - BexGasCounter portable = - BexGasCounter.fromCanonicalName(exactCounterName); - return new NamedWeight( - exactNamespace, - exactCounterName, - portable, - schedule.weight(portable)); - } - String qualified = - qualifiedCounterName(exactNamespace, exactCounterName); - Long weight = registeredNamedWeights.get(qualified); - if (weight == null) { - throw new IllegalArgumentException( - "Unregistered named gas counter: " + qualified); - } - return new NamedWeight( - exactNamespace, - exactCounterName, - null, - weight); - } - - private BexGasLimitExceededException exhausted( - String namespace, - String counterName, - BexGasCounter portableCounter, - long quantity, - long weight) { - return exhausted( - namespace, - counterName, - portableCounter, - quantity, - weight, - null); - } - - private BexGasLimitExceededException exhausted( - String namespace, - String counterName, - BexGasCounter portableCounter, - long quantity, - long weight, - GasLimitExceededException hostGasLimitExceeded) { - if (portableCounter != null) { - return new BexGasLimitExceededException( - portableCounter, - quantity, - weight, - totalGas, - effectiveBudget, - hostGasLimitExceeded); - } - return new BexGasLimitExceededException( - namespace, - counterName, - quantity, - weight, - totalGas, - effectiveBudget, - hostGasLimitExceeded); - } - - private static long requireBudget(long value, String name) { - if (value < 0L) { - throw new IllegalArgumentException(name + " must be non-negative"); - } - return value; - } - - private static long requireLocalLimit(long value) { - if (value < NO_LOCAL_LIMIT) { - throw new IllegalArgumentException( - "localLimit must be non-negative or NO_LOCAL_LIMIT"); - } - return value; - } - - private static String emptyToNull(String value) { - return value == null || value.isEmpty() ? null : value; - } - - private static String requireReason(String value) { - if (value == null || value.trim().isEmpty()) { - throw new IllegalArgumentException("Gas charge reason is required"); - } - return value; - } - - private static String requireName(String value, String label) { - if (value == null || value.trim().isEmpty()) { - throw new IllegalArgumentException(label + " is required"); - } - return value; - } - - private static Map - singletonHostLedger(GasMeter.ChildGasLedger hostLedger) { - LinkedHashMap singleton = - new LinkedHashMap<>(); - singleton.put( - BexGasCounter.NAMESPACE, - Objects.requireNonNull(hostLedger, "hostLedger")); - return singleton; - } - - private static long parentBudget( - Map hostLedgers) { - Objects.requireNonNull(hostLedgers, "hostLedgers"); - GasMeter.ChildGasLedger bexLedger = - hostLedgers.get(BexGasCounter.NAMESPACE); - if (bexLedger == null) { - throw new IllegalArgumentException( - "Hosted BEX ledger map must contain logical namespace " - + BexGasCounter.NAMESPACE); - } - return bexLedger.remainingGas(); - } - - private static Map - immutableHostLedgers( - Map hostLedgers) { - if (hostLedgers == null || hostLedgers.isEmpty()) { - return Collections.emptyMap(); - } - if (!hostLedgers.containsKey(BexGasCounter.NAMESPACE)) { - throw new IllegalArgumentException( - "Hosted BEX ledger map must contain logical namespace " - + BexGasCounter.NAMESPACE); - } - LinkedHashMap copy = - new LinkedHashMap<>(); - IdentityHashMap identities = - new IdentityHashMap<>(); - for (Map.Entry entry - : hostLedgers.entrySet()) { - String namespace = - requireName(entry.getKey(), "Logical gas namespace"); - GasMeter.ChildGasLedger ledger = - Objects.requireNonNull( - entry.getValue(), "host child ledger"); - if (identities.put(ledger, Boolean.TRUE) != null) { - throw new IllegalArgumentException( - "Each logical runtime namespace requires a distinct " - + "host child ledger"); - } - copy.put(namespace, ledger); - } - return Collections.unmodifiableMap(copy); - } - - private static Map immutableRegisteredWeights( - Map registeredNamedWeights) { - if (registeredNamedWeights == null || registeredNamedWeights.isEmpty()) { - return Collections.emptyMap(); - } - LinkedHashMap copy = new LinkedHashMap<>(); - for (Map.Entry entry : - registeredNamedWeights.entrySet()) { - String counterName = - requireName(entry.getKey(), "Registered gas counter"); - Long weight = Objects.requireNonNull( - entry.getValue(), "Registered gas weight"); - if (weight <= 0L) { - throw new IllegalArgumentException( - "Registered gas weight must be positive"); - } - copy.put(counterName, weight); - } - return Collections.unmodifiableMap(copy); - } - - private static long multiplyExact(long left, long right) { - if (left != 0L && right > Long.MAX_VALUE / left) { - throw new IllegalArgumentException("Gas subtotal exceeds long range"); - } - return left * right; - } - - private static final class NamedWeight { - private final String namespace; - private final String counterName; - private final BexGasCounter portableCounter; - private final long weight; - - private NamedWeight(String namespace, - String counterName, - BexGasCounter portableCounter, - long weight) { - this.namespace = namespace; - this.counterName = counterName; - this.portableCounter = portableCounter; - this.weight = weight; - } - } -} diff --git a/src/main/java/blue/bex/result/BexMetrics.java b/src/main/java/blue/bex/result/BexMetrics.java deleted file mode 100644 index ef9820b..0000000 --- a/src/main/java/blue/bex/result/BexMetrics.java +++ /dev/null @@ -1,142 +0,0 @@ -package blue.bex.result; - -/** - * Mutable execution metrics collected during compile and execution. - * - *

{@link BexExecutionResult#metrics()} returns a copy, so callers can inspect - * counters without mutating the stored result. Metrics distinguish hot-path - * reads, cache behavior, materialization, and output boundary conversions.

- */ -public final class BexMetrics { - private long compiledExecutions; - private long compileCacheHits; - private long compileCacheMisses; - private long interpretedFallbacks; - private long expressionEvaluations; - private long statementExecutions; - private long functionCalls; - private long loopIterations; - private long frozenDocumentReads; - private long resolvedDocumentReads; - private long eventReads; - private long stepsReads; - private long currentContractReads; - private long nodeMaterializations; - private long simpleMaterializations; - private long frozenOutputConversions; - private long nodeOutputConversions; - private long containsBexScans; - private long containsBexCacheHits; - private long containsBexCacheMisses; - private long resultValueReads; - private long resultOverlayExactHits; - private long resultOverlayAncestorHits; - private long resultOverlayDocumentFallbacks; - private long pointerParses; - private long pointerCacheHits; - private long pointerCacheMisses; - private long functionArgMapAllocations; - private long frozenWriterNodeFallbacks; - private long compileNanos; - private long executeNanos; - - public BexMetrics copy() { - BexMetrics copy = new BexMetrics(); - copy.compiledExecutions = compiledExecutions; - copy.compileCacheHits = compileCacheHits; - copy.compileCacheMisses = compileCacheMisses; - copy.interpretedFallbacks = interpretedFallbacks; - copy.expressionEvaluations = expressionEvaluations; - copy.statementExecutions = statementExecutions; - copy.functionCalls = functionCalls; - copy.loopIterations = loopIterations; - copy.frozenDocumentReads = frozenDocumentReads; - copy.resolvedDocumentReads = resolvedDocumentReads; - copy.eventReads = eventReads; - copy.stepsReads = stepsReads; - copy.currentContractReads = currentContractReads; - copy.nodeMaterializations = nodeMaterializations; - copy.simpleMaterializations = simpleMaterializations; - copy.frozenOutputConversions = frozenOutputConversions; - copy.nodeOutputConversions = nodeOutputConversions; - copy.containsBexScans = containsBexScans; - copy.containsBexCacheHits = containsBexCacheHits; - copy.containsBexCacheMisses = containsBexCacheMisses; - copy.resultValueReads = resultValueReads; - copy.resultOverlayExactHits = resultOverlayExactHits; - copy.resultOverlayAncestorHits = resultOverlayAncestorHits; - copy.resultOverlayDocumentFallbacks = resultOverlayDocumentFallbacks; - copy.pointerParses = pointerParses; - copy.pointerCacheHits = pointerCacheHits; - copy.pointerCacheMisses = pointerCacheMisses; - copy.functionArgMapAllocations = functionArgMapAllocations; - copy.frozenWriterNodeFallbacks = frozenWriterNodeFallbacks; - copy.compileNanos = compileNanos; - copy.executeNanos = executeNanos; - return copy; - } - - public void incrementCompiledExecutions() { compiledExecutions++; } - public void incrementCompileCacheHits() { compileCacheHits++; } - public void incrementCompileCacheMisses() { compileCacheMisses++; } - public void incrementInterpretedFallbacks() { interpretedFallbacks++; } - public void incrementExpressionEvaluations() { expressionEvaluations++; } - public void incrementStatementExecutions() { statementExecutions++; } - public void incrementFunctionCalls() { functionCalls++; } - public void incrementLoopIterations() { loopIterations++; } - public void incrementFrozenDocumentReads() { frozenDocumentReads++; } - public void incrementResolvedDocumentReads() { resolvedDocumentReads++; } - public void incrementEventReads() { eventReads++; } - public void incrementStepsReads() { stepsReads++; } - public void incrementCurrentContractReads() { currentContractReads++; } - public void incrementNodeMaterializations() { nodeMaterializations++; } - public void incrementSimpleMaterializations() { simpleMaterializations++; } - public void incrementFrozenOutputConversions() { frozenOutputConversions++; } - public void incrementNodeOutputConversions() { nodeOutputConversions++; } - public void incrementContainsBexScans() { containsBexScans++; } - public void incrementContainsBexCacheHits() { containsBexCacheHits++; } - public void incrementContainsBexCacheMisses() { containsBexCacheMisses++; } - public void incrementResultValueReads() { resultValueReads++; } - public void incrementResultOverlayExactHits() { resultOverlayExactHits++; } - public void incrementResultOverlayAncestorHits() { resultOverlayAncestorHits++; } - public void incrementResultOverlayDocumentFallbacks() { resultOverlayDocumentFallbacks++; } - public void incrementPointerParses() { pointerParses++; } - public void incrementPointerCacheHits() { pointerCacheHits++; } - public void incrementPointerCacheMisses() { pointerCacheMisses++; } - public void incrementFunctionArgMapAllocations() { functionArgMapAllocations++; } - public void incrementFrozenWriterNodeFallbacks() { frozenWriterNodeFallbacks++; } - public void addCompileNanos(long nanos) { compileNanos += Math.max(0L, nanos); } - public void addExecuteNanos(long nanos) { executeNanos += Math.max(0L, nanos); } - - public long compiledExecutions() { return compiledExecutions; } - public long compileCacheHits() { return compileCacheHits; } - public long compileCacheMisses() { return compileCacheMisses; } - public long interpretedFallbacks() { return interpretedFallbacks; } - public long expressionEvaluations() { return expressionEvaluations; } - public long statementExecutions() { return statementExecutions; } - public long functionCalls() { return functionCalls; } - public long loopIterations() { return loopIterations; } - public long frozenDocumentReads() { return frozenDocumentReads; } - public long resolvedDocumentReads() { return resolvedDocumentReads; } - public long eventReads() { return eventReads; } - public long stepsReads() { return stepsReads; } - public long currentContractReads() { return currentContractReads; } - public long nodeMaterializations() { return nodeMaterializations; } - public long simpleMaterializations() { return simpleMaterializations; } - public long frozenOutputConversions() { return frozenOutputConversions; } - public long nodeOutputConversions() { return nodeOutputConversions; } - public long containsBexScans() { return containsBexScans; } - public long containsBexCacheHits() { return containsBexCacheHits; } - public long containsBexCacheMisses() { return containsBexCacheMisses; } - public long resultValueReads() { return resultValueReads; } - public long resultOverlayExactHits() { return resultOverlayExactHits; } - public long resultOverlayAncestorHits() { return resultOverlayAncestorHits; } - public long resultOverlayDocumentFallbacks() { return resultOverlayDocumentFallbacks; } - public long pointerParses() { return pointerParses; } - public long pointerCacheHits() { return pointerCacheHits; } - public long pointerCacheMisses() { return pointerCacheMisses; } - public long functionArgMapAllocations() { return functionArgMapAllocations; } - public long frozenWriterNodeFallbacks() { return frozenWriterNodeFallbacks; } - public long compileNanos() { return compileNanos; } - public long executeNanos() { return executeNanos; } -} diff --git a/src/main/java/blue/bex/runtime/BexRuntime.java b/src/main/java/blue/bex/runtime/BexRuntime.java deleted file mode 100644 index 29a0457..0000000 --- a/src/main/java/blue/bex/runtime/BexRuntime.java +++ /dev/null @@ -1,511 +0,0 @@ -package blue.bex.runtime; - -import blue.bex.api.BexExecutionContext; -import blue.bex.api.BexGasLedgerHost; -import blue.bex.api.BexIntrinsicRegistry; -import blue.bex.compile.BexCompiledProgram; -import blue.bex.gas.BexGasCounter; -import blue.bex.gas.BexGasLimitExceededException; -import blue.bex.gas.BexGasMeter; -import blue.bex.gas.BexGasSchedule; -import blue.bex.output.BexAdmittedValue; -import blue.bex.output.BexOutputAdmission; -import blue.bex.output.BexOutputKind; -import blue.bex.pointer.BexPointerCache; -import blue.bex.result.BexChangeset; -import blue.bex.result.BexExecutionResult; -import blue.bex.result.BexMetrics; -import blue.bex.result.BexResultOverlay; -import blue.bex.type.BexBlueTypeMatcher; -import blue.bex.value.BexValue; -import blue.bex.value.BexValues; -import blue.language.runtime.BlueLanguage; -import blue.language.processor.GasMeter; -import blue.language.processor.GasLimitExceededException; -import blue.language.processor.ExecutionEvidenceUnavailableException; -import blue.language.processor.InvalidExecutionEvidenceException; -import blue.language.processor.PortableLimitExceededException; -import blue.language.processor.ProcessorFailureException; -import blue.language.processor.RuntimeWorkBudget; -import blue.language.model.wire.JsonPointer; - -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; - -/** - * Runtime for one compiled BEX execution. - */ -public final class BexRuntime { - private final BexCompiledProgram program; - private final BexExecutionContext context; - private final BexGasMeter gas; - private final BexMetrics metrics; - private final BexPointerCache pointerCache; - private final BexExecutionAccumulator accumulator; - private final BexBlueTypeMatcher typeMatcher; - private final BlueLanguage blue; - private final BexIntrinsicRegistry intrinsics; - private final BexOutputAdmission outputAdmission; - private final BexGasLedgerHost gasLedgerHost; - private final BexResultOverlay rollbackOverlay; - - public BexRuntime(BexCompiledProgram program, - BexExecutionContext context, - BlueLanguage blue, - BexGasSchedule gasSchedule, - BexMetrics metrics, - BexPointerCache pointerCache) { - this(program, context, blue, gasSchedule, metrics, pointerCache, BexIntrinsicRegistry.empty()); - } - - public BexRuntime(BexCompiledProgram program, - BexExecutionContext context, - BlueLanguage blue, - BexGasSchedule gasSchedule, - BexMetrics metrics, - BexPointerCache pointerCache, - BexIntrinsicRegistry intrinsics) { - this.program = program; - this.context = context; - this.blue = blue; - this.intrinsics = intrinsics != null - ? intrinsics - : BexIntrinsicRegistry.empty(); - this.gasLedgerHost = context.gasLedgerHost(); - this.gas = newGasMeter( - context, - gasSchedule, - gasLedgerHost, - this.intrinsics, - program.requiredIntrinsicBlueIds()); - this.metrics = metrics; - this.pointerCache = pointerCache; - this.outputAdmission = new BexOutputAdmission( - gas, context.semanticIdentityBoundary()); - BexResultOverlay activeOverlay = - new BexResultOverlay( - context.document(), metrics, blue); - this.accumulator = new BexExecutionAccumulator( - activeOverlay, - outputAdmission); - this.rollbackOverlay = new BexResultOverlay( - context.document(), metrics, blue); - this.typeMatcher = new BexBlueTypeMatcher(blue); - } - - public BexExecutionResult execute() { - try { - BexValue value = program.execute(this); - BexAdmittedValue output = - outputAdmission.admit(value, BexOutputKind.ROOT_RESULT); - BexExecutionResult result = new BexExecutionResult( - value, - accumulator.changeset(), - accumulator.events(), - gas.ledger(), - metrics, - output); - submitHostLedger(); - return result; - } catch (RuntimeException | Error ex) { - accumulator.discard(rollbackOverlay); - finishHostLedgerAfterFailure(ex); - throw ex; - } - } - - public BexCompiledProgram program() { return program; } - public BexExecutionContext context() { return context; } - public BexGasMeter gas() { return gas; } - public BexMetrics metrics() { return metrics; } - public BexPointerCache pointerCache() { return pointerCache; } - public BexExecutionAccumulator accumulator() { return accumulator; } - public BexBlueTypeMatcher typeMatcher() { return typeMatcher; } - public BexIntrinsicRegistry intrinsics() { return intrinsics; } - public BexOutputAdmission outputAdmission() { return outputAdmission; } - - public BexValue readDocument(String absolutePointer, List precompiledSegments, boolean resolved) { - gas.charge(BexGasCounter.DOCUMENT_READ); - if (resolved) { - metrics.incrementResolvedDocumentReads(); - } else { - metrics.incrementFrozenDocumentReads(); - } - - /* - * Traverse from the host's exact root so every intermediate reference - * can be materialized lazily through Blue's verified provider - * boundary. A final pure or cyclic-set reference stays opaque when the - * program only carries it or asks for its established identity. - */ - return readValuePointer(documentAt("/", resolved), - precompiledSegments); - } - - private BexValue documentAt(String absolutePointer, boolean resolved) { - return resolved - ? context.document().resolvedAt(absolutePointer) - : context.document().canonicalAt(absolutePointer); - } - - public BexValue readEvent(List precompiledSegments) { - gas.charge(BexGasCounter.EVENT_READ); - metrics.incrementEventReads(); - return readValuePointer(context.event(), precompiledSegments); - } - - public BexValue readProcessingEvent(List precompiledSegments) { - gas.charge(BexGasCounter.PROCESSING_EVENT_READ); - return readValuePointer(context.processingEvent(), precompiledSegments); - } - - public BexValue readCurrentContract(List precompiledSegments) { - gas.charge(BexGasCounter.CURRENT_CONTRACT_READ); - metrics.incrementCurrentContractReads(); - return readValuePointer(context.currentContract(), precompiledSegments); - } - - public BexValue readBinding(String name, List pathSegments) { - gas.charge(BexGasCounter.BINDING_READ); - if (name == null || name.isEmpty()) { - return BexValues.undefined(); - } - return readValuePointer(context.binding(name), pathSegments); - } - - public BexValue readSteps(String step, List pathSegments) { - gas.charge(BexGasCounter.STEPS_READ); - metrics.incrementStepsReads(); - return readValuePointer(context.steps().step(step), pathSegments); - } - - public BexValue readResultValue(String absolutePointer, List segments) { - gas.charge(BexGasCounter.RESULT_VALUE_READ); - metrics.incrementResultValueReads(); - return readValuePointer(accumulator.overlay().rootValue(), segments); - } - - public BexValue defaultResultValue() { - Map result = new LinkedHashMap<>(); - BexChangeset changeset = accumulator.changeset(); - result.put("changeset", changeset.asValue()); - result.put("events", accumulator.events().asValue()); - return BexValues.map(result); - } - - public BexValue invokeIntrinsic(String blueId, BexValue type, Map fields) { - return intrinsics.invoke( - blueId, type, fields, gas, outputAdmission); - } - - public BexValue nodeBlueId(BexValue value) { - gas.charge(BexGasCounter.NODE_IDENTITY_REQUESTED); - if (value == null || value.isUndefined()) { - throw new blue.bex.BexException( - "$nodeBlueId operand must not be undefined"); - } - if (value.isExact()) { - return BexValues.scalar(value.exactBlueId()); - } - return BexValues.scalar(outputAdmission - .admit(value, BexOutputKind.NODE_IDENTITY) - .nodeBlueId()); - } - - public String resolvePointer(String authoredPointer) { - return context.document().resolvePointer(authoredPointer); - } - - public List parseDynamicPointer(String pointer) { - return pointerCache.get(pointer, metrics).segments(); - } - - /** - * Traverses one semantic value pointer with canonical per-segment read - * ownership. The charge is admitted before examining each next member. - */ - public BexValue readValuePointer(BexValue root, List segments) { - BexValue current = BexValues.referenceBacked( - root != null ? root : BexValues.undefined(), - blue); - if (segments == null) { - return current; - } - for (String segment : segments) { - if (current.isUndefined()) { - return current; - } - gas.charge(BexGasCounter.POINTER_SEGMENT_READ); - if (current.isList()) { - gas.charge(BexGasCounter.LIST_ITEM_READ); - } else if (current.isObject()) { - gas.charge(BexGasCounter.OBJECT_MEMBER_READ); - } - current = current.get(segment); - } - return current; - } - - public String canonicalPointer(String pointer) { - return JsonPointer.canonicalize(pointer); - } - - private static BexGasMeter newGasMeter(BexExecutionContext context, - BexGasSchedule gasSchedule, - BexGasLedgerHost host, - BexIntrinsicRegistry intrinsics, - Set - requiredIntrinsicBlueIds) { - Map registered = - intrinsics.registeredNamedWeights( - requiredIntrinsicBlueIds); - Map> namespaceWeights = - intrinsics.registeredNamespaceWeights( - requiredIntrinsicBlueIds); - if (host == null) { - return new BexGasMeter( - gasSchedule, - context.parentRemainingGas(), - context.gasLimit(), - registered); - } - if (!host.separatesRuntimeNamespaces()) { - if (!namespaceWeights.isEmpty()) { - throw new IllegalArgumentException( - "A hosted intrinsic registry requires separate runtime namespaces"); - } - } - LinkedHashMap children = - new LinkedHashMap<>(); - RuntimeWorkBudget sharedBudget = null; - try { - if (context.gasLimit() != BexGasMeter.NO_LOCAL_LIMIT) { - sharedBudget = - host.openSharedBudget(context.gasLimit()); - if (sharedBudget != null - && sharedBudget.maximumGas() - != context.gasLimit()) { - throw new IllegalStateException( - "Gas host returned a shared budget with maximum " - + sharedBudget.maximumGas() - + " instead of " - + context.gasLimit()); - } - } - children.put( - BexGasCounter.NAMESPACE, - requireOpenedLedger( - openHostLedger( - host, - BexGasCounter.NAMESPACE, - gasSchedule.counterWeights(), - sharedBudget), - BexGasCounter.NAMESPACE)); - for (Map.Entry> intrinsic - : namespaceWeights.entrySet()) { - children.put( - intrinsic.getKey(), - requireOpenedLedger( - openHostLedger( - host, - intrinsic.getKey(), - intrinsic.getValue(), - sharedBudget), - intrinsic.getKey())); - } - return sharedBudget == null - ? new BexGasMeter( - gasSchedule, - children, - context.gasLimit(), - registered) - : BexGasMeter.hostedWithSharedLocalLimit( - gasSchedule, - children, - context.gasLimit(), - registered); - } catch (RuntimeException | Error openingFailure) { - finishOpenedLedgersAfterConstructionFailure( - host, children, openingFailure); - throw openingFailure; - } - } - - private static GasMeter.ChildGasLedger openHostLedger( - BexGasLedgerHost host, - String namespace, - Map counterWeights, - RuntimeWorkBudget sharedBudget) { - return sharedBudget == null - ? host.open(namespace, counterWeights) - : host.open( - namespace, - counterWeights, - sharedBudget); - } - - private static GasMeter.ChildGasLedger requireOpenedLedger( - GasMeter.ChildGasLedger ledger, - String namespace) { - if (ledger == null) { - throw new IllegalStateException( - "Gas host returned no child ledger for " - + namespace); - } - return ledger; - } - - private static void finishOpenedLedgersAfterConstructionFailure( - BexGasLedgerHost host, - Map opened, - Throwable openingFailure) { - boolean unavailable = - evidenceUnavailableWins(openingFailure); - for (GasMeter.ChildGasLedger ledger : opened.values()) { - try { - if (unavailable) { - host.evidenceUnavailable(ledger); - } else { - host.failedDeterministically(ledger); - } - } catch (RuntimeException | Error lifecycleFailure) { - addSuppressed( - openingFailure, lifecycleFailure); - } - } - } - - private void submitHostLedger() { - if (gasLedgerHost == null - || gas.hostLedgerFinalized()) { - return; - } - gas.submitHostLedger(gasLedgerHost::submit); - } - - private void finishHostLedgerAfterFailure(Throwable primaryFailure) { - if (gasLedgerHost == null || gas.hostLedgerFinalized()) { - return; - } - if (evidenceUnavailableWins(primaryFailure)) { - notifyFailureLifecycle( - primaryFailure, - () -> gas.unavailableHostLedger( - gasLedgerHost::evidenceUnavailable)); - return; - } - - GasLimitExceededException hostExhaustion = - findCause( - primaryFailure, - GasLimitExceededException.class); - if (hostExhaustion != null) { - try { - gas.propagateHostGasExhaustion( - hostExhaustion, - gasLedgerHost::failedDeterministically, - gasLedgerHost::propagateGasExhaustion); - } catch (GasLimitExceededException canonical) { - if (canonical == hostExhaustion) { - throw canonical; - } - addSuppressed(primaryFailure, canonical); - } catch (RuntimeException | Error lifecycleFailure) { - addSuppressed(primaryFailure, lifecycleFailure); - } - return; - } - - BexGasLimitExceededException localExhaustion = - findCause( - primaryFailure, - BexGasLimitExceededException.class); - if (localExhaustion != null) { - notifyFailureLifecycle( - primaryFailure, - () -> gas.failHostLedger( - gasLedgerHost::failedDeterministically)); - if (!(primaryFailure instanceof RuntimeException)) { - return; - } - throw Objects.requireNonNull( - gasLedgerHost.localGasLimitExceeded( - localExhaustion, - (RuntimeException) primaryFailure), - "local gas-limit mapping"); - } - - notifyFailureLifecycle( - primaryFailure, - () -> gas.failHostLedger( - gasLedgerHost::failedDeterministically)); - } - - private static void notifyFailureLifecycle( - Throwable primaryFailure, - Runnable lifecycle) { - try { - lifecycle.run(); - } catch (RuntimeException | Error lifecycleFailure) { - addSuppressed(primaryFailure, lifecycleFailure); - } - } - - private static void addSuppressed( - Throwable primaryFailure, - Throwable lifecycleFailure) { - if (primaryFailure != lifecycleFailure) { - primaryFailure.addSuppressed(lifecycleFailure); - } - } - - /** - * Resolves lifecycle classification in causal order. A directly reported - * deterministic category is authoritative and cannot be reclassified by - * an unavailable exception nested below it. - */ - private static boolean evidenceUnavailableWins( - Throwable failure) { - Throwable current = failure; - while (current != null) { - if (current instanceof ProcessorFailureException - || current - instanceof InvalidExecutionEvidenceException - || current - instanceof PortableLimitExceededException - || current - instanceof GasLimitExceededException - || current - instanceof BexGasLimitExceededException) { - return false; - } - if (current - instanceof ExecutionEvidenceUnavailableException) { - return true; - } - Throwable cause = current.getCause(); - if (cause == current) { - break; - } - current = cause; - } - return false; - } - - private static T findCause( - Throwable failure, - Class type) { - Throwable current = failure; - while (current != null) { - if (type.isInstance(current)) { - return type.cast(current); - } - current = current.getCause(); - } - return null; - } - -} diff --git a/src/main/java/blue/bex/runtime/CompiledStatement.java b/src/main/java/blue/bex/runtime/CompiledStatement.java deleted file mode 100644 index cfa031f..0000000 --- a/src/main/java/blue/bex/runtime/CompiledStatement.java +++ /dev/null @@ -1,5 +0,0 @@ -package blue.bex.runtime; - -public interface CompiledStatement { - Control exec(CompiledFrame frame); -} diff --git a/src/main/java/blue/bex/runtime/Control.java b/src/main/java/blue/bex/runtime/Control.java deleted file mode 100644 index 9297df7..0000000 --- a/src/main/java/blue/bex/runtime/Control.java +++ /dev/null @@ -1,6 +0,0 @@ -package blue.bex.runtime; - -public enum Control { - CONTINUE, - RETURN -} diff --git a/src/main/java/blue/bex/type/BexBlueTypeMatcher.java b/src/main/java/blue/bex/type/BexBlueTypeMatcher.java deleted file mode 100644 index 4bec15b..0000000 --- a/src/main/java/blue/bex/type/BexBlueTypeMatcher.java +++ /dev/null @@ -1,972 +0,0 @@ -package blue.bex.type; - -import blue.bex.BexSourcePath; -import blue.bex.gas.BexGasLimitExceededException; -import blue.bex.gas.BexGasCounter; -import blue.bex.gas.BexGasMeter; -import blue.bex.value.BexBlueNodeWriter; -import blue.bex.value.BexValue; -import blue.bex.value.BexValues; -import blue.language.model.Node; -import blue.language.model.Schema; -import blue.language.registry.BlueCoreTypeRegistry; -import blue.language.runtime.BlueLanguage; -import blue.language.processor.ExecutionEvidenceUnavailableException; -import blue.language.processor.GasLimitExceededException; -import blue.language.processor.InvalidExecutionEvidenceException; -import blue.language.processor.PortableLimitExceededException; -import blue.language.processor.ProcessorFailureException; -import blue.language.snapshot.FrozenNode; -import blue.language.matching.FrozenTypeMatcher; - -import java.math.BigDecimal; -import java.math.BigInteger; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -/** - * BEX boundary adapter for Blue's node/type matcher. - */ -public final class BexBlueTypeMatcher { - private static final int TEXT_BLOCK_CODE_POINTS = 64; - private static final String TEXT_TYPE_BLUE_ID = - BlueCoreTypeRegistry.INSTANCE.blueId("Text"); - private static final String INTEGER_TYPE_BLUE_ID = - BlueCoreTypeRegistry.INSTANCE.blueId("Integer"); - private static final String DOUBLE_TYPE_BLUE_ID = - BlueCoreTypeRegistry.INSTANCE.blueId("Double"); - private static final String BOOLEAN_TYPE_BLUE_ID = - BlueCoreTypeRegistry.INSTANCE.blueId("Boolean"); - - private final BlueLanguage blue; - private final FrozenTypeMatcher matcher; - - public BexBlueTypeMatcher(BlueLanguage blue) { - this.blue = blue != null - ? blue : BlueLanguage.builder().build(); - this.matcher = FrozenTypeMatcher.withVerifiedReferenceMaterializer( - reference -> FrozenNode.fromResolvedNode( - BexValues.referenceBacked( - BexValues.frozen(reference), - this.blue) - .toNode())); - } - - /** - * Matches a BEX value at the Blue Language boundary while recording the - * canonical BEX comparison work. - * - *

The metering walk deliberately does not depend on - * {@link FrozenTypeMatcher}'s caches. Every semantic occurrence that the - * pattern compares is admitted before that recursive match is performed, - * so warm and cold executions have the same BEX trace.

- */ - public boolean matches(BexValue value, - FrozenNode pattern, - BexGasMeter gas, - BexSourcePath sourcePath) { - if (value == null || value.isUndefined()) { - return false; - } - if (pattern == null) { - return true; - } - MatchGas matchGas = new MatchGas(gas, sourcePath); - return matchesMetered( - value, - pattern, - matchGas, - CandidatePosition.ROOT); - } - - private static RuntimeException classifiedBoundaryFailure( - RuntimeException failure) { - Throwable current = failure; - while (current != null) { - if (current - instanceof ExecutionEvidenceUnavailableException - || current - instanceof InvalidExecutionEvidenceException - || current - instanceof ProcessorFailureException - || current - instanceof PortableLimitExceededException - || current - instanceof GasLimitExceededException - || current - instanceof BexGasLimitExceededException) { - return (RuntimeException) current; - } - Throwable cause = current.getCause(); - if (cause == current) { - break; - } - current = cause; - } - /* - * An unexpected cursor/provider failure is still an execution - * failure. It must never be converted into a semantic non-match. - */ - return failure; - } - - private boolean matchesMetered(FrozenNode candidate, - FrozenNode pattern, - MatchGas gas) { - gas.comparisonNode(); - return matchesFrozenAfterAdmission( - candidate, pattern, gas); - } - - private boolean matchesFrozenAfterAdmission( - FrozenNode candidate, - FrozenNode pattern, - MatchGas gas) { - if (pattern.isEmptyNode()) { - return true; - } - - /* - * A pure BlueId pattern is an identity/type check. It has no nested - * semantic occurrences to compare. - */ - if (pattern.isReferenceOnly()) { - return matcher.matchesType(candidate, pattern); - } - - if (!meterScalarComparison(candidate.getValue(), - pattern.getValue(), gas)) { - return false; - } - - if (!meterItemType(candidate, pattern.getItemType(), gas)) { - return false; - } - if (!meterKeyType(candidate, pattern.getKeyType(), gas)) { - return false; - } - if (!meterValueType(candidate, pattern.getValueType(), gas)) { - return false; - } - if (!meterItems(candidate, pattern.getItems(), gas)) { - return false; - } - if (!meterProperties(candidate, pattern.getProperties(), gas)) { - return false; - } - - /* - * Blue remains authoritative for declared-type, schema, subtype, and - * provider semantics. The canonical recursive BEX work above has - * already been admitted before this call can perform it. - */ - return matcher.matchesType(candidate, pattern); - } - - /** - * Walks a candidate cursor lazily. The current occurrence is admitted - * before any semantic cursor access, and a descendant is not converted or - * materialized until its own recursive admission succeeds. - */ - private boolean matchesMetered( - BexValue candidate, - FrozenNode pattern, - MatchGas gas, - CandidatePosition position) { - gas.comparisonNode(); - return matchesAfterAdmission( - candidate, pattern, gas, position); - } - - private boolean matchesAfterAdmission( - BexValue candidate, - FrozenNode pattern, - MatchGas gas, - CandidatePosition position) { - if (pattern.isEmptyNode()) { - return true; - } - if (candidate == null - || candidate.isUndefined()) { - return !requiresPresence(pattern); - } - - /* - * Reference-only matching has no nested BEX occurrences. Blue may - * therefore materialize the admitted current occurrence directly. - */ - if (pattern.isReferenceOnly()) { - if (candidate.isExact() - && pattern.getReferenceBlueId().equals( - candidate.exactBlueId())) { - return true; - } - /* - * A transient candidate must still be a valid local Blue shape - * before its identity can be compared. Invalid local content is - * a semantic non-match; provider and cursor failures raised while - * inspecting the admitted occurrence continue to propagate. - */ - if (!candidate.isExact()) { - CandidateView referenceView; - try { - referenceView = CandidateView.from( - candidate, position); - } catch (RuntimeException viewFailure) { - throw classifiedBoundaryFailure( - viewFailure); - } - if (!referenceView.valid) { - return false; - } - } - return matchesAuthoritatively( - candidate, pattern, position); - } - - CandidateView view; - try { - view = CandidateView.from( - candidate, position); - } catch (RuntimeException viewFailure) { - throw classifiedBoundaryFailure(viewFailure); - } - if (!view.valid) { - return false; - } - if (view.materializeCurrent) { - FrozenNode materialized = freezeCandidate( - candidate, position); - return materialized != null - && matchesFrozenAfterAdmission( - materialized, pattern, gas); - } - - if (!meterScalarComparison( - view.scalar, pattern.getValue(), gas)) { - return false; - } - if (!meterItemType( - view, pattern.getItemType(), gas)) { - return false; - } - if (!meterKeyType( - view, pattern.getKeyType(), gas)) { - return false; - } - if (!meterValueType( - view, pattern.getValueType(), gas)) { - return false; - } - if (!meterItems( - view, pattern.getItems(), gas)) { - return false; - } - if (!meterProperties( - view, pattern.getProperties(), gas)) { - return false; - } - return matchesAuthoritatively( - candidate, pattern, position); - } - - private boolean matchesAuthoritatively( - BexValue candidate, - FrozenNode pattern, - CandidatePosition position) { - FrozenNode frozen = freezeCandidate( - candidate, position); - return frozen != null - && matcher.matchesType(frozen, pattern); - } - - private FrozenNode freezeCandidate( - BexValue candidate, - CandidatePosition position) { - try { - if (candidate.isExact()) { - return FrozenNode.fromResolvedNode( - candidate.toNode()); - } - if (position == CandidatePosition.ROOT) { - return FrozenNode.fromResolvedNode( - BexBlueNodeWriter.toSemanticNode( - candidate)); - } - if (position == CandidatePosition.LIST_ITEM) { - BexValue wrapper = BexValues.list( - Collections.singletonList(candidate)); - FrozenNode frozenWrapper = - FrozenNode.fromResolvedNode( - BexBlueNodeWriter - .toSemanticNode(wrapper)); - return frozenWrapper.getItems().get(0); - } - Map member = - Collections.singletonMap( - "_bexCandidate", candidate); - FrozenNode frozenWrapper = - FrozenNode.fromResolvedNode( - BexBlueNodeWriter.toSemanticNode( - BexValues.map(member))); - return frozenWrapper.getProperties().get( - "_bexCandidate"); - } catch (RuntimeException conversionFailure) { - throw classifiedBoundaryFailure( - conversionFailure); - } - } - - private boolean meterItemType(FrozenNode candidate, - FrozenNode targetItemType, - MatchGas gas) { - if (targetItemType == null) { - return true; - } - List items = candidate.getItems(); - if (items == null) { - return true; - } - for (FrozenNode item : items) { - if (!matchesMetered(item, targetItemType, gas)) { - return false; - } - } - return true; - } - - private boolean meterItemType( - CandidateView candidate, - FrozenNode targetItemType, - MatchGas gas) { - if (targetItemType == null - || candidate.items == null) { - return true; - } - for (int index = 0; - index < candidate.items.size(); - index++) { - gas.comparisonNode(); - BexValue item = candidate.items.get( - String.valueOf(index)); - if (!matchesAfterAdmission( - item, - targetItemType, - gas, - CandidatePosition.LIST_ITEM)) { - return false; - } - } - return true; - } - - private boolean meterKeyType(FrozenNode candidate, - FrozenNode targetKeyType, - MatchGas gas) { - if (targetKeyType == null || candidate.getProperties() == null) { - return true; - } - for (String key - : candidate.getProperties().keySet()) { - gas.comparisonNode(); - if (!keyMatchesType(key, targetKeyType)) { - return false; - } - } - return true; - } - - private boolean meterKeyType( - CandidateView candidate, - FrozenNode targetKeyType, - MatchGas gas) { - if (targetKeyType == null) { - return true; - } - for (String key : candidate.propertyKeys) { - gas.comparisonNode(); - if (!keyMatchesType(key, targetKeyType)) { - return false; - } - } - return true; - } - - private boolean meterValueType(FrozenNode candidate, - FrozenNode targetValueType, - MatchGas gas) { - if (targetValueType == null || candidate.getProperties() == null) { - return true; - } - for (String key - : candidate.getProperties().keySet()) { - if (!matchesMetered( - candidate.getProperties().get(key), - targetValueType, - gas)) { - return false; - } - } - return true; - } - - private boolean meterValueType( - CandidateView candidate, - FrozenNode targetValueType, - MatchGas gas) { - if (targetValueType == null) { - return true; - } - for (String key : candidate.propertyKeys) { - gas.comparisonNode(); - BexValue property = - candidate.source.get(key); - if (!matchesAfterAdmission( - property, - targetValueType, - gas, - CandidatePosition.OBJECT_MEMBER)) { - return false; - } - } - return true; - } - - private boolean meterItems(FrozenNode candidate, - List targetItems, - MatchGas gas) { - if (targetItems == null) { - return true; - } - List candidateItems = candidate.getItems() != null - ? candidate.getItems() - : Collections.emptyList(); - for (int index = 0; index < targetItems.size(); index++) { - FrozenNode targetItem = targetItems.get(index); - if (index < candidateItems.size()) { - if (!matchesMetered( - candidateItems.get(index), targetItem, gas)) { - return false; - } - } else if (requiresPresence(targetItem)) { - return false; - } - } - return true; - } - - private boolean meterItems( - CandidateView candidate, - List targetItems, - MatchGas gas) { - if (targetItems == null) { - return true; - } - int candidateSize = candidate.items != null - ? candidate.items.size() - : 0; - for (int index = 0; - index < targetItems.size(); - index++) { - FrozenNode targetItem = - targetItems.get(index); - if (index < candidateSize) { - gas.comparisonNode(); - BexValue item = candidate.items.get( - String.valueOf(index)); - if (!matchesAfterAdmission( - item, - targetItem, - gas, - CandidatePosition.LIST_ITEM)) { - return false; - } - } else if (requiresPresence(targetItem)) { - return false; - } - } - return true; - } - - private boolean meterProperties( - FrozenNode candidate, - Map targetProperties, - MatchGas gas) { - if (targetProperties == null) { - return true; - } - Map candidateProperties = - candidate.getProperties() != null - ? candidate.getProperties() - : Collections.emptyMap(); - for (Map.Entry candidateEntry - : candidateProperties.entrySet()) { - String key = candidateEntry.getKey(); - FrozenNode targetProperty = - targetProperties.get(key); - if (targetProperty != null) { - if (!matchesMetered( - candidateEntry.getValue(), - targetProperty, - gas)) { - return false; - } - } - } - for (Map.Entry targetEntry - : targetProperties.entrySet()) { - if (!candidateProperties.containsKey( - targetEntry.getKey()) - && requiresPresence( - targetEntry.getValue())) { - return false; - } - } - return true; - } - - private boolean meterProperties( - CandidateView candidate, - Map targetProperties, - MatchGas gas) { - if (targetProperties == null) { - return true; - } - for (String key : candidate.propertyKeys) { - FrozenNode targetProperty = - targetProperties.get(key); - if (targetProperty != null) { - gas.comparisonNode(); - BexValue candidateProperty = - candidate.source.get(key); - if (!matchesAfterAdmission( - candidateProperty, - targetProperty, - gas, - CandidatePosition.OBJECT_MEMBER)) { - return false; - } - } - } - for (Map.Entry targetEntry - : targetProperties.entrySet()) { - if (!candidate.hasProperty( - targetEntry.getKey()) - && requiresPresence( - targetEntry.getValue())) { - return false; - } - } - return true; - } - - private boolean meterScalarComparison(Object candidate, - Object target, - MatchGas gas) { - if (target == null) { - return true; - } - if (candidate == null) { - return false; - } - if (candidate instanceof Number && target instanceof Number) { - gas.integerLimbs( - integerLimbs(unscaled(candidate)) - + integerLimbs(unscaled(target)) - + (candidate instanceof BigDecimal - || target instanceof BigDecimal ? 1L : 0L)); - return number(candidate).compareTo(number(target)) == 0; - } - if (candidate instanceof String && target instanceof String) { - return gas.compareText( - (String) candidate, - (String) target) == 0; - } - return candidate.equals(target); - } - - private static boolean isOrdinaryProperty( - String key) { - return !"name".equals(key) - && !"description".equals(key) - && !"type".equals(key) - && !"itemType".equals(key) - && !"keyType".equals(key) - && !"valueType".equals(key) - && !"mergePolicy".equals(key) - && !"value".equals(key) - && !"items".equals(key) - && !"blueId".equals(key) - && !"contracts".equals(key) - && !"schema".equals(key) - && !isForbiddenField(key); - } - - private static boolean isForbiddenField( - String key) { - return "properties".equals(key) - || "constraints".equals(key) - || "allowMultiple".equals(key) - || "options".equals(key) - || "blue".equals(key) - || "$previous".equals(key) - || "$pos".equals(key) - || "$replace".equals(key) - || "$empty".equals(key); - } - - private enum CandidatePosition { - ROOT, - OBJECT_MEMBER, - LIST_ITEM - } - - /** - * Current-node-only view of the transient Blue conversion contract. - * Descendant values remain as cursors and are not converted here. - */ - private static final class CandidateView { - private final BexValue source; - private final Object scalar; - private final BexValue items; - private final List propertyKeys; - private final boolean materializeCurrent; - private final boolean valid; - - private CandidateView( - BexValue source, - Object scalar, - BexValue items, - List propertyKeys, - boolean materializeCurrent, - boolean valid) { - this.source = source; - this.scalar = scalar; - this.items = items; - this.propertyKeys = propertyKeys; - this.materializeCurrent = - materializeCurrent; - this.valid = valid; - } - - private static CandidateView from( - BexValue source, - CandidatePosition position) { - if (source == null || source.isUndefined()) { - return invalid(source); - } - if (source.isNull()) { - return empty(source); - } - if (source.isScalar()) { - return new CandidateView( - source, - source.toSimple(), - null, - Collections.emptyList(), - false, - true); - } - if (source.isList()) { - return new CandidateView( - source, - null, - source, - Collections.emptyList(), - false, - true); - } - if (!source.isObject()) { - return invalid(source); - } - - List keys = source.keys(); - if (isEmptyPlaceholder(source, keys)) { - return position == CandidatePosition.LIST_ITEM - ? empty(source) - : invalid(source); - } - - boolean hasBlueId = false; - boolean hasValue = false; - boolean hasItems = false; - int retainedFields = 0; - BexValue items = null; - ArrayList properties = - new ArrayList<>(); - for (String key : keys) { - retainedFields++; - if (isForbiddenField(key)) { - return invalid(source); - } - if ("blueId".equals(key)) { - hasBlueId = true; - } else if ("value".equals(key)) { - BexValue child = source.get(key); - hasValue = true; - if (child == null - || child.isUndefined() - || child.isNull() - || !child.isScalar()) { - return invalid(source); - } - } else if ("items".equals(key)) { - BexValue child = source.get(key); - hasItems = true; - if (child == null - || child.isUndefined() - || !child.isList()) { - return invalid(source); - } - items = child; - } else if (isOrdinaryProperty(key)) { - properties.add(key); - } - } - - if (hasBlueId) { - return retainedFields == 1 - ? materialized(source) - : invalid(source); - } - int payloadKinds = (hasValue ? 1 : 0) - + (hasItems ? 1 : 0) - + (!properties.isEmpty() ? 1 : 0); - if (payloadKinds > 1) { - return invalid(source); - } - /* - * Explicit scalar types normalize their raw value. Materializing - * this already-admitted current occurrence is the faithful, - * bounded way to obtain that scalar without touching any payload - * descendants (a scalar has none). - */ - if (hasValue) { - return materialized(source); - } - return new CandidateView( - source, - null, - items, - Collections.unmodifiableList( - properties), - false, - true); - } - - private boolean hasProperty(String key) { - return isOrdinaryProperty(key) - && propertyKeys.contains(key); - } - - private static CandidateView empty( - BexValue source) { - return new CandidateView( - source, - null, - null, - Collections.emptyList(), - false, - true); - } - - private static CandidateView materialized( - BexValue source) { - return new CandidateView( - source, - null, - null, - Collections.emptyList(), - true, - true); - } - - private static CandidateView invalid( - BexValue source) { - return new CandidateView( - source, - null, - null, - Collections.emptyList(), - false, - false); - } - - private static boolean isEmptyPlaceholder( - BexValue source, - List keys) { - if (keys.size() != 1 - || !"$empty".equals(keys.get(0))) { - return false; - } - BexValue marker = source.get("$empty"); - return marker != null - && marker.isScalar() - && Boolean.TRUE.equals( - marker.toSimple()); - } - } - - private boolean requiresPresence(FrozenNode target) { - Schema schema = target.getSchema(); - if (schema != null && Boolean.TRUE.equals( - schema.getRequiredValue())) { - return true; - } - return hasValueInNestedStructure(target); - } - - private boolean hasValueInNestedStructure(FrozenNode node) { - if (node.isReferenceOnly() || node.getValue() != null) { - return true; - } - if (node.getItems() != null) { - for (FrozenNode item : node.getItems()) { - if (hasValueInNestedStructure(item)) { - return true; - } - } - } - if (node.getProperties() != null) { - for (FrozenNode property : node.getProperties().values()) { - if (hasValueInNestedStructure(property)) { - return true; - } - } - } - return false; - } - - private boolean keyMatchesType(String key, FrozenNode targetKeyType) { - String identity = targetKeyType.getReferenceBlueId(); - if (identity == null && targetKeyType.getType() != null) { - identity = targetKeyType.getType().getReferenceBlueId(); - } - if (TEXT_TYPE_BLUE_ID.equals(identity)) { - return true; - } - if (INTEGER_TYPE_BLUE_ID.equals(identity)) { - try { - new BigInteger(key); - return true; - } catch (NumberFormatException invalidInteger) { - return false; - } - } - if (DOUBLE_TYPE_BLUE_ID.equals(identity)) { - try { - return Double.isFinite(Double.parseDouble(key)); - } catch (NumberFormatException invalidDouble) { - return false; - } - } - if (BOOLEAN_TYPE_BLUE_ID.equals(identity)) { - return "true".equalsIgnoreCase(key) - || "false".equalsIgnoreCase(key); - } - return false; - } - - private static BigDecimal number(Object value) { - if (value instanceof BigDecimal) { - return (BigDecimal) value; - } - if (value instanceof BigInteger) { - return new BigDecimal((BigInteger) value); - } - return new BigDecimal(value.toString()); - } - - private static BigInteger unscaled(Object value) { - return number(value).unscaledValue(); - } - - private static long integerLimbs(BigInteger value) { - int bits = value.abs().bitLength(); - return Math.max(1L, (bits + 31L) / 32L); - } - - private static final class MatchGas { - private final BexGasMeter gas; - private final BexSourcePath sourcePath; - - private MatchGas(BexGasMeter gas, BexSourcePath sourcePath) { - this.gas = java.util.Objects.requireNonNull(gas, "gas"); - this.sourcePath = sourcePath; - } - - private void comparisonNode() { - charge(BexGasCounter.COMPARISON_NODE_VISITED, 1L); - } - - private void textBlocks(long quantity) { - charge(BexGasCounter.TEXT_BLOCK_EXAMINED, quantity); - } - - private void integerLimbs(long quantity) { - charge(BexGasCounter.INTEGER_LIMB_OPERATION, quantity); - } - - /** - * Canonical comparison with each pair of 64-code-point blocks - * admitted before either block is inspected. - */ - private int compareText( - String left, - String right) { - if (left == right) { - return 0; - } - if (left == null) { - return -1; - } - if (right == null) { - return 1; - } - int leftOffset = 0; - int rightOffset = 0; - while (leftOffset < left.length() - && rightOffset < right.length()) { - textBlocks(2L); - int inBlock = 0; - while (inBlock - < TEXT_BLOCK_CODE_POINTS - && leftOffset < left.length() - && rightOffset < right.length()) { - int leftCodePoint = - left.codePointAt(leftOffset); - int rightCodePoint = - right.codePointAt(rightOffset); - leftOffset += Character.charCount( - leftCodePoint); - rightOffset += Character.charCount( - rightCodePoint); - if (leftCodePoint - != rightCodePoint) { - return Integer.compare( - leftCodePoint, - rightCodePoint); - } - inBlock++; - } - } - return Integer.compare( - left.length() - leftOffset, - right.length() - rightOffset); - } - - private void charge(BexGasCounter counter, long quantity) { - if (quantity <= 0L) { - return; - } - gas.charge( - counter, - quantity, - sourcePath, - sourcePath != null ? sourcePath.operator() : null, - counter.canonicalName()); - } - } -} diff --git a/src/test/java/blue/bex/BexBlueTypeMatchingGasTest.java b/src/test/java/blue/bex/BexBlueTypeMatchingGasTest.java index 49c1c3f..8789d3c 100644 --- a/src/test/java/blue/bex/BexBlueTypeMatchingGasTest.java +++ b/src/test/java/blue/bex/BexBlueTypeMatchingGasTest.java @@ -16,7 +16,6 @@ import blue.language.provider.NodeProvider; import blue.language.model.Node; import blue.language.processor.ExecutionEvidenceUnavailableException; -import blue.language.processor.InvalidExecutionEvidenceException; import blue.language.processor.ProcessorErrorCategory; import blue.language.processor.ProcessorFailureException; import blue.language.provider.NodeProviderResult; @@ -208,9 +207,9 @@ void unavailableNestedExactEvidenceIsNotConvertedToATypeMismatch() { BexGasMeter meter = new BexGasMeter( BexGasSchedule.defaults(), 1_000_000L); - ExecutionEvidenceUnavailableException failure = + BexExecutionEvidenceUnavailableException failure = assertThrows( - ExecutionEvidenceUnavailableException.class, + BexExecutionEvidenceUnavailableException.class, () -> new BexBlueTypeMatcher( unavailableBlue.runtime()) .matches( @@ -252,9 +251,9 @@ void invalidNestedExactEvidenceIsNotConvertedToATypeMismatch() { BexGasMeter meter = new BexGasMeter( BexGasSchedule.defaults(), 1_000_000L); - InvalidExecutionEvidenceException failure = + BexInvalidExecutionEvidenceException failure = assertThrows( - InvalidExecutionEvidenceException.class, + BexInvalidExecutionEvidenceException.class, () -> new BexBlueTypeMatcher( invalidBlue.runtime()) .matches( diff --git a/src/test/java/blue/bex/BexCompositeExhaustionEvidenceTest.java b/src/test/java/blue/bex/BexCompositeExhaustionEvidenceTest.java index cf201e8..6e2cd4c 100644 --- a/src/test/java/blue/bex/BexCompositeExhaustionEvidenceTest.java +++ b/src/test/java/blue/bex/BexCompositeExhaustionEvidenceTest.java @@ -10,16 +10,20 @@ import blue.bex.gas.BexGasCharge; import blue.bex.gas.BexGasCounter; import blue.bex.gas.BexGasLimitExceededException; +import blue.bex.gas.BexGasLedgerCapability; +import blue.bex.gas.BexHostGasExhaustion; import blue.bex.gas.BexGasSchedule; import blue.bex.output.BexEstablishedIdentity; import blue.bex.output.BexSemanticIdentityBoundary; import blue.bex.pointer.BexPointerCache; import blue.bex.result.BexExecutionResult; -import blue.bex.result.BexMetrics; +import blue.bex.result.BexMetricsRecorder; +import blue.bex.result.BexMetricsSnapshot; import blue.bex.runtime.BexRuntime; import blue.bex.value.BexValue; import blue.bex.value.BexValues; import blue.bex.test.TestBlue; +import blue.bex.test.TestGasLedgerCapability; import blue.language.provider.NodeProvider; import blue.language.model.Node; import blue.language.processor.GasLimitExceededException; @@ -81,9 +85,9 @@ void largeFiniteForEachStopsBeforeRejectedIterationAndIsColdWarmStable() { sentinelEvent(), op("$return", true)))); - List compileMetrics = new ArrayList<>(); + List compileMetrics = new ArrayList<>(); BexEngine warmEngine = BexEngine.builder() - .metrics(metrics -> compileMetrics.add(metrics.copy())) + .metrics(compileMetrics::add) .build(); BexCompiledProgram firstCompilation = warmEngine.compile(source); BexCompiledProgram cachedCompilation = warmEngine.compile(source); @@ -486,7 +490,7 @@ void rejectedPatchAppendDoesNotMutateChangesetOrOverlay() { target.prefixGas), blue.runtime(), BexGasSchedule.defaults(), - new BexMetrics(), + new BexMetricsRecorder(), new BexPointerCache(), BexIntrinsicRegistry.empty()); @@ -768,7 +772,7 @@ private static LimitedEvidence assertRejectedAtPrefix( failure.admittedGas()); assertEquals(target.prefixGas, failure.effectiveBudget()); - assertNull(failure.hostGasLimitExceeded(), + assertNull(failure.hostGasExhaustion(), "the stricter local sub-limit must reject before " + "touching the host ledger"); @@ -1099,7 +1103,7 @@ private static final class RecordingGasHost implements BexGasLedgerHost { private final GasMeter parent = new GasMeter(GasSchedule.contracts10()); - private final Map + private final Map openedLedgers = new IdentityHashMap<>(); private int successfulSubmissions; private int deterministicFailures; @@ -1107,11 +1111,11 @@ private static final class RecordingGasHost private int exhaustionPropagations; @Override - public GasMeter.ChildGasLedger open( + public BexGasLedgerCapability open( String namespace, Map counterWeights) { - GasMeter.ChildGasLedger ledger = - parent.childLedger(namespace, counterWeights); + BexGasLedgerCapability ledger = TestGasLedgerCapability.wrap( + parent.childLedger(namespace, counterWeights)); openedLedgers.put(ledger, Boolean.TRUE); return ledger; } @@ -1122,42 +1126,42 @@ public boolean separatesRuntimeNamespaces() { } @Override - public void submit(GasMeter.ChildGasLedger ledger) { + public void submit(BexGasLedgerCapability ledger) { successfulSubmissions++; mergeOwned(ledger); } @Override public void failedDeterministically( - GasMeter.ChildGasLedger ledger) { + BexGasLedgerCapability ledger) { deterministicFailures++; mergeOwned(ledger); } @Override public void evidenceUnavailable( - GasMeter.ChildGasLedger ledger) { + BexGasLedgerCapability ledger) { unavailableFinalizations++; requireOwned(ledger); } @Override public void propagateGasExhaustion( - GasMeter.ChildGasLedger ledger, - GasLimitExceededException exhaustion) { + BexGasLedgerCapability ledger, + BexHostGasExhaustion exhaustion) { exhaustionPropagations++; requireOwned(ledger); - throw exhaustion; + throw exhaustion.hostFailure(); } private void mergeOwned( - GasMeter.ChildGasLedger ledger) { + BexGasLedgerCapability ledger) { requireOwned(ledger); - parent.merge(ledger); + parent.merge(((TestGasLedgerCapability) ledger).delegate()); } private void requireOwned( - GasMeter.ChildGasLedger ledger) { + BexGasLedgerCapability ledger) { assertTrue(openedLedgers.containsKey(ledger), "host may finalize only a ledger it opened"); } diff --git a/src/test/java/blue/bex/BexConcurrentEngineIsolationTest.java b/src/test/java/blue/bex/BexConcurrentEngineIsolationTest.java new file mode 100644 index 0000000..59bae5a --- /dev/null +++ b/src/test/java/blue/bex/BexConcurrentEngineIsolationTest.java @@ -0,0 +1,179 @@ +package blue.bex; + +import blue.bex.api.BexEngine; +import blue.bex.api.BexExecutionContext; +import blue.bex.api.BexProgramSource; +import blue.bex.compile.LruBexCompiledProgramCache; +import blue.bex.gas.BexGasLedger; +import blue.bex.result.BexExecutionResult; +import blue.bex.result.BexMetricsSnapshot; +import blue.bex.value.BexValues; +import org.junit.jupiter.api.Test; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import static blue.bex.test.BexTestFixtures.defaultDocumentView; +import static blue.bex.test.BexTestFixtures.frozen; +import static blue.bex.test.BexTestFixtures.list; +import static blue.bex.test.BexTestFixtures.m; +import static blue.bex.test.BexTestFixtures.obj; +import static blue.bex.test.BexTestFixtures.op; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** Shared-engine concurrency proof for cache, context, result, metric and gas isolation. */ +class BexConcurrentEngineIsolationTest { + private static final int TASKS = 48; + + @Test + void oneColdEngineIsolatesConcurrentProgramsAndContexts() + throws Exception { + ConcurrentLinkedQueue deliveredMetrics = + new ConcurrentLinkedQueue(); + BexEngine shared = BexEngine.builder() + .cache(new LruBexCompiledProgramCache()) + .metrics(deliveredMetrics::add) + .build(); + BexProgramSource richSource = richSource(); + BexProgramSource textSource = textSource(); + + BexExecutionResult richReference = BexEngine.builder().build() + .compileAndExecute(richSource, context(0, new AtomicInteger())); + BexExecutionResult textReference = BexEngine.builder().build() + .compileAndExecute(textSource, context(0, new AtomicInteger())); + BexGasLedger richGas = richReference.gasLedger(); + BexGasLedger textGas = textReference.gasLedger(); + + ExecutorService executor = Executors.newFixedThreadPool(8); + CountDownLatch start = new CountDownLatch(1); + List> futures = + new ArrayList>(TASKS); + try { + for (int index = 0; index < TASKS; index++) { + final int taskIndex = index; + final boolean rich = (index & 1) == 0; + futures.add(executor.submit(() -> { + AtomicInteger lazyCalls = new AtomicInteger(); + start.await(); + BexExecutionResult result = shared.compileAndExecute( + rich ? richSource : textSource, + context(taskIndex, lazyCalls)); + return new Observation( + taskIndex, + rich, + lazyCalls.get(), + result); + })); + } + start.countDown(); + + for (Future future : futures) { + Observation observed = future.get(30L, TimeUnit.SECONDS); + if (observed.rich) { + assertEquals(1, observed.lazyCalls, + "lazy binding must be memoized per context"); + assertEquals(expectedRich(observed.index), + observed.result.value().toSimple()); + assertEquals(richGas, observed.result.gasLedger()); + assertEquals( + richReference.metrics().expressionEvaluations(), + observed.result.metrics().expressionEvaluations()); + } else { + assertEquals(0, observed.lazyCalls, + "unread lazy binding leaked across contexts"); + assertEquals("task-" + observed.index + "!", + observed.result.value().toSimple()); + assertEquals(textGas, observed.result.gasLedger()); + assertEquals( + textReference.metrics().expressionEvaluations(), + observed.result.metrics().expressionEvaluations()); + } + assertTrue(observed.result.changeset().entries().isEmpty()); + assertTrue(observed.result.events().events().isEmpty()); + assertFalse(observed.result.output().nodeBlueId() + .trim().isEmpty()); + } + } finally { + executor.shutdownNow(); + assertTrue(executor.awaitTermination( + 10L, TimeUnit.SECONDS)); + } + + assertEquals(TASKS, deliveredMetrics.size()); + assertFalse(deliveredMetrics.contains(null)); + } + + private static BexProgramSource richSource() { + return BexProgramSource.expression(frozen(obj( + "task", op("$binding", "task"), + "lazyFirst", op("$binding", "lazy"), + "lazySecond", op("$binding", "lazy"), + "sum", op("$add", list( + op("$binding", "number"), + 1)), + "pointer", op("$pointerGet", obj( + "object", op("$binding", "payload"), + "path", "/nested/value"))))); + } + + private static BexProgramSource textSource() { + return BexProgramSource.expression(frozen(op( + "$concat", + list(op("$binding", "task"), "!")))); + } + + private static BexExecutionContext context( + int index, + AtomicInteger lazyCalls) { + return BexExecutionContext.builder() + .document(defaultDocumentView()) + .binding("task", BexValues.scalar("task-" + index)) + .binding("number", BexValues.scalar(index)) + .binding("payload", BexValues.fromSimple(m( + "nested", m("value", "payload-" + index)))) + .lazyBinding("lazy", () -> { + lazyCalls.incrementAndGet(); + return BexValues.scalar("lazy-" + index); + }) + .gasLimit(1_000_000L) + .build(); + } + + private static Map expectedRich(int index) { + return m( + "lazyFirst", "lazy-" + index, + "lazySecond", "lazy-" + index, + "pointer", "payload-" + index, + "sum", BigInteger.valueOf(index + 1L), + "task", "task-" + index); + } + + private static final class Observation { + private final int index; + private final boolean rich; + private final int lazyCalls; + private final BexExecutionResult result; + + private Observation( + int index, + boolean rich, + int lazyCalls, + BexExecutionResult result) { + this.index = index; + this.rich = rich; + this.lazyCalls = lazyCalls; + this.result = result; + } + } +} diff --git a/src/test/java/blue/bex/BexDependencyBoundaryTest.java b/src/test/java/blue/bex/BexDependencyBoundaryTest.java index f873c4e..9beb6f0 100644 --- a/src/test/java/blue/bex/BexDependencyBoundaryTest.java +++ b/src/test/java/blue/bex/BexDependencyBoundaryTest.java @@ -20,16 +20,26 @@ void mainSourcesHaveNoContractQuickJsWasmOrProductSpecificReferences() throws Ex "package-linked", "processCustomerPayNote", "reseller-weekend-package", "myos" ); StringBuilder scanned = new StringBuilder(); - Files.walk(root.resolve("src/main/java")) - .filter(path -> path.toString().endsWith(".java")) - .forEach(path -> { - try { - scanned.append(new String(Files.readAllBytes(path), StandardCharsets.UTF_8)).append('\n'); - } catch (Exception ex) { - throw new RuntimeException(ex); - } - }); - scanned.append(new String(Files.readAllBytes(root.resolve("build.gradle.kts")), StandardCharsets.UTF_8)); + for (String module : Arrays.asList( + "blue-bex-core", "blue-bex-contracts", "blue-bex-java")) { + Path sourceRoot = root.resolve(module).resolve("src/main/java"); + try (java.util.stream.Stream paths = Files.walk(sourceRoot)) { + paths.filter(path -> path.toString().endsWith(".java")) + .forEach(path -> { + try { + scanned.append(new String( + Files.readAllBytes(path), + StandardCharsets.UTF_8)).append('\n'); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + }); + } + scanned.append(new String( + Files.readAllBytes(root.resolve(module) + .resolve("build.gradle.kts")), + StandardCharsets.UTF_8)); + } for (String value : forbidden) { assertFalse(scanned.toString().contains(value), "Forbidden dependency/reference found: " + value); diff --git a/src/test/java/blue/bex/BexDiagnosticAdmissionOrderingTest.java b/src/test/java/blue/bex/BexDiagnosticAdmissionOrderingTest.java index 123648e..835578f 100644 --- a/src/test/java/blue/bex/BexDiagnosticAdmissionOrderingTest.java +++ b/src/test/java/blue/bex/BexDiagnosticAdmissionOrderingTest.java @@ -11,7 +11,7 @@ import blue.bex.gas.BexGasSchedule; import blue.bex.pointer.BexPointerCache; import blue.bex.result.BexExecutionResult; -import blue.bex.result.BexMetrics; +import blue.bex.result.BexMetricsRecorder; import blue.bex.runtime.BexRuntime; import blue.bex.test.TestBlue; import blue.language.model.Node; @@ -86,27 +86,27 @@ void rejectedReadChargesDoNotMutateReadMetrics() { assertRejectedReadMetric( op("$document", "/"), BexGasCounter.DOCUMENT_READ, - BexMetrics::frozenDocumentReads); + BexMetricsRecorder::frozenDocumentReads); assertRejectedReadMetric( op("$document", obj("path", "/", "view", "resolved")), BexGasCounter.DOCUMENT_READ, - BexMetrics::resolvedDocumentReads); + BexMetricsRecorder::resolvedDocumentReads); assertRejectedReadMetric( op("$event", "/"), BexGasCounter.EVENT_READ, - BexMetrics::eventReads); + BexMetricsRecorder::eventReads); assertRejectedReadMetric( op("$currentContract", "/"), BexGasCounter.CURRENT_CONTRACT_READ, - BexMetrics::currentContractReads); + BexMetricsRecorder::currentContractReads); assertRejectedReadMetric( op("$steps", obj("step", "Build", "path", "/")), BexGasCounter.STEPS_READ, - BexMetrics::stepsReads); + BexMetricsRecorder::stepsReads); assertRejectedReadMetric( op("$resultValue", "/"), BexGasCounter.RESULT_VALUE_READ, - BexMetrics::resultValueReads); + BexMetricsRecorder::resultValueReads); } @Test @@ -138,7 +138,7 @@ void rejectedIterationReadDoesNotAddAnUnfinishedLoopMetric() { BexGasCounter.LIST_ITEM_READ, "$forEach"); - BexMetrics metrics = new BexMetrics(); + BexMetricsRecorder metrics = new BexMetricsRecorder(); BexRuntime runtime = new BexRuntime( program, context(prefixGas), @@ -166,7 +166,7 @@ void rejectedIterationReadDoesNotAddAnUnfinishedLoopMetric() { private static void assertRejectedReadMetric( Node expression, BexGasCounter expectedCounter, - ToLongFunction metric) { + ToLongFunction metric) { RejectedExecution rejected = reject( stepExpr(expression), 3L); @@ -183,7 +183,7 @@ private static RejectedExecution reject( .build(); BexCompiledProgram program = engine.compile( BexProgramSource.inline(frozen(programNode))); - BexMetrics metrics = new BexMetrics(); + BexMetricsRecorder metrics = new BexMetricsRecorder(); BexRuntime runtime = new BexRuntime( program, context(gasLimit), @@ -267,11 +267,11 @@ private static long gas(List trace) { private static final class RejectedExecution { private final BexGasLimitExceededException failure; - private final BexMetrics metrics; + private final BexMetricsRecorder metrics; private RejectedExecution( BexGasLimitExceededException failure, - BexMetrics metrics) { + BexMetricsRecorder metrics) { this.failure = failure; this.metrics = metrics; } diff --git a/src/test/java/blue/bex/BexExactGasRuleTest.java b/src/test/java/blue/bex/BexExactGasRuleTest.java index b7bb29e..9e07874 100644 --- a/src/test/java/blue/bex/BexExactGasRuleTest.java +++ b/src/test/java/blue/bex/BexExactGasRuleTest.java @@ -7,8 +7,10 @@ import blue.bex.api.FrozenBexDocumentView; import blue.bex.gas.BexGasCharge; import blue.bex.gas.BexGasCounter; +import blue.bex.gas.BexGasLedgerCapability; import blue.bex.output.BexSemanticIdentityBoundary; import blue.bex.result.BexExecutionResult; +import blue.bex.test.TestGasLedgerCapability; import blue.language.model.Node; import blue.language.processor.GasMeter; import blue.language.processor.GasSchedule; @@ -296,29 +298,30 @@ private static final class RecordingGasHost implements BexGasLedgerHost { private final GasMeter parent = new GasMeter(GasSchedule.contracts10()); - private GasMeter.ChildGasLedger child; + private BexGasLedgerCapability child; @Override - public GasMeter.ChildGasLedger open( + public BexGasLedgerCapability open( String namespace, Map counterWeights) { - child = parent.childLedger(namespace, counterWeights); + child = TestGasLedgerCapability.wrap( + parent.childLedger(namespace, counterWeights)); return child; } @Override - public void submit(GasMeter.ChildGasLedger ledger) { - parent.merge(ledger); + public void submit(BexGasLedgerCapability ledger) { + parent.merge(((TestGasLedgerCapability) ledger).delegate()); } @Override public void failedDeterministically( - GasMeter.ChildGasLedger ledger) { - parent.merge(ledger); + BexGasLedgerCapability ledger) { + submit(ledger); } @Override public void evidenceUnavailable( - GasMeter.ChildGasLedger ledger) { + BexGasLedgerCapability ledger) { // Detached child ledgers reserve nothing until merge. } diff --git a/src/test/java/blue/bex/BexExactReferenceDocumentTest.java b/src/test/java/blue/bex/BexExactReferenceDocumentTest.java index 035ff87..230bf69 100644 --- a/src/test/java/blue/bex/BexExactReferenceDocumentTest.java +++ b/src/test/java/blue/bex/BexExactReferenceDocumentTest.java @@ -9,8 +9,6 @@ import blue.bex.test.TestBlue; import blue.language.provider.NodeProvider; import blue.language.model.Node; -import blue.language.processor.ExecutionEvidenceUnavailableException; -import blue.language.processor.InvalidExecutionEvidenceException; import blue.language.provider.CyclicAwareNodeProvider; import blue.language.provider.CyclicSetProof; import blue.language.provider.CyclicSetProofResult; @@ -225,7 +223,7 @@ void cyclicMemberStructuralReadRequiresAndAcceptsCompleteSetProof() { assertEquals(memberBlueId, prooflessMember.exactBlueId()); assertThrows( - InvalidExecutionEvidenceException.class, + BexInvalidExecutionEvidenceException.class, prooflessMember::isObject); } @@ -331,9 +329,9 @@ void cyclicMemberContentFetchUnavailabilityStopsBeforeProofQuery() { assertEquals(fixture.memberBlueId, member.exactBlueId()); - ExecutionEvidenceUnavailableException failure = + BexExecutionEvidenceUnavailableException failure = assertThrows( - ExecutionEvidenceUnavailableException.class, + BexExecutionEvidenceUnavailableException.class, member::isObject); assertEquals( @@ -363,9 +361,9 @@ void nullCyclicProofAfterFoundContentIsInvalidNotUnavailable() { assertEquals(fixture.memberBlueId, member.exactBlueId()); - InvalidExecutionEvidenceException failure = + BexInvalidExecutionEvidenceException failure = assertThrows( - InvalidExecutionEvidenceException.class, + BexInvalidExecutionEvidenceException.class, member::isObject); assertTrue(failure.getMessage().contains( @@ -391,9 +389,9 @@ void cyclicProofUnavailabilityAfterFoundContentRemainsTransient() { assertEquals(fixture.memberBlueId, member.exactBlueId()); - ExecutionEvidenceUnavailableException failure = + BexExecutionEvidenceUnavailableException failure = assertThrows( - ExecutionEvidenceUnavailableException.class, + BexExecutionEvidenceUnavailableException.class, member::isObject); assertEquals( @@ -431,9 +429,9 @@ void malformedCyclicProofIsDeterministicInvalidEvidence() { assertEquals(fixture.memberBlueId, member.exactBlueId()); - InvalidExecutionEvidenceException failure = + BexInvalidExecutionEvidenceException failure = assertThrows( - InvalidExecutionEvidenceException.class, + BexInvalidExecutionEvidenceException.class, member::isObject); assertTrue(failure.getMessage().contains( diff --git a/src/test/java/blue/bex/BexExecutionEvidenceLedgerTest.java b/src/test/java/blue/bex/BexExecutionEvidenceLedgerTest.java index c1961ab..3ffd487 100644 --- a/src/test/java/blue/bex/BexExecutionEvidenceLedgerTest.java +++ b/src/test/java/blue/bex/BexExecutionEvidenceLedgerTest.java @@ -5,8 +5,11 @@ import blue.bex.api.BexGasLedgerHost; import blue.bex.api.BexProgramSource; import blue.bex.api.FrozenBexDocumentView; +import blue.bex.contracts.BexContractsFailureBoundary; +import blue.bex.gas.BexGasLedgerCapability; import blue.bex.output.BexSemanticIdentityBoundary; import blue.bex.test.TestBlue; +import blue.bex.test.TestGasLedgerCapability; import blue.language.provider.NodeProvider; import blue.language.model.Node; import blue.language.processor.ExecutionEvidenceUnavailableException; @@ -166,6 +169,7 @@ private static void executeKindRead( .gasLedgerHost(host) .semanticIdentityBoundary( BexSemanticIdentityBoundary.STANDALONE) + .failureBoundary(BexContractsFailureBoundary.INSTANCE) .build(); BexEngine.builder() .language(blue.runtime()) @@ -251,36 +255,37 @@ private static final class RecordingGasHost implements BexGasLedgerHost { private final GasMeter parent = new GasMeter(GasSchedule.contracts10(), 100_000L); - private GasMeter.ChildGasLedger child; + private BexGasLedgerCapability child; private int openCount; private int mergeCount; private int unavailableCount; @Override - public GasMeter.ChildGasLedger open( + public BexGasLedgerCapability open( String namespace, Map counterWeights) { openCount++; - child = parent.childLedger(namespace, counterWeights); + child = TestGasLedgerCapability.wrap( + parent.childLedger(namespace, counterWeights)); return child; } @Override - public void submit(GasMeter.ChildGasLedger ledger) { + public void submit(BexGasLedgerCapability ledger) { mergeCount++; assertEquals(child, ledger); - parent.merge(ledger); + parent.merge(((TestGasLedgerCapability) ledger).delegate()); } @Override public void failedDeterministically( - GasMeter.ChildGasLedger ledger) { + BexGasLedgerCapability ledger) { submit(ledger); } @Override public void evidenceUnavailable( - GasMeter.ChildGasLedger ledger) { + BexGasLedgerCapability ledger) { unavailableCount++; assertEquals(child, ledger); } diff --git a/src/test/java/blue/bex/BexIntrinsicTest.java b/src/test/java/blue/bex/BexIntrinsicTest.java index 9bf99e1..ee63092 100644 --- a/src/test/java/blue/bex/BexIntrinsicTest.java +++ b/src/test/java/blue/bex/BexIntrinsicTest.java @@ -3,6 +3,7 @@ import blue.bex.api.BexEngine; import blue.bex.api.BexExecutionContext; import blue.bex.api.BexProgramSource; +import blue.bex.compile.BexCompiledProgram; import blue.bex.compile.BexCompiledProgramCache; import blue.bex.compile.LruBexCompiledProgramCache; import blue.bex.gas.BexGasCounter; @@ -53,6 +54,59 @@ class BexIntrinsicTest { private static final Map CONSTANT_WORK_WEIGHTS = Collections.singletonMap(CONSTANT_WORK, 1L); + @Test + void compiledProgramRejectsARegistryWithTheSameBlueIdButDifferentIdentity() { + BexProgramSource source = source( + "type: Blue/BEX Program", + "expr:", + " $intrinsic:", + " type:", + " blueId: TestIntrinsicEcho", + " payload: bound"); + BexEngine compilingEngine = BexEngine.builder() + .intrinsic( + ECHO_BLUE_ID, + "registry-a", + CONSTANT_WORK_WEIGHTS, + invocation -> invocation.field("payload")) + .build(); + BexCompiledProgram compiled = compilingEngine.compile(source); + + BexEngine differentRegistry = BexEngine.builder() + .intrinsic( + ECHO_BLUE_ID, + "registry-b", + CONSTANT_WORK_WEIGHTS, + invocation -> invocation.field("payload")) + .build(); + + BexException failure = assertThrows( + BexException.class, + () -> differentRegistry.execute(compiled, defaultContext())); + assertTrue(failure.getMessage().contains( + "Compiled BEX environment identity mismatch")); + } + + @Test + void throwingMetricsSinkCannotChangeCompilationOrExecutionOutcome() { + BexEngine engine = BexEngine.builder() + .metrics(metrics -> { + throw new IllegalStateException("diagnostic sink failure"); + }) + .build(); + BexProgramSource source = source( + "type: Blue/BEX Program", + "expr: 42"); + + BexCompiledProgram first = engine.compile(source); + BexCompiledProgram cached = engine.compile(source); + BexExecutionResult result = engine.execute(cached, defaultContext()); + + assertSame(first, cached); + assertEquals("42", String.valueOf(simple(result.value()))); + assertTrue(result.gasUsed() > 0L); + } + @Test void registeredIntrinsicReceivesBlueIdAndEvaluatedFields() { BexEngine engine = BexEngine.builder() diff --git a/src/test/java/blue/bex/BexLazyBindingTest.java b/src/test/java/blue/bex/BexLazyBindingTest.java index e9f385b..10f58a3 100644 --- a/src/test/java/blue/bex/BexLazyBindingTest.java +++ b/src/test/java/blue/bex/BexLazyBindingTest.java @@ -7,7 +7,7 @@ import blue.bex.gas.BexGasSchedule; import blue.bex.pointer.BexPointerCache; import blue.bex.result.BexExecutionResult; -import blue.bex.result.BexMetrics; +import blue.bex.result.BexMetricsRecorder; import blue.bex.runtime.BexRuntime; import blue.bex.value.BexFrozenWriter; import blue.bex.value.BexNodeWriter; @@ -766,7 +766,7 @@ void supplierFailureAddsNoGasBeyondTheExistingBindingRead() { BexCompiledProgram program = BexEngine.builder().build() .compile(BexProgramSource.inline(frozen(stepExpr(op("$binding", "broken"))))); BexRuntime runtime = new BexRuntime(program, context, new TestBlue().runtime(), schedule, - new BexMetrics(), new BexPointerCache()); + new BexMetricsRecorder(), new BexPointerCache()); assertThrows(IllegalStateException.class, () -> runtime.readBinding("broken", Collections.emptyList())); assertEquals(schedule.bindingRead, runtime.gas().used()); diff --git a/src/test/java/blue/bex/BexPointerSet20Test.java b/src/test/java/blue/bex/BexPointerSet20Test.java index cc1da8c..a513e73 100644 --- a/src/test/java/blue/bex/BexPointerSet20Test.java +++ b/src/test/java/blue/bex/BexPointerSet20Test.java @@ -3,7 +3,7 @@ import blue.bex.api.FrozenBexDocumentView; import blue.bex.gas.BexGasCounter; import blue.bex.result.BexExecutionResult; -import blue.bex.result.BexMetrics; +import blue.bex.result.BexMetricsRecorder; import blue.bex.result.BexPatchEntry; import blue.bex.result.BexResultOverlay; import blue.bex.value.BexValue; @@ -78,7 +78,7 @@ void resultOverlayAloneKeepsNonShiftingSparseListRemoval() { FrozenBexDocumentView document = new FrozenBexDocumentView( frozen(obj("items", list("a", "b", "c")))); BexResultOverlay overlay = - new BexResultOverlay(document, new BexMetrics()); + new BexResultOverlay(document, new BexMetricsRecorder()); overlay.append(new BexPatchEntry( "remove", "/items/1", diff --git a/src/test/java/blue/bex/BexStructuredReferenceEvidenceTest.java b/src/test/java/blue/bex/BexStructuredReferenceEvidenceTest.java index 4aa8132..01e8393 100644 --- a/src/test/java/blue/bex/BexStructuredReferenceEvidenceTest.java +++ b/src/test/java/blue/bex/BexStructuredReferenceEvidenceTest.java @@ -5,8 +5,6 @@ import blue.bex.test.TestBlue; import blue.language.provider.NodeProvider; import blue.language.model.Node; -import blue.language.processor.ExecutionEvidenceUnavailableException; -import blue.language.processor.InvalidExecutionEvidenceException; import blue.language.api.NodeProviderOutcome; import blue.language.provider.NodeProviderResult; import blue.language.snapshot.FrozenNode; @@ -32,8 +30,8 @@ void providerNotFoundIsIncompleteExecutionEvidenceNotSemanticAbsence() { NodeProviderResult.notFound()); try (TestBlue blue = new TestBlue(provider)) { - ExecutionEvidenceUnavailableException failure = assertThrows( - ExecutionEvidenceUnavailableException.class, + BexExecutionEvidenceUnavailableException failure = assertThrows( + BexExecutionEvidenceUnavailableException.class, () -> exactReference(blue, blueId).isObject()); assertEquals( @@ -54,8 +52,8 @@ void providerUnavailableRetainsItsDiagnosticAndRequiredIdentity() { NodeProviderResult.unavailable("feeder is offline")); try (TestBlue blue = new TestBlue(provider)) { - ExecutionEvidenceUnavailableException failure = assertThrows( - ExecutionEvidenceUnavailableException.class, + BexExecutionEvidenceUnavailableException failure = assertThrows( + BexExecutionEvidenceUnavailableException.class, () -> exactReference(blue, blueId).keys()); assertEquals( @@ -74,8 +72,8 @@ void invalidProviderEvidenceIsASeparateDeterministicFailure() { "signature does not match")); try (TestBlue blue = new TestBlue(provider)) { - InvalidExecutionEvidenceException failure = assertThrows( - InvalidExecutionEvidenceException.class, + BexInvalidExecutionEvidenceException failure = assertThrows( + BexInvalidExecutionEvidenceException.class, () -> exactReference(blue, blueId).get("value")); assertEquals( @@ -94,8 +92,8 @@ void foundContentWithMismatchedIdentityIsInvalidEvidence() { obj("value", "different")))); try (TestBlue blue = new TestBlue(provider)) { - InvalidExecutionEvidenceException failure = assertThrows( - InvalidExecutionEvidenceException.class, + BexInvalidExecutionEvidenceException failure = assertThrows( + BexInvalidExecutionEvidenceException.class, () -> exactReference( blue, requestedBlueId).isObject()); @@ -148,8 +146,8 @@ void priorValidMaterializationDoesNotHideAChangedProviderOutcome() { provider.set(NodeProviderResult.unavailable( "second attempt cannot acquire evidence")); - ExecutionEvidenceUnavailableException failure = assertThrows( - ExecutionEvidenceUnavailableException.class, + BexExecutionEvidenceUnavailableException failure = assertThrows( + BexExecutionEvidenceUnavailableException.class, () -> exactReference(blue, blueId).isObject()); assertEquals( diff --git a/src/test/java/blue/bex/api/Bex20ApiSurfaceTest.java b/src/test/java/blue/bex/api/Bex20ApiSurfaceTest.java index b7d8b4a..582fff6 100644 --- a/src/test/java/blue/bex/api/Bex20ApiSurfaceTest.java +++ b/src/test/java/blue/bex/api/Bex20ApiSurfaceTest.java @@ -210,6 +210,22 @@ void recursiveSizeEstimatorAndMetricsAreAbsent() { NoSuchMethodException.class, () -> BexMetrics.class.getMethod(method)); } + + String[] mutableMetricMethods = { + "incrementCompiledExecutions", + "incrementCompileCacheHits", + "incrementExpressionEvaluations", + "incrementStatementExecutions", + "incrementFunctionCalls", + "incrementLoopIterations", + "addCompileNanos", + "addExecuteNanos" + }; + for (String method : mutableMetricMethods) { + assertThrows( + NoSuchMethodException.class, + () -> BexMetrics.class.getMethod(method)); + } } private static void assertNoAggregateGasConstructor(Class type) { diff --git a/src/test/java/blue/bex/compile/BexCompileBoundaryTest.java b/src/test/java/blue/bex/compile/BexCompileBoundaryTest.java new file mode 100644 index 0000000..0dcfaa7 --- /dev/null +++ b/src/test/java/blue/bex/compile/BexCompileBoundaryTest.java @@ -0,0 +1,86 @@ +package blue.bex.compile; + +import blue.bex.api.BexIntrinsicRegistry; +import blue.bex.api.BexProgramSource; +import blue.bex.runtime.BexRuntime; +import blue.language.model.Node; +import blue.language.snapshot.FrozenNode; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class BexCompileBoundaryTest { + @Test + void immutableApiTypesAdaptToCompileOwnedViews() { + FrozenNode node = FrozenNode.fromResolvedNode(new Node().value("value")); + BexProgramSource source = BexProgramSource.expression(node); + BexCompilationInput input = source; + BexIntrinsicCatalog catalog = BexIntrinsicRegistry.empty(); + + assertTrue(input.isExpression()); + assertSame(node, input.programNode()); + assertFalse(input.definitionNode().isPresent()); + assertFalse(input.entry().isPresent()); + assertFalse(catalog.supports("unsupported")); + } + + @Test + void publicCompilerBoundaryNamesOnlyCompileOwnedViews() throws Exception { + Method compile = BexCompilerRuntimeAccess.class.getMethod( + "compile", + BexCompilationInput.class, + blue.bex.result.BexMetricsRecorder.class, + BexIntrinsicCatalog.class, + String.class); + + assertFalse(Modifier.isPublic(BexCompiler.class.getModifiers())); + assertEquals(BexCompilationInput.class, + compile.getParameterTypes()[0]); + assertEquals(BexIntrinsicCatalog.class, + compile.getParameterTypes()[2]); + } + + @Test + void cacheIdentityRetainsSourceKindAcrossBoundary() { + FrozenNode node = FrozenNode.fromResolvedNode(new Node().value("value")); + + BexCompiledProgramKey full = BexCompiledProgramKey.from( + BexProgramSource.inline(node)); + BexCompiledProgramKey expression = BexCompiledProgramKey.from( + BexProgramSource.expression(node)); + + assertEquals(BexCompilationInput.Kind.FULL_PROGRAM, full.kind()); + assertEquals(BexCompilationInput.Kind.EXPRESSION, expression.kind()); + assertNotEquals(full, expression); + } + + @Test + void concreteRuntimeImplementsTheCompileOwnedExecutionPort() + throws Exception { + Method execute = BexCompiledProgramRuntimeAccess.class.getMethod( + "execute", BexCompiledProgram.class, + BexExecutionMachine.class); + + assertTrue(BexExecutionMachine.class.isAssignableFrom( + BexRuntime.class)); + assertEquals(BexExecutionMachine.class, + execute.getParameterTypes()[1]); + for (Method method : BexCompiledProgram.class.getMethods()) { + assertFalse(method.getReturnType() == BexExecutionMachine.class + || method.getReturnType() == CompiledExpression.class + || method.getReturnType() == CompiledStatement.class); + for (Class parameter : method.getParameterTypes()) { + assertFalse(parameter == BexExecutionMachine.class + || parameter == CompiledExpression.class + || parameter == CompiledStatement.class); + } + } + } +} diff --git a/src/test/java/blue/bex/compile/BexCompiledIrImmutabilityTest.java b/src/test/java/blue/bex/compile/BexCompiledIrImmutabilityTest.java new file mode 100644 index 0000000..2b26a35 --- /dev/null +++ b/src/test/java/blue/bex/compile/BexCompiledIrImmutabilityTest.java @@ -0,0 +1,217 @@ +package blue.bex.compile; + +import blue.bex.value.BexValue; +import blue.bex.value.BexValues; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class BexCompiledIrImmutabilityTest { + private static final CompiledExpression FIRST_EXPRESSION = + frame -> BexValues.scalar("first"); + private static final CompiledExpression SECOND_EXPRESSION = + frame -> BexValues.scalar("second"); + private static final CompiledStatement FIRST_STATEMENT = + frame -> Control.CONTINUE; + private static final CompiledStatement SECOND_STATEMENT = + frame -> Control.RETURN; + + @Test + void compiledIrConstructionIsCompilerOwned() { + Arrays.stream(BexCompiledProgram.class.getDeclaredConstructors()) + .forEach(constructor -> assertEquals(false, + Modifier.isPublic(constructor.getModifiers()))); + Arrays.stream(BexCompiledProgram.CompiledFunction.class + .getDeclaredConstructors()) + .forEach(constructor -> assertEquals(false, + Modifier.isPublic(constructor.getModifiers()))); + Arrays.stream(BexCompiledProgram.ArgSpec.class.getDeclaredConstructors()) + .forEach(constructor -> assertEquals(false, + Modifier.isPublic(constructor.getModifiers()))); + } + + @Test + void compiledProgramAndFunctionCopyAndFreezeCollectionInputs() + throws ReflectiveOperationException { + List args = new ArrayList<>(); + args.add(new BexCompiledProgram.ArgSpec("arg", 0, null, "/arg")); + List statements = new ArrayList<>(); + statements.add(FIRST_STATEMENT); + BexCompiledProgram.CompiledFunction function = + new BexCompiledProgram.CompiledFunction( + "function", args, statements, null, 1); + + args.clear(); + statements.clear(); + assertEquals(1, function.args().size()); + List storedStatements = field(function, "statements", List.class); + assertEquals(Collections.singletonList(FIRST_STATEMENT), storedStatements); + assertThrows(UnsupportedOperationException.class, + () -> storedStatements.clear()); + + Map functions = + new LinkedHashMap<>(); + functions.put("function", function); + Map constants = new LinkedHashMap<>(); + constants.put("constant", BexValues.scalar("value")); + Set intrinsicIds = new LinkedHashSet<>(); + intrinsicIds.add("intrinsic"); + BexCompiledProgram program = new BexCompiledProgram(function, + functions, constants, 1, "program", intrinsicIds); + + functions.clear(); + constants.clear(); + intrinsicIds.clear(); + assertEquals(1, program.functions().size()); + assertEquals(1, program.constants().size()); + assertEquals(1, program.requiredIntrinsicBlueIds().size()); + assertThrows(UnsupportedOperationException.class, + () -> program.functions().clear()); + assertThrows(UnsupportedOperationException.class, + () -> program.constants().clear()); + assertThrows(UnsupportedOperationException.class, + () -> program.requiredIntrinsicBlueIds().clear()); + } + + @Test + void callAndMultiLetCloneArrayInputs() + throws ReflectiveOperationException { + int[] callSlots = {1}; + CompiledExpression[] callExpressions = {FIRST_EXPRESSION}; + CallExpr call = new CallExpr("function", callSlots, callExpressions); + callSlots[0] = 99; + callExpressions[0] = SECOND_EXPRESSION; + int[] storedCallSlots = field(call, "targetSlots", int[].class); + CompiledExpression[] storedCallExpressions = + field(call, "argExpressions", CompiledExpression[].class); + assertNotSame(callSlots, storedCallSlots); + assertNotSame(callExpressions, storedCallExpressions); + assertArrayEquals(new int[]{1}, storedCallSlots); + assertArrayEquals(new CompiledExpression[]{FIRST_EXPRESSION}, + storedCallExpressions); + + int[] letSlots = {2}; + CompiledExpression[] letExpressions = {FIRST_EXPRESSION}; + MultiLetStatement multiLet = + new MultiLetStatement(letSlots, letExpressions, false); + letSlots[0] = 88; + letExpressions[0] = SECOND_EXPRESSION; + int[] storedLetSlots = field(multiLet, "slots", int[].class); + CompiledExpression[] storedLetExpressions = + field(multiLet, "expressions", CompiledExpression[].class); + assertNotSame(letSlots, storedLetSlots); + assertNotSame(letExpressions, storedLetExpressions); + assertArrayEquals(new int[]{2}, storedLetSlots); + assertArrayEquals(new CompiledExpression[]{FIRST_EXPRESSION}, + storedLetExpressions); + } + + @Test + void expressionAndStatementNodesCopyAndFreezeListsMapsAndSets() + throws ReflectiveOperationException { + assertExpressionListCopied( + input -> new CompareExpr(input, CompareOp.EQ), "expressions"); + assertExpressionListCopied( + input -> new LogicalExpr(input, true), "expressions"); + assertExpressionListCopied(CoalesceExpr::new, "expressions"); + assertExpressionListCopied( + input -> new NumericExpr(input, NumericOp.ADD), "expressions"); + assertExpressionListCopied( + input -> new VariadicExpr(input, VariadicOp.CONCAT), "expressions"); + assertExpressionListCopied(PointerJoinExpr::new, "segments"); + assertExpressionListCopied( + input -> new BinaryTextExpr(input, BinaryTextOp.STARTS_WITH), + "expressions"); + assertExpressionListCopied(ListExpr::new, "items"); + + List branch = new ArrayList<>(); + branch.add(FIRST_STATEMENT); + IfStatement ifStatement = new IfStatement( + FIRST_EXPRESSION, branch, branch); + ForEachStatement forEach = new ForEachStatement( + FIRST_EXPRESSION, 0, -1, -1, branch); + branch.set(0, SECOND_STATEMENT); + assertFrozenSingleton(ifStatement, "thenStatements", FIRST_STATEMENT); + assertFrozenSingleton(ifStatement, "elseStatements", FIRST_STATEMENT); + assertFrozenSingleton(forEach, "body", FIRST_STATEMENT); + + Set kinds = new LinkedHashSet<>(); + kinds.add("text"); + IsKindExpr isKind = new IsKindExpr(FIRST_EXPRESSION, kinds); + kinds.clear(); + Set storedKinds = field(isKind, "kinds", Set.class); + assertEquals(Collections.singleton("text"), storedKinds); + assertThrows(UnsupportedOperationException.class, + () -> storedKinds.clear()); + + List segments = new ArrayList<>(Arrays.asList("a", "b")); + ResolvedPointer pointer = new ResolvedPointer( + "/a/b", "/a/b", segments); + segments.clear(); + assertEquals(Arrays.asList("a", "b"), pointer.segments()); + assertThrows(UnsupportedOperationException.class, + () -> pointer.segments().clear()); + + Map fields = new LinkedHashMap<>(); + fields.put("field", FIRST_EXPRESSION); + ObjectExpr object = new ObjectExpr(fields); + IntrinsicExpr intrinsic = new IntrinsicExpr( + "intrinsic", BexValues.nullValue(), fields); + fields.clear(); + assertFrozenMap(object, "fields"); + assertFrozenMap(intrinsic, "fields"); + } + + private static void assertExpressionListCopied( + ExpressionListFactory factory, String fieldName) + throws ReflectiveOperationException { + List input = new ArrayList<>(); + input.add(FIRST_EXPRESSION); + Object node = factory.create(input); + input.set(0, SECOND_EXPRESSION); + List stored = field(node, fieldName, List.class); + assertEquals(Collections.singletonList(FIRST_EXPRESSION), stored); + assertThrows(UnsupportedOperationException.class, () -> stored.clear()); + } + + private static void assertFrozenSingleton( + Object target, String fieldName, Object expected) + throws ReflectiveOperationException { + List stored = field(target, fieldName, List.class); + assertEquals(Collections.singletonList(expected), stored); + assertThrows(UnsupportedOperationException.class, () -> stored.clear()); + } + + private static void assertFrozenMap(Object target, String fieldName) + throws ReflectiveOperationException { + Map stored = field(target, fieldName, Map.class); + assertEquals(1, stored.size()); + assertEquals(FIRST_EXPRESSION, stored.get("field")); + assertThrows(UnsupportedOperationException.class, () -> stored.clear()); + } + + private static T field(Object target, String name, Class type) + throws ReflectiveOperationException { + Field field = target.getClass().getDeclaredField(name); + field.setAccessible(true); + return type.cast(field.get(target)); + } + + private interface ExpressionListFactory { + Object create(List expressions); + } +} diff --git a/src/test/java/blue/bex/compile/BexOperatorCatalogTest.java b/src/test/java/blue/bex/compile/BexOperatorCatalogTest.java new file mode 100644 index 0000000..079750e --- /dev/null +++ b/src/test/java/blue/bex/compile/BexOperatorCatalogTest.java @@ -0,0 +1,256 @@ +package blue.bex.compile; + +import blue.bex.BexException; +import blue.bex.result.BexMetricsRecorder; +import org.junit.jupiter.api.Test; +import org.yaml.snakeyaml.Yaml; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class BexOperatorCatalogTest { + private static final String FIXTURE_ROOT = + "conformance/bex/fixtures/"; + private static final String COVERAGE_RESOURCE = + FIXTURE_ROOT + "operator-coverage.yaml"; + private static final String VECTOR_COVERAGE_RESOURCE = + FIXTURE_ROOT + "vector-coverage.yaml"; + + @Test + void catalogExactlyMatchesTheClosedOperatorCoveragePackage() { + CoveragePackage coverage = coveragePackage(); + List cataloguedOperators = new ArrayList<>(); + for (BexOperatorCatalog.Entry entry : BexOperatorCatalog.entries()) { + cataloguedOperators.add(entry.canonicalName()); + } + + assertEquals(BexOperatorCatalog.OPERATOR_COUNT, + coverage.declaredOperatorCount); + assertEquals(BexOperatorCatalog.OPERATOR_COUNT, + cataloguedOperators.size()); + assertEquals(BexOperatorCatalog.OPERATOR_COUNT, + new LinkedHashSet<>(cataloguedOperators).size()); + assertEquals(new ArrayList<>(coverage.fixturesByOperator.keySet()), + cataloguedOperators, + "Catalog order and membership must track the normative coverage evidence"); + + for (BexOperatorCatalog.Entry entry : BexOperatorCatalog.entries()) { + Set expectedFixtures = coverage.fixturesByOperator.get( + entry.canonicalName()); + assertEquals(expectedFixtures, entry.fixturePaths(), + entry.canonicalName() + " fixture coverage drifted"); + assertEquals(coverage.vectorsFor(expectedFixtures), + entry.vectorIdentifiers(), + entry.canonicalName() + " vector coverage drifted"); + for (String fixture : entry.fixturePaths()) { + assertNotNull(BexOperatorCatalogTest.class.getClassLoader() + .getResource(FIXTURE_ROOT + fixture), + "Catalog references missing fixture " + fixture); + } + } + } + + @Test + void everyEntryHasClosedCompilerRuntimeAndCoverageMetadata() { + int expressionCount = 0; + int statementCount = 0; + int dualRoleCount = 0; + for (BexOperatorCatalog.Entry entry : BexOperatorCatalog.entries()) { + assertTrue(entry.canonicalName().startsWith("$")); + assertFalse(entry.specificationSection().isEmpty()); + assertFalse(entry.operandGrammar().isEmpty()); + assertNotNull(entry.staticOperands()); + assertNotNull(entry.dynamicOperands()); + assertFalse(entry.evaluationContract().isEmpty()); + assertNotNull(entry.compilerFamily()); + assertNotNull(entry.runtimeFamily()); + assertFalse(entry.fixturePaths().isEmpty()); + assertFalse(entry.vectorIdentifiers().isEmpty()); + assertEquals(entry.role().supportsExpression(), + BexOperatorCatalog.supportsExpression(entry.canonicalName())); + assertEquals(entry.role().supportsStatement(), + BexOperatorCatalog.supportsStatement(entry.canonicalName())); + if (entry.role().supportsExpression()) { + expressionCount++; + } + if (entry.role().supportsStatement()) { + statementCount++; + } + if (entry.role() == BexOperatorCatalog.Role.EXPRESSION_AND_STATEMENT) { + dualRoleCount++; + } + } + + assertEquals(75, expressionCount); + assertEquals(13, statementCount); + assertEquals(2, dualRoleCount); + assertEquals(BexOperatorCatalog.Role.EXPRESSION_AND_STATEMENT, + BexOperatorCatalog.find("$call").role()); + assertEquals(BexOperatorCatalog.Role.EXPRESSION_AND_STATEMENT, + BexOperatorCatalog.find("$fail").role()); + assertFalse(BexOperatorCatalog.supportsExpression("$unknown")); + assertFalse(BexOperatorCatalog.supportsStatement("$unknown")); + } + + @Test + void representativeEntriesExplicitlyDistinguishStaticDynamicAndLazyOperands() { + BexOperatorCatalog.Entry is = BexOperatorCatalog.find("$is"); + assertEquals(singleton("pattern"), is.staticOperands()); + assertEquals(singleton("node"), is.dynamicOperands()); + + BexOperatorCatalog.Entry intrinsic = + BexOperatorCatalog.find("$intrinsic"); + assertEquals(setOf("type", "payload.keys"), + intrinsic.staticOperands()); + assertEquals(singleton("payload.values"), + intrinsic.dynamicOperands()); + + BexOperatorCatalog.Entry appendChange = + BexOperatorCatalog.find("$appendChange"); + assertEquals(setOf("op", "path"), + appendChange.staticOperands()); + assertEquals(setOf("op", "path", "val"), + appendChange.dynamicOperands()); + assertTrue(appendChange.evaluationContract() + .contains("val-skipped-for-remove")); + + BexOperatorCatalog.Entry choose = BexOperatorCatalog.find("$choose"); + assertTrue(choose.evaluationContract() + .contains("selected-branch-only")); + assertTrue(BexOperatorCatalog.find("$and").evaluationContract() + .startsWith("short-circuit")); + } + + @Test + void catalogViewsCannotBeMutated() { + assertThrows(UnsupportedOperationException.class, + () -> BexOperatorCatalog.entries().clear()); + assertThrows(UnsupportedOperationException.class, + () -> BexOperatorCatalog.find("$add") + .fixturePaths().clear()); + assertThrows(UnsupportedOperationException.class, + () -> BexOperatorCatalog.find("$add") + .vectorIdentifiers().clear()); + assertThrows(UnsupportedOperationException.class, + () -> BexOperatorCatalog.find("$is") + .staticOperands().clear()); + assertThrows(UnsupportedOperationException.class, + () -> BexOperatorCatalog.find("$is") + .dynamicOperands().clear()); + } + + @Test + void compilerRecognitionFailsClosedWithStableUnknownOperatorDiagnostics() { + BexProgramCompiler compiler = new BexProgramCompiler( + new BexMetricsRecorder(), null); + + assertUnknownExpression(compiler, "$unknown"); + assertUnknownExpression(compiler, "$let"); + assertNotNull(compiler.compileOperator( + "$null", null, new CompileScope(), "/$null")); + } + + @SuppressWarnings("unchecked") + private static CoveragePackage coveragePackage() { + Map operatorSource = loadYaml(COVERAGE_RESOURCE); + Map> fixturesByOperator = new LinkedHashMap<>(); + for (Map row + : (List>) operatorSource.get("operators")) { + fixturesByOperator.put((String) row.get("operator"), + immutableSet((List) row.get("fixtures"))); + } + + Map vectorSource = loadYaml(VECTOR_COVERAGE_RESOURCE); + Map> vectorsByFixture = new LinkedHashMap<>(); + Map> vectors = + (Map>) vectorSource.get("vectors"); + for (Map.Entry> vector : vectors.entrySet()) { + for (String fixture : vector.getValue()) { + Set fixtureVectors = vectorsByFixture.get(fixture); + if (fixtureVectors == null) { + fixtureVectors = new LinkedHashSet<>(); + vectorsByFixture.put(fixture, fixtureVectors); + } + fixtureVectors.add(vector.getKey()); + } + } + return new CoveragePackage( + ((Number) operatorSource.get("operatorCount")).intValue(), + fixturesByOperator, vectorsByFixture); + } + + @SuppressWarnings("unchecked") + private static Map loadYaml(String resource) { + try (InputStream stream = BexOperatorCatalogTest.class.getClassLoader() + .getResourceAsStream(resource)) { + assertNotNull(stream, "Missing " + resource); + return (Map) new Yaml().load(stream); + } catch (IOException ex) { + throw new UncheckedIOException("Cannot read " + resource, ex); + } + } + + private static Set singleton(String value) { + Set result = new LinkedHashSet<>(); + result.add(value); + return result; + } + + private static Set setOf(String... values) { + Set result = new LinkedHashSet<>(); + Collections.addAll(result, values); + return result; + } + + private static Set immutableSet(List values) { + return Collections.unmodifiableSet(new LinkedHashSet<>(values)); + } + + private static final class CoveragePackage { + private final int declaredOperatorCount; + private final Map> fixturesByOperator; + private final Map> vectorsByFixture; + + private CoveragePackage(int declaredOperatorCount, + Map> fixturesByOperator, + Map> vectorsByFixture) { + this.declaredOperatorCount = declaredOperatorCount; + this.fixturesByOperator = fixturesByOperator; + this.vectorsByFixture = vectorsByFixture; + } + + private Set vectorsFor(Set fixtures) { + Set result = new LinkedHashSet<>(); + for (String fixture : fixtures) { + Set vectors = vectorsByFixture.get(fixture); + assertNotNull(vectors, + "Fixture missing from vector coverage: " + fixture); + result.addAll(vectors); + } + return result; + } + } + + private static void assertUnknownExpression(BexProgramCompiler compiler, + String operator) { + BexException failure = assertThrows(BexException.class, + () -> compiler.compileOperator(operator, null, + new CompileScope(), "/" + operator)); + assertEquals("Unknown expression operator: " + operator, + failure.getMessage()); + } +} diff --git a/src/test/java/blue/bex/conformance/BexConformancePropertyTest.java b/src/test/java/blue/bex/conformance/BexConformancePropertyTest.java index c0c82bf..1916496 100644 --- a/src/test/java/blue/bex/conformance/BexConformancePropertyTest.java +++ b/src/test/java/blue/bex/conformance/BexConformancePropertyTest.java @@ -6,7 +6,7 @@ import blue.bex.compile.LruBexCompiledProgramCache; import blue.bex.gas.BexGasCharge; import blue.bex.result.BexExecutionResult; -import blue.bex.result.BexMetrics; +import blue.bex.result.BexMetricsSnapshot; import blue.language.model.Node; import blue.language.snapshot.FrozenNode; import blue.language.model.wire.JsonPointer; @@ -117,10 +117,10 @@ void fixtureAdapterActuallyExecutesDeclaredBatchingPreparation() { @Test void compileCacheHitAndMissHaveIdenticalResultAndGas() { - List observed = new ArrayList(); + List observed = new ArrayList(); BexEngine engine = BexEngine.builder() .cache(new LruBexCompiledProgramCache()) - .metrics(metrics -> observed.add(metrics.copy())) + .metrics(observed::add) .build(); BexProgramSource source = BexProgramSource.inline( FrozenNode.fromResolvedNode( diff --git a/src/test/java/blue/bex/conformance/BexConformanceReportMain.java b/src/test/java/blue/bex/conformance/BexConformanceReportMain.java index 396013c..4118bbb 100644 --- a/src/test/java/blue/bex/conformance/BexConformanceReportMain.java +++ b/src/test/java/blue/bex/conformance/BexConformanceReportMain.java @@ -31,6 +31,8 @@ import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -44,11 +46,12 @@ private BexConformanceReportMain() { } public static void main(String[] args) throws Exception { - if (args.length != 8) { + if (args.length != 9) { throw new IllegalArgumentException( "Expected projectDir, buildDir, Gradle version, project " + "version, dependency mode, declared dependency, " - + "persistent evidence root, and composite path"); + + "persistent evidence root, composite path, and " + + "artifact build directory"); } Path projectDir = Paths.get(args[0]).toAbsolutePath().normalize(); Path buildDir = Paths.get(args[1]).toAbsolutePath().normalize(); @@ -61,6 +64,8 @@ public static void main(String[] args) throws Exception { Path compositePath = args[7].isEmpty() ? null : Paths.get(args[7]).toAbsolutePath().normalize(); + Path artifactBuildDir = + Paths.get(args[8]).toAbsolutePath().normalize(); TestEvidence tests = readTests( buildDir.resolve("test-results").resolve("test")); @@ -76,13 +81,14 @@ public static void main(String[] args) throws Exception { Map normativeVectorCoverage = normativeVectorCoverage(tests); List artifacts = artifactEvidence( - projectDir, buildDir, projectVersion); + projectDir, artifactBuildDir, projectVersion); Map dependencyResolution = dependencyResolutionEvidence( buildDir, dependencyMode, declaredDependency, - publishedApiInspection); + publishedApiInspection, + compositePath); Map releaseGates = releaseGateEvidence( projectDir, buildDir, @@ -480,114 +486,124 @@ private static Map dependencyResolutionEvidence( Path buildDir, String expectedMode, String declaredDependency, - Map publishedInspection) + Map publishedInspection, + Path compositePath) throws Exception { Path evidencePath = buildDir.resolve("reports") - .resolve("bex-release") - .resolve("dependency-resolution.properties"); - Map evidence = readEvidence(evidencePath); - if (evidence.isEmpty()) { + .resolve("dependencies") + .resolve("language.json"); + if (!Files.isRegularFile(evidencePath)) { return map( "status", "not-executed", "evidencePresent", false); } - Path artifact = pathOrNull(evidence.get("artifact.path")); - boolean artifactPresent = - artifact != null && Files.isRegularFile(artifact); - String actualHash = artifactPresent - ? sha256(artifact) - : "unavailable"; - boolean artifactValid = artifactPresent - && actualHash.equals(evidence.get("artifact.sha256")); - boolean standalone = - "standalone-published".equals(expectedMode); - boolean repositoryPolicyValid = - "maven-central-only".equals( - evidence.get("repository.policy")); - boolean provenanceValid = standalone - ? "verified-against-recorded-maven-central-hash" - .equals(evidence.get("provenance.status")) - && declaredDependency.equals( - evidence.get( - "provenance.recorded.coordinate")) - && publishedInspection.get( - "repository").equals( - evidence.get( - "provenance.recorded.repository")) - && publishedInspection.get( - "artifact.sha256").equals( - evidence.get( - "provenance.recorded.sha256")) - && actualHash.equals( - evidence.get( - "provenance.recorded.sha256")) - : "not-applicable-local-composite".equals( - evidence.get("provenance.status")); - boolean passed = - "resolved".equals(evidence.get("status")) - && expectedMode.equals(evidence.get("mode")) - && declaredDependency.equals( - evidence.get("declared.coordinate")) - && artifactValid - && repositoryPolicyValid - && provenanceValid; + String evidence = new String( + Files.readAllBytes(evidencePath), StandardCharsets.UTF_8); + String mode = jsonString(evidence, "mode"); + String declaredVersion = jsonString( + evidence, "declaredLanguageVersion"); + String languageCommit = jsonString(evidence, "languageCommit"); + String languageCheckoutState = jsonString( + evidence, "languageCheckoutState"); + boolean standalone = "standalone-published".equals(expectedMode); + boolean cleanCacheInitiallyAbsent = jsonBoolean( + evidence, "exactVersionCacheInitiallyAbsent"); + List> artifacts = + dependencyArtifacts(evidence); + boolean artifactsValid = !artifacts.isEmpty(); + Map aggregateArtifact = null; + for (Map artifact : artifacts) { + Path path = Paths.get(String.valueOf(artifact.get("path"))); + boolean valid = Files.isRegularFile(path) + && Files.size(path) == ((Long) artifact.get("bytes")) + && sha256(path).equals(artifact.get("sha256")); + artifact.put("matchesRecordedEvidence", valid); + artifactsValid &= valid; + if (String.valueOf(artifact.get("name")) + .startsWith("blue-language-java-")) { + aggregateArtifact = artifact; + } + } + String[] focusedModules = { + "blue-language-model", + "blue-language-core", + "blue-language-mapping", + "blue-contracts-core" + }; + boolean focusedModulesResolved = true; + for (String module : focusedModules) { + focusedModulesResolved &= evidence.contains(module); + } + String expectedVersion = declaredDependency.substring( + declaredDependency.lastIndexOf(':') + 1); + boolean sourceProvenanceValid = standalone + ? "compatible-with-final-hosted-adapter".equals( + publishedInspection.get("status")) + && aggregateArtifact != null + && publishedInspection.get("artifact.sha256").equals( + aggregateArtifact.get("sha256")) + : languageCommit.matches("[0-9a-f]{40}") + && "clean".equals(languageCheckoutState); + boolean cleanCacheAccepted = !standalone + || cleanCacheInitiallyAbsent; + boolean passed = "passed".equals(jsonString(evidence, "status")) + && expectedMode.equals(mode) + && expectedVersion.equals(declaredVersion) + && artifactsValid + && aggregateArtifact != null + && focusedModulesResolved + && sourceProvenanceValid + && cleanCacheAccepted; return map( "status", passed ? "passed" : "stale-or-failed", "evidencePresent", true, - "mode", evidence.get("mode"), - "declaredCoordinate", - evidence.get("declared.coordinate"), - "effectiveComponent", - evidence.get("effective.component"), - "effectiveCoordinate", joinCoordinate( - evidence.get("effective.group"), - evidence.get("effective.name"), - evidence.get("effective.version")), - "artifact", map( - "path", evidence.get("artifact.path"), - "bytes", evidence.get("artifact.bytes"), - "sha256", actualHash, - "matchesRecordedEvidence", artifactValid), + "receiptPath", evidencePath.toString(), + "receiptSha256", sha256(evidencePath), + "mode", mode, + "declaredCoordinate", declaredDependency, + "effectiveComponent", standalone + ? declaredDependency : "project :blue-language-java", + "effectiveCoordinate", declaredDependency, + "declaredLanguageVersion", declaredVersion, + "languageCommit", languageCommit, + "languageCheckoutState", languageCheckoutState, + "focusedModulesResolved", focusedModulesResolved, + "artifactCount", artifacts.size(), + "artifacts", artifacts, + "artifact", aggregateArtifact != null + ? aggregateArtifact : Collections.emptyMap(), "provenance", map( - "status", evidence.get("provenance.status"), - "repositoryPolicy", - evidence.get("repository.policy"), + "status", sourceProvenanceValid + ? "passed" : "failed", + "kind", standalone + ? "reviewed-published-focused-modules" + : "exact-clean-local-composite", + "publishedReviewStatus", + publishedInspection.get("status"), + "repositoryPolicy", "maven-central-only", "recordedRepository", - evidence.get( - "provenance.recorded.repository"), + publishedInspection.get("repository"), "recordedCoordinate", - evidence.get( - "provenance.recorded.coordinate"), + publishedInspection.get("coordinate"), "recordedSha256", - evidence.get( - "provenance.recorded.sha256"), + publishedInspection.get("artifact.sha256"), "resolvedHashMatchesRecordedMavenCentralHash", - provenanceValid && standalone, + standalone && sourceProvenanceValid, "networkFetchObservation", - evidence.get( - "provenance.networkFetchObservation")), + standalone ? "isolated-resolution" : "not-applicable"), "cleanDependencyCacheAcceptance", map( - "status", evidence.get("cache.acceptance"), - "freshProofRequired", - Boolean.parseBoolean(evidence.get( - "cache.freshProofRequired")), - "scope", - evidence.get("cache.acceptanceScope"), - "moduleVersionPath", - evidence.get( - "cache.blueLanguageModuleVersionPath"), + "status", cleanCacheAccepted + ? "passed" : "failed", + "freshProofRequired", standalone, + "scope", "all focused and aggregate Language modules", + "moduleVersionPath", "Gradle module cache for exact version", "moduleVersionInitiallyAbsentAtProjectConfiguration", - Boolean.parseBoolean(evidence.get( - "cache.blueLanguageModuleVersionInitiallyAbsent")), - "reason", - "passed".equals( - evidence.get("cache.acceptance")) - ? "exact-blue-language-module-version-cache-was-absent-before-resolution" - : Boolean.parseBoolean(evidence.get( - "cache.freshProofRequired")) - ? "exact-blue-language-module-version-cache-was-not-proven-absent-before-resolution" - : "fresh-module-cache-proof-not-required-for-current-run"), - "compositePath", evidence.get("composite.path")); + cleanCacheInitiallyAbsent, + "reason", standalone + ? "exact focused-module version cache must be absent before isolated resolution" + : "fresh module cache proof is not required for local composite mode"), + "compositePath", compositePath != null + ? compositePath.toString() : ""); } static boolean modeRunCanPersistEvidence( @@ -603,14 +619,40 @@ static boolean modeRunCanPersistEvidence( && "passed".equals(cache.get("status")); } - private static String joinCoordinate( - String group, - String name, - String version) { - if (group == null || name == null || version == null) { - return "unavailable"; + private static String jsonString(String json, String field) { + Matcher matcher = Pattern.compile("\\\"" + Pattern.quote(field) + + "\\\"\\s*:\\s*\\\"((?:\\\\.|[^\\\"])*)\\\"") + .matcher(json); + return matcher.find() + ? matcher.group(1).replace("\\\\", "\\") + .replace("\\\"", "\"") + : ""; + } + + private static boolean jsonBoolean(String json, String field) { + return Pattern.compile("\\\"" + Pattern.quote(field) + + "\\\"\\s*:\\s*true").matcher(json).find(); + } + + private static List> dependencyArtifacts( + String json) { + Pattern artifactPattern = Pattern.compile( + "\\{\\\"name\\\":\\\"((?:\\\\.|[^\\\"])*)\\\"," + + "\\\"path\\\":\\\"((?:\\\\.|[^\\\"])*)\\\"," + + "\\\"bytes\\\":([0-9]+)," + + "\\\"sha256\\\":\\\"([0-9a-f]{64})\\\"\\}"); + Matcher matcher = artifactPattern.matcher(json); + List> artifacts = + new ArrayList>(); + while (matcher.find()) { + artifacts.add(map( + "name", matcher.group(1), + "path", matcher.group(2).replace("\\\\", "\\") + .replace("\\\"", "\""), + "bytes", Long.valueOf(matcher.group(3)), + "sha256", matcher.group(4))); } - return group + ":" + name + ":" + version; + return artifacts; } private static Map specificationEvidence( @@ -4494,6 +4536,12 @@ private static boolean isReleaseSourcePath(String path) { || "settings.gradle.kts".equals(path) || path.startsWith(".github/") || path.startsWith("docs/") + || path.startsWith("build-logic/") + || path.startsWith("blue-bex-core/") + || path.startsWith("blue-bex-contracts/") + || path.startsWith("blue-bex-conformance/") + || path.startsWith("blue-bex-java/") + || path.startsWith("examples/") || path.startsWith("gradle/") || path.startsWith("specifications/") || path.startsWith("src/"); diff --git a/src/test/java/blue/bex/conformance/BexEngineFixtureAdapter.java b/src/test/java/blue/bex/conformance/BexEngineFixtureAdapter.java index df21d06..6fced8c 100644 --- a/src/test/java/blue/bex/conformance/BexEngineFixtureAdapter.java +++ b/src/test/java/blue/bex/conformance/BexEngineFixtureAdapter.java @@ -12,6 +12,7 @@ import blue.bex.api.FrozenBexDocumentView; import blue.bex.compile.BexCompiledProgram; import blue.bex.gas.BexGasCharge; +import blue.bex.gas.BexGasLedgerCapability; import blue.bex.gas.BexGasLimitExceededException; import blue.bex.output.BexEstablishedIdentity; import blue.bex.output.BexSemanticIdentityBoundary; @@ -19,6 +20,7 @@ import blue.bex.value.BexValue; import blue.bex.value.BexValues; import blue.bex.test.TestBlue; +import blue.bex.test.TestGasLedgerCapability; import blue.language.provider.NodeProvider; import blue.language.model.Node; import blue.language.processor.GasMeter; @@ -664,7 +666,7 @@ private void finishWarmup() { private static final class RecordingGasHost implements BexGasLedgerHost { private final GasMeter parent; private final long parentBudgetBefore; - private final Map opened = + private final Map opened = new IdentityHashMap<>(); private int openCount; private int mergeCount; @@ -676,12 +678,12 @@ private RecordingGasHost(long budget) { } @Override - public GasMeter.ChildGasLedger open( + public BexGasLedgerCapability open( String namespace, Map counterWeights) { openCount++; - GasMeter.ChildGasLedger ledger = - parent.childLedger(namespace, counterWeights); + BexGasLedgerCapability ledger = TestGasLedgerCapability.wrap( + parent.childLedger(namespace, counterWeights)); opened.put(ledger, Boolean.TRUE); liveBounded = openCount == 1 ? ledger.remainingGas() == parent.remainingGas() @@ -691,24 +693,24 @@ public GasMeter.ChildGasLedger open( } @Override - public void submit(GasMeter.ChildGasLedger ledger) { + public void submit(BexGasLedgerCapability ledger) { mergeCount++; if (!opened.containsKey(ledger)) { throw new IllegalArgumentException( "BEX submitted a different child ledger"); } - parent.merge(ledger); + parent.merge(((TestGasLedgerCapability) ledger).delegate()); } @Override public void failedDeterministically( - GasMeter.ChildGasLedger ledger) { + BexGasLedgerCapability ledger) { submit(ledger); } @Override public void evidenceUnavailable( - GasMeter.ChildGasLedger ledger) { + BexGasLedgerCapability ledger) { if (!opened.containsKey(ledger)) { throw new IllegalArgumentException( "BEX finalized a different child ledger"); diff --git a/src/test/java/blue/bex/conformance/BexModernizationPropertyTest.java b/src/test/java/blue/bex/conformance/BexModernizationPropertyTest.java new file mode 100644 index 0000000..ac2685f --- /dev/null +++ b/src/test/java/blue/bex/conformance/BexModernizationPropertyTest.java @@ -0,0 +1,375 @@ +package blue.bex.conformance; + +import blue.bex.BexException; +import blue.bex.api.BexEngine; +import blue.bex.api.BexExecutionContext; +import blue.bex.api.BexProgramSource; +import blue.bex.api.FrozenBexDocumentView; +import blue.bex.compile.BexCompiledProgram; +import blue.bex.compile.LruBexCompiledProgramCache; +import blue.bex.gas.BexGasCounter; +import blue.bex.gas.BexGasMeter; +import blue.bex.gas.BexGasSchedule; +import blue.bex.output.BexOutputAdmission; +import blue.bex.output.BexOutputKind; +import blue.bex.output.BexSemanticIdentityBoundary; +import blue.bex.result.BexExecutionResult; +import blue.bex.test.TestBlue; +import blue.bex.value.BexValue; +import blue.bex.value.BexValues; +import blue.language.model.Node; +import blue.language.model.wire.JsonPointer; +import blue.language.provider.ExactNodeGraphFragments; +import blue.language.snapshot.FrozenNode; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.atomic.AtomicInteger; + +import static blue.bex.test.BexTestFixtures.defaultDocumentView; +import static blue.bex.test.BexTestFixtures.frozen; +import static blue.bex.test.BexTestFixtures.list; +import static blue.bex.test.BexTestFixtures.m; +import static blue.bex.test.BexTestFixtures.obj; +import static blue.bex.test.BexTestFixtures.op; +import static blue.bex.test.BexTestFixtures.runExpr; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** Bounded deterministic properties for modernization-sensitive boundaries. */ +class BexModernizationPropertyTest { + @Test + void escapedPointerSegmentsRoundTripThroughBex() { + List segments = Arrays.asList( + "plain", + "slash/inside", + "tilde~inside", + "both~/inside", + "emoji-\uD83D\uDE80", + "space inside", + "line\nbreak"); + Random random = new Random(0xBEE2201L); + + for (int example = 0; example < 64; example++) { + String segment = segments.get( + random.nextInt(segments.size())) + + '-' + example; + String expected = "value-" + example; + String pointer = JsonPointer.toPointer( + Collections.singletonList(segment)); + BexExecutionResult first = runExpr(op( + "$pointerGet", + obj( + "object", obj(segment, expected), + "path", pointer))); + BexExecutionResult second = runExpr(op( + "$pointerGet", + obj( + "object", obj(segment, expected), + "path", pointer))); + + assertEquals(expected, first.value().toSimple()); + assertEquivalent(first, second); + } + } + + @Test + void generatedUnicodeTextHasStableResultAndGasIdentity() { + int[] alphabet = { + 'A', 'z', 0x00E9, 0x03A9, 0x4E2D, + 0x1F680, 0x1F642, 0x10437 + }; + Random random = new Random(0xBEE2202L); + + for (int example = 0; example < 48; example++) { + String left = unicode(random, alphabet, 1 + random.nextInt(10)); + String right = unicode(random, alphabet, 1 + random.nextInt(10)); + BexProgramSource source = BexProgramSource.expression( + frozen(op("$concat", list(left, "|", right)))); + BexEngine engine = BexEngine.builder().build(); + BexExecutionResult first = engine.compileAndExecute( + source, defaultContext()); + BexExecutionResult second = engine.compileAndExecute( + source, defaultContext()); + + assertEquals(left + '|' + right, + first.value().toSimple()); + assertEquivalent(first, second); + } + } + + @Test + void generatedIntegerAndDecimalKindsRemainDistinct() { + Random random = new Random(0xBEE2203L); + for (int example = 0; example < 48; example++) { + long value = random.nextInt(2_000_001) - 1_000_000L; + BexExecutionResult result = runExpr(obj( + "integer", op("$kind", value), + "decimal", op("$kind", + new Node().value( + BigDecimal.valueOf(value) + .setScale(1))))); + assertEquals(m( + "decimal", "double", + "integer", "integer"), + result.value().toSimple()); + } + } + + @Test + void allLazyOperatorFamiliesSkipUnselectedFailuresAndGas() { + List pairs = Arrays.asList( + new Node[]{ + op("$or", list( + true, + op("$integer", "must-not-run"))), + op("$or", list(true, false))}, + new Node[]{ + op("$and", list( + false, + op("$integer", "must-not-run"))), + op("$and", list(false, true))}, + new Node[]{ + op("$coalesce", list( + "chosen", + op("$integer", "must-not-run"))), + op("$coalesce", list("chosen", "unused"))}, + new Node[]{ + op("$choose", obj( + "cond", true, + "then", "chosen", + "else", op("$integer", "must-not-run"))), + op("$choose", obj( + "cond", true, + "then", "chosen", + "else", "unused"))}); + + for (Node[] pair : pairs) { + BexExecutionResult failingBranch = runExpr(pair[0]); + BexExecutionResult harmlessBranch = runExpr(pair[1]); + assertEquivalent(failingBranch, harmlessBranch); + } + } + + @Test + void generatedProgramsAreInvariantAcrossColdMissAndCacheHit() { + Random random = new Random(0xBEE2204L); + BexEngine cached = BexEngine.builder() + .cache(new LruBexCompiledProgramCache()) + .build(); + + for (int example = 0; example < 32; example++) { + int width = 2 + random.nextInt(8); + Object[] operands = new Object[width]; + for (int index = 0; index < width; index++) { + operands[index] = random.nextInt(2_000_001) + - 1_000_000L; + } + BexProgramSource source = BexProgramSource.expression( + frozen(op("$add", list(operands)))); + BexExecutionResult cold = BexEngine.builder().build() + .compileAndExecute(source, defaultContext()); + BexExecutionResult miss = cached.compileAndExecute( + source, defaultContext()); + BexExecutionResult hit = cached.compileAndExecute( + source, defaultContext()); + + assertEquivalent(cold, miss); + assertEquivalent(miss, hit); + } + } + + @Test + void malformedBlueOutputFuzzFailsBeforeIdentityEstablishment() { + String blueId = FrozenNode.fromResolvedNode( + new Node().value("target")).blueId(); + Random random = new Random(0xBEE2205L); + + for (int example = 0; example < 60; example++) { + String sibling = "field-" + random.nextInt(10_000); + Object malformed; + switch (example % 6) { + case 0: + malformed = m( + "blueId", blueId, + sibling, example); + break; + case 1: + malformed = m( + "value", example, + sibling, example + 1); + break; + case 2: + malformed = m( + "properties", m(sibling, example)); + break; + case 3: + malformed = m( + "schema", m( + "unsupported-" + example, + true)); + break; + case 4: + malformed = m("blue", "forbidden-" + example); + break; + default: + malformed = Collections.singletonList(m( + "$pos", example, + "value", example)); + break; + } + + BexGasMeter gas = new BexGasMeter( + BexGasSchedule.defaults(), + 1_000_000L); + AtomicInteger identityCalls = new AtomicInteger(); + BexOutputAdmission admission = new BexOutputAdmission( + gas, + node -> { + identityCalls.incrementAndGet(); + return BexSemanticIdentityBoundary.STANDALONE + .establishIdentity(node); + }); + + assertThrows(BexException.class, + () -> admission.admit( + BexValues.fromSimple(malformed), + BexOutputKind.ROOT_RESULT)); + assertEquals(0, identityCalls.get(), + "malformed output reached identity boundary"); + assertEquals(1, gas.trace().size()); + assertEquals(BexGasCounter.BLUE_OUTPUT_BOUNDARY, + gas.trace().get(0).counter()); + } + } + + @Test + void generatedInlineAndReferenceCursorsRemainRepresentationBlind() { + Map providerNodes = + new LinkedHashMap(); + try (TestBlue blue = new TestBlue(blueId -> { + Node provided = providerNodes.get(blueId); + return provided != null + ? Collections.singletonList(provided.clone()) + : Collections.emptyList(); + })) { + BexEngine engine = BexEngine.builder() + .language(blue.runtime()) + .build(); + Node observation = obj( + "identity", op("$nodeBlueId", + op("$binding", "subject")), + "kind", op("$kind", + op("$binding", "subject")), + "nested", op("$pointerGet", obj( + "object", op("$binding", "subject"), + "path", "/nested/value")), + "same", op("$eq", list( + op("$binding", "subject"), + op("$binding", "peer"))), + "size", op("$size", + op("$binding", "subject"))); + BexProgramSource source = BexProgramSource.expression( + frozen(observation)); + BexCompiledProgram program = engine.compile(source); + + for (int example = 0; example < 32; example++) { + Node logical = blue.resolveToSnapshot(obj( + "nested", obj( + "value", "value-" + example), + "number", example, + "values", list(example, example + 1))) + .frozenResolvedRoot() + .toNode(); + ExactNodeGraphFragments graph = + ExactNodeGraphFragments.split( + logical, + Collections.singletonList("")); + ExactNodeGraphFragments.RootRepresentation root = + graph.roots().get(0); + providerNodes.putAll(graph.fragments()); + FrozenNode materialized = FrozenNode.fromResolvedNode( + root.original()); + String blueId = root.blueId(); + FrozenNode reference = FrozenNode.fromNode( + root.pureReference()); + BexValue inline = BexValues.exact( + FrozenNode.fromNode(root.original()), + materialized, + blueId); + BexValue referenced = BexValues.exact( + reference, materialized, blueId); + + BexExecutionResult inlineResult = engine.execute( + program, + exactContext(inline)); + BexExecutionResult referenceResult = engine.execute( + program, + exactContext(referenced)); + + assertEquivalent(inlineResult, referenceResult); + assertEquals(blueId, + valueMap(inlineResult).get("identity")); + assertEquals(true, + valueMap(inlineResult).get("same")); + } + } + } + + private static BexExecutionContext defaultContext() { + return BexExecutionContext.builder() + .document(defaultDocumentView()) + .gasLimit(10_000_000L) + .build(); + } + + private static BexExecutionContext exactContext( + BexValue value) { + return BexExecutionContext.builder() + .document(new FrozenBexDocumentView( + FrozenNode.fromResolvedNode(new Node()))) + .binding("subject", value) + .binding("peer", value) + .gasLimit(10_000_000L) + .build(); + } + + private static void assertEquivalent( + BexExecutionResult left, + BexExecutionResult right) { + assertNotNull(left.output()); + assertNotNull(right.output()); + assertEquals(left.value().toSimple(), + right.value().toSimple()); + assertEquals(left.output().nodeBlueId(), + right.output().nodeBlueId()); + assertEquals(left.gasLedger(), right.gasLedger()); + } + + @SuppressWarnings("unchecked") + private static Map valueMap( + BexExecutionResult result) { + return (Map) result.value().toSimple(); + } + + private static String unicode( + Random random, + int[] alphabet, + int length) { + StringBuilder value = new StringBuilder(); + for (int index = 0; index < length; index++) { + value.appendCodePoint(alphabet[ + random.nextInt(alphabet.length)]); + } + return value.toString(); + } +} diff --git a/src/test/java/blue/bex/gas/BexGasPrimitivesTest.java b/src/test/java/blue/bex/gas/BexGasPrimitivesTest.java index b14e02d..6567f43 100644 --- a/src/test/java/blue/bex/gas/BexGasPrimitivesTest.java +++ b/src/test/java/blue/bex/gas/BexGasPrimitivesTest.java @@ -1,7 +1,8 @@ package blue.bex.gas; import blue.bex.result.BexExecutionResult; -import blue.bex.result.BexMetrics; +import blue.bex.result.BexMetricsRecorder; +import blue.bex.test.TestGasLedgerCapability; import blue.language.processor.GasMeter; import org.junit.jupiter.api.Test; @@ -190,7 +191,8 @@ void ledgerIsAnImmutableValidatedSnapshotAndResultDerivesTotalFromIt() { "bad-sequence")))); BexExecutionResult result = - new BexExecutionResult(null, null, null, ledger, new BexMetrics()); + new BexExecutionResult(null, null, null, ledger, + new BexMetricsRecorder().snapshot()); assertEquals(ledger, result.gasLedger()); assertEquals(ledger.trace(), result.gasTrace()); assertEquals(12L, result.gasUsed()); @@ -205,7 +207,9 @@ void hostChildLedgerIsChargedLiveAndSubmittedExactlyOnce() { GasMeter.ChildGasLedger child = host.childLedger(BexGasCounter.NAMESPACE, schedule.counterWeights()); - BexGasMeter meter = new BexGasMeter(schedule, child, 20L); + TestGasLedgerCapability hosted = + TestGasLedgerCapability.wrap(child); + BexGasMeter meter = new BexGasMeter(schedule, hosted, 20L); meter.charge(BexGasCounter.INTRINSIC_CALLED, 2L, "intrinsic"); assertTrue(meter.hasHostLedger()); @@ -213,7 +217,8 @@ void hostChildLedgerIsChargedLiveAndSubmittedExactlyOnce() { assertEquals(10L, child.totalGas()); assertEquals(0L, host.totalGas()); - meter.submitHostLedger(host::merge); + meter.submitHostLedger(ledger -> host.merge( + ((TestGasLedgerCapability) ledger).delegate())); assertTrue(meter.hostLedgerSubmitted()); assertEquals(10L, host.totalGas()); @@ -221,7 +226,8 @@ void hostChildLedgerIsChargedLiveAndSubmittedExactlyOnce() { assertEquals("bex", host.trace().get(0).namespace()); assertEquals("intrinsicCalled", host.trace().get(0).counter()); assertThrows(IllegalStateException.class, - () -> meter.submitHostLedger(host::merge)); + () -> meter.submitHostLedger(ledger -> host.merge( + ((TestGasLedgerCapability) ledger).delegate()))); assertThrows(IllegalStateException.class, () -> meter.charge(BexGasCounter.EVENT_READ)); } @@ -296,12 +302,35 @@ void chargeAndLedgerRejectInvalidArithmeticAndMutation() { 0L, BexGasCounter.EVENT_READ, 1L, - 1L, + BexGasCounter.EVENT_READ.defaultWeight(), null, null, "read")); BexGasLedger ledger = new BexGasLedger(external); - assertEquals(1L, ledger.totalGas()); + assertEquals(BexGasCounter.EVENT_READ.defaultWeight(), + ledger.totalGas()); + assertThrows(IllegalArgumentException.class, + () -> new BexGasLedger(Collections.singletonList( + new BexGasCharge( + 0L, + BexGasCounter.EVENT_READ, + 1L, + BexGasCounter.EVENT_READ.defaultWeight() + 1L, + null, + null, + "wrong-manifest")))); + BexGasLedger explicit = new BexGasLedger( + Collections.singletonList(new BexGasCharge( + 0L, + BexGasCounter.EVENT_READ, + 1L, + 1L, + null, + null, + "custom")), + "custom-schedule", + "sha256:custom-manifest"); + assertEquals("sha256:custom-manifest", explicit.manifestIdentity()); } @Test @@ -316,10 +345,14 @@ void everyPhysicalLedgerReceivesItsFinalCallbackEvenWhenOneThrows() { GasMeter.ChildGasLedger intrinsic = parent.childLedger( "bex-run/intrinsic-test", Collections.singletonMap("work", 1L)); - Map ledgers = + TestGasLedgerCapability bexCapability = + TestGasLedgerCapability.wrap(bex); + TestGasLedgerCapability intrinsicCapability = + TestGasLedgerCapability.wrap(intrinsic); + Map ledgers = new LinkedHashMap<>(); - ledgers.put(BexGasCounter.NAMESPACE, bex); - ledgers.put("intrinsic-test", intrinsic); + ledgers.put(BexGasCounter.NAMESPACE, bexCapability); + ledgers.put("intrinsic-test", intrinsicCapability); Map registered = Collections.singletonMap( BexGasMeter.qualifiedCounterName( "intrinsic-test", "work"), @@ -329,14 +362,14 @@ void everyPhysicalLedgerReceivesItsFinalCallbackEvenWhenOneThrows() { ledgers, BexGasMeter.NO_LOCAL_LIMIT, registered); - Map callbacks = + Map callbacks = new IdentityHashMap<>(); IllegalStateException failure = assertThrows( IllegalStateException.class, () -> meter.failHostLedger(ledger -> { callbacks.put(ledger, Boolean.TRUE); - if (ledger == bex) { + if (ledger == bexCapability) { throw new IllegalStateException( "first callback failed"); } @@ -344,8 +377,8 @@ void everyPhysicalLedgerReceivesItsFinalCallbackEvenWhenOneThrows() { assertEquals("first callback failed", failure.getMessage()); assertEquals(2, callbacks.size()); - assertTrue(callbacks.containsKey(bex)); - assertTrue(callbacks.containsKey(intrinsic)); + assertTrue(callbacks.containsKey(bexCapability)); + assertTrue(callbacks.containsKey(intrinsicCapability)); assertTrue(meter.hostLedgerFinalized()); assertThrows( IllegalStateException.class, diff --git a/src/test/java/blue/bex/output/BexSemanticIdentityIntegrationTest.java b/src/test/java/blue/bex/output/BexSemanticIdentityIntegrationTest.java index b3f391e..268909d 100644 --- a/src/test/java/blue/bex/output/BexSemanticIdentityIntegrationTest.java +++ b/src/test/java/blue/bex/output/BexSemanticIdentityIntegrationTest.java @@ -4,10 +4,11 @@ import blue.bex.api.BexEngine; import blue.bex.api.BexExecutionContext; import blue.bex.api.BexProgramSource; +import blue.bex.contracts.BexContractsFailureBoundary; import blue.bex.gas.BexGasMeter; import blue.bex.gas.BexGasSchedule; import blue.bex.result.BexExecutionResult; -import blue.bex.result.BexMetrics; +import blue.bex.result.BexMetricsRecorder; import blue.bex.result.BexPatchEntry; import blue.bex.result.BexResultOverlay; import blue.bex.runtime.BexExecutionAccumulator; @@ -513,7 +514,7 @@ void failedAdmissionMutatesNeitherPatchNorEventBuffers() { new BexExecutionAccumulator( new BexResultOverlay( defaultDocumentView(), - new BexMetrics()), + new BexMetricsRecorder()), admission); ExecutionEvidenceUnavailableException patchFailure = @@ -725,7 +726,8 @@ private static BexOutputAdmission admission( new BexGasMeter( BexGasSchedule.defaults(), 1_000_000L), - boundary); + boundary, + BexContractsFailureBoundary.INSTANCE); } private static Map map( diff --git a/src/test/java/blue/bex/test/TestGasLedgerCapability.java b/src/test/java/blue/bex/test/TestGasLedgerCapability.java new file mode 100644 index 0000000..4baefd2 --- /dev/null +++ b/src/test/java/blue/bex/test/TestGasLedgerCapability.java @@ -0,0 +1,66 @@ +package blue.bex.test; + +import blue.bex.gas.BexGasChargeContext; +import blue.bex.gas.BexGasLedgerCapability; +import blue.bex.gas.BexHostGasExhaustion; +import blue.language.processor.GasChargeContext; +import blue.language.processor.GasLimitExceededException; +import blue.language.processor.GasMeter; + +import java.util.Map; +import java.util.Objects; + +/** Test-only adapter for detached Contracts gas ledgers. */ +public final class TestGasLedgerCapability + implements BexGasLedgerCapability { + private final GasMeter.ChildGasLedger delegate; + + private TestGasLedgerCapability(GasMeter.ChildGasLedger delegate) { + this.delegate = Objects.requireNonNull(delegate, "delegate"); + } + + public static TestGasLedgerCapability wrap( + GasMeter.ChildGasLedger delegate) { + return new TestGasLedgerCapability(delegate); + } + + public GasMeter.ChildGasLedger delegate() { + return delegate; + } + + @Override public String namespace() { return delegate.namespace(); } + @Override public long totalGas() { return delegate.totalGas(); } + @Override public long remainingGas() { return delegate.remainingGas(); } + @Override public long effectiveBudget() { return delegate.effectiveBudget(); } + @Override public Map counterWeights() { + return delegate.counterWeights(); + } + + @Override + public void charge( + String counter, + long quantity, + BexGasChargeContext context) { + BexGasChargeContext exact = context != null + ? context : BexGasChargeContext.empty(); + try { + delegate.charge( + counter, + quantity, + GasChargeContext.of( + exact.scopePath(), + exact.contractKey(), + exact.logicalPath(), + exact.reason())); + } catch (GasLimitExceededException exhausted) { + throw new BexHostGasExhaustion( + exhausted.namespace(), + exhausted.counter(), + exhausted.quantity(), + exhausted.weight(), + exhausted.admittedGas(), + exhausted.effectiveBudget(), + exhausted); + } + } +} diff --git a/src/test/java/blue/language/processor/BexHostedRuntimeWorkSessionTest.java b/src/test/java/blue/language/processor/BexHostedRuntimeWorkSessionTest.java index 5de952c..9699448 100644 --- a/src/test/java/blue/language/processor/BexHostedRuntimeWorkSessionTest.java +++ b/src/test/java/blue/language/processor/BexHostedRuntimeWorkSessionTest.java @@ -7,18 +7,24 @@ import blue.bex.api.BexIntrinsicRegistry; import blue.bex.api.BexProgramSource; import blue.bex.api.FrozenBexDocumentView; -import blue.bex.api.ProcessorExecutionContextBexGasLedgerHost; +import blue.bex.contracts.BexContractsExecutionContext; +import blue.bex.contracts.BexContractsFailureBoundary; +import blue.bex.contracts.ProcessorExecutionContextBexGasLedgerHost; import blue.bex.compile.BexCompiledProgram; import blue.bex.gas.BexGasCounter; +import blue.bex.gas.BexGasLedgerCapability; import blue.bex.gas.BexGasLimitExceededException; import blue.bex.gas.BexGasSchedule; +import blue.bex.gas.BexHostGasExhaustion; +import blue.bex.gas.BexSharedGasBudget; import blue.bex.output.BexSemanticIdentityBoundary; import blue.bex.pointer.BexPointerCache; import blue.bex.result.BexExecutionResult; -import blue.bex.result.BexMetrics; +import blue.bex.result.BexMetricsRecorder; import blue.bex.runtime.BexRuntime; import blue.bex.value.BexValues; import blue.bex.test.TestBlue; +import blue.bex.test.TestGasLedgerCapability; import blue.language.provider.NodeProvider; import blue.language.model.Node; import blue.language.provider.CyclicAwareNodeProvider; @@ -191,10 +197,10 @@ void physicalNamespacesShareOneBudgetAndSubmittedLedgersAreFinal() { new ProcessorExecutionContextBexGasLedgerHost( session, "bex:first"); - GasMeter.ChildGasLedger bex = host.open( + BexGasLedgerCapability bex = host.open( "bex", Collections.singletonMap("expressionEvaluated", 1L)); - GasMeter.ChildGasLedger intrinsic = host.open( + BexGasLedgerCapability intrinsic = host.open( "intrinsic:test", Collections.singletonMap("work", 2L)); @@ -429,7 +435,7 @@ void deterministicFailureDiscardsBufferedOutputsAndLeavesPrefixForOwner() { BexProgramSource.inline(frozen(program)); BexCompiledProgram compiled = BexEngine.builder().build().compile(source); - BexMetrics metrics = new BexMetrics(); + BexMetricsRecorder metrics = new BexMetricsRecorder(); try (TestBlue blue = new TestBlue()) { BexRuntime runtime = new BexRuntime( compiled, @@ -581,6 +587,8 @@ public NodeProviderResult fetchResultByBlueId( .semanticIdentityBoundary( BexSemanticIdentityBoundary .STANDALONE) + .failureBoundary( + BexContractsFailureBoundary.INSTANCE) .build(); try (TestBlue blue = new TestBlue(provider)) { @@ -652,7 +660,7 @@ void hostRejectionPropagatesTheExactRecordedException() { }), blue.runtime(), BexGasSchedule.defaults(), - new BexMetrics(), + new BexMetricsRecorder(), new BexPointerCache(), BexIntrinsicRegistry.empty()); @@ -1091,10 +1099,9 @@ void processorExecutionContextUsesItsInvocationSemanticOutputBoundary() { new Node(), false)) { BexExecutionContext context = - BexExecutionContext.builder() - .processorExecutionContext( - processorContext, - "bex:semantic-adapter") + BexContractsExecutionContext.builder( + processorContext, + "bex:semantic-adapter") .build(); result = BexEngine.builder() .build() @@ -1236,6 +1243,8 @@ void hostedCyclicProofUnavailabilityUsesSessionDiscardLifecycle() { .semanticIdentityBoundary( BexSemanticIdentityBoundary .STANDALONE) + .failureBoundary( + BexContractsFailureBoundary.INSTANCE) .build(); ExecutionEvidenceUnavailableException failure = @@ -1549,7 +1558,9 @@ private static BexExecutionContext context( BexExecutionContext.builder() .document(new FrozenBexDocumentView(document)) .gasLedgerHost(host) - .semanticIdentityBoundary(identityBoundary); + .semanticIdentityBoundary(identityBoundary) + .failureBoundary( + BexContractsFailureBoundary.INSTANCE); if (localLimit >= 0L) { builder.gasLimit(localLimit); } @@ -1594,13 +1605,13 @@ private static final class RecordingSessionHost private final ProcessorExecutionContextBexGasLedgerHost delegate; private final List openLogicalNamespaces = new ArrayList<>(); - private final List openedLedgers = + private final List openedLedgers = new ArrayList<>(); private int submitCount; private int deterministicFailureCount; private int unavailableCount; private int openSharedBudgetCount; - private RuntimeWorkBudget sharedBudget; + private BexSharedGasBudget sharedBudget; private GasLimitExceededException propagatedExhaustion; private RecordingSessionHost( @@ -1612,7 +1623,7 @@ private RecordingSessionHost( } @Override - public RuntimeWorkBudget openSharedBudget( + public BexSharedGasBudget openSharedBudget( long maximumGas) { openSharedBudgetCount++; sharedBudget = @@ -1621,7 +1632,7 @@ public RuntimeWorkBudget openSharedBudget( } @Override - public GasMeter.ChildGasLedger open( + public BexGasLedgerCapability open( String namespace, Map counterWeights) { return open( @@ -1631,12 +1642,12 @@ public GasMeter.ChildGasLedger open( } @Override - public GasMeter.ChildGasLedger open( + public BexGasLedgerCapability open( String namespace, Map counterWeights, - RuntimeWorkBudget sharedBudget) { + BexSharedGasBudget sharedBudget) { openLogicalNamespaces.add(namespace); - GasMeter.ChildGasLedger ledger = + BexGasLedgerCapability ledger = delegate.open( namespace, counterWeights, @@ -1646,7 +1657,7 @@ public GasMeter.ChildGasLedger open( } @Override - public void submit(GasMeter.ChildGasLedger ledger) { + public void submit(BexGasLedgerCapability ledger) { submitCount++; delegate.submit(ledger); } @@ -1658,14 +1669,14 @@ public boolean separatesRuntimeNamespaces() { @Override public void failedDeterministically( - GasMeter.ChildGasLedger ledger) { + BexGasLedgerCapability ledger) { deterministicFailureCount++; delegate.failedDeterministically(ledger); } @Override public void evidenceUnavailable( - GasMeter.ChildGasLedger ledger) { + BexGasLedgerCapability ledger) { unavailableCount++; delegate.evidenceUnavailable(ledger); } @@ -1680,9 +1691,10 @@ public RuntimeException localGasLimitExceeded( @Override public void propagateGasExhaustion( - GasMeter.ChildGasLedger ledger, - GasLimitExceededException exhaustion) { - propagatedExhaustion = exhaustion; + BexGasLedgerCapability ledger, + BexHostGasExhaustion exhaustion) { + propagatedExhaustion = (GasLimitExceededException) + exhaustion.hostFailure(); delegate.propagateGasExhaustion( ledger, exhaustion); } @@ -1711,7 +1723,7 @@ private static final class NonSeparatingHost private int openCount; @Override - public GasMeter.ChildGasLedger open( + public BexGasLedgerCapability open( String namespace, Map counterWeights) { openCount++; @@ -1725,17 +1737,17 @@ public boolean separatesRuntimeNamespaces() { } @Override - public void submit(GasMeter.ChildGasLedger ledger) { + public void submit(BexGasLedgerCapability ledger) { } @Override public void failedDeterministically( - GasMeter.ChildGasLedger ledger) { + BexGasLedgerCapability ledger) { } @Override public void evidenceUnavailable( - GasMeter.ChildGasLedger ledger) { + BexGasLedgerCapability ledger) { } } @@ -1760,31 +1772,31 @@ private FailingSecondOpenHost( } @Override - public GasMeter.ChildGasLedger open( + public BexGasLedgerCapability open( String namespace, Map counterWeights) { openCount++; if (openCount == 2) { throw secondOpenFailure; } - return parent.childLedger( - namespace, counterWeights); + return TestGasLedgerCapability.wrap( + parent.childLedger(namespace, counterWeights)); } @Override - public void submit(GasMeter.ChildGasLedger ledger) { + public void submit(BexGasLedgerCapability ledger) { successfulSubmissions++; } @Override public void failedDeterministically( - GasMeter.ChildGasLedger ledger) { + BexGasLedgerCapability ledger) { deterministicFinalizations++; } @Override public void evidenceUnavailable( - GasMeter.ChildGasLedger ledger) { + BexGasLedgerCapability ledger) { unavailableFinalizations++; } } diff --git a/src/test/resources/hosted-release/required-public-api.txt b/src/test/resources/hosted-release/required-public-api.txt index f05fe3c..58e098e 100644 --- a/src/test/resources/hosted-release/required-public-api.txt +++ b/src/test/resources/hosted-release/required-public-api.txt @@ -6,6 +6,12 @@ class public blue.bex.BexException extends java.lang.RuntimeException method public static at(blue.bex.BexSourcePath,java.lang.String):blue.bex.BexException method public static at(blue.bex.BexSourcePath,java.lang.String,java.lang.Throwable):blue.bex.BexException method public withSourcePath(blue.bex.BexSourcePath):blue.bex.BexException +class public final blue.bex.BexExecutionEvidenceUnavailableException extends blue.bex.BexException + constructor public (java.lang.String) + constructor public (java.lang.String,java.util.Collection) + method public requiredExactBlueIds():java.util.List +class public final blue.bex.BexInvalidExecutionEvidenceException extends blue.bex.BexException + constructor public (java.lang.String) class public final blue.bex.BexSourcePath constructor public (java.lang.String,java.lang.String,java.lang.String) method public equals(java.lang.Object):boolean @@ -15,12 +21,13 @@ class public final blue.bex.BexSourcePath method public pointer():java.lang.String method public static of(java.lang.String,java.lang.String,java.lang.String):blue.bex.BexSourcePath method public toString():java.lang.String -class public abstract interface blue.bex.api.BexDocumentView +class public abstract interface blue.bex.api.BexDocumentView implements blue.bex.spi.BexDocumentAccess method public abstract canonicalAt(java.lang.String):blue.bex.value.BexValue method public abstract currentScopePath():java.lang.String method public abstract resolvePointer(java.lang.String):java.lang.String method public abstract resolvedAt(java.lang.String):blue.bex.value.BexValue -class public final blue.bex.api.BexEngine +class public final blue.bex.api.BexEngine implements java.lang.AutoCloseable + method public close():void method public compile(blue.bex.api.BexProgramSource):blue.bex.compile.BexCompiledProgram method public compileAndExecute(blue.bex.api.BexProgramSource,blue.bex.api.BexExecutionContext):blue.bex.result.BexExecutionResult method public execute(blue.bex.compile.BexCompiledProgram,blue.bex.api.BexExecutionContext):blue.bex.result.BexExecutionResult @@ -36,13 +43,14 @@ class public static final blue.bex.api.BexEngine$Builder method public intrinsics(blue.bex.api.BexIntrinsicRegistry):blue.bex.api.BexEngine$Builder method public language(blue.language.runtime.BlueLanguage):blue.bex.api.BexEngine$Builder method public metrics(blue.bex.api.BexMetricsSink):blue.bex.api.BexEngine$Builder -class public final blue.bex.api.BexExecutionContext +class public final blue.bex.api.BexExecutionContext implements blue.bex.runtime.BexRuntimeContext method public binding(java.lang.String):blue.bex.value.BexValue method public bindings():java.util.Map method public currentContract():blue.bex.value.BexValue method public currentScopePath():java.lang.String method public document():blue.bex.api.BexDocumentView method public event():blue.bex.value.BexValue + method public failureBoundary():blue.bex.api.BexFailureBoundary method public gasLedgerHost():blue.bex.api.BexGasLedgerHost method public gasLimit():long method public parentRemainingGas():long @@ -50,6 +58,10 @@ class public final blue.bex.api.BexExecutionContext method public semanticIdentityBoundary():blue.bex.output.BexSemanticIdentityBoundary method public static builder():blue.bex.api.BexExecutionContext$Builder method public steps():blue.bex.api.BexStepResults + method public volatile document():blue.bex.spi.BexDocumentAccess synthetic bridge + method public volatile failureBoundary():blue.bex.output.BexFailurePolicy synthetic bridge + method public volatile gasLedgerHost():blue.bex.gas.BexGasLedgerLifecycle synthetic bridge + method public volatile steps():blue.bex.runtime.BexStepResultView synthetic bridge class public static final blue.bex.api.BexExecutionContext$Builder constructor public () method public binding(java.lang.String,blue.bex.value.BexValue):blue.bex.api.BexExecutionContext$Builder @@ -58,24 +70,35 @@ class public static final blue.bex.api.BexExecutionContext$Builder method public currentContract(blue.bex.value.BexValue):blue.bex.api.BexExecutionContext$Builder method public document(blue.bex.api.BexDocumentView):blue.bex.api.BexExecutionContext$Builder method public event(blue.bex.value.BexValue):blue.bex.api.BexExecutionContext$Builder + method public failureBoundary(blue.bex.api.BexFailureBoundary):blue.bex.api.BexExecutionContext$Builder method public gasLedgerHost(blue.bex.api.BexGasLedgerHost):blue.bex.api.BexExecutionContext$Builder method public gasLimit(long):blue.bex.api.BexExecutionContext$Builder method public lazyBinding(java.lang.String,java.util.function.Supplier):blue.bex.api.BexExecutionContext$Builder method public parentRemainingGas(long):blue.bex.api.BexExecutionContext$Builder method public processingEvent(blue.bex.value.BexValue):blue.bex.api.BexExecutionContext$Builder - method public processorExecutionContext(blue.language.processor.ProcessorExecutionContext):blue.bex.api.BexExecutionContext$Builder - method public processorExecutionContext(blue.language.processor.ProcessorExecutionContext,java.lang.String):blue.bex.api.BexExecutionContext$Builder method public semanticIdentityBoundary(blue.bex.output.BexSemanticIdentityBoundary):blue.bex.api.BexExecutionContext$Builder method public steps(blue.bex.api.BexStepResults):blue.bex.api.BexExecutionContext$Builder -class public abstract interface blue.bex.api.BexGasLedgerHost - method public abstract evidenceUnavailable(blue.language.processor.GasMeter$ChildGasLedger):void - method public abstract failedDeterministically(blue.language.processor.GasMeter$ChildGasLedger):void - method public abstract open(java.lang.String,java.util.Map):blue.language.processor.GasMeter$ChildGasLedger - method public abstract submit(blue.language.processor.GasMeter$ChildGasLedger):void +class public abstract interface blue.bex.api.BexFailureBoundary implements blue.bex.output.BexFailurePolicy + field public static final STANDALONE:blue.bex.api.BexFailureBoundary + method public abstract classify(java.lang.Throwable):blue.bex.api.BexFailureBoundary$Classification + method public evidenceUnavailable(java.lang.Throwable):boolean + method public preserveOrWrap(java.lang.String,java.lang.RuntimeException):java.lang.RuntimeException + method public translate(java.lang.RuntimeException):java.lang.RuntimeException +class public static final blue.bex.api.BexFailureBoundary$Classification extends java.lang.Enum + field public static final DETERMINISTIC:blue.bex.api.BexFailureBoundary$Classification + field public static final EVIDENCE_UNAVAILABLE:blue.bex.api.BexFailureBoundary$Classification + field public static final UNCLASSIFIED:blue.bex.api.BexFailureBoundary$Classification + method public static valueOf(java.lang.String):blue.bex.api.BexFailureBoundary$Classification + method public static values():blue.bex.api.BexFailureBoundary$Classification[] +class public abstract interface blue.bex.api.BexGasLedgerHost implements blue.bex.gas.BexGasLedgerLifecycle + method public abstract evidenceUnavailable(blue.bex.gas.BexGasLedgerCapability):void + method public abstract failedDeterministically(blue.bex.gas.BexGasLedgerCapability):void + method public abstract open(java.lang.String,java.util.Map):blue.bex.gas.BexGasLedgerCapability + method public abstract submit(blue.bex.gas.BexGasLedgerCapability):void method public localGasLimitExceeded(blue.bex.gas.BexGasLimitExceededException,java.lang.RuntimeException):java.lang.RuntimeException - method public open(java.lang.String,java.util.Map,blue.language.processor.RuntimeWorkBudget):blue.language.processor.GasMeter$ChildGasLedger - method public openSharedBudget(long):blue.language.processor.RuntimeWorkBudget - method public propagateGasExhaustion(blue.language.processor.GasMeter$ChildGasLedger,blue.language.processor.GasLimitExceededException):void + method public open(java.lang.String,java.util.Map,blue.bex.gas.BexSharedGasBudget):blue.bex.gas.BexGasLedgerCapability + method public openSharedBudget(long):blue.bex.gas.BexSharedGasBudget + method public propagateGasExhaustion(blue.bex.gas.BexGasLedgerCapability,blue.bex.gas.BexHostGasExhaustion):void method public separatesRuntimeNamespaces():boolean class public final blue.bex.api.BexIntrinsicInvocation method public blueId():java.lang.String @@ -90,7 +113,7 @@ class public final blue.bex.api.BexIntrinsicInvocation method public type():blue.bex.value.BexValue class public abstract interface blue.bex.api.BexIntrinsicProcessor method public abstract execute(blue.bex.api.BexIntrinsicInvocation):blue.bex.value.BexValue -class public final blue.bex.api.BexIntrinsicRegistry +class public final blue.bex.api.BexIntrinsicRegistry implements blue.bex.compile.BexIntrinsicCatalog,blue.bex.runtime.BexRuntimeIntrinsics method public identity():java.lang.String method public invoke(java.lang.String,blue.bex.value.BexValue,java.util.Map,blue.bex.gas.BexGasMeter,blue.bex.output.BexOutputAdmission):blue.bex.value.BexValue method public registeredNamedWeights():java.util.Map @@ -113,8 +136,8 @@ class public static final blue.bex.api.BexIntrinsicRegistry$Builder method public register(java.lang.String,java.lang.String,java.util.Map,blue.bex.api.BexIntrinsicProcessor):blue.bex.api.BexIntrinsicRegistry$Builder class public abstract interface blue.bex.api.BexMetricsSink field public static final NOOP:blue.bex.api.BexMetricsSink - method public abstract accept(blue.bex.result.BexMetrics):void -class public final blue.bex.api.BexProgramSource + method public abstract accept(blue.bex.result.BexMetricsSnapshot):void +class public final blue.bex.api.BexProgramSource implements blue.bex.compile.BexCompilationInput method public definitionNode():java.util.Optional method public entry():java.util.Optional method public isExpression():boolean @@ -128,7 +151,7 @@ class public static final blue.bex.api.BexProgramSource$Kind extends java.lang.E field public static final FULL_PROGRAM:blue.bex.api.BexProgramSource$Kind method public static valueOf(java.lang.String):blue.bex.api.BexProgramSource$Kind method public static values():blue.bex.api.BexProgramSource$Kind[] -class public final blue.bex.api.BexStepResults +class public final blue.bex.api.BexStepResults implements blue.bex.runtime.BexStepResultView method public asValue():blue.bex.value.BexValue method public static builder():blue.bex.api.BexStepResults$Builder method public static empty():blue.bex.api.BexStepResults @@ -147,61 +170,27 @@ class public final blue.bex.api.FrozenBexDocumentView implements blue.bex.api.Be method public currentScopePath():java.lang.String method public resolvePointer(java.lang.String):java.lang.String method public resolvedAt(java.lang.String):blue.bex.value.BexValue -class public final blue.bex.api.ProcessorExecutionContextBexDocumentView implements blue.bex.api.BexDocumentView - constructor public (blue.language.processor.ProcessorExecutionContext) - method public canonicalAt(java.lang.String):blue.bex.value.BexValue - method public currentScopePath():java.lang.String - method public resolvePointer(java.lang.String):java.lang.String - method public resolvedAt(java.lang.String):blue.bex.value.BexValue -class public final blue.bex.api.ProcessorExecutionContextBexGasLedgerHost implements blue.bex.api.BexGasLedgerHost - constructor public (blue.language.processor.ProcessorExecutionContext) - constructor public (blue.language.processor.ProcessorExecutionContext,java.lang.String) - constructor public (blue.language.processor.RuntimeWorkSession,java.lang.String) - method public evidenceUnavailable(blue.language.processor.GasMeter$ChildGasLedger):void - method public failedDeterministically(blue.language.processor.GasMeter$ChildGasLedger):void - method public localGasLimitExceeded(blue.bex.gas.BexGasLimitExceededException,java.lang.RuntimeException):java.lang.RuntimeException - method public open(java.lang.String,java.util.Map):blue.language.processor.GasMeter$ChildGasLedger - method public open(java.lang.String,java.util.Map,blue.language.processor.RuntimeWorkBudget):blue.language.processor.GasMeter$ChildGasLedger - method public openSharedBudget(long):blue.language.processor.RuntimeWorkBudget - method public physicalNamespace(java.lang.String):java.lang.String - method public propagateGasExhaustion(blue.language.processor.GasMeter$ChildGasLedger,blue.language.processor.GasLimitExceededException):void - method public runtimeNamespace():java.lang.String - method public separatesRuntimeNamespaces():boolean - method public submit(blue.language.processor.GasMeter$ChildGasLedger):void +class public abstract interface blue.bex.compile.BexCompilationInput + method public abstract definitionNode():java.util.Optional + method public abstract entry():java.util.Optional + method public abstract isExpression():boolean + method public abstract programNode():blue.language.snapshot.FrozenNode +class public static final blue.bex.compile.BexCompilationInput$Kind extends java.lang.Enum + field public static final EXPRESSION:blue.bex.compile.BexCompilationInput$Kind + field public static final FULL_PROGRAM:blue.bex.compile.BexCompilationInput$Kind + method public static valueOf(java.lang.String):blue.bex.compile.BexCompilationInput$Kind + method public static values():blue.bex.compile.BexCompilationInput$Kind[] class public final blue.bex.compile.BexCompiledProgram - constructor public (blue.bex.compile.BexCompiledProgram$CompiledFunction,java.util.Map,java.util.Map,int,java.lang.String) - constructor public (blue.bex.compile.BexCompiledProgram$CompiledFunction,java.util.Map,java.util.Map,int,java.lang.String,java.util.Set) + method public compilationEnvironmentIdentity():java.lang.String method public constant(java.lang.String):blue.bex.value.BexValue - method public constants():java.util.Map - method public entry():blue.bex.compile.BexCompiledProgram$CompiledFunction - method public execute(blue.bex.runtime.BexRuntime):blue.bex.value.BexValue - method public functions():java.util.Map method public programBlueId():java.lang.String method public requiredIntrinsicBlueIds():java.util.Set - method public rootFrameSize():int -class public static final blue.bex.compile.BexCompiledProgram$ArgSpec - constructor public (java.lang.String,int,blue.language.snapshot.FrozenNode,java.lang.String) - method public name():java.lang.String - method public pattern():blue.language.snapshot.FrozenNode - method public slot():int - method public sourcePointer():java.lang.String - method public typed():boolean -class public static final blue.bex.compile.BexCompiledProgram$CompiledFunction - constructor public (java.lang.String,java.util.List,java.util.List,blue.bex.runtime.CompiledExpression,int) - method public arg(java.lang.String):blue.bex.compile.BexCompiledProgram$ArgSpec - method public argSlot(java.lang.String):int - method public args():java.util.Collection - method public frameSize():int - method public hasArg(java.lang.String):boolean - method public invokePrepared(blue.bex.runtime.BexRuntime,blue.bex.runtime.CompiledFrame,int[],blue.bex.value.BexValue[]):blue.bex.value.BexValue - method public invokeRoot(blue.bex.runtime.BexRuntime):blue.bex.value.BexValue - method public name():java.lang.String class public abstract interface blue.bex.compile.BexCompiledProgramCache method public abstract get(blue.bex.compile.BexCompiledProgramKey):blue.bex.compile.BexCompiledProgram method public abstract put(blue.bex.compile.BexCompiledProgramKey,blue.bex.compile.BexCompiledProgram):void class public final blue.bex.compile.BexCompiledProgramKey - constructor public (blue.bex.api.BexProgramSource$Kind,java.lang.String,java.lang.String,java.lang.String) - constructor public (blue.bex.api.BexProgramSource$Kind,java.lang.String,java.lang.String,java.lang.String,java.lang.String) + constructor public (blue.bex.compile.BexCompilationInput$Kind,java.lang.String,java.lang.String,java.lang.String) + constructor public (blue.bex.compile.BexCompilationInput$Kind,java.lang.String,java.lang.String,java.lang.String,java.lang.String) constructor public (java.lang.String,java.lang.String,java.lang.String) field public static final BEX_RUNTIME_REGISTRY_IDENTITY:java.lang.String field public static final COMPILER_IDENTITY:java.lang.String @@ -210,26 +199,126 @@ class public final blue.bex.compile.BexCompiledProgramKey method public entryName():java.lang.String method public equals(java.lang.Object):boolean method public hashCode():int - method public kind():blue.bex.api.BexProgramSource$Kind + method public kind():blue.bex.compile.BexCompilationInput$Kind method public programIdentity():java.lang.String - method public static from(blue.bex.api.BexProgramSource):blue.bex.compile.BexCompiledProgramKey - method public static from(blue.bex.api.BexProgramSource,java.lang.String):blue.bex.compile.BexCompiledProgramKey -class public final blue.bex.compile.BexCompiler - constructor public (blue.bex.result.BexMetrics) - constructor public (blue.bex.result.BexMetrics,blue.bex.api.BexIntrinsicRegistry) - method public compile(blue.bex.api.BexProgramSource):blue.bex.compile.BexCompiledProgram + method public static from(blue.bex.compile.BexCompilationInput):blue.bex.compile.BexCompiledProgramKey + method public static from(blue.bex.compile.BexCompilationInput,java.lang.String):blue.bex.compile.BexCompiledProgramKey +class public final blue.bex.compile.BexCompiledProgramRuntimeAccess + method public static execute(blue.bex.compile.BexCompiledProgram,blue.bex.compile.BexExecutionMachine):blue.bex.value.BexValue +class public final blue.bex.compile.BexCompilerRuntimeAccess + method public static compile(blue.bex.compile.BexCompilationInput,blue.bex.result.BexMetricsRecorder,blue.bex.compile.BexIntrinsicCatalog,java.lang.String):blue.bex.compile.BexCompiledProgram class public final blue.bex.compile.BexContainsCache constructor public () constructor public (int) - method public synchronized containsBex(blue.language.snapshot.FrozenNode,blue.bex.result.BexMetrics):boolean + method public synchronized containsBex(blue.language.snapshot.FrozenNode,blue.bex.result.BexMetricsRecorder):boolean +class public abstract interface blue.bex.compile.BexExecutionMachine + method public abstract appendChange(blue.bex.result.BexPatchEntry):void + method public abstract appendEvent(blue.bex.value.BexValue):void + method public abstract canonicalPointer(java.lang.String):java.lang.String + method public abstract changesetValue():blue.bex.value.BexValue + method public abstract defaultResultValue():blue.bex.value.BexValue + method public abstract eventsValue():blue.bex.value.BexValue + method public abstract gas():blue.bex.gas.BexGasMeter + method public abstract invokeIntrinsic(java.lang.String,blue.bex.value.BexValue,java.util.Map):blue.bex.value.BexValue + method public abstract matchesType(blue.bex.value.BexValue,blue.language.snapshot.FrozenNode,blue.bex.BexSourcePath):boolean + method public abstract metrics():blue.bex.result.BexMetricsRecorder + method public abstract nodeBlueId(blue.bex.value.BexValue):blue.bex.value.BexValue + method public abstract parseDynamicPointer(java.lang.String):java.util.List + method public abstract program():blue.bex.compile.BexCompiledProgram + method public abstract readBinding(java.lang.String,java.util.List):blue.bex.value.BexValue + method public abstract readCurrentContract(java.util.List):blue.bex.value.BexValue + method public abstract readDocument(java.lang.String,java.util.List,boolean):blue.bex.value.BexValue + method public abstract readEvent(java.util.List):blue.bex.value.BexValue + method public abstract readProcessingEvent(java.util.List):blue.bex.value.BexValue + method public abstract readResultValue(java.lang.String,java.util.List):blue.bex.value.BexValue + method public abstract readSteps(java.lang.String,java.util.List):blue.bex.value.BexValue + method public abstract readValuePointer(blue.bex.value.BexValue,java.util.List):blue.bex.value.BexValue + method public abstract resolvePointer(java.lang.String):java.lang.String +class public abstract interface blue.bex.compile.BexIntrinsicCatalog + method public abstract supports(java.lang.String):boolean class public final blue.bex.compile.BexNodeIdentity method public static safeBlueId(blue.language.snapshot.FrozenNode):java.lang.String method public static stable(blue.language.snapshot.FrozenNode):java.lang.String +class public final blue.bex.compile.CompileScope + constructor public () + constructor public (blue.bex.compile.CompileScope) + method public captureVisibility():blue.bex.compile.CompileScope$Visibility + method public declareOrGetSlot(java.lang.String):int + method public frameSize():int + method public hasSlot(java.lang.String):boolean + method public resolveSlot(java.lang.String):int + method public restoreVisibility(blue.bex.compile.CompileScope$Visibility):void +class public static final blue.bex.compile.CompileScope$Visibility +class public abstract interface blue.bex.compile.CompiledExpression + method public abstract eval(blue.bex.compile.CompiledFrame):blue.bex.value.BexValue +class public final blue.bex.compile.CompiledFrame + constructor public (blue.bex.compile.BexExecutionMachine,int,blue.bex.compile.CompiledFrame) + method public appendChange(blue.bex.result.BexPatchEntry):void + method public appendEvent(blue.bex.value.BexValue):void + method public changesetValue():blue.bex.value.BexValue + method public clear(int):void + method public enter(blue.bex.BexSourcePath):blue.bex.BexSourcePath + method public eventsValue():blue.bex.value.BexValue + method public get(int):blue.bex.value.BexValue + method public getRequired(int):blue.bex.value.BexValue + method public isInitialized(int):boolean + method public machine():blue.bex.compile.BexExecutionMachine + method public parent():blue.bex.compile.CompiledFrame + method public readBinding(java.lang.String,java.util.List):blue.bex.value.BexValue + method public readCurrentContract(java.util.List):blue.bex.value.BexValue + method public readDocument(java.lang.String,java.util.List,boolean):blue.bex.value.BexValue + method public readEvent(java.util.List):blue.bex.value.BexValue + method public readProcessingEvent(java.util.List):blue.bex.value.BexValue + method public restore(blue.bex.BexSourcePath):void + method public returnValue():blue.bex.value.BexValue + method public returnValue(blue.bex.value.BexValue):void + method public set(int,blue.bex.value.BexValue):void + method public sourcePath():blue.bex.BexSourcePath +class public abstract interface blue.bex.compile.CompiledStatement + method public abstract exec(blue.bex.compile.CompiledFrame):blue.bex.compile.Control +class public final blue.bex.compile.Control extends java.lang.Enum + field public static final CONTINUE:blue.bex.compile.Control + field public static final RETURN:blue.bex.compile.Control + method public static valueOf(java.lang.String):blue.bex.compile.Control + method public static values():blue.bex.compile.Control[] class public final blue.bex.compile.LruBexCompiledProgramCache implements blue.bex.compile.BexCompiledProgramCache constructor public () constructor public (int) method public synchronized get(blue.bex.compile.BexCompiledProgramKey):blue.bex.compile.BexCompiledProgram method public synchronized put(blue.bex.compile.BexCompiledProgramKey,blue.bex.compile.BexCompiledProgram):void +class public final blue.bex.contracts.BexContractsExecutionContext + method public static builder(blue.language.processor.ProcessorExecutionContext):blue.bex.api.BexExecutionContext$Builder + method public static builder(blue.language.processor.ProcessorExecutionContext,java.lang.String):blue.bex.api.BexExecutionContext$Builder + method public static configure(blue.bex.api.BexExecutionContext$Builder,blue.language.processor.ProcessorExecutionContext):blue.bex.api.BexExecutionContext$Builder + method public static configure(blue.bex.api.BexExecutionContext$Builder,blue.language.processor.ProcessorExecutionContext,java.lang.String):blue.bex.api.BexExecutionContext$Builder +class public final blue.bex.contracts.BexContractsFailureBoundary implements blue.bex.api.BexFailureBoundary + field public static final INSTANCE:blue.bex.contracts.BexContractsFailureBoundary + method public classify(java.lang.Throwable):blue.bex.api.BexFailureBoundary$Classification + method public translate(java.lang.RuntimeException):java.lang.RuntimeException +class public final blue.bex.contracts.ProcessorExecutionContextBexDocumentView implements blue.bex.api.BexDocumentView + constructor public (blue.language.processor.ProcessorExecutionContext) + method public canonicalAt(java.lang.String):blue.bex.value.BexValue + method public currentScopePath():java.lang.String + method public resolvePointer(java.lang.String):java.lang.String + method public resolvedAt(java.lang.String):blue.bex.value.BexValue +class public final blue.bex.contracts.ProcessorExecutionContextBexGasLedgerHost implements blue.bex.api.BexGasLedgerHost + constructor public (blue.language.processor.ProcessorExecutionContext) + constructor public (blue.language.processor.ProcessorExecutionContext,java.lang.String) + constructor public (blue.language.processor.RuntimeWorkSession,java.lang.String) + method public evidenceUnavailable(blue.bex.gas.BexGasLedgerCapability):void + method public failedDeterministically(blue.bex.gas.BexGasLedgerCapability):void + method public localGasLimitExceeded(blue.bex.gas.BexGasLimitExceededException,java.lang.RuntimeException):java.lang.RuntimeException + method public open(java.lang.String,java.util.Map):blue.bex.gas.BexGasLedgerCapability + method public open(java.lang.String,java.util.Map,blue.bex.gas.BexSharedGasBudget):blue.bex.gas.BexGasLedgerCapability + method public openSharedBudget(long):blue.bex.gas.BexSharedGasBudget + method public physicalNamespace(java.lang.String):java.lang.String + method public propagateGasExhaustion(blue.bex.gas.BexGasLedgerCapability,blue.bex.gas.BexHostGasExhaustion):void + method public runtimeNamespace():java.lang.String + method public separatesRuntimeNamespaces():boolean + method public submit(blue.bex.gas.BexGasLedgerCapability):void +class public final blue.bex.contracts.ProcessorExecutionContextBexSemanticIdentityBoundary implements blue.bex.output.BexSemanticIdentityBoundary + constructor public (blue.language.processor.ProcessorExecutionContext) + method public establishIdentity(blue.language.model.Node):blue.bex.output.BexEstablishedIdentity class public final blue.bex.gas.BexGasCharge constructor public (long,blue.bex.gas.BexGasCounter,long,long,java.lang.String,java.lang.String,java.lang.String) constructor public (long,blue.bex.gas.BexGasCounter,long,long,long,java.lang.String,java.lang.String,java.lang.String) @@ -250,6 +339,13 @@ class public final blue.bex.gas.BexGasCharge method public sourcePath():java.lang.String method public toString():java.lang.String method public weight():long +class public final blue.bex.gas.BexGasChargeContext + method public contractKey():java.lang.String + method public logicalPath():java.lang.String + method public reason():java.lang.String + method public scopePath():java.lang.String + method public static empty():blue.bex.gas.BexGasChargeContext + method public static of(java.lang.String,java.lang.String,java.lang.String,java.lang.String):blue.bex.gas.BexGasChargeContext class public final blue.bex.gas.BexGasCounter extends java.lang.Enum field public static final BINDING_READ:blue.bex.gas.BexGasCounter field public static final BLUE_OUTPUT_BOUNDARY:blue.bex.gas.BexGasCounter @@ -295,6 +391,7 @@ class public final blue.bex.gas.BexGasCounter extends java.lang.Enum method public toString():java.lang.String class public final blue.bex.gas.BexGasLedger constructor public (java.util.List) + constructor public (java.util.List,java.lang.String,java.lang.String) method public equals(java.lang.Object):boolean method public gasUsed():long method public hashCode():int @@ -308,18 +405,36 @@ class public final blue.bex.gas.BexGasLedger method public toString():java.lang.String method public totalGas():long method public trace():java.util.List +class public abstract interface blue.bex.gas.BexGasLedgerCapability + method public abstract charge(java.lang.String,long,blue.bex.gas.BexGasChargeContext):void + method public abstract counterWeights():java.util.Map + method public abstract effectiveBudget():long + method public abstract namespace():java.lang.String + method public abstract remainingGas():long + method public abstract totalGas():long + method public charge(java.lang.String,long):void +class public abstract interface blue.bex.gas.BexGasLedgerLifecycle + method public abstract evidenceUnavailable(blue.bex.gas.BexGasLedgerCapability):void + method public abstract failedDeterministically(blue.bex.gas.BexGasLedgerCapability):void + method public abstract open(java.lang.String,java.util.Map):blue.bex.gas.BexGasLedgerCapability + method public abstract submit(blue.bex.gas.BexGasLedgerCapability):void + method public localGasLimitExceeded(blue.bex.gas.BexGasLimitExceededException,java.lang.RuntimeException):java.lang.RuntimeException + method public open(java.lang.String,java.util.Map,blue.bex.gas.BexSharedGasBudget):blue.bex.gas.BexGasLedgerCapability + method public openSharedBudget(long):blue.bex.gas.BexSharedGasBudget + method public propagateGasExhaustion(blue.bex.gas.BexGasLedgerCapability,blue.bex.gas.BexHostGasExhaustion):void + method public separatesRuntimeNamespaces():boolean class public final blue.bex.gas.BexGasLimitExceededException extends blue.bex.BexException method public admittedGas():long method public counter():blue.bex.gas.BexGasCounter method public counterName():java.lang.String method public effectiveBudget():long - method public hostGasLimitExceeded():blue.language.processor.GasLimitExceededException + method public hostGasExhaustion():blue.bex.gas.BexHostGasExhaustion method public namespace():java.lang.String method public quantity():long method public weight():long class public final blue.bex.gas.BexGasMeter - constructor public (blue.bex.gas.BexGasSchedule,blue.language.processor.GasMeter$ChildGasLedger) - constructor public (blue.bex.gas.BexGasSchedule,blue.language.processor.GasMeter$ChildGasLedger,long) + constructor public (blue.bex.gas.BexGasSchedule,blue.bex.gas.BexGasLedgerCapability) + constructor public (blue.bex.gas.BexGasSchedule,blue.bex.gas.BexGasLedgerCapability,long) constructor public (blue.bex.gas.BexGasSchedule,java.util.Map,long,java.util.Map) constructor public (blue.bex.gas.BexGasSchedule,long) constructor public (blue.bex.gas.BexGasSchedule,long,long) @@ -346,7 +461,7 @@ class public final blue.bex.gas.BexGasMeter method public ledger():blue.bex.gas.BexGasLedger method public localLimit():long method public parentRemainingGas():long - method public propagateHostGasExhaustion(blue.language.processor.GasLimitExceededException,java.util.function.Consumer,java.util.function.BiConsumer):void + method public propagateHostGasExhaustion(blue.bex.gas.BexHostGasExhaustion,java.util.function.Consumer,java.util.function.BiConsumer):void method public registeredNamedWeights():java.util.Map method public remaining():long method public remainingGas():long @@ -436,6 +551,19 @@ class public static final blue.bex.gas.BexGasSchedule$Builder method public variableRead(long):blue.bex.gas.BexGasSchedule$Builder method public weight(blue.bex.gas.BexGasCounter,long):blue.bex.gas.BexGasSchedule$Builder method public weight(java.lang.String,long):blue.bex.gas.BexGasSchedule$Builder +class public final blue.bex.gas.BexHostGasExhaustion extends java.lang.RuntimeException + constructor public (java.lang.String,java.lang.String,long,long,long,long,java.lang.RuntimeException) + method public admittedGas():long + method public counterName():java.lang.String + method public effectiveBudget():long + method public hostFailure():java.lang.RuntimeException + method public namespace():java.lang.String + method public quantity():long + method public weight():long +class public abstract interface blue.bex.gas.BexSharedGasBudget + method public abstract admittedGas():long + method public abstract maximumGas():long + method public abstract remainingGas():long class public final blue.bex.output.BexAdmittedValue method public node():blue.language.model.Node method public nodeBlueId():java.lang.String @@ -446,8 +574,14 @@ class public final blue.bex.output.BexEstablishedIdentity constructor public (java.lang.String,blue.language.snapshot.FrozenNode) method public blueId():java.lang.String method public frozenValue():blue.language.snapshot.FrozenNode +class public abstract interface blue.bex.output.BexFailurePolicy + field public static final STANDALONE:blue.bex.output.BexFailurePolicy + method public abstract evidenceUnavailable(java.lang.Throwable):boolean + method public preserveOrWrap(java.lang.String,java.lang.RuntimeException):java.lang.RuntimeException + method public translate(java.lang.RuntimeException):java.lang.RuntimeException class public final blue.bex.output.BexOutputAdmission constructor public (blue.bex.gas.BexGasMeter,blue.bex.output.BexSemanticIdentityBoundary) + constructor public (blue.bex.gas.BexGasMeter,blue.bex.output.BexSemanticIdentityBoundary,blue.bex.output.BexFailurePolicy) method public admit(blue.bex.value.BexValue,blue.bex.output.BexOutputKind):blue.bex.output.BexAdmittedValue method public semanticIdentityMergeCount():long class public final blue.bex.output.BexOutputKind extends java.lang.Enum @@ -462,9 +596,6 @@ class public final blue.bex.output.BexOutputKind extends java.lang.Enum class public abstract interface blue.bex.output.BexSemanticIdentityBoundary field public static final STANDALONE:blue.bex.output.BexSemanticIdentityBoundary method public abstract establishIdentity(blue.language.model.Node):blue.bex.output.BexEstablishedIdentity -class public final blue.bex.output.ProcessorExecutionContextBexSemanticIdentityBoundary implements blue.bex.output.BexSemanticIdentityBoundary - constructor public (blue.language.processor.ProcessorExecutionContext) - method public establishIdentity(blue.language.model.Node):blue.bex.output.BexEstablishedIdentity class public final blue.bex.pointer.BexPointer method public descendant(java.util.List):blue.bex.pointer.BexPointer method public equals(java.lang.Object):boolean @@ -478,22 +609,22 @@ class public final blue.bex.pointer.BexPointerCache constructor public () constructor public (int) method public capacity():int - method public synchronized get(java.lang.String,blue.bex.result.BexMetrics):blue.bex.pointer.BexPointer -class public final blue.bex.result.BexChangeset + method public synchronized get(java.lang.String,blue.bex.result.BexMetricsRecorder):blue.bex.pointer.BexPointer +class public final blue.bex.result.BexChangeset implements blue.bex.value.BexChangesetValueView constructor public (java.util.List) method public asValue():blue.bex.value.BexValue method public entries():java.util.List method public static patchEntryValue(blue.bex.result.BexPatchEntry):blue.bex.value.BexValue -class public final blue.bex.result.BexEvents +class public final blue.bex.result.BexEvents implements blue.bex.value.BexEventsValueView constructor public (java.util.List) constructor public (java.util.List,java.util.List) method public admittedEvents():java.util.List method public asValue():blue.bex.value.BexValue method public events():java.util.List class public final blue.bex.result.BexExecutionResult - constructor public (blue.bex.value.BexValue,blue.bex.result.BexChangeset,blue.bex.result.BexEvents,blue.bex.gas.BexGasLedger,blue.bex.result.BexMetrics) - constructor public (blue.bex.value.BexValue,blue.bex.result.BexChangeset,blue.bex.result.BexEvents,blue.bex.gas.BexGasLedger,blue.bex.result.BexMetrics,blue.bex.output.BexAdmittedValue) - constructor public (blue.bex.value.BexValue,blue.bex.result.BexChangeset,blue.bex.result.BexEvents,java.util.List,blue.bex.result.BexMetrics) + constructor public (blue.bex.value.BexValue,blue.bex.result.BexChangeset,blue.bex.result.BexEvents,blue.bex.gas.BexGasLedger,blue.bex.result.BexMetricsSnapshot) + constructor public (blue.bex.value.BexValue,blue.bex.result.BexChangeset,blue.bex.result.BexEvents,blue.bex.gas.BexGasLedger,blue.bex.result.BexMetricsSnapshot,blue.bex.output.BexAdmittedValue) + constructor public (blue.bex.value.BexValue,blue.bex.result.BexChangeset,blue.bex.result.BexEvents,java.util.List,blue.bex.result.BexMetricsSnapshot) method public changeset():blue.bex.result.BexChangeset method public events():blue.bex.result.BexEvents method public gasLedger():blue.bex.gas.BexGasLedger @@ -501,10 +632,47 @@ class public final blue.bex.result.BexExecutionResult method public gasUsed():long method public ledger():blue.bex.gas.BexGasLedger method public metrics():blue.bex.result.BexMetrics + method public metricsSnapshot():blue.bex.result.BexMetricsSnapshot method public output():blue.bex.output.BexAdmittedValue method public trace():java.util.List method public value():blue.bex.value.BexValue class public final blue.bex.result.BexMetrics + constructor public () + method public compileCacheHits():long + method public compileCacheMisses():long + method public compileNanos():long + method public compiledExecutions():long + method public containsBexCacheHits():long + method public containsBexCacheMisses():long + method public containsBexScans():long + method public copy():blue.bex.result.BexMetrics + method public currentContractReads():long + method public eventReads():long + method public executeNanos():long + method public expressionEvaluations():long + method public frozenDocumentReads():long + method public frozenOutputConversions():long + method public frozenWriterNodeFallbacks():long + method public functionArgMapAllocations():long + method public functionCalls():long + method public interpretedFallbacks():long + method public loopIterations():long + method public nodeMaterializations():long + method public nodeOutputConversions():long + method public pointerCacheHits():long + method public pointerCacheMisses():long + method public pointerParses():long + method public resolvedDocumentReads():long + method public resultOverlayAncestorHits():long + method public resultOverlayDocumentFallbacks():long + method public resultOverlayExactHits():long + method public resultValueReads():long + method public simpleMaterializations():long + method public snapshot():blue.bex.result.BexMetricsSnapshot + method public statementExecutions():long + method public static fromSnapshot(blue.bex.result.BexMetricsSnapshot):blue.bex.result.BexMetrics + method public stepsReads():long +class public final blue.bex.result.BexMetricsRecorder implements blue.bex.value.BexValueMetrics constructor public () method public addCompileNanos(long):void method public addExecuteNanos(long):void @@ -515,7 +683,6 @@ class public final blue.bex.result.BexMetrics method public containsBexCacheHits():long method public containsBexCacheMisses():long method public containsBexScans():long - method public copy():blue.bex.result.BexMetrics method public currentContractReads():long method public eventReads():long method public executeNanos():long @@ -567,9 +734,42 @@ class public final blue.bex.result.BexMetrics method public resultOverlayExactHits():long method public resultValueReads():long method public simpleMaterializations():long + method public snapshot():blue.bex.result.BexMetricsSnapshot method public statementExecutions():long method public stepsReads():long -class public final blue.bex.result.BexPatchEntry +class public final blue.bex.result.BexMetricsSnapshot + method public compileCacheHits():long + method public compileCacheMisses():long + method public compileNanos():long + method public compiledExecutions():long + method public containsBexCacheHits():long + method public containsBexCacheMisses():long + method public containsBexScans():long + method public currentContractReads():long + method public eventReads():long + method public executeNanos():long + method public expressionEvaluations():long + method public frozenDocumentReads():long + method public frozenOutputConversions():long + method public frozenWriterNodeFallbacks():long + method public functionArgMapAllocations():long + method public functionCalls():long + method public interpretedFallbacks():long + method public loopIterations():long + method public nodeMaterializations():long + method public nodeOutputConversions():long + method public pointerCacheHits():long + method public pointerCacheMisses():long + method public pointerParses():long + method public resolvedDocumentReads():long + method public resultOverlayAncestorHits():long + method public resultOverlayDocumentFallbacks():long + method public resultOverlayExactHits():long + method public resultValueReads():long + method public simpleMaterializations():long + method public statementExecutions():long + method public stepsReads():long +class public final blue.bex.result.BexPatchEntry implements blue.bex.value.BexPatchValueView constructor public (java.lang.String,java.lang.String,java.lang.String,blue.bex.value.BexValue) constructor public (java.lang.String,java.lang.String,java.lang.String,blue.bex.value.BexValue,blue.bex.output.BexAdmittedValue) method public absolutePath():java.lang.String @@ -579,8 +779,8 @@ class public final blue.bex.result.BexPatchEntry method public op():java.lang.String method public val():blue.bex.value.BexValue class public final blue.bex.result.BexResultOverlay - constructor public (blue.bex.api.BexDocumentView,blue.bex.result.BexMetrics) - constructor public (blue.bex.api.BexDocumentView,blue.bex.result.BexMetrics,blue.language.runtime.BlueLanguage) + constructor public (blue.bex.spi.BexDocumentAccess,blue.bex.result.BexMetricsRecorder) + constructor public (blue.bex.spi.BexDocumentAccess,blue.bex.result.BexMetricsRecorder,blue.language.runtime.BlueLanguage) method public append(blue.bex.result.BexPatchEntry):void method public rootValue():blue.bex.value.BexValue method public valueAt(java.lang.String,java.util.List):blue.bex.value.BexValue @@ -592,18 +792,23 @@ class public final blue.bex.runtime.BexExecutionAccumulator method public changeset():blue.bex.result.BexChangeset method public events():blue.bex.result.BexEvents method public overlay():blue.bex.result.BexResultOverlay -class public final blue.bex.runtime.BexRuntime - constructor public (blue.bex.compile.BexCompiledProgram,blue.bex.api.BexExecutionContext,blue.language.runtime.BlueLanguage,blue.bex.gas.BexGasSchedule,blue.bex.result.BexMetrics,blue.bex.pointer.BexPointerCache) - constructor public (blue.bex.compile.BexCompiledProgram,blue.bex.api.BexExecutionContext,blue.language.runtime.BlueLanguage,blue.bex.gas.BexGasSchedule,blue.bex.result.BexMetrics,blue.bex.pointer.BexPointerCache,blue.bex.api.BexIntrinsicRegistry) +class public final blue.bex.runtime.BexRuntime implements blue.bex.compile.BexExecutionMachine + constructor public (blue.bex.compile.BexCompiledProgram,blue.bex.runtime.BexRuntimeContext,blue.language.runtime.BlueLanguage,blue.bex.gas.BexGasSchedule,blue.bex.result.BexMetricsRecorder,blue.bex.pointer.BexPointerCache) + constructor public (blue.bex.compile.BexCompiledProgram,blue.bex.runtime.BexRuntimeContext,blue.language.runtime.BlueLanguage,blue.bex.gas.BexGasSchedule,blue.bex.result.BexMetricsRecorder,blue.bex.pointer.BexPointerCache,blue.bex.runtime.BexRuntimeIntrinsics) method public accumulator():blue.bex.runtime.BexExecutionAccumulator + method public appendChange(blue.bex.result.BexPatchEntry):void + method public appendEvent(blue.bex.value.BexValue):void method public canonicalPointer(java.lang.String):java.lang.String - method public context():blue.bex.api.BexExecutionContext + method public changesetValue():blue.bex.value.BexValue + method public context():blue.bex.runtime.BexRuntimeContext method public defaultResultValue():blue.bex.value.BexValue + method public eventsValue():blue.bex.value.BexValue method public execute():blue.bex.result.BexExecutionResult method public gas():blue.bex.gas.BexGasMeter - method public intrinsics():blue.bex.api.BexIntrinsicRegistry + method public intrinsics():blue.bex.runtime.BexRuntimeIntrinsics method public invokeIntrinsic(java.lang.String,blue.bex.value.BexValue,java.util.Map):blue.bex.value.BexValue - method public metrics():blue.bex.result.BexMetrics + method public matchesType(blue.bex.value.BexValue,blue.language.snapshot.FrozenNode,blue.bex.BexSourcePath):boolean + method public metrics():blue.bex.result.BexMetricsRecorder method public nodeBlueId(blue.bex.value.BexValue):blue.bex.value.BexValue method public outputAdmission():blue.bex.output.BexOutputAdmission method public parseDynamicPointer(java.lang.String):java.util.List @@ -619,59 +824,66 @@ class public final blue.bex.runtime.BexRuntime method public readValuePointer(blue.bex.value.BexValue,java.util.List):blue.bex.value.BexValue method public resolvePointer(java.lang.String):java.lang.String method public typeMatcher():blue.bex.type.BexBlueTypeMatcher -class public final blue.bex.runtime.CompileScope - constructor public () - constructor public (blue.bex.runtime.CompileScope) - method public captureVisibility():blue.bex.runtime.CompileScope$Visibility - method public declareOrGetSlot(java.lang.String):int - method public frameSize():int - method public hasSlot(java.lang.String):boolean - method public resolveSlot(java.lang.String):int - method public restoreVisibility(blue.bex.runtime.CompileScope$Visibility):void -class public static final blue.bex.runtime.CompileScope$Visibility -class public abstract interface blue.bex.runtime.CompiledExpression - method public abstract eval(blue.bex.runtime.CompiledFrame):blue.bex.value.BexValue -class public final blue.bex.runtime.CompiledFrame - constructor public (blue.bex.runtime.BexRuntime,int,blue.bex.runtime.CompiledFrame) - method public accumulator():blue.bex.runtime.BexExecutionAccumulator - method public clear(int):void - method public enter(blue.bex.BexSourcePath):blue.bex.BexSourcePath - method public get(int):blue.bex.value.BexValue - method public getRequired(int):blue.bex.value.BexValue - method public isInitialized(int):boolean - method public parent():blue.bex.runtime.CompiledFrame - method public readBinding(java.lang.String,java.util.List):blue.bex.value.BexValue - method public readCurrentContract(java.util.List):blue.bex.value.BexValue - method public readDocument(java.lang.String,java.util.List,boolean):blue.bex.value.BexValue - method public readEvent(java.util.List):blue.bex.value.BexValue - method public readProcessingEvent(java.util.List):blue.bex.value.BexValue - method public restore(blue.bex.BexSourcePath):void - method public returnValue():blue.bex.value.BexValue - method public returnValue(blue.bex.value.BexValue):void - method public runtime():blue.bex.runtime.BexRuntime - method public set(int,blue.bex.value.BexValue):void - method public sourcePath():blue.bex.BexSourcePath -class public abstract interface blue.bex.runtime.CompiledStatement - method public abstract exec(blue.bex.runtime.CompiledFrame):blue.bex.runtime.Control -class public final blue.bex.runtime.Control extends java.lang.Enum - field public static final CONTINUE:blue.bex.runtime.Control - field public static final RETURN:blue.bex.runtime.Control - method public static valueOf(java.lang.String):blue.bex.runtime.Control - method public static values():blue.bex.runtime.Control[] +class public abstract interface blue.bex.runtime.BexRuntimeContext + method public abstract binding(java.lang.String):blue.bex.value.BexValue + method public abstract currentContract():blue.bex.value.BexValue + method public abstract currentScopePath():java.lang.String + method public abstract document():blue.bex.spi.BexDocumentAccess + method public abstract event():blue.bex.value.BexValue + method public abstract failureBoundary():blue.bex.output.BexFailurePolicy + method public abstract gasLedgerHost():blue.bex.gas.BexGasLedgerLifecycle + method public abstract gasLimit():long + method public abstract parentRemainingGas():long + method public abstract processingEvent():blue.bex.value.BexValue + method public abstract semanticIdentityBoundary():blue.bex.output.BexSemanticIdentityBoundary + method public abstract steps():blue.bex.runtime.BexStepResultView +class public abstract interface blue.bex.runtime.BexRuntimeIntrinsics + field public static final EMPTY:blue.bex.runtime.BexRuntimeIntrinsics + method public abstract invoke(java.lang.String,blue.bex.value.BexValue,java.util.Map,blue.bex.gas.BexGasMeter,blue.bex.output.BexOutputAdmission):blue.bex.value.BexValue + method public abstract registeredNamedWeights(java.util.Set):java.util.Map + method public abstract registeredNamespaceWeights(java.util.Set):java.util.Map +class public abstract interface blue.bex.runtime.BexStepResultView + method public abstract asValue():blue.bex.value.BexValue + method public abstract step(java.lang.String):blue.bex.value.BexValue +class public abstract interface blue.bex.spi.BexDocumentAccess + method public abstract canonicalAt(java.lang.String):blue.bex.value.BexValue + method public abstract currentScopePath():java.lang.String + method public abstract resolvePointer(java.lang.String):java.lang.String + method public abstract resolvedAt(java.lang.String):blue.bex.value.BexValue class public final blue.bex.type.BexBlueTypeMatcher constructor public (blue.language.runtime.BlueLanguage) method public matches(blue.bex.value.BexValue,blue.language.snapshot.FrozenNode,blue.bex.gas.BexGasMeter,blue.bex.BexSourcePath):boolean +class public final blue.bex.type.BexPatternValidator + constructor public () + method public keyMatchesType(java.lang.String,blue.language.snapshot.FrozenNode):boolean + method public requiresPresence(blue.language.snapshot.FrozenNode):boolean +class public final blue.bex.type.BexTypeMatchWorkRecorder + constructor public (blue.bex.gas.BexGasMeter,blue.bex.BexSourcePath) + method public compareText(java.lang.String,java.lang.String):int + method public comparisonNode():void + method public scalarMatches(java.lang.Object,java.lang.Object):boolean +class public final blue.bex.type.BexTypeMatcher + constructor public (blue.language.runtime.BlueLanguage) + method public matches(blue.bex.value.BexValue,blue.language.snapshot.FrozenNode,blue.bex.gas.BexGasMeter,blue.bex.BexSourcePath):boolean class public final blue.bex.value.BexBlueNodeWriter method public static hasLanguageField(blue.bex.value.BexValue):boolean method public static isLanguageField(java.lang.String):boolean method public static toNode(blue.bex.value.BexValue):blue.language.model.Node method public static toSemanticNode(blue.bex.value.BexValue):blue.language.model.Node +class public abstract interface blue.bex.value.BexChangesetValueView + method public abstract entries():java.util.List +class public abstract interface blue.bex.value.BexEventsValueView + method public abstract events():java.util.List class public final blue.bex.value.BexFrozenWriter method public static toFrozen(blue.bex.value.BexValue):blue.language.snapshot.FrozenNode - method public static toFrozen(blue.bex.value.BexValue,blue.bex.result.BexMetrics):blue.language.snapshot.FrozenNode + method public static toFrozen(blue.bex.value.BexValue,blue.bex.value.BexValueMetrics):blue.language.snapshot.FrozenNode method public toFrozenValue(blue.bex.value.BexValue):blue.language.snapshot.FrozenNode class public final blue.bex.value.BexNodeWriter method public static toNode(blue.bex.value.BexValue):blue.language.model.Node +class public abstract interface blue.bex.value.BexPatchValueView + method public abstract absolutePath():java.lang.String + method public abstract op():java.lang.String + method public abstract val():blue.bex.value.BexValue class public final blue.bex.value.BexSimpleWriter method public static toSimple(blue.bex.value.BexValue):java.lang.Object class public final blue.bex.value.BexUnicodeOrder @@ -700,6 +912,22 @@ class public abstract interface blue.bex.value.BexValue method public abstract toSimple():java.lang.Object method public exactBlueId():java.lang.String method public isExact():boolean + method public semanticKind():blue.bex.value.BexValueKind +class public final blue.bex.value.BexValueKind extends java.lang.Enum + field public static final BOOLEAN:blue.bex.value.BexValueKind + field public static final DECIMAL:blue.bex.value.BexValueKind + field public static final INTEGER:blue.bex.value.BexValueKind + field public static final LIST:blue.bex.value.BexValueKind + field public static final NULL:blue.bex.value.BexValueKind + field public static final OBJECT:blue.bex.value.BexValueKind + field public static final TEXT:blue.bex.value.BexValueKind + field public static final UNDEFINED:blue.bex.value.BexValueKind + method public operatorName():java.lang.String + method public static valueOf(java.lang.String):blue.bex.value.BexValueKind + method public static values():blue.bex.value.BexValueKind[] +class public abstract interface blue.bex.value.BexValueMetrics + method public abstract incrementFrozenOutputConversions():void + method public abstract incrementFrozenWriterNodeFallbacks():void class public final blue.bex.value.BexValues field public static final NULL:blue.bex.value.BexValue field public static final UNDEFINED:blue.bex.value.BexValue @@ -722,11 +950,12 @@ class public final blue.bex.value.BexValues method public static referenceBacked(blue.bex.value.BexValue,blue.language.runtime.BlueLanguage):blue.bex.value.BexValue method public static resultOverlayPointerSet(blue.bex.value.BexValue,java.util.List,blue.bex.value.BexValue,java.lang.String):blue.bex.value.BexValue method public static scalar(java.lang.Object):blue.bex.value.BexValue + method public static semanticKind(blue.bex.value.BexValue):blue.bex.value.BexValueKind method public static transientFrozen(blue.language.snapshot.FrozenNode):blue.bex.value.BexValue method public static truthy(blue.bex.value.BexValue):boolean method public static undefined():blue.bex.value.BexValue class public final blue.bex.value.ChangesetBexValue extends blue.bex.value.AbstractBexValue - constructor public (blue.bex.result.BexChangeset) + constructor public (blue.bex.value.BexChangesetValueView) method public get(java.lang.String):blue.bex.value.BexValue method public isList():boolean method public size():int @@ -744,7 +973,7 @@ class public final blue.bex.value.ChangesetBexValue extends blue.bex.value.Abstr method public volatile isUndefined():boolean synthetic bridge method public volatile keys():java.util.List synthetic bridge class public final blue.bex.value.EventsBexValue extends blue.bex.value.AbstractBexValue - constructor public (blue.bex.result.BexEvents) + constructor public (blue.bex.value.BexEventsValueView) method public get(java.lang.String):blue.bex.value.BexValue method public isList():boolean method public size():int @@ -780,7 +1009,7 @@ class public final blue.bex.value.OverlayListBexValue extends blue.bex.value.Abs method public volatile isUndefined():boolean synthetic bridge method public volatile keys():java.util.List synthetic bridge class public final blue.bex.value.PatchEntryBexValue extends blue.bex.value.AbstractBexValue - constructor public (blue.bex.result.BexPatchEntry) + constructor public (blue.bex.value.BexPatchValueView) method public get(java.lang.String):blue.bex.value.BexValue method public isObject():boolean method public keys():java.util.List From f7cd0548b86ae3a5f2ae1a095e367aea40d2ddf5 Mon Sep 17 00:00:00 2001 From: piotr-blue Date: Sun, 2 Aug 2026 23:08:42 +0100 Subject: [PATCH 08/13] fix: harden BEX modernization and release evidence --- .../scripts/compare-independent-builds.mjs | 257 ++++++++- .../scripts/run-final-publication-gates.sh | 6 + README.md | 20 +- blue-bex-conformance/build.gradle.kts | 355 ++++++++++++- .../blue/bex/benchmark/BexCoreBenchmark.java | 51 +- .../processor/BexHostedGasBenchmark.java | 6 + .../src/main/java/blue/bex/api/BexEngine.java | 21 +- .../blue/bex/compile/BexCompiledProgram.java | 36 +- .../BexCompiledProgramRuntimeAccess.java | 7 + .../blue/bex/compile/BexProgramCompiler.java | 6 +- .../main/java/blue/bex/value/BexValues.java | 12 +- .../buildlogic/RootOrchestrationPlugin.java | 2 + .../GenerateModernizationReportTask.java | 59 +- .../tasks/GenerateReleaseReportTask.java | 99 +--- .../buildlogic/tasks/ReleaseEvidenceJson.java | 502 ++++++++++++++++++ .../blue/bex/buildlogic/tasks/StrictJson.java | 370 +++++++++++++ .../tasks/VerifyPublishedLanguageTask.java | 23 +- docs/LATEST_LANGUAGE_API_MIGRATION.md | 6 +- docs/contracts-hosting.md | 8 +- docs/gas-and-exhaustion.md | 2 +- docs/intrinsics.md | 4 +- docs/latest-language-api-migration.json | 8 +- docs/public-api-classification.json | 4 +- docs/start-here.md | 6 +- .../bex/examples/HostedConsumerSmoke.java | 9 +- .../bex/examples/StandaloneBexExample.java | 17 +- .../api/modernization-added-descriptors.txt | 1 + .../blue/bex/BexCompiledProgramCacheTest.java | 52 ++ .../api/BexEngineBuilderValidationTest.java | 27 + .../BexCompiledIrImmutabilityTest.java | 7 +- .../conformance/BexConformanceReportMain.java | 229 +++++++- .../BexValuesIdentityValidationTest.java | 80 +++ .../hosted-release/required-public-api.txt | 1 + 33 files changed, 2072 insertions(+), 221 deletions(-) create mode 100644 build-logic/src/main/java/blue/bex/buildlogic/tasks/ReleaseEvidenceJson.java create mode 100644 build-logic/src/main/java/blue/bex/buildlogic/tasks/StrictJson.java create mode 100644 src/test/java/blue/bex/api/BexEngineBuilderValidationTest.java create mode 100644 src/test/java/blue/bex/value/BexValuesIdentityValidationTest.java diff --git a/.github/scripts/compare-independent-builds.mjs b/.github/scripts/compare-independent-builds.mjs index f225f93..59e0b37 100644 --- a/.github/scripts/compare-independent-builds.mjs +++ b/.github/scripts/compare-independent-builds.mjs @@ -1,51 +1,258 @@ #!/usr/bin/env node +import { execFileSync } from 'node:child_process'; import { createHash } from 'node:crypto'; -import { readFileSync, writeFileSync } from 'node:fs'; +import { + readFileSync, + realpathSync, + statSync, + writeFileSync +} from 'node:fs'; +import { isAbsolute, relative, resolve, sep } from 'node:path'; -const [standaloneOnePath, standaloneTwoPath, localOnePath, localTwoPath, +const [standaloneOneManifest, standaloneTwoManifest, + localOneManifest, localTwoManifest, + standaloneOneRoot, standaloneTwoRoot, localOneRoot, localTwoRoot, + standaloneOneGradle, standaloneTwoGradle, localOneGradle, localTwoGradle, bexCommit, outputPath] = process.argv.slice(2); -if (!standaloneOnePath || !standaloneTwoPath || !localOnePath || - !localTwoPath || !bexCommit || !outputPath) { +if (!standaloneOneManifest || !standaloneTwoManifest || + !localOneManifest || !localTwoManifest || + !standaloneOneRoot || !standaloneTwoRoot || + !localOneRoot || !localTwoRoot || + !standaloneOneGradle || !standaloneTwoGradle || + !localOneGradle || !localTwoGradle || !bexCommit || !outputPath) { throw new Error( - 'usage: compare-independent-builds.mjs S1 S2 L1 L2 COMMIT OUTPUT' + 'usage: compare-independent-builds.mjs S1 S2 L1 L2 ' + + 'S1_ROOT S2_ROOT L1_ROOT L2_ROOT S1_GRADLE S2_GRADLE ' + + 'L1_GRADLE L2_GRADLE COMMIT OUTPUT' ); } -function load(path) { - const text = readFileSync(path, 'utf8'); - if (!text.trim()) { - throw new Error(`empty artifact manifest: ${path}`); +const SHA_256 = /^[0-9a-f]{64}$/; +const BEX_COMMIT = /^[0-9a-f]{40}$/; +const ARTIFACT_PATH = + /^(?:blue-bex-(?:core|contracts|java)\/build\/libs\/[^/]+\.jar|build\/distributions\/[^/]+-source-release\.zip)$/; + +function digest(value) { + return createHash('sha256').update(value).digest('hex'); +} + +function requireAbsoluteDirectory(path, label) { + if (!isAbsolute(path)) { + throw new Error(`${label} must be absolute: ${path}`); + } + const real = realpathSync(path); + if (!statSync(real).isDirectory()) { + throw new Error(`${label} must be a directory: ${path}`); + } + return real; +} + +function requireAbsoluteFile(path, label) { + if (!isAbsolute(path)) { + throw new Error(`${label} must be absolute: ${path}`); + } + const real = realpathSync(path); + if (!statSync(real).isFile()) { + throw new Error(`${label} must be a file: ${path}`); } - return text; + return real; } -function digest(text) { - return createHash('sha256').update(text).digest('hex'); +function contained(root, child) { + const value = relative(root, child); + return value !== '' && value !== '..' && + !value.startsWith(`..${sep}`) && !isAbsolute(value); } -function pair(firstPath, secondPath) { - const first = load(firstPath); - const second = load(secondPath); - const passed = first === second; +function safeArtifactPath(path) { + return !path.startsWith('/') && !path.includes('\\') && + path !== '..' && !path.startsWith('../') && + !path.includes('/../') && !path.endsWith('/..'); +} + +function loadManifest(path, checkoutRoot) { + const manifestPath = requireAbsoluteFile(path, 'artifact manifest'); + const bytes = readFileSync(manifestPath); + const text = new TextDecoder('utf-8', { fatal: true }).decode(bytes); + if (!text || !text.endsWith('\n') || text.includes('\r')) { + throw new Error( + `artifact manifest must be non-empty LF text: ${manifestPath}` + ); + } + const lines = text.slice(0, -1).split('\n'); + const artifacts = []; + const paths = new Set(); + for (const line of lines) { + const match = /^([0-9a-f]{64}) (\S(?:.*\S)?)$/.exec(line); + if (!match || !SHA_256.test(match[1]) || + !safeArtifactPath(match[2]) || !ARTIFACT_PATH.test(match[2])) { + throw new Error( + `invalid artifact manifest line in ${manifestPath}: ${line}` + ); + } + if (paths.has(match[2])) { + throw new Error( + `duplicate artifact path in ${manifestPath}: ${match[2]}` + ); + } + paths.add(match[2]); + const artifactPath = realpathSync(resolve(checkoutRoot, match[2])); + if (!contained(checkoutRoot, artifactPath) || + !statSync(artifactPath).isFile()) { + throw new Error(`artifact escapes checkout: ${match[2]}`); + } + const artifactBytes = readFileSync(artifactPath); + const actualHash = digest(artifactBytes); + if (actualHash !== match[1]) { + throw new Error(`artifact hash is stale: ${match[2]}`); + } + artifacts.push({ + path: match[2], + bytes: artifactBytes.length, + sha256: actualHash + }); + } + artifacts.sort((left, right) => left.path < right.path + ? -1 : left.path > right.path ? 1 : 0); + const artifactSet = artifacts.map( + (artifact) => `${artifact.sha256} ${artifact.path}\n` + ).join(''); + if (!bytes.equals(Buffer.from(artifactSet, 'utf8'))) { + throw new Error(`artifact manifest is not bytewise canonical: ${path}`); + } + return { + bytes, + artifacts, + evidence: { + path: manifestPath, + bytes: bytes.length, + sha256: digest(bytes), + artifactCount: artifacts.length, + artifactSetSha256: digest(artifactSet) + } + }; +} + +function requiredArtifactRolesPresent(artifacts) { + return [ + 'blue-bex-core/build/libs/', + 'blue-bex-contracts/build/libs/', + 'blue-bex-java/build/libs/' + ].every((prefix) => artifacts.some( + (artifact) => artifact.path.startsWith(prefix) && + artifact.path.endsWith('.jar') + )) && artifacts.some( + (artifact) => artifact.path.startsWith('build/distributions/') && + artifact.path.endsWith('-source-release.zip') + ); +} + +function git(checkoutRoot, ...gitArguments) { + return execFileSync('git', ['-C', checkoutRoot, ...gitArguments], { + encoding: null, + stdio: ['ignore', 'pipe', 'pipe'] + }); +} + +function buildEvidence(manifestPath, rootPath, gradlePath) { + const checkoutRoot = requireAbsoluteDirectory(rootPath, 'checkout root'); + const gradleHome = requireAbsoluteDirectory(gradlePath, 'Gradle home'); + const head = git(checkoutRoot, 'rev-parse', 'HEAD') + .toString('utf8').trim(); + const reportedGitDirectory = git(checkoutRoot, 'rev-parse', '--git-dir') + .toString('utf8').trim(); + const gitDirectory = realpathSync(isAbsolute(reportedGitDirectory) + ? reportedGitDirectory : resolve(checkoutRoot, reportedGitDirectory)); + const status = git( + checkoutRoot, 'status', '--porcelain', '-z', '--untracked-files=all' + ); + if (head !== bexCommit || status.length !== 0) { + throw new Error(`checkout is not clean at ${bexCommit}: ${checkoutRoot}`); + } + const manifest = loadManifest(manifestPath, checkoutRoot); + return { + internalManifestBytes: manifest.bytes, + report: { + checkoutRoot, + gitDirectory, + gradleHome, + head, + clean: true, + manifest: manifest.evidence, + artifacts: manifest.artifacts + } + }; +} + +function pair(first, second) { + const firstArtifacts = first.report.artifacts; + const secondArtifacts = second.report.artifacts; + const exactManifestBytesMatch = + first.internalManifestBytes.equals(second.internalManifestBytes); + const firstPaths = firstArtifacts.map((artifact) => artifact.path); + const secondPaths = secondArtifacts.map((artifact) => artifact.path); + const artifactPathSetMatch = JSON.stringify(firstPaths) === + JSON.stringify(secondPaths); + const exactArtifactBytesMatch = artifactPathSetMatch && + firstArtifacts.every((artifact, index) => + artifact.bytes === secondArtifacts[index].bytes && + artifact.sha256 === secondArtifacts[index].sha256 + ); + const requiredArtifactsPresent = + requiredArtifactRolesPresent(firstArtifacts) && + requiredArtifactRolesPresent(secondArtifacts); + const passed = exactManifestBytesMatch && exactArtifactBytesMatch && + requiredArtifactsPresent; return { status: passed ? 'passed' : 'failed', - firstManifestSha256: digest(first), - secondManifestSha256: digest(second), - exactArtifactBytesMatch: passed, - artifactCount: first.trim().split(/\r?\n/).length + exactManifestBytesMatch, + exactArtifactBytesMatch, + artifactPathSetMatch, + requiredArtifactRolesPresent: requiredArtifactsPresent, + artifactCount: firstArtifacts.length, + firstBuild: first.report, + secondBuild: second.report }; } -const standalonePublished = pair(standaloneOnePath, standaloneTwoPath); -const localComposite = pair(localOnePath, localTwoPath); -const passed = standalonePublished.status === 'passed' && +if (!BEX_COMMIT.test(bexCommit)) { + throw new Error(`invalid BEX commit: ${bexCommit}`); +} +const builds = [ + buildEvidence(standaloneOneManifest, + standaloneOneRoot, standaloneOneGradle), + buildEvidence(standaloneTwoManifest, + standaloneTwoRoot, standaloneTwoGradle), + buildEvidence(localOneManifest, localOneRoot, localOneGradle), + buildEvidence(localTwoManifest, localTwoRoot, localTwoGradle) +]; +const checkoutRoots = builds.map((build) => build.report.checkoutRoot); +const gitDirectories = builds.map((build) => build.report.gitDirectory); +const gradleHomes = builds.map((build) => build.report.gradleHome); +const manifestPaths = builds.map((build) => build.report.manifest.path); +const distinctCheckoutRoots = new Set(checkoutRoots).size === 4; +const distinctGitDirectories = new Set(gitDirectories).size === 4; +const distinctGradleHomes = new Set(gradleHomes).size === 4; +const distinctInputManifestFiles = new Set(manifestPaths).size === 4; +const standalonePublished = pair(builds[0], builds[1]); +const localComposite = pair(builds[2], builds[3]); +const passed = distinctCheckoutRoots && distinctGitDirectories && + distinctGradleHomes && distinctInputManifestFiles && + standalonePublished.status === 'passed' && localComposite.status === 'passed'; const report = { - schema: 'blue-bex-independent-clean-builds/1.0', + schema: 'blue-bex-independent-clean-builds/2.1', status: passed ? 'passed' : 'failed', bexCommit, - isolatedGradleHomes: 4, + checkoutCount: checkoutRoots.length, + gitDirectoryCount: gitDirectories.length, + gradleHomeCount: gradleHomes.length, + inputManifestCount: manifestPaths.length, + distinctCheckoutRoots, + distinctGitDirectories, + distinctGradleHomes, + distinctInputManifestFiles, standalonePublished, localComposite }; diff --git a/.github/scripts/run-final-publication-gates.sh b/.github/scripts/run-final-publication-gates.sh index 2f683b9..56a6e13 100644 --- a/.github/scripts/run-final-publication-gates.sh +++ b/.github/scripts/run-final-publication-gates.sh @@ -183,6 +183,12 @@ artifact_manifest "${checkouts[3]}" "$LOCAL_TWO_MANIFEST" node "$SCRIPT_DIR/compare-independent-builds.mjs" \ "$STANDALONE_ONE_MANIFEST" "$STANDALONE_TWO_MANIFEST" \ "$LOCAL_ONE_MANIFEST" "$LOCAL_TWO_MANIFEST" \ + "${checkouts[0]}" "${checkouts[1]}" \ + "${checkouts[2]}" "${checkouts[3]}" \ + "$RELEASE_ROOT/gradle-standalone-one" \ + "$RELEASE_ROOT/gradle-standalone-two" \ + "$RELEASE_ROOT/gradle-local-one" \ + "$RELEASE_ROOT/gradle-local-two" \ "$BEX_COMMIT" "$INDEPENDENT_REPORT" node "$SCRIPT_DIR/compare-local-published-evidence.mjs" \ diff --git a/README.md b/README.md index 1edc7ce..5272b62 100644 --- a/README.md +++ b/README.md @@ -32,15 +32,17 @@ FrozenNode expression = FrozenNode.fromResolvedNode( new Node().value(2L)))); FrozenNode document = FrozenNode.fromResolvedNode(new Node()); -BexExecutionResult result = BexEngine.builder().build().compileAndExecute( - BexProgramSource.expression(expression), - BexExecutionContext.builder() - .document(new FrozenBexDocumentView(document)) - .gasLimit(10_000L) - .build()); - -System.out.println(result.value().toSimple()); // 42 -System.out.println(result.gasLedger().trace()); +try (BexEngine engine = BexEngine.builder().build()) { + BexExecutionResult result = engine.compileAndExecute( + BexProgramSource.expression(expression), + BexExecutionContext.builder() + .document(new FrozenBexDocumentView(document)) + .gasLimit(10_000L) + .build()); + + System.out.println(result.value().toSimple()); // 42 + System.out.println(result.gasLedger().trace()); +} ``` A runnable version lives in diff --git a/blue-bex-conformance/build.gradle.kts b/blue-bex-conformance/build.gradle.kts index ac67bd4..4295f14 100644 --- a/blue-bex-conformance/build.gradle.kts +++ b/blue-bex-conformance/build.gradle.kts @@ -1,7 +1,13 @@ +import org.gradle.api.Project import org.gradle.api.tasks.JavaExec import org.gradle.api.tasks.Copy import org.gradle.api.tasks.bundling.Jar +import org.gradle.api.tasks.bundling.Zip +import org.gradle.api.tasks.javadoc.Javadoc +import java.io.File +import java.nio.charset.StandardCharsets import java.security.MessageDigest +import java.util.jar.JarFile plugins { id("blue.bex.conformance") @@ -12,6 +18,41 @@ plugins { description = "BEX fixtures, package integrity, properties, and release evidence" +// Release receipts consume core archive task providers during this project's +// configuration; register those providers before resolving them below. +evaluationDependsOn(":blue-bex-core") + +fun sha256Of(file: File): String = + MessageDigest.getInstance("SHA-256") + .digest(file.readBytes()) + .joinToString("") { "%02x".format(it) } + +fun Project.rootRelativePath(file: File): String { + val root = rootProject.projectDir.toPath().toAbsolutePath().normalize() + val target = file.toPath().toAbsolutePath().normalize() + check(target.startsWith(root)) { + "Evidence artifact is outside the BEX checkout: $target" + } + return root.relativize(target).toString().replace(File.separatorChar, '/') +} + +fun writeEvidenceReceipt(file: File, values: Map) { + values.forEach { (key, value) -> + check(key.isNotBlank() && !key.contains('=') && !key.contains('\n')) { + "Invalid evidence key: $key" + } + check(!value.contains('\n') && !value.contains('\r')) { + "Invalid multiline evidence value for $key" + } + } + file.parentFile.mkdirs() + file.writeText( + values.toSortedMap().entries.joinToString( + separator = "\n", + postfix = "\n") { (key, value) -> "$key=$value" }, + StandardCharsets.UTF_8) +} + val languageVersion = extensions .getByType() .version.get() @@ -154,6 +195,10 @@ val binaryApiCheck = tasks.register("binaryApiCheck") { } val beforeLines = ownedDescriptors(checkpoint.asFile) val afterLines = ownedDescriptors(required.asFile) + val afterTypeCount = required.asFile.readLines() + .count { it.startsWith("class ") } + val afterDescriptorCount = required.asFile.readLines() + .count { it.startsWith("class ") || it.startsWith(" ") } val removed = (beforeLines - afterLines).toList() val added = (afterLines - beforeLines).toList() check(removed == removedDescriptors.asFile.readLines()) { @@ -173,6 +218,9 @@ val binaryApiCheck = tasks.register("binaryApiCheck") { && ledger.contains(afterHash) && ledger.contains(sha256(removedDescriptors.asFile)) && ledger.contains(sha256(addedDescriptors.asFile)) + && ledger.contains("\"publicTypeCount\": $afterTypeCount") + && ledger.contains( + "\"publicDescriptorCount\": $afterDescriptorCount") && ledger.contains("\"removedDescriptorLines\": ${removed.size}") && ledger.contains("\"addedDescriptorLines\": ${added.size}")) { "Migration ledger does not authenticate the complete API delta" @@ -180,8 +228,307 @@ val binaryApiCheck = tasks.register("binaryApiCheck") { } } -tasks.named("bexApiEvidence") { +val binaryApiEvidenceReceipt = layout.buildDirectory.file( + "reports/bex-release/binary-api.properties") +val requiredApiManifest = rootProject.layout.projectDirectory.file( + "src/test/resources/hosted-release/required-public-api.txt") +val writeBinaryApiEvidence = tasks.register( + "writeBinaryApiEvidence") { + group = "verification" + description = "Writes hashes for the verified packaged binary API." dependsOn(binaryApiCheck) + inputs.files( + apiInspectionJar.flatMap(Jar::getArchiveFile), + layout.buildDirectory.file("reports/bex-release/public-api.txt"), + requiredApiManifest) + outputs.file(binaryApiEvidenceReceipt) + outputs.upToDateWhen { false } + doLast { + val artifact = apiInspectionJar.get().archiveFile.get().asFile + val manifest = layout.buildDirectory.file( + "reports/bex-release/public-api.txt").get().asFile + val required = requiredApiManifest.asFile + check(artifact.isFile) { "API inspection JAR is missing: $artifact" } + check(manifest.isFile) { "Generated API manifest is missing: $manifest" } + check(required.isFile) { "Required API manifest is missing: $required" } + + val manifestLines = manifest.readLines(StandardCharsets.UTF_8) + val requiredLines = required.readLines(StandardCharsets.UTF_8) + check(manifestLines.isNotEmpty() + && manifestLines.first() + == "schema=blue-bex-binary-api-manifest/1.0") { + "Generated API manifest has no recognized schema" + } + check(manifestLines == requiredLines) { + "Generated and required public API manifests differ" + } + + writeEvidenceReceipt( + binaryApiEvidenceReceipt.get().asFile, + linkedMapOf( + "schema" to "blue-bex-binary-api-evidence/1.0", + "status" to "passed", + "verificationTask" to + ":blue-bex-conformance:binaryApiCheck", + "verificationTaskStatus" to "passed", + "artifact.path" to rootProject.rootRelativePath(artifact), + "artifact.sha256" to sha256Of(artifact), + "manifest.path" to rootProject.rootRelativePath(manifest), + "manifest.sha256" to sha256Of(manifest), + "manifest.schema" to + "blue-bex-binary-api-manifest/1.0", + "required.path" to rootProject.rootRelativePath(required), + "required.sha256" to sha256Of(required), + "required.comparison" to "exact-match", + "required.signatureCount" to requiredLines.size.toString(), + "required.missingCount" to "0", + "required.unexpectedCount" to "0")) + } +} + +val jmhSourceSet = sourceSets.named("jmh") +val benchmarkCompilationEvidenceReceipt = layout.buildDirectory.file( + "reports/bex-release/benchmark-compilation.properties") +val writeBenchmarkCompilationEvidence = tasks.register( + "writeBenchmarkCompilationEvidence") { + group = "verification" + description = "Writes evidence from the actual compiled JMH source set." + dependsOn(tasks.named("jmhClasses")) + outputs.file(benchmarkCompilationEvidenceReceipt) + outputs.upToDateWhen { false } + doLast { + val source = file( + "src/jmh/java/blue/bex/benchmark/BexCoreBenchmark.java") + val relativeClass = "blue/bex/benchmark/BexCoreBenchmark.class" + val candidates = jmhSourceSet.get().output.classesDirs.files + .map { directory -> File(directory, relativeClass) } + .filter(File::isFile) + check(source.isFile) { "Benchmark source is missing: $source" } + check(candidates.size == 1) { + "Expected one compiled BexCoreBenchmark class, found $candidates" + } + val compiledClass = candidates.single() + val allSources = fileTree("src/jmh/java") { + include("**/*.java") + }.files + val allClasses = jmhSourceSet.get().output.classesDirs.files + .flatMap { directory -> + fileTree(directory) { include("**/*.class") }.files + } + check(allSources.isNotEmpty() && allClasses.isNotEmpty()) { + "JMH compilation produced no source/class evidence" + } + + writeEvidenceReceipt( + benchmarkCompilationEvidenceReceipt.get().asFile, + linkedMapOf( + "schema" to + "blue-bex-benchmark-compilation-evidence/1.0", + "status" to "passed", + "timingExecuted" to "false", + "source.path" to rootProject.rootRelativePath(source), + "source.sha256" to sha256Of(source), + "class.path" to + rootProject.rootRelativePath(compiledClass), + "class.sha256" to sha256Of(compiledClass), + "sourceCount" to allSources.size.toString(), + "classCount" to allClasses.size.toString())) + } +} + +val java8BytecodeEvidenceReceipt = layout.buildDirectory.file( + "reports/bex-release/java8-bytecode.properties") +val writeJava8BytecodeEvidence = tasks.register( + "writeJava8BytecodeEvidence") { + group = "verification" + description = "Verifies and records packaged Java 8 classfile evidence." + dependsOn( + apiInspectionJar, + project(":blue-bex-core").tasks.named("java8BytecodeCheck"), + project(":blue-bex-contracts").tasks.named("java8BytecodeCheck")) + inputs.file(apiInspectionJar.flatMap(Jar::getArchiveFile)) + outputs.file(java8BytecodeEvidenceReceipt) + outputs.upToDateWhen { false } + doLast { + val artifact = apiInspectionJar.get().archiveFile.get().asFile + check(artifact.isFile) { "Packaged API inspection JAR is missing" } + var classCount = 0 + val observedMajors = sortedSetOf() + JarFile(artifact).use { jar -> + val entries = jar.entries() + while (entries.hasMoreElements()) { + val entry = entries.nextElement() + if (entry.isDirectory || !entry.name.endsWith(".class")) { + continue + } + val header = ByteArray(8) + var offset = 0 + jar.getInputStream(entry).use { input -> + while (offset < header.size) { + val read = input.read( + header, offset, header.size - offset) + check(read >= 0) { + "Truncated classfile in $artifact: ${entry.name}" + } + offset += read + } + } + val validMagic = + (header[0].toInt() and 0xff) == 0xca + && (header[1].toInt() and 0xff) == 0xfe + && (header[2].toInt() and 0xff) == 0xba + && (header[3].toInt() and 0xff) == 0xbe + check(validMagic) { + "Invalid classfile magic in $artifact: ${entry.name}" + } + val major = ((header[6].toInt() and 0xff) shl 8) or + (header[7].toInt() and 0xff) + observedMajors.add(major) + classCount++ + } + } + check(classCount > 0) { "No packaged production classes were verified" } + check(observedMajors == sortedSetOf(52)) { + "Packaged classes are not uniformly Java 8: $observedMajors" + } + + writeEvidenceReceipt( + java8BytecodeEvidenceReceipt.get().asFile, + linkedMapOf( + "schema" to "blue-bex-java8-bytecode-evidence/1.0", + "status" to "passed", + "artifact.path" to rootProject.rootRelativePath(artifact), + "artifact.sha256" to sha256Of(artifact), + "classCount" to classCount.toString(), + "expected.magic" to "CAFEBABE", + "observed.magic" to "CAFEBABE", + "expected.major" to "52", + "observed.major" to observedMajors.single().toString())) + } +} + +val archiveEvidenceProject = project(":blue-bex-core") +val archiveEvidenceJar = archiveEvidenceProject.tasks.named("jar") +val archiveEvidenceReplicaJar = + archiveEvidenceProject.tasks.named("replicaJar") +val archiveEvidenceSourcesJar = + archiveEvidenceProject.tasks.named("sourcesJar") +val archiveEvidenceReplicaSourcesJar = + archiveEvidenceProject.tasks.named("replicaSourcesJar") +val archiveEvidenceJavadocJar = + archiveEvidenceProject.tasks.named("javadocJar") +val archiveEvidenceJavadoc = + archiveEvidenceProject.tasks.named("javadoc") +val archiveEvidenceReplicaJavadocJar = + archiveEvidenceProject.tasks.named("replicaJavadocJar") +val sourceReleaseArchive = + rootProject.tasks.named("sourceReleaseArchive") +val replicaSourceReleaseArchive = + rootProject.tasks.named("replicaSourceReleaseArchive") + +archiveEvidenceJavadoc.configure { + outputs.upToDateWhen { false } +} +archiveEvidenceReplicaJavadocJar.configure { + outputs.upToDateWhen { false } +} +replicaSourceReleaseArchive.configure { + outputs.upToDateWhen { false } +} + +val deterministicArchiveEvidenceReceipt = layout.buildDirectory.file( + "reports/bex-release/deterministic-archives.properties") +val writeDeterministicArchiveEvidence = tasks.register( + "writeDeterministicArchiveEvidence") { + group = "verification" + description = "Writes evidence from byte-compared archive replicas." + dependsOn( + archiveEvidenceProject.tasks.named("verifyReproducibleArchives"), + rootProject.tasks.named("verifySourceReleaseArchiveReproducibility")) + inputs.files( + archiveEvidenceJar.flatMap(Jar::getArchiveFile), + archiveEvidenceReplicaJar.flatMap(Jar::getArchiveFile), + archiveEvidenceSourcesJar.flatMap(Jar::getArchiveFile), + archiveEvidenceReplicaSourcesJar.flatMap(Jar::getArchiveFile), + archiveEvidenceJavadocJar.flatMap(Jar::getArchiveFile), + archiveEvidenceReplicaJavadocJar.flatMap(Jar::getArchiveFile), + sourceReleaseArchive.flatMap(Zip::getArchiveFile), + replicaSourceReleaseArchive.flatMap(Zip::getArchiveFile)) + outputs.file(deterministicArchiveEvidenceReceipt) + outputs.upToDateWhen { false } + doLast { + val archives = linkedMapOf( + "main.original" to archiveEvidenceJar.get() + .archiveFile.get().asFile, + "main.rebuild" to archiveEvidenceReplicaJar.get() + .archiveFile.get().asFile, + "sources.original" to archiveEvidenceSourcesJar.get() + .archiveFile.get().asFile, + "sources.rebuild" to archiveEvidenceReplicaSourcesJar.get() + .archiveFile.get().asFile, + "javadoc.original" to archiveEvidenceJavadocJar.get() + .archiveFile.get().asFile, + "javadoc.rebuild" to archiveEvidenceReplicaJavadocJar.get() + .archiveFile.get().asFile, + "sourceRelease.original" to sourceReleaseArchive.get() + .archiveFile.get().asFile, + "sourceRelease.replica" to replicaSourceReleaseArchive.get() + .archiveFile.get().asFile) + archives.forEach { (name, archive) -> + check(archive.isFile) { "$name archive is missing: $archive" } + } + fun byteIdentical(first: String, second: String): Boolean = + archives.getValue(first).readBytes().contentEquals( + archives.getValue(second).readBytes()) + check(byteIdentical("main.original", "main.rebuild")) { + "Main JAR replica is not byte-identical" + } + check(byteIdentical("sources.original", "sources.rebuild")) { + "Sources JAR replica is not byte-identical" + } + check(byteIdentical("javadoc.original", "javadoc.rebuild")) { + "Javadoc JAR replica is not byte-identical" + } + check(byteIdentical( + "sourceRelease.original", "sourceRelease.replica")) { + "Source release replica is not byte-identical" + } + val javadocFreshlyRegenerated = + archiveEvidenceJavadoc.get().state.didWork + val independentSourceAssembly = + replicaSourceReleaseArchive.get().state.didWork + check(javadocFreshlyRegenerated) { + "Javadoc replica was not freshly regenerated" + } + check(independentSourceAssembly) { + "Source release replica was not independently assembled" + } + + val receipt = linkedMapOf( + "schema" to "blue-bex-deterministic-archives-evidence/1.0", + "status" to "passed", + "scope" to + "jar-packaging-determinism-and-source-release-reassembly-from-the-same-working-tree", + "independentCleanCompilation" to "false", + "javadoc.freshlyRegenerated" to + javadocFreshlyRegenerated.toString(), + "sourceRelease.byteIdentity" to "true", + "sourceRelease.hashIdentity" to "true", + "sourceRelease.independentAssembly" to + independentSourceAssembly.toString(), + "sourceRelease.independentCleanCheckout" to "false") + archives.forEach { (name, archive) -> + receipt["$name.path"] = rootProject.rootRelativePath(archive) + receipt["$name.sha256"] = sha256Of(archive) + } + writeEvidenceReceipt( + deterministicArchiveEvidenceReceipt.get().asFile, + receipt) + } +} + +tasks.named("bexApiEvidence") { + dependsOn(binaryApiCheck, writeBinaryApiEvidence) } tasks.check { @@ -213,7 +560,11 @@ val writeBexConformanceReport = tasks.register( dependsOn( tasks.test, syncConformanceEvidenceArtifacts, - tasks.named("writeLanguageDependencyEvidence") + tasks.named("writeLanguageDependencyEvidence"), + writeDeterministicArchiveEvidence, + writeBinaryApiEvidence, + writeBenchmarkCompilationEvidence, + writeJava8BytecodeEvidence ) classpath = sourceSets.test.get().runtimeClasspath mainClass.set("blue.bex.conformance.BexConformanceReportMain") diff --git a/blue-bex-conformance/src/jmh/java/blue/bex/benchmark/BexCoreBenchmark.java b/blue-bex-conformance/src/jmh/java/blue/bex/benchmark/BexCoreBenchmark.java index 76fe87a..db58c2b 100644 --- a/blue-bex-conformance/src/jmh/java/blue/bex/benchmark/BexCoreBenchmark.java +++ b/blue-bex-conformance/src/jmh/java/blue/bex/benchmark/BexCoreBenchmark.java @@ -21,6 +21,7 @@ import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.Setup; import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; import org.openjdk.jmh.annotations.Warmup; import java.math.BigDecimal; @@ -50,13 +51,14 @@ public class BexCoreBenchmark { @Benchmark public BexExecutionResult coldCompileAndExecute( BasicState state) { - BexExecutionResult result = BexEngine.builder() + try (BexEngine engine = BexEngine.builder() .cache(new LruBexCompiledProgramCache()) - .build() - .compileAndExecute( - state.coldSource, - context()); - return verify(result, state.coldExpected); + .build()) { + BexExecutionResult result = engine.compileAndExecute( + state.coldSource, + context()); + return verify(result, state.coldExpected); + } } @Benchmark @@ -240,8 +242,10 @@ public void setup() { 1)), "label", op("$concat", list( "cold-", "compile")))))); - coldExpected = expected(BexEngine.builder().build() - .compileAndExecute(coldSource, context())); + try (BexEngine coldEngine = BexEngine.builder().build()) { + coldExpected = expected(coldEngine.compileAndExecute( + coldSource, context())); + } cacheSource = BexProgramSource.expression( frozen(op("$pointerGet", obj( @@ -268,6 +272,12 @@ public void setup() { standaloneGasProgram, context())); } + + @TearDown(Level.Trial) + public void tearDown() { + cacheEngine.close(); + engine.close(); + } } @State(Scope.Thread) @@ -293,6 +303,11 @@ public void setup() { program = engine.compile(source); expected = expected(engine.execute(program, context())); } + + @TearDown(Level.Trial) + public void tearDown() { + engine.close(); + } } @State(Scope.Thread) @@ -323,6 +338,11 @@ public void setup() { program = engine.compile(source); expected = expected(engine.execute(program, context())); } + + @TearDown(Level.Trial) + public void tearDown() { + engine.close(); + } } @State(Scope.Thread) @@ -375,6 +395,11 @@ public void setup() { engine.execute(numericProgram, context())); } + @TearDown(Level.Trial) + public void tearDown() { + engine.close(); + } + private static String repeat(String value, int count) { StringBuilder repeated = new StringBuilder( value.length() * count); @@ -442,6 +467,11 @@ public void setup() { transientIdentityExpected = expected(engine.execute( transientIdentityProgram, context())); } + + @TearDown(Level.Trial) + public void tearDown() { + engine.close(); + } } @State(Scope.Thread) @@ -479,5 +509,10 @@ public void setup() { program = engine.compile(source); expected = expected(engine.execute(program, context())); } + + @TearDown(Level.Trial) + public void tearDown() { + engine.close(); + } } } diff --git a/blue-bex-conformance/src/jmh/java/blue/language/processor/BexHostedGasBenchmark.java b/blue-bex-conformance/src/jmh/java/blue/language/processor/BexHostedGasBenchmark.java index 538e8c2..e18f7e0 100644 --- a/blue-bex-conformance/src/jmh/java/blue/language/processor/BexHostedGasBenchmark.java +++ b/blue-bex-conformance/src/jmh/java/blue/language/processor/BexHostedGasBenchmark.java @@ -23,6 +23,7 @@ import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.Setup; import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; import org.openjdk.jmh.annotations.Warmup; import java.util.List; @@ -74,6 +75,11 @@ public void setup() { expected = expected(executeHosted()); } + @TearDown(Level.Trial) + public void tearDown() { + engine.close(); + } + private BexExecutionResult executeAndVerify() { return verify(executeHosted(), expected); } diff --git a/blue-bex-core/src/main/java/blue/bex/api/BexEngine.java b/blue-bex-core/src/main/java/blue/bex/api/BexEngine.java index 8a4083e..e86b527 100644 --- a/blue-bex-core/src/main/java/blue/bex/api/BexEngine.java +++ b/blue-bex-core/src/main/java/blue/bex/api/BexEngine.java @@ -4,6 +4,7 @@ import blue.bex.compile.BexCompiledProgram; import blue.bex.compile.BexCompiledProgramCache; import blue.bex.compile.BexCompiledProgramKey; +import blue.bex.compile.BexCompiledProgramRuntimeAccess; import blue.bex.compile.BexCompilerRuntimeAccess; import blue.bex.compile.LruBexCompiledProgramCache; import blue.bex.gas.BexGasSchedule; @@ -16,6 +17,7 @@ import blue.language.runtime.BlueLanguage; import java.util.Map; +import java.util.Objects; /** * Public entry point for compiling and executing selected BEX programs. @@ -65,13 +67,14 @@ private BexCompiledProgram compile( BexCompiledProgram cached = cache.get(key); if (cached != null) { metrics.incrementCompileCacheHits(); - validateCompilationEnvironment(cached); + validateCompilationKey(cached, key); validateIntrinsicSupport(cached); return cached; } metrics.incrementCompileCacheMisses(); BexCompiledProgram program = BexCompilerRuntimeAccess.compile( source, metrics, intrinsics, compileEnvironmentIdentity()); + validateCompilationKey(program, key); validateIntrinsicSupport(program); cache.put(key, program); return program; @@ -146,6 +149,17 @@ private void validateCompilationEnvironment(BexCompiledProgram program) { } } + private void validateCompilationKey( + BexCompiledProgram program, + BexCompiledProgramKey expected) { + if (!BexCompiledProgramRuntimeAccess.matchesCompilationKey( + program, expected)) { + throw new BexException( + "Compiled BEX cache key does not match the requested " + + "program, definition, entry, source kind, and environment"); + } + } + private void publishMetrics(BexMetricsSnapshot metrics) { try { metricsSink.accept(metrics); @@ -175,12 +189,13 @@ public Builder language(BlueLanguage blue) { } public Builder gasSchedule(BexGasSchedule gasSchedule) { - this.gasSchedule = gasSchedule; + this.gasSchedule = Objects.requireNonNull( + gasSchedule, "gasSchedule"); return this; } public Builder cache(BexCompiledProgramCache cache) { - this.cache = cache; + this.cache = Objects.requireNonNull(cache, "cache"); return this; } diff --git a/blue-bex-core/src/main/java/blue/bex/compile/BexCompiledProgram.java b/blue-bex-core/src/main/java/blue/bex/compile/BexCompiledProgram.java index 0839bb8..33fbecc 100644 --- a/blue-bex-core/src/main/java/blue/bex/compile/BexCompiledProgram.java +++ b/blue-bex-core/src/main/java/blue/bex/compile/BexCompiledProgram.java @@ -19,34 +19,13 @@ * Lazy-compiled BEX program. */ public final class BexCompiledProgram { - private static final String UNBOUND_ENVIRONMENT_IDENTITY = - "blue-bex/unbound-compile-environment"; - private final CompiledFunction entry; private final Map functions; private final Map constants; private final int rootFrameSize; private final String programBlueId; private final Set requiredIntrinsicBlueIds; - private final String compilationEnvironmentIdentity; - - BexCompiledProgram(CompiledFunction entry, - Map functions, - Map constants, - int rootFrameSize, - String programBlueId) { - this(entry, functions, constants, rootFrameSize, programBlueId, Collections.emptySet()); - } - - BexCompiledProgram(CompiledFunction entry, - Map functions, - Map constants, - int rootFrameSize, - String programBlueId, - Set requiredIntrinsicBlueIds) { - this(entry, functions, constants, rootFrameSize, programBlueId, - requiredIntrinsicBlueIds, UNBOUND_ENVIRONMENT_IDENTITY); - } + private final BexCompiledProgramKey compilationKey; BexCompiledProgram(CompiledFunction entry, Map functions, @@ -54,7 +33,7 @@ public final class BexCompiledProgram { int rootFrameSize, String programBlueId, Set requiredIntrinsicBlueIds, - String compilationEnvironmentIdentity) { + BexCompiledProgramKey compilationKey) { this.entry = entry; this.functions = Collections.unmodifiableMap(new LinkedHashMap<>(functions)); this.constants = Collections.unmodifiableMap(new LinkedHashMap<>(constants)); @@ -63,9 +42,8 @@ public final class BexCompiledProgram { this.requiredIntrinsicBlueIds = Collections.unmodifiableSet(new LinkedHashSet<>(requiredIntrinsicBlueIds != null ? requiredIntrinsicBlueIds : Collections.emptySet())); - this.compilationEnvironmentIdentity = java.util.Objects.requireNonNull( - compilationEnvironmentIdentity, - "compilationEnvironmentIdentity"); + this.compilationKey = java.util.Objects.requireNonNull( + compilationKey, "compilationKey"); } BexValue execute(BexExecutionMachine machine) { @@ -78,9 +56,13 @@ BexValue execute(BexExecutionMachine machine) { int rootFrameSize() { return rootFrameSize; } public String programBlueId() { return programBlueId; } public Set requiredIntrinsicBlueIds() { return requiredIntrinsicBlueIds; } + boolean matchesCompilationKey(BexCompiledProgramKey expected) { + return compilationKey.equals(java.util.Objects.requireNonNull( + expected, "expected")); + } /** Exact registry, gas, Language, and compiler identity used to compile. */ public String compilationEnvironmentIdentity() { - return compilationEnvironmentIdentity; + return compilationKey.compileEnvironmentIdentity(); } public BexValue constant(String name) { diff --git a/blue-bex-core/src/main/java/blue/bex/compile/BexCompiledProgramRuntimeAccess.java b/blue-bex-core/src/main/java/blue/bex/compile/BexCompiledProgramRuntimeAccess.java index 2f3475e..f6408eb 100644 --- a/blue-bex-core/src/main/java/blue/bex/compile/BexCompiledProgramRuntimeAccess.java +++ b/blue-bex-core/src/main/java/blue/bex/compile/BexCompiledProgramRuntimeAccess.java @@ -17,4 +17,11 @@ public static BexValue execute( BexExecutionMachine machine) { return program.execute(machine); } + + /** Validates the complete opaque compile/cache binding for engine use. */ + public static boolean matchesCompilationKey( + BexCompiledProgram program, + BexCompiledProgramKey expected) { + return program.matchesCompilationKey(expected); + } } diff --git a/blue-bex-core/src/main/java/blue/bex/compile/BexProgramCompiler.java b/blue-bex-core/src/main/java/blue/bex/compile/BexProgramCompiler.java index dd15636..ea07265 100644 --- a/blue-bex-core/src/main/java/blue/bex/compile/BexProgramCompiler.java +++ b/blue-bex-core/src/main/java/blue/bex/compile/BexProgramCompiler.java @@ -25,6 +25,8 @@ final class BexProgramCompiler extends BexStatementCompiler { final BexCompiledProgram compileProgram( BexCompilationInput source, String compilationEnvironmentIdentity) { + BexCompiledProgramKey compilationKey = BexCompiledProgramKey.from( + source, compilationEnvironmentIdentity); requiredIntrinsicBlueIds.clear(); FrozenNode step = source.programNode(); FrozenNode definition = source.definitionNode().orElse(null); @@ -46,7 +48,7 @@ final BexCompiledProgram compileProgram( scope.frameSize()); return new BexCompiledProgram(root, Collections.emptyMap(), constants, scope.frameSize(), BexNodeIdentity.safeBlueId(step), requiredIntrinsicBlueIds, - compilationEnvironmentIdentity); + compilationKey); } requireProgramNode(step, "program"); if (definition != null) { @@ -115,7 +117,7 @@ final BexCompiledProgram compileProgram( return new BexCompiledProgram(root, compiledFunctions, constants, rootFrameSize, BexNodeIdentity.safeBlueId(step), requiredIntrinsicBlueIds, - compilationEnvironmentIdentity); + compilationKey); } BexCompiledProgram.CompiledFunction compileFunction(String name, FrozenNode functionNode, FunctionSignature signature) { diff --git a/blue-bex-core/src/main/java/blue/bex/value/BexValues.java b/blue-bex-core/src/main/java/blue/bex/value/BexValues.java index 4fd09ac..829a088 100644 --- a/blue-bex-core/src/main/java/blue/bex/value/BexValues.java +++ b/blue-bex-core/src/main/java/blue/bex/value/BexValues.java @@ -4,6 +4,7 @@ import blue.language.api.BlueOperationLimits; import blue.language.api.BlueOperationOutcome; import blue.language.api.BlueOperationResult; +import blue.language.identity.BlueIds; import blue.language.model.Node; import blue.language.model.Schema; import blue.bex.BexExecutionEvidenceUnavailableException; @@ -107,9 +108,14 @@ public static BexValue exact(FrozenNode canonicalNode, } FrozenNode canonical = canonicalNode != null ? canonicalNode : resolvedNode; FrozenNode semantic = resolvedNode != null ? resolvedNode : canonicalNode; - String retainedBlueId = exactBlueId; + String retainedBlueId = exactBlueId != null + ? BlueIds.requireBlueIdOrCyclicMember( + exactBlueId, "BEX exact value blueId") + : null; if (retainedBlueId == null && canonical.isReferenceOnly()) { - retainedBlueId = canonical.getReferenceBlueId(); + retainedBlueId = BlueIds.requireBlueIdOrCyclicMember( + canonical.getReferenceBlueId(), + "BEX exact value reference blueId"); } /* * A finalized cyclic member has no independently hashable body. @@ -124,7 +130,7 @@ public static BexValue exact(FrozenNode canonicalNode, new Node().blueId(retainedBlueId)); semantic = canonical; } - return new FrozenNodeBexValue(canonical, semantic, exactBlueId); + return new FrozenNodeBexValue(canonical, semantic, retainedBlueId); } /** diff --git a/build-logic/src/main/java/blue/bex/buildlogic/RootOrchestrationPlugin.java b/build-logic/src/main/java/blue/bex/buildlogic/RootOrchestrationPlugin.java index d8ef9ae..948a283 100644 --- a/build-logic/src/main/java/blue/bex/buildlogic/RootOrchestrationPlugin.java +++ b/build-logic/src/main/java/blue/bex/buildlogic/RootOrchestrationPlugin.java @@ -423,6 +423,8 @@ private static TaskProvider sourceArchive( project.getRootProject().getProjectDir(), spec -> spec.exclude( ".git/**", ".gradle/**", "**/build/**", + ".idea/**", "**/.idea/**", + "out/**", "**/out/**", "*.iml", "**/.DS_Store", "*.zip", "work-status.txt"))); }); } diff --git a/build-logic/src/main/java/blue/bex/buildlogic/tasks/GenerateModernizationReportTask.java b/build-logic/src/main/java/blue/bex/buildlogic/tasks/GenerateModernizationReportTask.java index b3cae00..9ca7bea 100644 --- a/build-logic/src/main/java/blue/bex/buildlogic/tasks/GenerateModernizationReportTask.java +++ b/build-logic/src/main/java/blue/bex/buildlogic/tasks/GenerateModernizationReportTask.java @@ -93,10 +93,7 @@ && sectionPassed(conformance, "cyclicProofEvidence") && sectionPassed(conformance, "intrinsicEvidence") && sectionPassed(conformance, "referenceEvidenceClassificationEvidence"); - boolean benchmarkPresent = jmh.startsWith("[") - && jmh.contains("gc.alloc.rate.norm") - && jmh.contains("scoreConfidence") - && !jmh.contains("NaN") + boolean benchmarkPresent = seriousJmhEvidence(jmh) && jmhEnvironment.contains( "\"schema\": \"blue-bex-jmh-environment/1.0\"") && jmhEnvironment.contains("\"profilers\":[\"gc\"]"); @@ -179,7 +176,7 @@ && sectionPassed(conformance, + totals.requiredOperators + "\n" + "- Architecture: " + (architecturePassed ? "passed" : "failed") + "\n" - + "- JMH smoke evidence: " + + "- JMH benchmark evidence: " + (benchmarkPresent ? "serious campaign present" : "missing or incomplete") + "\n" + "- Concurrency/property gates: " @@ -208,6 +205,58 @@ && sectionPassed(conformance, } } + private static boolean seriousJmhEvidence(String json) { + if (!json.trim().startsWith("[")) { + return false; + } + int benchmarks = matchCount(json, + Pattern.compile("\\\"benchmark\\\"\\s*:")); + if (benchmarks == 0 + || matchCount(json, Pattern.compile( + "\\\"forks\\\"\\s*:\\s*2(?:\\s*[,}])")) != benchmarks + || matchCount(json, Pattern.compile( + "\\\"warmupIterations\\\"\\s*:\\s*3(?:\\s*[,}])")) + != benchmarks + || matchCount(json, Pattern.compile( + "\\\"measurementIterations\\\"\\s*:\\s*5(?:\\s*[,}])")) + != benchmarks + || matchCount(json, Pattern.compile( + "\\\"warmupTime\\\"\\s*:\\s*\\\"250 ms\\\"")) + != benchmarks + || matchCount(json, Pattern.compile( + "\\\"measurementTime\\\"\\s*:\\s*\\\"250 ms\\\"")) + != benchmarks) { + return false; + } + String number = "-?(?:[0-9]+(?:\\.[0-9]*)?|\\.[0-9]+)" + + "(?:[eE][+-]?[0-9]+)?"; + Pattern finitePrimary = Pattern.compile( + "\\\"primaryMetric\\\"\\s*:\\s*\\{[^}]*?" + + "\\\"score\\\"\\s*:\\s*" + number + "[^}]*?" + + "\\\"scoreConfidence\\\"\\s*:\\s*\\[\\s*" + + number + "\\s*,\\s*" + number + "\\s*\\]", + Pattern.DOTALL); + Pattern finiteAllocation = Pattern.compile( + "\\\"gc\\.alloc\\.rate\\.norm\\\"\\s*:\\s*\\{[^}]*?" + + "\\\"score\\\"\\s*:\\s*" + number + "[^}]*?" + + "\\\"scoreConfidence\\\"\\s*:\\s*\\[\\s*" + + number + "\\s*,\\s*" + number + "\\s*\\]", + Pattern.DOTALL); + return matchCount(json, finitePrimary) == benchmarks + && matchCount(json, Pattern.compile( + "\\\"gc\\.alloc\\.rate\\.norm\\\"\\s*:")) == benchmarks + && matchCount(json, finiteAllocation) == benchmarks; + } + + private static int matchCount(String text, Pattern pattern) { + int count = 0; + Matcher matcher = pattern.matcher(text); + while (matcher.find()) { + count++; + } + return count; + } + private static TestTotals readTests(File directory) throws Exception { TestTotals totals = new TestTotals(); if (!directory.isDirectory()) { diff --git a/build-logic/src/main/java/blue/bex/buildlogic/tasks/GenerateReleaseReportTask.java b/build-logic/src/main/java/blue/bex/buildlogic/tasks/GenerateReleaseReportTask.java index e9cc2a0..d01d04b 100644 --- a/build-logic/src/main/java/blue/bex/buildlogic/tasks/GenerateReleaseReportTask.java +++ b/build-logic/src/main/java/blue/bex/buildlogic/tasks/GenerateReleaseReportTask.java @@ -8,8 +8,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; +import java.util.Map; import java.util.stream.Collectors; import org.gradle.api.DefaultTask; import org.gradle.api.GradleException; @@ -66,6 +65,14 @@ public void generate() { getPublishedLanguageReport().get().getAsFile()); String independent = optionalText(getIndependentCleanBuildReport()); String differential = optionalText(getDifferentialReport()); + Map modernizationEvidence = + ReleaseEvidenceJson.parseOrEmpty(modernization); + Map publishedEvidence = + ReleaseEvidenceJson.parseOrEmpty(published); + Map independentEvidence = + ReleaseEvidenceJson.parseOrEmpty(independent); + Map differentialEvidence = + ReleaseEvidenceJson.parseOrEmpty(differential); File repository = getRepositoryDirectory().get().getAsFile(); String commit = gitText(repository, "rev-parse", "HEAD").trim(); boolean clean = gitBytes(repository, "status", "--porcelain", "-z") @@ -74,22 +81,21 @@ public void generate() { repository, "tag", "--points-at", "HEAD")); boolean exactTag = tags.contains(getExpectedReleaseTag().get()); - boolean modernizationReady = booleanField( - modernization, "modernizationReady"); - boolean conformanceReleaseReady = booleanField( - modernization, "conformanceReleaseReady"); - boolean publishedReady = "passed".equals( - stringField(published, "status")); - boolean independentReady = "passed".equals( - stringField(independent, "status")) - && sectionPassed(independent, "standalonePublished") - && sectionPassed(independent, "localComposite") - && commit.equals(stringField(independent, "bexCommit")); - boolean differentialReady = "passed".equals( - stringField(differential, "status")) - && fieldPassed(differential, "semanticAndGasParity") - && fieldPassed(differential, "exactGasTraceParity") - && commit.equals(stringField(differential, "bexCommit")); + boolean modernizationReady = + ReleaseEvidenceJson.modernizationPassed( + modernizationEvidence); + boolean conformanceReleaseReady = + ReleaseEvidenceJson.conformanceReleasePassed( + modernizationEvidence); + boolean publishedReady = + ReleaseEvidenceJson.publishedLanguagePassed( + publishedEvidence); + boolean independentReady = + ReleaseEvidenceJson.independentBuildsPassed( + independentEvidence, commit); + boolean differentialReady = + ReleaseEvidenceJson.differentialPassed( + differentialEvidence, commit); List blockers = new ArrayList<>(); addBlocker(blockers, modernizationReady, @@ -122,7 +128,8 @@ && fieldPassed(differential, "exactGasTraceParity") + quote(conformanceReleaseReady ? "passed" : "failed") + ",\n" + " \"publishedLanguageStatus\": " - + quote(stringField(published, "status")) + ",\n" + + quote(ReleaseEvidenceJson.stringOrEmpty( + publishedEvidence, "status")) + ",\n" + " \"independentCleanBuildStatus\": " + quote(independentReady ? "passed" : "not-executed") + ",\n" @@ -185,57 +192,6 @@ private static String pass(boolean value) { return value ? "passed" : "not passed"; } - private static boolean fieldPassed(String text, String field) { - return text.matches("(?s).*\\\"" + Pattern.quote(field) - + "\\\"\\s*:\\s*(?:\\\"passed\\\"|true).*?"); - } - - private static boolean sectionPassed(String text, String section) { - return "passed".equals(stringField(objectSection(text, section), - "status")); - } - - private static boolean booleanField(String text, String field) { - return text.matches("(?s).*\\\"" + Pattern.quote(field) - + "\\\"\\s*:\\s*true.*"); - } - - private static String stringField(String text, String field) { - Matcher matcher = Pattern.compile("\\\"" + Pattern.quote(field) - + "\\\"\\s*:\\s*\\\"([^\\\"]*)\\\"").matcher(text); - return matcher.find() ? matcher.group(1) : ""; - } - - private static String objectSection(String text, String name) { - int key = text.indexOf("\"" + name + "\""); - int start = key < 0 ? -1 : text.indexOf('{', key); - if (start < 0) { - return ""; - } - int depth = 0; - boolean quoted = false; - boolean escaped = false; - for (int index = start; index < text.length(); index++) { - char value = text.charAt(index); - if (quoted) { - if (escaped) { - escaped = false; - } else if (value == '\\') { - escaped = true; - } else if (value == '"') { - quoted = false; - } - } else if (value == '"') { - quoted = true; - } else if (value == '{') { - depth++; - } else if (value == '}' && --depth == 0) { - return text.substring(start, index + 1); - } - } - return ""; - } - private static List lines(String text) { String trimmed = text.trim(); return trimmed.isEmpty() ? new ArrayList<>() @@ -277,7 +233,6 @@ private static String jsonStrings(List values) { } private static String quote(String value) { - return "\"" + value.replace("\\", "\\\\") - .replace("\"", "\\\"") + "\""; + return StrictJson.quote(value); } } diff --git a/build-logic/src/main/java/blue/bex/buildlogic/tasks/ReleaseEvidenceJson.java b/build-logic/src/main/java/blue/bex/buildlogic/tasks/ReleaseEvidenceJson.java new file mode 100644 index 0000000..9e032e7 --- /dev/null +++ b/build-logic/src/main/java/blue/bex/buildlogic/tasks/ReleaseEvidenceJson.java @@ -0,0 +1,502 @@ +package blue.bex.buildlogic.tasks; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** Exact schema checks for JSON supplied directly to release tasks. */ +final class ReleaseEvidenceJson { + private static final String SHA_256 = "[0-9a-f]{64}"; + private static final String COMMIT = "[0-9a-f]{40}"; + private static final String ARTIFACT_PATH = + "(?:blue-bex-(?:core|contracts|java)/build/libs/[^/]+\\.jar" + + "|build/distributions/[^/]+-source-release\\.zip)"; + + private ReleaseEvidenceJson() { + } + + static Map parseOrEmpty(String text) { + if (text == null || text.trim().isEmpty()) { + return Collections.emptyMap(); + } + try { + return StrictJson.object(text); + } catch (IllegalArgumentException invalid) { + return Collections.emptyMap(); + } + } + + static String stringOrEmpty( + Map evidence, + String field) { + Object value = evidence.get(field); + return value instanceof String ? (String) value : ""; + } + + static boolean modernizationPassed(Map evidence) { + return hasString(evidence, "schema", + "blue-bex-modernization-report/1.0") + && hasBoolean(evidence, "modernizationReady", true); + } + + static boolean conformanceReleasePassed( + Map evidence) { + return hasString(evidence, "schema", + "blue-bex-modernization-report/1.0") + && hasBoolean(evidence, "conformanceReleaseReady", true); + } + + static boolean publishedLanguagePassed(Map evidence) { + if (!hasString(evidence, "schema", + "blue-bex-published-language/2.0") + || !hasString(evidence, "status", "passed") + || !hasBoolean(evidence, + "configuredAssertionsMatch", true) + || !hasBoolean(evidence, "apiInspectionPassed", true) + || !hasString(evidence, + "differentialStatus", "passed") + || !matches(evidence, "artifactSha256", SHA_256) + || !matches(evidence, "sourceCommit", COMMIT) + || !coordinate(stringOrEmpty(evidence, "coordinate"))) { + return false; + } + try { + List artifacts = StrictJson.array( + evidence, "resolvedArtifacts"); + List blockers = StrictJson.array(evidence, "blockers"); + if (artifacts.isEmpty() || !blockers.isEmpty()) { + return false; + } + String reviewedHash = StrictJson.string( + evidence, "artifactSha256"); + boolean reviewedArtifactPresent = false; + Set paths = new HashSet<>(); + for (Object value : artifacts) { + Map artifact = object(value); + String path = StrictJson.string(artifact, "path"); + String hash = StrictJson.string(artifact, "sha256"); + if (path.trim().isEmpty() || !paths.add(path) + || !hash.matches(SHA_256) + || StrictJson.integer(artifact, "bytes") <= 0) { + return false; + } + reviewedArtifactPresent |= reviewedHash.equals(hash); + } + return reviewedArtifactPresent; + } catch (IllegalArgumentException invalid) { + return false; + } + } + + static boolean differentialPassed( + Map evidence, + String expectedCommit) { + if (!hasString(evidence, "schema", + "blue-bex-local-published-differential/1.0") + || !hasString(evidence, "status", "passed") + || !hasString(evidence, + "localMode", "local-composite") + || !hasString(evidence, + "publishedMode", "standalone-published") + || !hasBoolean(evidence, "sourceBound", true) + || !hasBoolean(evidence, "dependenciesDistinct", true) + || !hasString(evidence, + "semanticAndGasParity", "passed") + || !hasString(evidence, + "exactGasTraceParity", "passed")) { + return false; + } + String commit = stringOrEmpty(evidence, "bexCommit"); + if (!commit.matches(COMMIT) + || expectedCommit != null + && !expectedCommit.equals(commit)) { + return false; + } + try { + return digestPairMatches(StrictJson.object( + evidence, "semanticEvidenceSha256")) + && digestPairMatches(StrictJson.object( + evidence, "gasEvidenceSha256")); + } catch (IllegalArgumentException invalid) { + return false; + } + } + + static boolean independentBuildsPassed( + Map evidence, + String expectedCommit) { + if (!hasString(evidence, "schema", + "blue-bex-independent-clean-builds/2.1") + || !hasString(evidence, "status", "passed") + || !hasBoolean(evidence, + "distinctCheckoutRoots", true) + || !hasBoolean(evidence, + "distinctGitDirectories", true) + || !hasBoolean(evidence, + "distinctGradleHomes", true) + || !hasBoolean(evidence, + "distinctInputManifestFiles", true) + || !hasInteger(evidence, "checkoutCount", 4) + || !hasInteger(evidence, "gitDirectoryCount", 4) + || !hasInteger(evidence, "gradleHomeCount", 4) + || !hasInteger(evidence, "inputManifestCount", 4)) { + return false; + } + String commit = stringOrEmpty(evidence, "bexCommit"); + if (!commit.matches(COMMIT) + || expectedCommit != null + && !expectedCommit.equals(commit)) { + return false; + } + try { + Set checkoutRoots = new HashSet<>(); + Set gitDirectories = new HashSet<>(); + Set gradleHomes = new HashSet<>(); + Set manifests = new HashSet<>(); + boolean pairsPassed = buildPairPassed( + StrictJson.object(evidence, "standalonePublished"), + commit, + checkoutRoots, + gitDirectories, + gradleHomes, + manifests) + && buildPairPassed( + StrictJson.object(evidence, "localComposite"), + commit, + checkoutRoots, + gitDirectories, + gradleHomes, + manifests); + return pairsPassed + && checkoutRoots.size() == 4 + && gitDirectories.size() == 4 + && gradleHomes.size() == 4 + && manifests.size() == 4; + } catch (Exception invalid) { + return false; + } + } + + private static boolean buildPairPassed( + Map pair, + String expectedCommit, + Set checkoutRoots, + Set gitDirectories, + Set gradleHomes, + Set manifests) throws Exception { + if (!hasString(pair, "status", "passed") + || !hasBoolean(pair, "exactManifestBytesMatch", true) + || !hasBoolean(pair, "exactArtifactBytesMatch", true) + || !hasBoolean(pair, "artifactPathSetMatch", true) + || !hasBoolean(pair, + "requiredArtifactRolesPresent", true)) { + return false; + } + long artifactCount = StrictJson.integer(pair, "artifactCount"); + if (artifactCount <= 0) { + return false; + } + LiveBuild first = liveBuild( + StrictJson.object(pair, "firstBuild"), + expectedCommit, + artifactCount); + LiveBuild second = liveBuild( + StrictJson.object(pair, "secondBuild"), + expectedCommit, + artifactCount); + checkoutRoots.add(first.checkoutRoot); + checkoutRoots.add(second.checkoutRoot); + gitDirectories.add(first.gitDirectory); + gitDirectories.add(second.gitDirectory); + gradleHomes.add(first.gradleHome); + gradleHomes.add(second.gradleHome); + manifests.add(first.manifestPath); + manifests.add(second.manifestPath); + return Arrays.equals(first.manifestBytes, second.manifestBytes); + } + + private static LiveBuild liveBuild( + Map build, + String expectedCommit, + long expectedArtifactCount) throws Exception { + if (!hasBoolean(build, "clean", true) + || !hasString(build, "head", expectedCommit)) { + throw new IllegalArgumentException( + "independent checkout identity is stale"); + } + Path checkoutRoot = realDirectory( + StrictJson.string(build, "checkoutRoot")); + Path gitDirectory = realDirectory( + StrictJson.string(build, "gitDirectory")); + Path gradleHome = realDirectory( + StrictJson.string(build, "gradleHome")); + String liveHead = gitText( + checkoutRoot, "rev-parse", "HEAD").trim(); + String liveGitValue = gitText( + checkoutRoot, "rev-parse", "--git-dir").trim(); + Path liveGitPath = Paths.get(liveGitValue); + Path liveGitDirectory = realDirectory( + liveGitPath.isAbsolute() + ? liveGitPath.toString() + : checkoutRoot.resolve(liveGitPath) + .normalize().toString()); + if (!expectedCommit.equals(liveHead) + || !gitDirectory.equals(liveGitDirectory) + || gitBytes(checkoutRoot, + "status", + "--porcelain", + "-z", + "--untracked-files=all").length != 0) { + throw new IllegalArgumentException( + "independent checkout is absent, dirty, or stale"); + } + + Map manifest = StrictJson.object(build, "manifest"); + Path manifestPath = realFile( + StrictJson.string(manifest, "path")); + byte[] recordedManifest = Files.readAllBytes(manifestPath); + List artifacts = StrictJson.array(build, "artifacts"); + if (artifacts.size() != expectedArtifactCount + || !hasInteger(manifest, + "artifactCount", expectedArtifactCount) + || !hasInteger(manifest, + "bytes", recordedManifest.length) + || !sha256(recordedManifest).equals( + StrictJson.string(manifest, "sha256"))) { + throw new IllegalArgumentException( + "independent manifest evidence is stale"); + } + Set paths = new HashSet<>(); + boolean core = false; + boolean contracts = false; + boolean aggregate = false; + boolean sourceRelease = false; + String previousPath = null; + StringBuilder canonicalManifest = new StringBuilder(); + for (Object value : artifacts) { + Map artifact = object(value); + String path = StrictJson.string(artifact, "path"); + String hash = StrictJson.string(artifact, "sha256"); + long bytes = StrictJson.integer(artifact, "bytes"); + if (!safeArtifactPath(path) || !path.matches(ARTIFACT_PATH) + || !paths.add(path) || !hash.matches(SHA_256) + || bytes <= 0 + || (previousPath != null + && previousPath.compareTo(path) >= 0)) { + throw new IllegalArgumentException( + "invalid independent artifact evidence"); + } + Path artifactPath = checkoutRoot.resolve(path).normalize(); + if (!artifactPath.startsWith(checkoutRoot) + || !Files.isRegularFile(artifactPath)) { + throw new IllegalArgumentException( + "independent artifact is absent"); + } + Path realArtifact = artifactPath.toRealPath(); + if (!realArtifact.startsWith(checkoutRoot) + || Files.size(realArtifact) != bytes + || !hash.equals(sha256( + Files.readAllBytes(realArtifact)))) { + throw new IllegalArgumentException( + "independent artifact bytes are stale"); + } + canonicalManifest.append(hash).append(" ") + .append(path).append('\n'); + previousPath = path; + core |= path.startsWith("blue-bex-core/build/libs/") + && path.endsWith(".jar"); + contracts |= path.startsWith( + "blue-bex-contracts/build/libs/") + && path.endsWith(".jar"); + aggregate |= path.startsWith( + "blue-bex-java/build/libs/") + && path.endsWith(".jar"); + sourceRelease |= path.startsWith("build/distributions/") + && path.endsWith("-source-release.zip"); + } + byte[] manifestBytes = canonicalManifest.toString() + .getBytes(StandardCharsets.UTF_8); + String manifestHash = sha256(manifestBytes); + if (!core || !contracts || !aggregate || !sourceRelease + || !Arrays.equals(recordedManifest, manifestBytes) + || !manifestHash.equals( + StrictJson.string(manifest, "artifactSetSha256"))) { + throw new IllegalArgumentException( + "independent manifest does not match live artifacts"); + } + return new LiveBuild( + checkoutRoot, + gitDirectory, + gradleHome, + manifestPath, + manifestBytes); + } + + private static boolean digestPairMatches(Map pair) { + String local = stringOrEmpty(pair, "local"); + String published = stringOrEmpty(pair, "published"); + return local.matches(SHA_256) && local.equals(published); + } + + private static boolean safeArtifactPath(String path) { + return !path.isEmpty() && !path.startsWith("/") + && path.indexOf('\\') < 0 + && !path.equals("..") + && !path.startsWith("../") + && !path.contains("/../") + && !path.endsWith("/.."); + } + + private static Path realDirectory(String value) throws IOException { + Path claimed = Paths.get(value); + if (!claimed.isAbsolute()) { + throw new IllegalArgumentException( + "evidence directory must be absolute"); + } + Path normalized = claimed.normalize(); + Path real = normalized.toRealPath(); + if (!normalized.equals(real) || !Files.isDirectory(real)) { + throw new IllegalArgumentException( + "evidence directory is absent or not canonical"); + } + return real; + } + + private static Path realFile(String value) throws IOException { + Path claimed = Paths.get(value); + if (!claimed.isAbsolute()) { + throw new IllegalArgumentException( + "evidence file must be absolute"); + } + Path normalized = claimed.normalize(); + Path real = normalized.toRealPath(); + if (!normalized.equals(real) || !Files.isRegularFile(real)) { + throw new IllegalArgumentException( + "evidence file is absent or not canonical"); + } + return real; + } + + private static String gitText( + Path directory, + String... arguments) throws IOException, InterruptedException { + return new String( + gitBytes(directory, arguments), + StandardCharsets.UTF_8); + } + + private static byte[] gitBytes( + Path directory, + String... arguments) throws IOException, InterruptedException { + List command = new ArrayList<>(); + command.add("git"); + command.add("-C"); + command.add(directory.toString()); + command.addAll(Arrays.asList(arguments)); + Process process = new ProcessBuilder(command) + .redirectErrorStream(true) + .start(); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + byte[] buffer = new byte[8192]; + int read; + while ((read = process.getInputStream().read(buffer)) >= 0) { + output.write(buffer, 0, read); + } + if (process.waitFor() != 0) { + throw new IOException( + output.toString(StandardCharsets.UTF_8.name())); + } + return output.toByteArray(); + } + + private static boolean coordinate(String value) { + return value.matches("[^:]+:[^:]+:[^:]+"); + } + + private static boolean hasString( + Map evidence, + String field, + String expected) { + return expected.equals(evidence.get(field)); + } + + private static boolean hasBoolean( + Map evidence, + String field, + boolean expected) { + return Boolean.valueOf(expected).equals(evidence.get(field)); + } + + private static boolean hasInteger( + Map evidence, + String field, + long expected) { + try { + return StrictJson.integer(evidence, field) == expected; + } catch (IllegalArgumentException invalid) { + return false; + } + } + + private static boolean matches( + Map evidence, + String field, + String pattern) { + return stringOrEmpty(evidence, field).matches(pattern); + } + + private static Map object(Object value) { + if (!(value instanceof Map)) { + throw new IllegalArgumentException("array entry must be an object"); + } + @SuppressWarnings("unchecked") + Map result = (Map) value; + return result; + } + + private static String sha256(byte[] value) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + StringBuilder result = new StringBuilder(64); + for (byte item : digest.digest(value)) { + result.append(String.format("%02x", item)); + } + return result.toString(); + } catch (NoSuchAlgorithmException exception) { + throw new IllegalStateException("SHA-256 is unavailable", exception); + } + } + + private static final class LiveBuild { + private final Path checkoutRoot; + private final Path gitDirectory; + private final Path gradleHome; + private final Path manifestPath; + private final byte[] manifestBytes; + + private LiveBuild( + Path checkoutRoot, + Path gitDirectory, + Path gradleHome, + Path manifestPath, + byte[] manifestBytes) { + this.checkoutRoot = checkoutRoot; + this.gitDirectory = gitDirectory; + this.gradleHome = gradleHome; + this.manifestPath = manifestPath; + this.manifestBytes = manifestBytes; + } + } +} diff --git a/build-logic/src/main/java/blue/bex/buildlogic/tasks/StrictJson.java b/build-logic/src/main/java/blue/bex/buildlogic/tasks/StrictJson.java new file mode 100644 index 0000000..6f436e1 --- /dev/null +++ b/build-logic/src/main/java/blue/bex/buildlogic/tasks/StrictJson.java @@ -0,0 +1,370 @@ +package blue.bex.buildlogic.tasks; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** Minimal strict JSON reader for release evidence supplied to build tasks. */ +final class StrictJson { + private StrictJson() { + } + + static Map object(String text) { + Object value = new Parser(text).parse(); + if (!(value instanceof Map)) { + throw new IllegalArgumentException("JSON root must be an object"); + } + @SuppressWarnings("unchecked") + Map result = (Map) value; + return result; + } + + static Map object( + Map parent, + String field) { + Object value = parent.get(field); + if (!(value instanceof Map)) { + throw new IllegalArgumentException(field + " must be an object"); + } + @SuppressWarnings("unchecked") + Map result = (Map) value; + return result; + } + + static List array( + Map parent, + String field) { + Object value = parent.get(field); + if (!(value instanceof List)) { + throw new IllegalArgumentException(field + " must be an array"); + } + @SuppressWarnings("unchecked") + List result = (List) value; + return result; + } + + static String string( + Map parent, + String field) { + Object value = parent.get(field); + if (!(value instanceof String)) { + throw new IllegalArgumentException(field + " must be a string"); + } + return (String) value; + } + + static boolean bool( + Map parent, + String field) { + Object value = parent.get(field); + if (!(value instanceof Boolean)) { + throw new IllegalArgumentException(field + " must be a boolean"); + } + return (Boolean) value; + } + + static long integer( + Map parent, + String field) { + Object value = parent.get(field); + if (!(value instanceof BigDecimal)) { + throw new IllegalArgumentException(field + " must be a number"); + } + try { + return ((BigDecimal) value).longValueExact(); + } catch (ArithmeticException exception) { + throw new IllegalArgumentException( + field + " must be an integer", exception); + } + } + + static String quote(String value) { + StringBuilder result = new StringBuilder(value.length() + 2); + result.append('"'); + for (int index = 0; index < value.length(); index++) { + char next = value.charAt(index); + switch (next) { + case '"': + result.append("\\\""); + break; + case '\\': + result.append("\\\\"); + break; + case '\b': + result.append("\\b"); + break; + case '\f': + result.append("\\f"); + break; + case '\n': + result.append("\\n"); + break; + case '\r': + result.append("\\r"); + break; + case '\t': + result.append("\\t"); + break; + default: + if (next < 0x20) { + result.append(String.format("\\u%04x", (int) next)); + } else { + result.append(next); + } + } + } + return result.append('"').toString(); + } + + private static final class Parser { + private static final int MAX_DEPTH = 64; + + private final String text; + private int index; + + private Parser(String text) { + if (text == null) { + throw new IllegalArgumentException("JSON text is required"); + } + this.text = text; + } + + private Object parse() { + skipWhitespace(); + Object value = value(0); + skipWhitespace(); + if (index != text.length()) { + fail("trailing content"); + } + return value; + } + + private Object value(int depth) { + if (depth > MAX_DEPTH) { + fail("nesting exceeds " + MAX_DEPTH); + } + if (index >= text.length()) { + fail("unexpected end of input"); + } + char next = text.charAt(index); + if (next == '{') { + return object(depth + 1); + } + if (next == '[') { + return array(depth + 1); + } + if (next == '"') { + return string(); + } + if (next == 't') { + literal("true"); + return Boolean.TRUE; + } + if (next == 'f') { + literal("false"); + return Boolean.FALSE; + } + if (next == 'n') { + literal("null"); + return null; + } + if (next == '-' || isDigit(next)) { + return number(); + } + fail("unexpected character"); + return null; + } + + private Map object(int depth) { + expect('{'); + skipWhitespace(); + Map result = new LinkedHashMap<>(); + if (take('}')) { + return result; + } + while (true) { + if (index >= text.length() || text.charAt(index) != '"') { + fail("object key must be a string"); + } + String key = string(); + if (result.containsKey(key)) { + fail("duplicate object key " + key); + } + skipWhitespace(); + expect(':'); + skipWhitespace(); + result.put(key, value(depth)); + skipWhitespace(); + if (take('}')) { + return result; + } + expect(','); + skipWhitespace(); + } + } + + private List array(int depth) { + expect('['); + skipWhitespace(); + List result = new ArrayList<>(); + if (take(']')) { + return result; + } + while (true) { + result.add(value(depth)); + skipWhitespace(); + if (take(']')) { + return result; + } + expect(','); + skipWhitespace(); + } + } + + private String string() { + expect('"'); + StringBuilder result = new StringBuilder(); + while (index < text.length()) { + char next = text.charAt(index++); + if (next == '"') { + return result.toString(); + } + if (next < 0x20) { + fail("unescaped control character in string"); + } + if (next != '\\') { + result.append(next); + continue; + } + if (index >= text.length()) { + fail("incomplete string escape"); + } + char escaped = text.charAt(index++); + switch (escaped) { + case '"': + case '\\': + case '/': + result.append(escaped); + break; + case 'b': + result.append('\b'); + break; + case 'f': + result.append('\f'); + break; + case 'n': + result.append('\n'); + break; + case 'r': + result.append('\r'); + break; + case 't': + result.append('\t'); + break; + case 'u': + result.append(unicode()); + break; + default: + fail("invalid string escape"); + } + } + fail("unterminated string"); + return ""; + } + + private char unicode() { + if (index + 4 > text.length()) { + fail("incomplete unicode escape"); + } + int value = 0; + for (int offset = 0; offset < 4; offset++) { + int digit = Character.digit(text.charAt(index++), 16); + if (digit < 0) { + fail("invalid unicode escape"); + } + value = (value << 4) | digit; + } + return (char) value; + } + + private BigDecimal number() { + int start = index; + take('-'); + if (take('0')) { + if (index < text.length() && isDigit(text.charAt(index))) { + fail("number has a leading zero"); + } + } else { + digits("integer digits are required"); + } + if (take('.')) { + digits("fraction digits are required"); + } + if (take('e') || take('E')) { + if (!take('+')) { + take('-'); + } + digits("exponent digits are required"); + } + try { + return new BigDecimal(text.substring(start, index)); + } catch (NumberFormatException exception) { + throw new IllegalArgumentException( + "Invalid JSON number at offset " + start, exception); + } + } + + private void digits(String message) { + int start = index; + while (index < text.length() && isDigit(text.charAt(index))) { + index++; + } + if (start == index) { + fail(message); + } + } + + private void literal(String expected) { + if (!text.regionMatches(index, expected, 0, expected.length())) { + fail("expected " + expected); + } + index += expected.length(); + } + + private void skipWhitespace() { + while (index < text.length()) { + char next = text.charAt(index); + if (next != ' ' && next != '\n' + && next != '\r' && next != '\t') { + return; + } + index++; + } + } + + private boolean take(char expected) { + if (index < text.length() && text.charAt(index) == expected) { + index++; + return true; + } + return false; + } + + private void expect(char expected) { + if (!take(expected)) { + fail("expected " + expected); + } + } + + private void fail(String message) { + throw new IllegalArgumentException( + "Invalid JSON at offset " + index + ": " + message); + } + + private static boolean isDigit(char value) { + return value >= '0' && value <= '9'; + } + } +} diff --git a/build-logic/src/main/java/blue/bex/buildlogic/tasks/VerifyPublishedLanguageTask.java b/build-logic/src/main/java/blue/bex/buildlogic/tasks/VerifyPublishedLanguageTask.java index b2fcfbd..a899481 100644 --- a/build-logic/src/main/java/blue/bex/buildlogic/tasks/VerifyPublishedLanguageTask.java +++ b/build-logic/src/main/java/blue/bex/buildlogic/tasks/VerifyPublishedLanguageTask.java @@ -12,10 +12,10 @@ import java.util.Comparator; import java.util.Enumeration; import java.util.List; +import java.util.Map; import java.util.Properties; import java.util.jar.JarEntry; import java.util.jar.JarFile; -import java.util.regex.Pattern; import java.util.stream.Collectors; import org.gradle.api.DefaultTask; import org.gradle.api.GradleException; @@ -139,9 +139,11 @@ public void verify() { String differential = getDifferentialReport().isPresent() && getDifferentialReport().get().getAsFile().isFile() ? read(getDifferentialReport().get().getAsFile()) : ""; - boolean differentialPassed = containsStatus(differential, "passed") - && fieldPassed(differential, "semanticAndGasParity") - && fieldPassed(differential, "exactGasTraceParity"); + Map differentialEvidence = + ReleaseEvidenceJson.parseOrEmpty(differential); + boolean differentialPassed = + ReleaseEvidenceJson.differentialPassed( + differentialEvidence, null); if (!differential.isEmpty() && !differentialPassed) { reasons.add("local/published semantic and exact-gas differential " + "did not pass"); @@ -251,16 +253,6 @@ private static String property(Properties properties, String key) { return properties.getProperty(key, "").trim(); } - private static boolean containsStatus(String text, String status) { - return text.matches("(?s).*\\\"status\\\"\\s*:\\s*\\\"" - + Pattern.quote(status) + "\\\".*"); - } - - private static boolean fieldPassed(String text, String field) { - return text.matches("(?s).*\\\"" + Pattern.quote(field) - + "\\\"\\s*:\\s*(?:\\\"passed\\\"|true).*?"); - } - private static String artifactsJson(List artifacts) { return artifacts.stream().map(item -> "{\"path\":" + quote(unix(item.file)) + ",\"bytes\":" + item.file.length() @@ -300,8 +292,7 @@ private static String unix(File file) { } private static String quote(String value) { - return "\"" + value.replace("\\", "\\\\") - .replace("\"", "\\\"") + "\""; + return StrictJson.quote(value); } private static final class ArtifactEvidence { diff --git a/docs/LATEST_LANGUAGE_API_MIGRATION.md b/docs/LATEST_LANGUAGE_API_MIGRATION.md index 747e699..c18a3d2 100644 --- a/docs/LATEST_LANGUAGE_API_MIGRATION.md +++ b/docs/LATEST_LANGUAGE_API_MIGRATION.md @@ -17,7 +17,7 @@ It is the human-readable companion to | Language target delta | `LICENSE`, one migration report, and one modernization report only | | Previous API manifest SHA-256 | `830caa187023079ba53fa76d2932e6e12cb8c93be3f90ac887ad374d6642b315` | | Working-checkpoint API manifest SHA-256 | `43aea6ae9de6f39729f93c146ff5453887da9a2303f6f4f33573ba47f37d5be0` | -| Final modular API manifest SHA-256 | `df602fa6b14afc285053fc8e34e349d7fa5ce26810a6c79ba14ac6361de463ee` | +| Final modular API manifest SHA-256 | `5acb4712e3e03c5ba9a58d87b4a40bed4e1dcca76ed58ef355a77f0efc9d3c92` | The migration target cannot truthfully name its eventual BEX commit while that commit is being assembled. The Git commit containing this ledger is the target @@ -26,7 +26,7 @@ entry was audited. ## Exact descriptor changes -The subsequent modular modernization contains 254 removed and 483 added exact +The subsequent modular modernization contains 254 removed and 484 added exact owner-qualified descriptors relative to commit `169e589`. Both compared manifests, both classifications, and the complete sorted addition/removal sets are source-controlled under `gradle/verification/api/`. The `binaryApiCheck` task @@ -136,6 +136,6 @@ machine-comparable inventory, while the JSON file supplies intent metadata. At this audited state the required inventory is byte-for-byte identical to `blue-bex-conformance/build/reports/bex-release/public-api.txt`, and both have -SHA-256 `df602fa6b14afc285053fc8e34e349d7fa5ce26810a6c79ba14ac6361de463ee`. +SHA-256 `5acb4712e3e03c5ba9a58d87b4a40bed4e1dcca76ed58ef355a77f0efc9d3c92`. Build wiring generates the latter from compiled classes and fails on any diff from the reviewed source-controlled baseline. diff --git a/docs/contracts-hosting.md b/docs/contracts-hosting.md index e79c872..3fa21fc 100644 --- a/docs/contracts-hosting.md +++ b/docs/contracts-hosting.md @@ -26,9 +26,11 @@ public BexExecutionResult run( BexExecutionContext.Builder context = BexExecutionContext.builder(); BexContractsExecutionContext.configure(context, processor, "bex:policy"); - return BexEngine.builder().build().compileAndExecute( - BexProgramSource.expression(selectedExpression), - context.build()); + try (BexEngine engine = BexEngine.builder().build()) { + return engine.compileAndExecute( + BexProgramSource.expression(selectedExpression), + context.build()); + } } ``` diff --git a/docs/gas-and-exhaustion.md b/docs/gas-and-exhaustion.md index 744afde..a420ee5 100644 --- a/docs/gas-and-exhaustion.md +++ b/docs/gas-and-exhaustion.md @@ -16,7 +16,7 @@ The closed vocabulary covers expression/statement/function/intrinsic work; document and binding reads; pointer/object/list work; collection production; text/numeric/comparison/sort work; patch/event append; transient construction; Blue output admission; and identity requests. The exact names and weights live -in `src/main/resources/blue/bex/gas/blue-bex-gas-2.0.yaml` and are explained in +in `blue-bex-core/src/main/resources/blue/bex/gas/blue-bex-gas-2.0.yaml` and are explained in [`GAS.md`](GAS.md). Changing a counter name, order, or weight is a semantic versioned change. This diff --git a/docs/intrinsics.md b/docs/intrinsics.md index 9ed1f8d..07c4b88 100644 --- a/docs/intrinsics.md +++ b/docs/intrinsics.md @@ -21,7 +21,9 @@ BexIntrinsicRegistry registry = BexIntrinsicRegistry.builder() }) .build(); -BexEngine engine = BexEngine.builder().intrinsics(registry).build(); +try (BexEngine engine = BexEngine.builder().intrinsics(registry).build()) { + // Compile and execute programs for this host lifecycle. +} ``` A class convenience is valid only when an explicit `BexTypeBlueIdResolver` or diff --git a/docs/latest-language-api-migration.json b/docs/latest-language-api-migration.json index 4405c06..577d1f1 100644 --- a/docs/latest-language-api-migration.json +++ b/docs/latest-language-api-migration.json @@ -17,14 +17,14 @@ "manifests": { "beforeSha256": "830caa187023079ba53fa76d2932e6e12cb8c93be3f90ac887ad374d6642b315", "workingCheckpointSha256": "43aea6ae9de6f39729f93c146ff5453887da9a2303f6f4f33573ba47f37d5be0", - "afterSha256": "df602fa6b14afc285053fc8e34e349d7fa5ce26810a6c79ba14ac6361de463ee", + "afterSha256": "5acb4712e3e03c5ba9a58d87b4a40bed4e1dcca76ed58ef355a77f0efc9d3c92", "workingCheckpointPath": "gradle/verification/api/working-checkpoint-public-api.txt", "requiredPath": "src/test/resources/hosted-release/required-public-api.txt", "generatedPath": "blue-bex-conformance/build/reports/bex-release/public-api.txt", "workingCheckpointClassificationPath": "gradle/verification/api/working-checkpoint-public-api-classification.json", "workingCheckpointClassificationSha256": "4e0738794bcf042f399ac0a15f153aa9ac5d07fd7eae95bc410ce3bf780f343e", "afterClassificationPath": "docs/public-api-classification.json", - "afterClassificationSha256": "e4bb7931f6cbf07a3597348eb5de482376c2752b26265195060f9f19717be5b8", + "afterClassificationSha256": "69ffe62280a4feb1eafb93cb4d26ccd730713d2ddf7f132e426016db51e92753", "publicTypeCount": 101, "publicDescriptorCount": 1028 }, @@ -32,11 +32,11 @@ "baselineCommit": "169e589", "baselineManifestSha256": "43aea6ae9de6f39729f93c146ff5453887da9a2303f6f4f33573ba47f37d5be0", "removedDescriptorLines": 254, - "addedDescriptorLines": 483, + "addedDescriptorLines": 484, "removedDescriptorsPath": "gradle/verification/api/modernization-removed-descriptors.txt", "removedDescriptorsSha256": "7d3ba5a5e69ddf8e2fee0d92c924cae3c4dd36d42e465dc9931c550a99db2d99", "addedDescriptorsPath": "gradle/verification/api/modernization-added-descriptors.txt", - "addedDescriptorsSha256": "0d94702570948e6f4c3f5dd88b1670f57db60ec11e8bcaff5220f02599ce56fe", + "addedDescriptorsSha256": "5cb3e8a2ef872d5f2334e9251feca3c083f5c887a76549ce3dd60e2b40a03987", "comparison": "complete bytewise set difference after qualifying every member descriptor with its owning class; :blue-bex-conformance:binaryApiCheck recomputes and compares every line", "completeMachineAuditableDelta": true, "packageMoves": [ diff --git a/docs/public-api-classification.json b/docs/public-api-classification.json index 976ddd2..2d44283 100644 --- a/docs/public-api-classification.json +++ b/docs/public-api-classification.json @@ -2,10 +2,10 @@ "schema": "blue-bex-public-api-classification/2.0", "inventory": { "path": "src/test/resources/hosted-release/required-public-api.txt", - "sha256": "df602fa6b14afc285053fc8e34e349d7fa5ce26810a6c79ba14ac6361de463ee", + "sha256": "5acb4712e3e03c5ba9a58d87b4a40bed4e1dcca76ed58ef355a77f0efc9d3c92", "manifestSchema": "blue-bex-binary-api-manifest/1.0", "publicTypeCount": 101, - "publicDescriptorCount": 1027 + "publicDescriptorCount": 1028 }, "classifications": { "stable API": [ diff --git a/docs/start-here.md b/docs/start-here.md index 626338e..b6de159 100644 --- a/docs/start-here.md +++ b/docs/start-here.md @@ -32,8 +32,10 @@ BexExecutionContext context = BexExecutionContext.builder() .gasLimit(10_000L) .build(); -BexExecutionResult result = BexEngine.builder().build() - .compileAndExecute(BexProgramSource.expression(expression), context); +try (BexEngine engine = BexEngine.builder().build()) { + BexExecutionResult result = engine.compileAndExecute( + BexProgramSource.expression(expression), context); +} ``` The complete runnable source is in the `examples` module. diff --git a/examples/src/main/java/blue/bex/examples/HostedConsumerSmoke.java b/examples/src/main/java/blue/bex/examples/HostedConsumerSmoke.java index ff8da9b..8707ff0 100644 --- a/examples/src/main/java/blue/bex/examples/HostedConsumerSmoke.java +++ b/examples/src/main/java/blue/bex/examples/HostedConsumerSmoke.java @@ -82,9 +82,14 @@ public static SmokeResult runSmoke() { return BexValues.map(value); }) .build(); - BexEngine engine = BexEngine.builder() + try (BexEngine engine = BexEngine.builder() .intrinsics(intrinsics) - .build(); + .build()) { + return runSmoke(engine); + } + } + + private static SmokeResult runSmoke(BexEngine engine) { HostedHandlerProcessor handler = new HostedHandlerProcessor(engine); diff --git a/examples/src/main/java/blue/bex/examples/StandaloneBexExample.java b/examples/src/main/java/blue/bex/examples/StandaloneBexExample.java index 4ef1a24..7c3f70b 100644 --- a/examples/src/main/java/blue/bex/examples/StandaloneBexExample.java +++ b/examples/src/main/java/blue/bex/examples/StandaloneBexExample.java @@ -24,14 +24,15 @@ public static void main(String[] args) { new Node().value(2L)))); FrozenNode document = FrozenNode.fromResolvedNode(new Node()); - BexExecutionResult result = BexEngine.builder() - .build() - .compileAndExecute( - BexProgramSource.expression(expression), - BexExecutionContext.builder() - .document(new FrozenBexDocumentView(document)) - .gasLimit(10_000L) - .build()); + BexExecutionResult result; + try (BexEngine engine = BexEngine.builder().build()) { + result = engine.compileAndExecute( + BexProgramSource.expression(expression), + BexExecutionContext.builder() + .document(new FrozenBexDocumentView(document)) + .gasLimit(10_000L) + .build()); + } Object value = result.value().toSimple(); if (!BigInteger.valueOf(42L).equals(value)) { diff --git a/gradle/verification/api/modernization-added-descriptors.txt b/gradle/verification/api/modernization-added-descriptors.txt index caebf91..dcf7cbb 100644 --- a/gradle/verification/api/modernization-added-descriptors.txt +++ b/gradle/verification/api/modernization-added-descriptors.txt @@ -186,6 +186,7 @@ class public final blue.bex.compile.BexCompiledProgramKey :: method public stati class public final blue.bex.compile.BexCompiledProgramKey :: method public static from(blue.bex.compile.BexCompilationInput,java.lang.String):blue.bex.compile.BexCompiledProgramKey class public final blue.bex.compile.BexCompiledProgramRuntimeAccess class public final blue.bex.compile.BexCompiledProgramRuntimeAccess :: method public static execute(blue.bex.compile.BexCompiledProgram,blue.bex.compile.BexExecutionMachine):blue.bex.value.BexValue +class public final blue.bex.compile.BexCompiledProgramRuntimeAccess :: method public static matchesCompilationKey(blue.bex.compile.BexCompiledProgram,blue.bex.compile.BexCompiledProgramKey):boolean class public final blue.bex.compile.BexCompilerRuntimeAccess class public final blue.bex.compile.BexCompilerRuntimeAccess :: method public static compile(blue.bex.compile.BexCompilationInput,blue.bex.result.BexMetricsRecorder,blue.bex.compile.BexIntrinsicCatalog,java.lang.String):blue.bex.compile.BexCompiledProgram class public final blue.bex.compile.BexContainsCache :: method public synchronized containsBex(blue.language.snapshot.FrozenNode,blue.bex.result.BexMetricsRecorder):boolean diff --git a/src/test/java/blue/bex/BexCompiledProgramCacheTest.java b/src/test/java/blue/bex/BexCompiledProgramCacheTest.java index 7c7c1e9..334be09 100644 --- a/src/test/java/blue/bex/BexCompiledProgramCacheTest.java +++ b/src/test/java/blue/bex/BexCompiledProgramCacheTest.java @@ -1,15 +1,20 @@ package blue.bex; import blue.bex.api.BexEngine; +import blue.bex.api.BexExecutionContext; import blue.bex.api.BexProgramSource; import blue.bex.compile.BexCompiledProgram; +import blue.bex.compile.BexCompiledProgramCache; import blue.bex.compile.BexCompiledProgramKey; import blue.bex.compile.BexNodeIdentity; import blue.bex.compile.LruBexCompiledProgramCache; +import blue.bex.value.BexValues; import blue.bex.test.TestBlue; import blue.language.snapshot.FrozenNode; import org.junit.jupiter.api.Test; +import java.util.concurrent.atomic.AtomicInteger; + import static blue.bex.test.BexTestFixtures.*; import static org.junit.jupiter.api.Assertions.*; @@ -39,6 +44,36 @@ void sameNodeProducesSameIdentityAndCacheHit() { assertEquals(BexNodeIdentity.stable(source.programNode()), BexNodeIdentity.stable(source.programNode())); } + @Test + void cacheReturningAnotherProgramsEntryIsRejectedBeforeExecution() { + PoisoningCache cache = new PoisoningCache(); + BexEngine engine = BexEngine.builder() + .language(blue.runtime()) + .cache(cache) + .build(); + BexProgramSource first = BexProgramSource.expression( + frozen(op("$binding", "poisonProbe"))); + BexProgramSource second = BexProgramSource.expression( + frozen(v("expected"))); + engine.compile(first); + + AtomicInteger reads = new AtomicInteger(); + BexExecutionContext context = BexExecutionContext.builder() + .document(defaultDocumentView()) + .lazyBinding("poisonProbe", () -> { + reads.incrementAndGet(); + return BexValues.scalar("poisoned"); + }) + .build(); + + BexException failure = assertThrows( + BexException.class, + () -> engine.compileAndExecute(second, context)); + assertTrue(failure.getMessage().contains("cache key")); + assertEquals(0, reads.get(), + "a mismatched cached program must be rejected before execution"); + } + @Test void entryNameParticipatesInCacheKey() { FrozenNode step = frozen(obj("type", "Blue/BEX Program")); @@ -198,4 +233,21 @@ private void assertDifferentProgramIdentities(String firstYaml, String secondYam assertNotEquals(BexNodeIdentity.stable(first.programNode()), BexNodeIdentity.stable(second.programNode())); assertNotEquals(BexCompiledProgramKey.from(first), BexCompiledProgramKey.from(second)); } + + private static final class PoisoningCache + implements BexCompiledProgramCache { + private BexCompiledProgram program; + + @Override + public BexCompiledProgram get(BexCompiledProgramKey key) { + return program; + } + + @Override + public void put( + BexCompiledProgramKey key, + BexCompiledProgram program) { + this.program = program; + } + } } diff --git a/src/test/java/blue/bex/api/BexEngineBuilderValidationTest.java b/src/test/java/blue/bex/api/BexEngineBuilderValidationTest.java new file mode 100644 index 0000000..63ae4c9 --- /dev/null +++ b/src/test/java/blue/bex/api/BexEngineBuilderValidationTest.java @@ -0,0 +1,27 @@ +package blue.bex.api; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class BexEngineBuilderValidationTest { + + @Test + void gasScheduleRejectsNullImmediately() { + NullPointerException failure = assertThrows( + NullPointerException.class, + () -> BexEngine.builder().gasSchedule(null)); + + assertEquals("gasSchedule", failure.getMessage()); + } + + @Test + void cacheRejectsNullImmediately() { + NullPointerException failure = assertThrows( + NullPointerException.class, + () -> BexEngine.builder().cache(null)); + + assertEquals("cache", failure.getMessage()); + } +} diff --git a/src/test/java/blue/bex/compile/BexCompiledIrImmutabilityTest.java b/src/test/java/blue/bex/compile/BexCompiledIrImmutabilityTest.java index 2b26a35..f35c37d 100644 --- a/src/test/java/blue/bex/compile/BexCompiledIrImmutabilityTest.java +++ b/src/test/java/blue/bex/compile/BexCompiledIrImmutabilityTest.java @@ -19,6 +19,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotSame; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; class BexCompiledIrImmutabilityTest { private static final CompiledExpression FIRST_EXPRESSION = @@ -70,8 +71,11 @@ void compiledProgramAndFunctionCopyAndFreezeCollectionInputs() constants.put("constant", BexValues.scalar("value")); Set intrinsicIds = new LinkedHashSet<>(); intrinsicIds.add("intrinsic"); + BexCompiledProgramKey compilationKey = + new BexCompiledProgramKey("program", "definition", "entry"); BexCompiledProgram program = new BexCompiledProgram(function, - functions, constants, 1, "program", intrinsicIds); + functions, constants, 1, "program", intrinsicIds, + compilationKey); functions.clear(); constants.clear(); @@ -79,6 +83,7 @@ void compiledProgramAndFunctionCopyAndFreezeCollectionInputs() assertEquals(1, program.functions().size()); assertEquals(1, program.constants().size()); assertEquals(1, program.requiredIntrinsicBlueIds().size()); + assertTrue(program.matchesCompilationKey(compilationKey)); assertThrows(UnsupportedOperationException.class, () -> program.functions().clear()); assertThrows(UnsupportedOperationException.class, diff --git a/src/test/java/blue/bex/conformance/BexConformanceReportMain.java b/src/test/java/blue/bex/conformance/BexConformanceReportMain.java index 4118bbb..2dc3ca4 100644 --- a/src/test/java/blue/bex/conformance/BexConformanceReportMain.java +++ b/src/test/java/blue/bex/conformance/BexConformanceReportMain.java @@ -4,6 +4,7 @@ import blue.language.processor.RuntimeWorkBudget; import blue.language.provider.CyclicAwareNodeProvider; import blue.language.provider.CyclicSetProofResult; +import org.yaml.snakeyaml.Yaml; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; @@ -116,7 +117,7 @@ public static void main(String[] args) throws Exception { Map cyclicProofUnavailabilityCapability = cyclicProofUnavailabilityCapability(tests); Map hostLongTrace = - hostLongTraceEvidence(buildDir); + hostLongTraceEvidence(projectDir); Map hostedOutcomes = hostedOutcomeEvidence(tests); Map languageReleaseIdentity = @@ -1136,11 +1137,6 @@ private static List currentModeFailures( versionAutomation.get( "matchesProjectVersion")), "cz-toml-version-differs-from-project-version"); - require( - failures, - "1.8".equals(System.getProperty( - "java.specification.version")), - "report-not-running-on-java-8"); require( failures, "passed".equals( @@ -1190,8 +1186,9 @@ private static List currentModeFailures( } private static Map hostLongTraceEvidence( - Path buildDir) throws IOException { - Path evidencePath = buildDir.resolve("reports") + Path projectDir) throws IOException { + Path evidencePath = projectDir.resolve("build") + .resolve("reports") .resolve("bex-release") .resolve("host-long-trace.properties"); Map evidence = @@ -3443,26 +3440,38 @@ private static Map releaseGateEvidence( Path compositePath) throws Exception { Path releaseRoot = buildDir.resolve("reports") .resolve("bex-release"); - return map( - "deterministicArchives", - deterministicArchiveEvidence( - projectDir, - releaseRoot.resolve( - "deterministic-archives.properties")), - "independentCleanBuilds", - independentCleanBuildEvidence( + Path independentProperties = persistentEvidenceRoot.resolve( + "independent-clean-builds-" + dependencyMode + + ".properties"); + Map independentCleanBuilds = + Files.isRegularFile(independentProperties) + ? independentCleanBuildEvidence( projectDir, buildDir, - persistentEvidenceRoot.resolve( - "independent-clean-builds-" - + dependencyMode - + ".properties"), + independentProperties, projectVersion, sourceCommit, dependencyMode, declaredDependency, dependencyResolution, - compositePath), + compositePath) + : independentCleanBuildJsonEvidence( + projectDir, + projectDir.resolve("build") + .resolve("reports") + .resolve("bex-release") + .resolve("inputs") + .resolve("independent-clean-builds.json"), + sourceCommit, + dependencyMode); + return map( + "deterministicArchives", + deterministicArchiveEvidence( + projectDir, + releaseRoot.resolve( + "deterministic-archives.properties")), + "independentCleanBuilds", + independentCleanBuilds, "binaryApi", binaryApiEvidence( projectDir, @@ -3481,6 +3490,172 @@ private static Map releaseGateEvidence( "java8-bytecode.properties"))); } + private static Map independentCleanBuildJsonEvidence( + Path projectDir, + Path evidencePath, + String sourceCommit, + String dependencyMode) throws Exception { + if (!Files.isRegularFile(evidencePath)) { + return map( + "status", "not-executed", + "evidencePresent", false, + "evidencePath", evidencePath.toString()); + } + Map evidence; + try { + Object loaded = new Yaml().load(new String( + Files.readAllBytes(evidencePath), + StandardCharsets.UTF_8)); + evidence = castMap(loaded); + } catch (RuntimeException invalid) { + return map( + "status", "stale-or-failed", + "evidencePresent", true, + "evidencePath", evidencePath.toString(), + "parseStatus", "invalid-json"); + } + String sectionName = "local-composite".equals(dependencyMode) + ? "localComposite" : "standalonePublished"; + Map pair = castMap(evidence.get(sectionName)); + Map firstBuild = castMap(pair.get("firstBuild")); + Map secondBuild = castMap(pair.get("secondBuild")); + Map first = castMap(firstBuild.get("manifest")); + Map second = castMap(secondBuild.get("manifest")); + List recordedArtifacts = objectList( + firstBuild.get("artifacts")); + List secondArtifacts = objectList( + secondBuild.get("artifacts")); + List currentArtifacts = new ArrayList(); + Set paths = new LinkedHashSet(); + String previousPath = null; + StringBuilder canonicalManifest = new StringBuilder(); + boolean artifactsValid = !recordedArtifacts.isEmpty(); + boolean corePresent = false; + boolean contractsPresent = false; + boolean aggregatePresent = false; + boolean sourceReleasePresent = false; + Pattern allowedPath = Pattern.compile( + "(?:blue-bex-(?:core|contracts|java)/build/libs/[^/]+\\.jar|" + + "build/distributions/[^/]+-source-release\\.zip)"); + for (Object item : recordedArtifacts) { + Map artifact = castMap(item); + String path = stringValue(artifact.get("path")); + String hash = stringValue(artifact.get("sha256")); + boolean ordered = previousPath == null + || previousPath.compareTo(path) < 0; + boolean safePath = allowedPath.matcher(path).matches() + && !path.startsWith("/") + && path.indexOf('\\') < 0 + && !path.contains("/../") + && paths.add(path); + FileCheck current = safePath && hash.matches("[0-9a-f]{64}") + ? checkFile(projectDir, path, hash) + : new FileCheck(path, 0L, hash, false); + artifactsValid &= ordered && safePath && current.valid + && longValue(artifact.get("bytes")) == current.bytes; + currentArtifacts.add(current.report()); + canonicalManifest.append(hash).append(" ") + .append(path).append('\n'); + previousPath = path; + corePresent |= path.startsWith("blue-bex-core/build/libs/") + && path.endsWith(".jar"); + contractsPresent |= path.startsWith( + "blue-bex-contracts/build/libs/") + && path.endsWith(".jar"); + aggregatePresent |= path.startsWith( + "blue-bex-java/build/libs/") + && path.endsWith(".jar"); + sourceReleasePresent |= path.startsWith("build/distributions/") + && path.endsWith("-source-release.zip"); + } + byte[] canonicalBytes = canonicalManifest.toString() + .getBytes(StandardCharsets.UTF_8); + String canonicalHash = ConformancePackage.sha256(canonicalBytes); + long artifactCount = longValue(pair.get("artifactCount")); + boolean manifestsValid = manifestEvidenceMatches( + first, canonicalHash, canonicalBytes.length, artifactCount) + && manifestEvidenceMatches( + second, canonicalHash, canonicalBytes.length, artifactCount); + boolean rolesPresent = corePresent && contractsPresent + && aggregatePresent && sourceReleasePresent; + boolean pairValid = "passed".equals(pair.get("status")) + && Boolean.TRUE.equals(pair.get("exactManifestBytesMatch")) + && Boolean.TRUE.equals(pair.get("exactArtifactBytesMatch")) + && Boolean.TRUE.equals(pair.get("artifactPathSetMatch")) + && Boolean.TRUE.equals(pair.get( + "requiredArtifactRolesPresent")) + && artifactCount == recordedArtifacts.size() + && recordedArtifacts.equals(secondArtifacts) + && Boolean.TRUE.equals(firstBuild.get("clean")) + && Boolean.TRUE.equals(secondBuild.get("clean")) + && sourceCommit.equals(firstBuild.get("head")) + && sourceCommit.equals(secondBuild.get("head")) + && manifestsValid && artifactsValid && rolesPresent; + boolean passed = "blue-bex-independent-clean-builds/2.1".equals( + evidence.get("schema")) + && "passed".equals(evidence.get("status")) + && sourceCommit.equals(evidence.get("bexCommit")) + && longValue(evidence.get("checkoutCount")) == 4L + && longValue(evidence.get("gitDirectoryCount")) == 4L + && longValue(evidence.get("gradleHomeCount")) == 4L + && longValue(evidence.get("inputManifestCount")) == 4L + && Boolean.TRUE.equals(evidence.get( + "distinctCheckoutRoots")) + && Boolean.TRUE.equals(evidence.get( + "distinctGitDirectories")) + && Boolean.TRUE.equals(evidence.get( + "distinctGradleHomes")) + && Boolean.TRUE.equals(evidence.get( + "distinctInputManifestFiles")) + && pairValid; + return map( + "status", passed ? "passed" : "stale-or-failed", + "evidencePresent", true, + "evidencePath", evidencePath.toString(), + "evidenceSha256", sha256(evidencePath), + "schema", evidence.get("schema"), + "commit", evidence.get("bexCommit"), + "dependencyMode", dependencyMode, + "validatedSection", sectionName, + "distinctInputManifestFiles", + evidence.get("distinctInputManifestFiles"), + "artifactCount", recordedArtifacts.size(), + "manifestSha256", canonicalHash, + "manifestsValid", manifestsValid, + "requiredArtifactRolesPresent", rolesPresent, + "currentArtifactsMatch", artifactsValid, + "currentArtifacts", currentArtifacts); + } + + private static boolean manifestEvidenceMatches( + Map manifest, + String expectedHash, + long expectedBytes, + long expectedArtifactCount) { + return expectedHash.matches("[0-9a-f]{64}") + && expectedHash.equals(manifest.get("sha256")) + && expectedHash.equals(manifest.get("artifactSetSha256")) + && longValue(manifest.get("bytes")) == expectedBytes + && longValue(manifest.get("artifactCount")) + == expectedArtifactCount; + } + + @SuppressWarnings("unchecked") + private static List objectList(Object value) { + return value instanceof List + ? (List) value + : Collections.emptyList(); + } + + private static String stringValue(Object value) { + return value instanceof String ? (String) value : ""; + } + + private static long longValue(Object value) { + return value instanceof Number + ? ((Number) value).longValue() : -1L; + } + private static Map independentCleanBuildEvidence( Path projectDir, @@ -4271,7 +4446,14 @@ && parseLong(evidence.get( "required.missingCount")) == 0L && parseLong(evidence.get( "required.unexpectedCount")) == 0L; - String testStatus = apiTests.overallStatus(); + boolean receiptTaskPassed = + ":blue-bex-conformance:binaryApiCheck".equals( + evidence.get("verificationTask")) + && "passed".equals( + evidence.get("verificationTaskStatus")); + String testStatus = apiTests.present + ? apiTests.overallStatus() + : receiptTaskPassed ? "passed" : "not-executed"; boolean passed = "passed".equals(evidence.get("status")) && artifact.valid && manifest.valid @@ -4282,6 +4464,9 @@ && parseLong(evidence.get( "status", passed ? "passed" : "stale-or-failed", "evidencePresent", !evidence.isEmpty(), "testStatus", testStatus, + "verificationTask", evidence.get("verificationTask"), + "verificationTaskStatus", + evidence.get("verificationTaskStatus"), "testClass", evidence.get("testClass"), "artifact", artifact.report(), "publicApiManifest", map( diff --git a/src/test/java/blue/bex/value/BexValuesIdentityValidationTest.java b/src/test/java/blue/bex/value/BexValuesIdentityValidationTest.java new file mode 100644 index 0000000..a7b3a82 --- /dev/null +++ b/src/test/java/blue/bex/value/BexValuesIdentityValidationTest.java @@ -0,0 +1,80 @@ +package blue.bex.value; + +import blue.bex.BexException; +import blue.language.identity.DirectBlueIdCalculator; +import blue.language.model.Node; +import blue.language.snapshot.FrozenNode; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class BexValuesIdentityValidationTest { + + @Test + void rejectsMalformedRetainedExactBlueIdsAtThePublicBoundary() { + FrozenNode value = frozen("value"); + + for (String malformed : new String[] { + "", + "not-a-blue-id", + "1234", + "this#0", + ordinaryBlueId("master") + "#01" + }) { + assertThrows( + IllegalArgumentException.class, + () -> BexValues.exact(value, value, malformed), + malformed); + } + } + + @Test + void retainsValidatedOrdinaryBlueIdWithoutRehashingTheValue() { + FrozenNode value = frozen("value"); + String contentBlueId = ordinaryBlueId("value"); + String retainedBlueId = ordinaryBlueId("different-value"); + assertNotEquals(contentBlueId, retainedBlueId); + + BexValue exact = BexValues.exact( + value, value, retainedBlueId); + + assertSame(retainedBlueId, exact.exactBlueId()); + } + + @Test + void retainsValidatedCyclicMemberBlueIdAsAnOpaqueReferenceWithoutRehashing() { + FrozenNode value = frozen("value"); + String retainedBlueId = ordinaryBlueId("cycle-master") + "#7"; + + BexValue exact = BexValues.exact( + value, value, retainedBlueId); + + assertSame(retainedBlueId, exact.exactBlueId()); + assertTrue(exact.isExact()); + BexException unavailable = assertThrows( + BexException.class, exact::isObject); + assertTrue(unavailable.getMessage().contains(retainedBlueId)); + } + + @Test + void admittedExactAlsoRejectsMalformedRetainedIdentity() { + FrozenNode value = frozen("value"); + + assertThrows( + IllegalArgumentException.class, + () -> BexValues.admittedExact( + value, "malformed", BexValues.scalar("value"))); + } + + private static FrozenNode frozen(String value) { + return FrozenNode.fromResolvedNode(new Node().value(value)); + } + + private static String ordinaryBlueId(String value) { + return DirectBlueIdCalculator.calculateBlueId( + new Node().value(value)); + } +} diff --git a/src/test/resources/hosted-release/required-public-api.txt b/src/test/resources/hosted-release/required-public-api.txt index 58e098e..a456a76 100644 --- a/src/test/resources/hosted-release/required-public-api.txt +++ b/src/test/resources/hosted-release/required-public-api.txt @@ -205,6 +205,7 @@ class public final blue.bex.compile.BexCompiledProgramKey method public static from(blue.bex.compile.BexCompilationInput,java.lang.String):blue.bex.compile.BexCompiledProgramKey class public final blue.bex.compile.BexCompiledProgramRuntimeAccess method public static execute(blue.bex.compile.BexCompiledProgram,blue.bex.compile.BexExecutionMachine):blue.bex.value.BexValue + method public static matchesCompilationKey(blue.bex.compile.BexCompiledProgram,blue.bex.compile.BexCompiledProgramKey):boolean class public final blue.bex.compile.BexCompilerRuntimeAccess method public static compile(blue.bex.compile.BexCompilationInput,blue.bex.result.BexMetricsRecorder,blue.bex.compile.BexIntrinsicCatalog,java.lang.String):blue.bex.compile.BexCompiledProgram class public final blue.bex.compile.BexContainsCache From 6ccff35bbdc1166e718cb9b6fca2924557b71e69 Mon Sep 17 00:00:00 2001 From: piotr-blue Date: Sun, 2 Aug 2026 23:19:28 +0100 Subject: [PATCH 09/13] chore: mark BEX modernization finished --- work-status.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/work-status.txt b/work-status.txt index a2ae71b..f656398 100644 --- a/work-status.txt +++ b/work-status.txt @@ -1 +1 @@ -running +finished From c3e36c65b9928c5ae7ef0d839b56ff35a0b70d97 Mon Sep 17 00:00:00 2001 From: piotr-blue Date: Mon, 3 Aug 2026 13:03:29 +0100 Subject: [PATCH 10/13] chore: bind BEX to current modular Language runtime --- .../buildlogic/RootOrchestrationPlugin.java | 95 +++++++++++++++++-- docs/LATEST_LANGUAGE_API_MIGRATION.md | 6 +- docs/latest-language-api-migration.json | 14 +-- ...-checkpoint-public-api-classification.json | 4 +- .../latest-language-baseline.json | 12 +-- 5 files changed, 99 insertions(+), 32 deletions(-) diff --git a/build-logic/src/main/java/blue/bex/buildlogic/RootOrchestrationPlugin.java b/build-logic/src/main/java/blue/bex/buildlogic/RootOrchestrationPlugin.java index 948a283..abea912 100644 --- a/build-logic/src/main/java/blue/bex/buildlogic/RootOrchestrationPlugin.java +++ b/build-logic/src/main/java/blue/bex/buildlogic/RootOrchestrationPlugin.java @@ -4,10 +4,14 @@ import blue.bex.buildlogic.tasks.GenerateReleaseReportTask; import blue.bex.buildlogic.tasks.GenerateWorkingReportTask; import blue.bex.buildlogic.tasks.VerifyPublishedLanguageTask; +import groovy.json.JsonSlurper; import java.io.File; import java.io.IOException; import java.nio.file.Files; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; +import java.util.Map; import org.gradle.api.GradleException; import org.gradle.api.Plugin; import org.gradle.api.Project; @@ -143,11 +147,14 @@ public void apply(Project project) { project.getLayout().getBuildDirectory().file( "reports/bex-modernization/final.md")); }); + File latestLanguageBaseline = project.getLayout().getProjectDirectory().file( + "gradle/verification/latest-language-baseline.json").getAsFile(); + LanguageBaseline languageBaseline = readLanguageBaseline( + latestLanguageBaseline); TaskProvider baselineReceipt = project.getTasks().register( "writeLatestLanguageBaselineReport", Copy.class, task -> { task.setGroup("verification"); - task.from(project.getLayout().getProjectDirectory().file( - "gradle/verification/latest-language-baseline.json")); + task.from(latestLanguageBaseline); task.into(project.getLayout().getBuildDirectory().dir( "reports/latest-language-migration")); task.rename(ignored -> "baseline.json"); @@ -172,15 +179,11 @@ public void apply(Project project) { "blueLanguageCompositePath") .orElse("")); task.getExpectedLanguageCommit().set( - "9a607e584ff5dd973684d35d71eb4022d946b760"); + languageBaseline.exactHead); task.getVerifiedImplementationCommit().set( - "63a9ed6a1a66d47119a80d16ed2ab0beda0d2453"); - task.getAllowedLanguageDeltaPaths().set(Arrays.asList( - "LICENSE", - "docs/collection-paths-and-cohesion-" - + "migration-report.md", - "reports/modernization/" - + "phase-collection-paths-final.json")); + languageBaseline.verifiedImplementationCommit); + task.getAllowedLanguageDeltaPaths().set( + languageBaseline.documentationOnlyDiffPaths); task.getLocalCompositeCommand().set( "./gradlew --no-daemon clean " + "bexWorkingVerification " @@ -429,6 +432,78 @@ private static TaskProvider sourceArchive( }); } + private static LanguageBaseline readLanguageBaseline(File baselineFile) { + final Object parsed; + try { + parsed = new JsonSlurper().parseText( + Files.readString(baselineFile.toPath())); + } catch (IOException | RuntimeException exception) { + throw new GradleException( + "Cannot read latest Language baseline: " + baselineFile, + exception); + } + + Map root = requireObject(parsed, "Language baseline root"); + Map language = requireObject( + root.get("language"), "Language baseline language"); + return new LanguageBaseline( + requireString(language, "exactHead"), + requireString(language, "verifiedImplementationCommit"), + requireStringList(language, "documentationOnlyDiffPaths")); + } + + private static Map requireObject(Object value, String description) { + if (!(value instanceof Map)) { + throw new GradleException(description + " must be a JSON object"); + } + return (Map) value; + } + + private static String requireString(Map object, String field) { + Object value = object.get(field); + if (!(value instanceof String) + || ((String) value).trim().isEmpty()) { + throw new GradleException( + "Language baseline " + field + " must be a non-empty string"); + } + return (String) value; + } + + private static List requireStringList( + Map object, String field) { + Object value = object.get(field); + if (!(value instanceof List)) { + throw new GradleException( + "Language baseline " + field + " must be a JSON array"); + } + List result = new ArrayList<>(); + for (Object item : (List) value) { + if (!(item instanceof String) + || ((String) item).trim().isEmpty()) { + throw new GradleException( + "Language baseline " + field + + " must contain only non-empty strings"); + } + result.add((String) item); + } + return result; + } + + private static final class LanguageBaseline { + private final String exactHead; + private final String verifiedImplementationCommit; + private final List documentationOnlyDiffPaths; + + private LanguageBaseline( + String exactHead, + String verifiedImplementationCommit, + List documentationOnlyDiffPaths) { + this.exactHead = exactHead; + this.verifiedImplementationCommit = verifiedImplementationCommit; + this.documentationOnlyDiffPaths = documentationOnlyDiffPaths; + } + } + private static TaskProvider lifecycle( Project project, String name, String description) { return project.getTasks().register(name, task -> { diff --git a/docs/LATEST_LANGUAGE_API_MIGRATION.md b/docs/LATEST_LANGUAGE_API_MIGRATION.md index c18a3d2..9dac71e 100644 --- a/docs/LATEST_LANGUAGE_API_MIGRATION.md +++ b/docs/LATEST_LANGUAGE_API_MIGRATION.md @@ -12,9 +12,9 @@ It is the human-readable companion to | BEX baseline | `395c484111f8c4e9e0e98d2db7f1c5b0777bd5a8` | | Working compatibility checkpoint | `169e589` | | BEX migration | Modernization delta rooted at the working checkpoint; the commit containing this ledger is the final target revision | -| Language target | `9a607e584ff5dd973684d35d71eb4022d946b760` | -| Language verified implementation | `63a9ed6a1a66d47119a80d16ed2ab0beda0d2453` | -| Language target delta | `LICENSE`, one migration report, and one modernization report only | +| Language target | `a3b38ca9a1d0b9ca8527b26d23b05cfdbc6af7d9` | +| Language verified implementation | `a3b38ca9a1d0b9ca8527b26d23b05cfdbc6af7d9` | +| Language target delta | None; the target and verified implementation commits are identical | | Previous API manifest SHA-256 | `830caa187023079ba53fa76d2932e6e12cb8c93be3f90ac887ad374d6642b315` | | Working-checkpoint API manifest SHA-256 | `43aea6ae9de6f39729f93c146ff5453887da9a2303f6f4f33573ba47f37d5be0` | | Final modular API manifest SHA-256 | `5acb4712e3e03c5ba9a58d87b4a40bed4e1dcca76ed58ef355a77f0efc9d3c92` | diff --git a/docs/latest-language-api-migration.json b/docs/latest-language-api-migration.json index 577d1f1..93241c9 100644 --- a/docs/latest-language-api-migration.json +++ b/docs/latest-language-api-migration.json @@ -5,14 +5,10 @@ "bexBaselineCommit": "395c484111f8c4e9e0e98d2db7f1c5b0777bd5a8", "bexWorkingCheckpointCommit": "169e589", "bexMigrationState": "modernization delta rooted at bexWorkingCheckpointCommit; the containing commit is the final target revision", - "languageExactCommit": "9a607e584ff5dd973684d35d71eb4022d946b760", - "languageVerifiedImplementationCommit": "63a9ed6a1a66d47119a80d16ed2ab0beda0d2453", - "languageDeltaClassification": "documentation-and-license-only", - "languageDeltaPaths": [ - "LICENSE", - "docs/collection-paths-and-cohesion-migration-report.md", - "reports/modernization/phase-collection-paths-final.json" - ] + "languageExactCommit": "a3b38ca9a1d0b9ca8527b26d23b05cfdbc6af7d9", + "languageVerifiedImplementationCommit": "a3b38ca9a1d0b9ca8527b26d23b05cfdbc6af7d9", + "languageDeltaClassification": "none", + "languageDeltaPaths": [] }, "manifests": { "beforeSha256": "830caa187023079ba53fa76d2932e6e12cb8c93be3f90ac887ad374d6642b315", @@ -22,7 +18,7 @@ "requiredPath": "src/test/resources/hosted-release/required-public-api.txt", "generatedPath": "blue-bex-conformance/build/reports/bex-release/public-api.txt", "workingCheckpointClassificationPath": "gradle/verification/api/working-checkpoint-public-api-classification.json", - "workingCheckpointClassificationSha256": "4e0738794bcf042f399ac0a15f153aa9ac5d07fd7eae95bc410ce3bf780f343e", + "workingCheckpointClassificationSha256": "5ff3680eaf637656f600f590c5baad39a1c9db9fc1a5e28ace7ee3300cf75840", "afterClassificationPath": "docs/public-api-classification.json", "afterClassificationSha256": "69ffe62280a4feb1eafb93cb4d26ccd730713d2ddf7f132e426016db51e92753", "publicTypeCount": 101, diff --git a/gradle/verification/api/working-checkpoint-public-api-classification.json b/gradle/verification/api/working-checkpoint-public-api-classification.json index b793ab2..573f0e5 100644 --- a/gradle/verification/api/working-checkpoint-public-api-classification.json +++ b/gradle/verification/api/working-checkpoint-public-api-classification.json @@ -10,8 +10,8 @@ "sourceState": { "bexBaselineCommit": "395c484111f8c4e9e0e98d2db7f1c5b0777bd5a8", "bexMigrationState": "working-tree delta rooted at bexBaselineCommit", - "languageExactCommit": "9a607e584ff5dd973684d35d71eb4022d946b760", - "languageVerifiedImplementationCommit": "63a9ed6a1a66d47119a80d16ed2ab0beda0d2453" + "languageExactCommit": "a3b38ca9a1d0b9ca8527b26d23b05cfdbc6af7d9", + "languageVerifiedImplementationCommit": "a3b38ca9a1d0b9ca8527b26d23b05cfdbc6af7d9" }, "classifications": { "stable API": [ diff --git a/gradle/verification/latest-language-baseline.json b/gradle/verification/latest-language-baseline.json index ecd6c24..71a9659 100644 --- a/gradle/verification/latest-language-baseline.json +++ b/gradle/verification/latest-language-baseline.json @@ -78,17 +78,13 @@ } }, "language": { - "exactHead": "9a607e584ff5dd973684d35d71eb4022d946b760", - "verifiedImplementationCommit": "63a9ed6a1a66d47119a80d16ed2ab0beda0d2453", + "exactHead": "a3b38ca9a1d0b9ca8527b26d23b05cfdbc6af7d9", + "verifiedImplementationCommit": "a3b38ca9a1d0b9ca8527b26d23b05cfdbc6af7d9", "verifiedReleaseVersion": "3.1.0-rc.18", "localCompositeProjectVersion": "3.1.0-rc.18-SNAPSHOT", "bexDeclaredPublishedCandidateVersion": "3.1.0-rc.19", "publishedCandidateStatus": "not-executed", - "documentationOnlyDiffPaths": [ - "LICENSE", - "docs/collection-paths-and-cohesion-migration-report.md", - "reports/modernization/phase-collection-paths-final.json" - ], + "documentationOnlyDiffPaths": [], "czTomlSha256": "f6717f9a9e38df0dea4b5eeec5a0264c0490856ef4c261c053a06883ba3e69f2", "focusedModules": [ { @@ -113,7 +109,7 @@ "declaredPublishedCoordinate": "blue.language:blue-contracts-core:3.1.0-rc.19", "projectPath": ":blue-contracts-core", "localArtifact": "blue-contracts-core-3.1.0-rc.18-SNAPSHOT.jar", - "verifiedLocalArtifactSha256": "ec45224ffee3e0c47246869d89c002657c9d1f348af8c553be3b6c0874bf7bae" + "verifiedLocalArtifactSha256": "9fdc03c12b7da8262bddec59a7230b548a33683c211b602311a27266bc2ffcd0" } ], "hostingPackageIdentities": { From 09f89f0b63a84007fcf7ae13b7439bc24dbb1d03 Mon Sep 17 00:00:00 2001 From: piotr-blue Date: Tue, 4 Aug 2026 05:35:46 +0100 Subject: [PATCH 11/13] chore: bind BEX to latest Language release --- docs/LATEST_LANGUAGE_API_MIGRATION.md | 4 ++-- docs/latest-language-api-migration.json | 6 +++--- .../api/working-checkpoint-public-api-classification.json | 4 ++-- gradle/verification/latest-language-baseline.json | 8 ++++---- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/LATEST_LANGUAGE_API_MIGRATION.md b/docs/LATEST_LANGUAGE_API_MIGRATION.md index 9dac71e..6257649 100644 --- a/docs/LATEST_LANGUAGE_API_MIGRATION.md +++ b/docs/LATEST_LANGUAGE_API_MIGRATION.md @@ -12,8 +12,8 @@ It is the human-readable companion to | BEX baseline | `395c484111f8c4e9e0e98d2db7f1c5b0777bd5a8` | | Working compatibility checkpoint | `169e589` | | BEX migration | Modernization delta rooted at the working checkpoint; the commit containing this ledger is the final target revision | -| Language target | `a3b38ca9a1d0b9ca8527b26d23b05cfdbc6af7d9` | -| Language verified implementation | `a3b38ca9a1d0b9ca8527b26d23b05cfdbc6af7d9` | +| Language target | `c3d58561220e6de6be6e302cb16799c1a1b5159f` | +| Language verified implementation | `c3d58561220e6de6be6e302cb16799c1a1b5159f` | | Language target delta | None; the target and verified implementation commits are identical | | Previous API manifest SHA-256 | `830caa187023079ba53fa76d2932e6e12cb8c93be3f90ac887ad374d6642b315` | | Working-checkpoint API manifest SHA-256 | `43aea6ae9de6f39729f93c146ff5453887da9a2303f6f4f33573ba47f37d5be0` | diff --git a/docs/latest-language-api-migration.json b/docs/latest-language-api-migration.json index 93241c9..7c56f7a 100644 --- a/docs/latest-language-api-migration.json +++ b/docs/latest-language-api-migration.json @@ -5,8 +5,8 @@ "bexBaselineCommit": "395c484111f8c4e9e0e98d2db7f1c5b0777bd5a8", "bexWorkingCheckpointCommit": "169e589", "bexMigrationState": "modernization delta rooted at bexWorkingCheckpointCommit; the containing commit is the final target revision", - "languageExactCommit": "a3b38ca9a1d0b9ca8527b26d23b05cfdbc6af7d9", - "languageVerifiedImplementationCommit": "a3b38ca9a1d0b9ca8527b26d23b05cfdbc6af7d9", + "languageExactCommit": "c3d58561220e6de6be6e302cb16799c1a1b5159f", + "languageVerifiedImplementationCommit": "c3d58561220e6de6be6e302cb16799c1a1b5159f", "languageDeltaClassification": "none", "languageDeltaPaths": [] }, @@ -18,7 +18,7 @@ "requiredPath": "src/test/resources/hosted-release/required-public-api.txt", "generatedPath": "blue-bex-conformance/build/reports/bex-release/public-api.txt", "workingCheckpointClassificationPath": "gradle/verification/api/working-checkpoint-public-api-classification.json", - "workingCheckpointClassificationSha256": "5ff3680eaf637656f600f590c5baad39a1c9db9fc1a5e28ace7ee3300cf75840", + "workingCheckpointClassificationSha256": "0f5c4043f922d90de42326a5ced356cb9c844942797afe9d6e82febec396d5d6", "afterClassificationPath": "docs/public-api-classification.json", "afterClassificationSha256": "69ffe62280a4feb1eafb93cb4d26ccd730713d2ddf7f132e426016db51e92753", "publicTypeCount": 101, diff --git a/gradle/verification/api/working-checkpoint-public-api-classification.json b/gradle/verification/api/working-checkpoint-public-api-classification.json index 573f0e5..8b37fb3 100644 --- a/gradle/verification/api/working-checkpoint-public-api-classification.json +++ b/gradle/verification/api/working-checkpoint-public-api-classification.json @@ -10,8 +10,8 @@ "sourceState": { "bexBaselineCommit": "395c484111f8c4e9e0e98d2db7f1c5b0777bd5a8", "bexMigrationState": "working-tree delta rooted at bexBaselineCommit", - "languageExactCommit": "a3b38ca9a1d0b9ca8527b26d23b05cfdbc6af7d9", - "languageVerifiedImplementationCommit": "a3b38ca9a1d0b9ca8527b26d23b05cfdbc6af7d9" + "languageExactCommit": "c3d58561220e6de6be6e302cb16799c1a1b5159f", + "languageVerifiedImplementationCommit": "c3d58561220e6de6be6e302cb16799c1a1b5159f" }, "classifications": { "stable API": [ diff --git a/gradle/verification/latest-language-baseline.json b/gradle/verification/latest-language-baseline.json index 71a9659..a7f7fc6 100644 --- a/gradle/verification/latest-language-baseline.json +++ b/gradle/verification/latest-language-baseline.json @@ -78,8 +78,8 @@ } }, "language": { - "exactHead": "a3b38ca9a1d0b9ca8527b26d23b05cfdbc6af7d9", - "verifiedImplementationCommit": "a3b38ca9a1d0b9ca8527b26d23b05cfdbc6af7d9", + "exactHead": "c3d58561220e6de6be6e302cb16799c1a1b5159f", + "verifiedImplementationCommit": "c3d58561220e6de6be6e302cb16799c1a1b5159f", "verifiedReleaseVersion": "3.1.0-rc.18", "localCompositeProjectVersion": "3.1.0-rc.18-SNAPSHOT", "bexDeclaredPublishedCandidateVersion": "3.1.0-rc.19", @@ -97,7 +97,7 @@ "declaredPublishedCoordinate": "blue.language:blue-language-core:3.1.0-rc.19", "projectPath": ":blue-language-core", "localArtifact": "blue-language-core-3.1.0-rc.18-SNAPSHOT.jar", - "verifiedLocalArtifactSha256": "a7d3c72640ab8ac5832feaad576cd1a56457cb87eaf07323fe04a88ae5730740" + "verifiedLocalArtifactSha256": "916d5e6315f34d25ad4a2ddbc5587a209506871ea70dd2daa7aa69dbdbe1263d" }, { "declaredPublishedCoordinate": "blue.language:blue-language-mapping:3.1.0-rc.19", @@ -109,7 +109,7 @@ "declaredPublishedCoordinate": "blue.language:blue-contracts-core:3.1.0-rc.19", "projectPath": ":blue-contracts-core", "localArtifact": "blue-contracts-core-3.1.0-rc.18-SNAPSHOT.jar", - "verifiedLocalArtifactSha256": "9fdc03c12b7da8262bddec59a7230b548a33683c211b602311a27266bc2ffcd0" + "verifiedLocalArtifactSha256": "5845c6bead274dffd8d22afcb323f7cdf6e53b5656e0070bd241a1a660516280" } ], "hostingPackageIdentities": { From 96adb8f94d6c10855cd1aad5d9cb27814c0bcf50 Mon Sep 17 00:00:00 2001 From: piotr-blue Date: Wed, 5 Aug 2026 02:22:18 +0100 Subject: [PATCH 12/13] chore: use published Blue Language 3.1.0-rc.20 --- .github/workflows/build.yml | 31 +++++----- .../LanguageDependencyModeExtension.java | 2 +- docs/LATEST_LANGUAGE_API_MIGRATION.md | 4 +- docs/latest-language-api-migration.json | 6 +- ...-checkpoint-public-api-classification.json | 4 +- .../latest-language-baseline.json | 30 +++++----- .../published-api-inspection.properties | 57 ++++++++++--------- work-status.txt | 2 +- 8 files changed, 71 insertions(+), 65 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a9213fe..44ce10b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,13 +26,6 @@ jobs: path: blue-bex-java fetch-depth: 0 - - name: Check out exact verified Blue Language - uses: actions/checkout@v4 - with: - repository: bluecontract/blue-language-java - ref: 9a607e584ff5dd973684d35d71eb4022d946b760 - path: blue-language-java - - name: Set up JDK 25 uses: actions/setup-java@v4 with: @@ -42,15 +35,25 @@ jobs: - name: Set up Gradle uses: gradle/actions/setup-gradle@v4 - - name: Run clean local-composite working gate + - name: Run clean published-Language verification + env: + GRADLE_USER_HOME: ${{ runner.temp }}/blue-bex-gradle-home run: >- - ./gradlew --no-daemon clean bexWorkingVerification - -PblueLanguageCompositePath=../blue-language-java + ./gradlew --no-daemon clean + bexCheck + bexConformance + bexCompatibilityCheck + bexReproducibilityCheck + :blue-bex-core:writeLanguageDependencyEvidence + :blue-bex-contracts:writeLanguageDependencyEvidence + :blue-bex-conformance:writeLanguageDependencyEvidence + :blue-bex-java:writeLanguageDependencyEvidence + :examples:writeLanguageDependencyEvidence - - name: Run modernization and serious benchmark gate - run: >- - ./gradlew --no-daemon bexModernizationVerification - -PblueLanguageCompositePath=../blue-language-java + - name: Run serious benchmark gate + env: + GRADLE_USER_HOME: ${{ runner.temp }}/blue-bex-gradle-home + run: ./gradlew --no-daemon :blue-bex-conformance:jmh - name: Archive reports and test results uses: actions/upload-artifact@v4 diff --git a/build-logic/src/main/java/blue/bex/buildlogic/LanguageDependencyModeExtension.java b/build-logic/src/main/java/blue/bex/buildlogic/LanguageDependencyModeExtension.java index a8e4e7d..2e1696c 100644 --- a/build-logic/src/main/java/blue/bex/buildlogic/LanguageDependencyModeExtension.java +++ b/build-logic/src/main/java/blue/bex/buildlogic/LanguageDependencyModeExtension.java @@ -6,7 +6,7 @@ /** Typed coordinates and dependency mode shared by BEX module builds. */ public abstract class LanguageDependencyModeExtension { public LanguageDependencyModeExtension(ObjectFactory objects) { - getVersion().convention("3.1.0-rc.19"); + getVersion().convention("3.1.0-rc.20"); getCompositePropertyName().convention("blueLanguageCompositePath"); } diff --git a/docs/LATEST_LANGUAGE_API_MIGRATION.md b/docs/LATEST_LANGUAGE_API_MIGRATION.md index 6257649..d1765e3 100644 --- a/docs/LATEST_LANGUAGE_API_MIGRATION.md +++ b/docs/LATEST_LANGUAGE_API_MIGRATION.md @@ -12,8 +12,8 @@ It is the human-readable companion to | BEX baseline | `395c484111f8c4e9e0e98d2db7f1c5b0777bd5a8` | | Working compatibility checkpoint | `169e589` | | BEX migration | Modernization delta rooted at the working checkpoint; the commit containing this ledger is the final target revision | -| Language target | `c3d58561220e6de6be6e302cb16799c1a1b5159f` | -| Language verified implementation | `c3d58561220e6de6be6e302cb16799c1a1b5159f` | +| Language target | `505a654699b86b42bf0e282ddf94560a91529bcf` (`v3.1.0-rc.20`) | +| Language verified implementation | `505a654699b86b42bf0e282ddf94560a91529bcf` | | Language target delta | None; the target and verified implementation commits are identical | | Previous API manifest SHA-256 | `830caa187023079ba53fa76d2932e6e12cb8c93be3f90ac887ad374d6642b315` | | Working-checkpoint API manifest SHA-256 | `43aea6ae9de6f39729f93c146ff5453887da9a2303f6f4f33573ba47f37d5be0` | diff --git a/docs/latest-language-api-migration.json b/docs/latest-language-api-migration.json index 7c56f7a..9802d03 100644 --- a/docs/latest-language-api-migration.json +++ b/docs/latest-language-api-migration.json @@ -5,8 +5,8 @@ "bexBaselineCommit": "395c484111f8c4e9e0e98d2db7f1c5b0777bd5a8", "bexWorkingCheckpointCommit": "169e589", "bexMigrationState": "modernization delta rooted at bexWorkingCheckpointCommit; the containing commit is the final target revision", - "languageExactCommit": "c3d58561220e6de6be6e302cb16799c1a1b5159f", - "languageVerifiedImplementationCommit": "c3d58561220e6de6be6e302cb16799c1a1b5159f", + "languageExactCommit": "505a654699b86b42bf0e282ddf94560a91529bcf", + "languageVerifiedImplementationCommit": "505a654699b86b42bf0e282ddf94560a91529bcf", "languageDeltaClassification": "none", "languageDeltaPaths": [] }, @@ -18,7 +18,7 @@ "requiredPath": "src/test/resources/hosted-release/required-public-api.txt", "generatedPath": "blue-bex-conformance/build/reports/bex-release/public-api.txt", "workingCheckpointClassificationPath": "gradle/verification/api/working-checkpoint-public-api-classification.json", - "workingCheckpointClassificationSha256": "0f5c4043f922d90de42326a5ced356cb9c844942797afe9d6e82febec396d5d6", + "workingCheckpointClassificationSha256": "0c3486c3bc2e7f4426e8e3973e74a2319457ccb911bb6da5ab03e6ea23f4a742", "afterClassificationPath": "docs/public-api-classification.json", "afterClassificationSha256": "69ffe62280a4feb1eafb93cb4d26ccd730713d2ddf7f132e426016db51e92753", "publicTypeCount": 101, diff --git a/gradle/verification/api/working-checkpoint-public-api-classification.json b/gradle/verification/api/working-checkpoint-public-api-classification.json index 8b37fb3..8bbe5bf 100644 --- a/gradle/verification/api/working-checkpoint-public-api-classification.json +++ b/gradle/verification/api/working-checkpoint-public-api-classification.json @@ -10,8 +10,8 @@ "sourceState": { "bexBaselineCommit": "395c484111f8c4e9e0e98d2db7f1c5b0777bd5a8", "bexMigrationState": "working-tree delta rooted at bexBaselineCommit", - "languageExactCommit": "c3d58561220e6de6be6e302cb16799c1a1b5159f", - "languageVerifiedImplementationCommit": "c3d58561220e6de6be6e302cb16799c1a1b5159f" + "languageExactCommit": "505a654699b86b42bf0e282ddf94560a91529bcf", + "languageVerifiedImplementationCommit": "505a654699b86b42bf0e282ddf94560a91529bcf" }, "classifications": { "stable API": [ diff --git a/gradle/verification/latest-language-baseline.json b/gradle/verification/latest-language-baseline.json index a7f7fc6..d8c616a 100644 --- a/gradle/verification/latest-language-baseline.json +++ b/gradle/verification/latest-language-baseline.json @@ -78,37 +78,37 @@ } }, "language": { - "exactHead": "c3d58561220e6de6be6e302cb16799c1a1b5159f", - "verifiedImplementationCommit": "c3d58561220e6de6be6e302cb16799c1a1b5159f", - "verifiedReleaseVersion": "3.1.0-rc.18", - "localCompositeProjectVersion": "3.1.0-rc.18-SNAPSHOT", - "bexDeclaredPublishedCandidateVersion": "3.1.0-rc.19", - "publishedCandidateStatus": "not-executed", + "exactHead": "505a654699b86b42bf0e282ddf94560a91529bcf", + "verifiedImplementationCommit": "505a654699b86b42bf0e282ddf94560a91529bcf", + "verifiedReleaseVersion": "3.1.0-rc.20", + "localCompositeProjectVersion": "3.1.0-rc.20", + "bexDeclaredPublishedCandidateVersion": "3.1.0-rc.20", + "publishedCandidateStatus": "passed", "documentationOnlyDiffPaths": [], - "czTomlSha256": "f6717f9a9e38df0dea4b5eeec5a0264c0490856ef4c261c053a06883ba3e69f2", + "czTomlSha256": "32b6457085126ec8b2701840c3a8da6a4c6714509c867ecfb4cace3632cf13f6", "focusedModules": [ { - "declaredPublishedCoordinate": "blue.language:blue-language-model:3.1.0-rc.19", + "declaredPublishedCoordinate": "blue.language:blue-language-model:3.1.0-rc.20", "projectPath": ":blue-language-model", - "localArtifact": "blue-language-model-3.1.0-rc.18-SNAPSHOT.jar", + "localArtifact": "blue-language-model-3.1.0-rc.20.jar", "verifiedLocalArtifactSha256": "ef55be8331147442b858474add4782489d993568effe30202a9c4a8b014d5bd8" }, { - "declaredPublishedCoordinate": "blue.language:blue-language-core:3.1.0-rc.19", + "declaredPublishedCoordinate": "blue.language:blue-language-core:3.1.0-rc.20", "projectPath": ":blue-language-core", - "localArtifact": "blue-language-core-3.1.0-rc.18-SNAPSHOT.jar", + "localArtifact": "blue-language-core-3.1.0-rc.20.jar", "verifiedLocalArtifactSha256": "916d5e6315f34d25ad4a2ddbc5587a209506871ea70dd2daa7aa69dbdbe1263d" }, { - "declaredPublishedCoordinate": "blue.language:blue-language-mapping:3.1.0-rc.19", + "declaredPublishedCoordinate": "blue.language:blue-language-mapping:3.1.0-rc.20", "projectPath": ":blue-language-mapping", - "localArtifact": "blue-language-mapping-3.1.0-rc.18-SNAPSHOT.jar", + "localArtifact": "blue-language-mapping-3.1.0-rc.20.jar", "verifiedLocalArtifactSha256": "d9141d5c611bde7eb6a21bce3dc4bc0df7d8167f013eeaef2a365dd0a6af329b" }, { - "declaredPublishedCoordinate": "blue.language:blue-contracts-core:3.1.0-rc.19", + "declaredPublishedCoordinate": "blue.language:blue-contracts-core:3.1.0-rc.20", "projectPath": ":blue-contracts-core", - "localArtifact": "blue-contracts-core-3.1.0-rc.18-SNAPSHOT.jar", + "localArtifact": "blue-contracts-core-3.1.0-rc.20.jar", "verifiedLocalArtifactSha256": "5845c6bead274dffd8d22afcb323f7cdf6e53b5656e0070bd241a1a660516280" } ], diff --git a/src/test/resources/hosted-release/published-api-inspection.properties b/src/test/resources/hosted-release/published-api-inspection.properties index de0e17e..1d1686b 100644 --- a/src/test/resources/hosted-release/published-api-inspection.properties +++ b/src/test/resources/hosted-release/published-api-inspection.properties @@ -1,29 +1,32 @@ schema=blue-bex-published-host-api-inspection/1.0 repository=https://repo1.maven.org/maven2 -metadata.lastUpdated=20260723002725 -metadata.latest=3.1.0-rc.19 -coordinate=blue.language:blue-language-java:3.1.0-rc.19 -artifact.sha256=e33e04065c6f9aa5189040a5816786953e1b5c58862f37a542e84ea2a3379235 -source.tag=v3.1.0-rc.19 -source.commit=8fd8af2ad90147336114774fb47c030a252682b3 -inspection=jar-tf-and-javap -standaloneCompile=failed-with-missing-symbols -standaloneCompile.minimumErrorCount=100 -class.blue.language.BlueOperationLimits=false -class.blue.language.BlueOperationOutcome=false -class.blue.language.BlueOperationResult=false -class.blue.language.processor.ExecutionEvidenceUnavailableException=false -class.blue.language.processor.RuntimeWorkSession=false -class.blue.language.processor.SemanticOutputBoundary=false -class.blue.language.processor.ExactBlueValue=false -class.blue.language.processor.GasChargeContext=false -class.blue.language.processor.GasLimitExceededException=false -class.blue.language.processor.InvalidExecutionEvidenceException=false -class.blue.language.processor.PortableLimitExceededException=false -visibility.blue.language.processor.GasMeter.public=false -method.blue.language.processor.ProcessorExecutionContext.runtimeWorkSession=false -method.blue.language.processor.ProcessorExecutionContext.semanticOutputBoundary=false -class.blue.language.processor.GasMeter.ChildGasLedger=false -method.blue.language.snapshot.ResolvedSnapshot.isResolutionComplete=false -method.blue.language.model.Schema.blueId=false -status=incompatible-with-current-hosted-adapter +metadata.lastUpdated=20260805010321 +metadata.latest=3.1.0-rc.20 +coordinate=blue.language:blue-language-java:3.1.0-rc.20 +artifact.sha256=0de1584be094515ddd27938819464dc024a993c7eb06e4145cac129ad5bbfed0 +source.tag=v3.1.0-rc.20 +source.commit=505a654699b86b42bf0e282ddf94560a91529bcf +inspection=central-sha256-sidecar-jar-tf-and-javap +standaloneCompile=passed +class.blue.language.api.BlueOperationLimits=true +class.blue.language.api.BlueOperationOutcome=true +class.blue.language.api.BlueOperationResult=true +class.blue.language.processor.ExecutionEvidenceUnavailableException=true +class.blue.language.processor.RuntimeWorkSession=true +class.blue.language.processor.SemanticOutputBoundary=true +class.blue.language.processor.ExactBlueValue=true +class.blue.language.processor.GasChargeContext=true +class.blue.language.processor.GasLimitExceededException=true +class.blue.language.processor.InvalidExecutionEvidenceException=true +class.blue.language.processor.PortableLimitExceededException=true +class.blue.language.processor.GasMeter=true +visibility.blue.language.processor.GasMeter.public=true +class.blue.language.processor.ProcessorExecutionContext=true +method.blue.language.processor.ProcessorExecutionContext.newRuntimeGasLedger=true +method.blue.language.processor.ProcessorExecutionContext.semanticOutputBoundary=true +class.blue.language.processor.GasMeter$ChildGasLedger=true +class.blue.language.merge.ResolvedSnapshot=true +method.blue.language.merge.ResolvedSnapshot.isResolutionComplete=true +class.blue.language.model.Schema=true +method.blue.language.model.Schema.blueId=true +status=compatible-with-final-hosted-adapter diff --git a/work-status.txt b/work-status.txt index f656398..a2ae71b 100644 --- a/work-status.txt +++ b/work-status.txt @@ -1 +1 @@ -finished +running From 3ebd2d93be7f24ce44840f0aba02b1c40c27f5f8 Mon Sep 17 00:00:00 2001 From: piotr-blue Date: Wed, 5 Aug 2026 02:28:16 +0100 Subject: [PATCH 13/13] chore: remove obsolete work-status.txt file --- work-status.txt | 1 - 1 file changed, 1 deletion(-) delete mode 100644 work-status.txt diff --git a/work-status.txt b/work-status.txt deleted file mode 100644 index a2ae71b..0000000 --- a/work-status.txt +++ /dev/null @@ -1 +0,0 @@ -running