diff --git a/.github/scripts/compare-independent-builds.mjs b/.github/scripts/compare-independent-builds.mjs
new file mode 100644
index 0000000..59e0b37
--- /dev/null
+++ b/.github/scripts/compare-independent-builds.mjs
@@ -0,0 +1,262 @@
+#!/usr/bin/env node
+
+import { execFileSync } from 'node:child_process';
+import { createHash } from 'node:crypto';
+import {
+ readFileSync,
+ realpathSync,
+ statSync,
+ writeFileSync
+} from 'node:fs';
+import { isAbsolute, relative, resolve, sep } from 'node:path';
+
+const [standaloneOneManifest, standaloneTwoManifest,
+ localOneManifest, localTwoManifest,
+ standaloneOneRoot, standaloneTwoRoot, localOneRoot, localTwoRoot,
+ standaloneOneGradle, standaloneTwoGradle, localOneGradle, localTwoGradle,
+ bexCommit, outputPath] = process.argv.slice(2);
+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 ' +
+ 'S1_ROOT S2_ROOT L1_ROOT L2_ROOT S1_GRADLE S2_GRADLE ' +
+ 'L1_GRADLE L2_GRADLE COMMIT OUTPUT'
+ );
+}
+
+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 real;
+}
+
+function contained(root, child) {
+ const value = relative(root, child);
+ return value !== '' && value !== '..' &&
+ !value.startsWith(`..${sep}`) && !isAbsolute(value);
+}
+
+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',
+ exactManifestBytesMatch,
+ exactArtifactBytesMatch,
+ artifactPathSetMatch,
+ requiredArtifactRolesPresent: requiredArtifactsPresent,
+ artifactCount: firstArtifacts.length,
+ firstBuild: first.report,
+ secondBuild: second.report
+ };
+}
+
+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/2.1',
+ status: passed ? 'passed' : 'failed',
+ bexCommit,
+ checkoutCount: checkoutRoots.length,
+ gitDirectoryCount: gitDirectories.length,
+ gradleHomeCount: gradleHomes.length,
+ inputManifestCount: manifestPaths.length,
+ distinctCheckoutRoots,
+ distinctGitDirectories,
+ distinctGradleHomes,
+ distinctInputManifestFiles,
+ 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
new file mode 100644
index 0000000..56a6e13
--- /dev/null
+++ b/.github/scripts/run-final-publication-gates.sh
@@ -0,0 +1,263 @@
+#!/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 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"
+ awk -F= -v requested="$key" '$1 == requested {
+ sub(/^[^=]*=/, "")
+ print
+ exit
+ }' "$INSPECTION_FILE"
+}
+
+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
+}
+
+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_COMMIT" =~ ^[0-9a-fA-F]{40}$ ]]; then
+ echo "Published Language source commit is missing." >&2
+ exit 1
+fi
+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
+
+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 --quiet --detach "refs/tags/$LANGUAGE_TAG"
+if [[ "$(git -C "$LANGUAGE_CHECKOUT" rev-parse HEAD)" != "$LANGUAGE_COMMIT" ]]; then
+ 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 "Language release checkout is dirty." >&2
+ exit 1
+fi
+
+export CI=true
+export SOURCE_DATE_EPOCH="$SOURCE_COMMIT_EPOCH"
+mkdir -p "$RECEIPT_ROOT"
+
+cd "$BEX_REPOSITORY"
+./gradlew --no-daemon clean bexWorkingVerification \
+ "-PblueLanguageCompositePath=$LANGUAGE_CHECKOUT"
+./gradlew --no-daemon bexModernizationVerification \
+ "-PblueLanguageCompositePath=$LANGUAGE_CHECKOUT"
+
+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" \
+ "${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" \
+ "${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
+
+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..44ce10b 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -7,44 +7,70 @@ 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: Set up Java 8 test runtime
- uses: actions/setup-java@v3
+ - name: Check out BEX
+ uses: actions/checkout@v4
with:
- java-version: '8'
- distribution: 'corretto'
+ path: blue-bex-java
+ fetch-depth: 0
- 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 published-Language verification
+ env:
+ GRADLE_USER_HOME: ${{ runner.temp }}/blue-bex-gradle-home
+ run: >-
+ ./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: Execute Gradle build
- run: ./gradlew clean build
+ - 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 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 b435c37..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:
@@ -67,11 +75,15 @@ 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
+ run: >-
+ ./gradlew publish
+ -PblueLanguageCompositePath="$BLUE_LANGUAGE_COMPOSITE_PATH"
- name: Execute Gradle release
env:
@@ -81,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
@@ -92,6 +106,14 @@ jobs:
with:
name: rc-artifacts
path: |
- build/libs
- build/publications
+ build/distributions
+ 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/reports
+ **/build/test-results
build/jreleaser
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 2998b83..a049b70 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -37,11 +37,15 @@ 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
+ run: >-
+ ./gradlew publish
+ -PblueLanguageCompositePath="$BLUE_LANGUAGE_COMPOSITE_PATH"
- name: Execute Gradle release
env:
@@ -51,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
@@ -59,6 +65,14 @@ jobs:
with:
name: artifacts
path: |
- build/libs
- build/publications
+ build/distributions
+ 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/reports
+ **/build/test-results
build/jreleaser
diff --git a/README.md b/README.md
index e5c710f..5272b62 100644
--- a/README.md
+++ b/README.md
@@ -1,840 +1,127 @@
# 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:
+This expression returns `42`:
```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"
-);
+$add:
+ - 40
+ - 2
```
-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:
+The standalone API has three inputs: a selected `BexProgramSource`, an immutable
+`BexExecutionContext`, and a reusable `BexEngine`.
```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));
- })
- .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:
-
-```yaml
-$document: /status
-```
-
-Resolved view reads are explicit:
-
-```yaml
-$document:
- path: /status
- view: resolved
-```
-
-## Runtime Bindings
-
-Hosts can provide arbitrary named bindings:
-
-```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 is responsible for its own gas accounting by calling
- `invocation.chargeGas(...)`.
-
-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;
-- `gasUsed`;
-- `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.
-
-`$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
-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/`.
-
-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
-
-```bash
-./gradlew test --tests '*BexRichFixtureTest'
-./gradlew test --tests '*BexTranslatedCorpusTest'
-./gradlew test
-./gradlew build
-```
+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());
+
+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
+[`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..4295f14
--- /dev/null
+++ b/blue-bex-conformance/build.gradle.kts
@@ -0,0 +1,599 @@
+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")
+ id("blue.bex.language-dependencies")
+ id("blue.bex.api-evidence")
+ id("blue.bex.jmh")
+}
+
+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()
+
+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 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()) {
+ "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("\"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"
+ }
+ }
+}
+
+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 {
+ 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"),
+ writeDeterministicArchiveEvidence,
+ writeBinaryApiEvidence,
+ writeBenchmarkCompilationEvidence,
+ writeJava8BytecodeEvidence
+ )
+ 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..db58c2b
--- /dev/null
+++ b/blue-bex-conformance/src/jmh/java/blue/bex/benchmark/BexCoreBenchmark.java
@@ -0,0 +1,518 @@
+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.TearDown;
+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) {
+ try (BexEngine engine = BexEngine.builder()
+ .cache(new LruBexCompiledProgramCache())
+ .build()) {
+ BexExecutionResult result = engine.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"))))));
+ try (BexEngine coldEngine = BexEngine.builder().build()) {
+ coldExpected = expected(coldEngine.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()));
+ }
+
+ @TearDown(Level.Trial)
+ public void tearDown() {
+ cacheEngine.close();
+ engine.close();
+ }
+ }
+
+ @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()));
+ }
+
+ @TearDown(Level.Trial)
+ public void tearDown() {
+ engine.close();
+ }
+ }
+
+ @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()));
+ }
+
+ @TearDown(Level.Trial)
+ public void tearDown() {
+ engine.close();
+ }
+ }
+
+ @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()));
+ }
+
+ @TearDown(Level.Trial)
+ public void tearDown() {
+ engine.close();
+ }
+
+ 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()));
+ }
+
+ @TearDown(Level.Trial)
+ public void tearDown() {
+ engine.close();
+ }
+ }
+
+ @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()));
+ }
+
+ @TearDown(Level.Trial)
+ public void tearDown() {
+ engine.close();
+ }
+ }
+}
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..e18f7e0
--- /dev/null
+++ b/blue-bex-conformance/src/jmh/java/blue/language/processor/BexHostedGasBenchmark.java
@@ -0,0 +1,146 @@
+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.TearDown;
+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());
+ }
+
+ @TearDown(Level.Trial)
+ public void tearDown() {
+ engine.close();
+ }
+
+ 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/blue-bex-contracts/src/main/java/blue/bex/contracts/ProcessorExecutionContextBexDocumentView.java b/blue-bex-contracts/src/main/java/blue/bex/contracts/ProcessorExecutionContextBexDocumentView.java
new file mode 100644
index 0000000..0a320b5
--- /dev/null
+++ b/blue-bex-contracts/src/main/java/blue/bex/contracts/ProcessorExecutionContextBexDocumentView.java
@@ -0,0 +1,50 @@
+package blue.bex.contracts;
+
+import blue.bex.api.BexDocumentView;
+import blue.bex.value.BexValue;
+import blue.bex.value.BexValues;
+import blue.language.model.wire.JsonPointer;
+import blue.language.processor.ProcessorExecutionContext;
+import blue.language.snapshot.FrozenNode;
+
+import java.util.Objects;
+
+/** 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) {
+ this.context = Objects.requireNonNull(context, "context");
+ }
+
+ @Override
+ public String resolvePointer(String authoredPointer) {
+ return context.resolvePointer(authoredPointer);
+ }
+
+ @Override
+ public BexValue canonicalAt(String absolutePointer) {
+ return exactAt(absolutePointer);
+ }
+
+ @Override
+ public BexValue resolvedAt(String absolutePointer) {
+ return exactAt(absolutePointer);
+ }
+
+ @Override
+ public String currentScopePath() {
+ String pointer = context.resolvePointer("");
+ return pointer != null ? JsonPointer.canonicalize(pointer) : "/";
+ }
+
+ 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/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/blue-bex-contracts/src/main/java/blue/bex/contracts/ProcessorExecutionContextBexSemanticIdentityBoundary.java b/blue-bex-contracts/src/main/java/blue/bex/contracts/ProcessorExecutionContextBexSemanticIdentityBoundary.java
new file mode 100644
index 0000000..cf760b4
--- /dev/null
+++ b/blue-bex-contracts/src/main/java/blue/bex/contracts/ProcessorExecutionContextBexSemanticIdentityBoundary.java
@@ -0,0 +1,29 @@
+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 semantic-output boundary owned by one 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/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/blue-bex-core/src/main/java/blue/bex/api/BexEngine.java b/blue-bex-core/src/main/java/blue/bex/api/BexEngine.java
new file mode 100644
index 0000000..e86b527
--- /dev/null
+++ b/blue-bex-core/src/main/java/blue/bex/api/BexEngine.java
@@ -0,0 +1,251 @@
+package blue.bex.api;
+
+import blue.bex.BexException;
+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;
+import blue.bex.pointer.BexPointerCache;
+import blue.bex.result.BexExecutionResult;
+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;
+
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Public entry point for compiling and executing selected BEX programs.
+ *
+ * The engine is compiled-only. It compiles a {@link BexProgramSource} after a
+ * host has selected a BEX program, caches the compiled form, and executes it
+ * against a {@link BexExecutionContext}. It does not apply document patches,
+ * emit events, or perform host actions.
+ */
+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;
+ private final BexIntrinsicRegistry intrinsics;
+ private final BexPointerCache pointerCache = new BexPointerCache();
+
+ private BexEngine(Builder builder) {
+ 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;
+ this.intrinsics = builder.intrinsics;
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public BexCompiledProgram compile(BexProgramSource source) {
+ BexMetricsRecorder metrics = new BexMetricsRecorder();
+ BexCompiledProgram program = compile(source, metrics);
+ publishMetrics(metrics.snapshot());
+ return program;
+ }
+
+ 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();
+ 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;
+ } finally {
+ metrics.addCompileNanos(System.nanoTime() - start);
+ }
+ }
+
+ public BexExecutionResult execute(BexCompiledProgram program, BexExecutionContext context) {
+ BexMetricsRecorder metrics = new BexMetricsRecorder();
+ BexExecutionResult result = execute(program, context, metrics);
+ publishMetrics(result.metricsSnapshot());
+ return result;
+ }
+
+ 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();
+ metrics.addExecuteNanos(System.nanoTime() - start);
+ return new BexExecutionResult(result.value(),
+ result.changeset(),
+ result.events(),
+ result.gasLedger(),
+ metrics.snapshot(),
+ result.output());
+ }
+
+ public BexExecutionResult compileAndExecute(BexProgramSource source, BexExecutionContext context) {
+ BexMetricsRecorder metrics = new BexMetricsRecorder();
+ BexCompiledProgram program = compile(source, metrics);
+ BexExecutionResult result = execute(program, context, metrics);
+ publishMetrics(result.metricsSnapshot());
+ return result;
+ }
+
+ private BexCompiledProgramKey key(BexProgramSource 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) {
+ for (String blueId : program.requiredIntrinsicBlueIds()) {
+ if (!intrinsics.supports(blueId)) {
+ throw new BexException("Unsupported intrinsic BlueId: " + blueId);
+ }
+ }
+ }
+
+ 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 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);
+ } 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;
+ 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;
+ return this;
+ }
+
+ public Builder gasSchedule(BexGasSchedule gasSchedule) {
+ this.gasSchedule = Objects.requireNonNull(
+ gasSchedule, "gasSchedule");
+ return this;
+ }
+
+ public Builder cache(BexCompiledProgramCache cache) {
+ this.cache = Objects.requireNonNull(cache, "cache");
+ return this;
+ }
+
+ public Builder metrics(BexMetricsSink metrics) {
+ this.metricsSink = metrics != null ? metrics : BexMetricsSink.NOOP;
+ return this;
+ }
+
+ public Builder intrinsics(BexIntrinsicRegistry intrinsics) {
+ this.intrinsics = intrinsics != null ? intrinsics : BexIntrinsicRegistry.empty();
+ return this;
+ }
+
+ 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,
+ String registryIdentity,
+ Map counterWeights,
+ BexIntrinsicProcessor processor) {
+ this.intrinsics = this.intrinsics.with(
+ typeClass,
+ registryIdentity,
+ counterWeights,
+ processor);
+ 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/BexExecutionContext.java b/blue-bex-core/src/main/java/blue/bex/api/BexExecutionContext.java
similarity index 84%
rename from src/main/java/blue/bex/api/BexExecutionContext.java
rename to blue-bex-core/src/main/java/blue/bex/api/BexExecutionContext.java
index 557650f..465fa93 100644
--- a/src/main/java/blue/bex/api/BexExecutionContext.java
+++ b/blue-bex-core/src/main/java/blue/bex/api/BexExecutionContext.java
@@ -1,7 +1,10 @@
package blue.bex.api;
+import blue.bex.output.BexSemanticIdentityBoundary;
+import blue.bex.gas.BexGasMeter;
import blue.bex.value.BexValue;
import blue.bex.value.BexValues;
+import blue.bex.runtime.BexRuntimeContext;
import java.util.ArrayDeque;
import java.util.Collections;
@@ -20,20 +23,40 @@
* 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;
+ 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 BexFailureBoundary failureBoundary;
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;
+ this.failureBoundary = builder.failureBoundary != null
+ ? builder.failureBoundary
+ : BexFailureBoundary.STANDALONE;
if (document == null) {
throw new IllegalArgumentException("document is required");
}
@@ -47,6 +70,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 +86,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 +139,34 @@ 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 BexFailureBoundary failureBoundary() {
+ return failureBoundary;
+ }
+
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 BexFailureBoundary failureBoundary;
private final LinkedHashMap bindings = new LinkedHashMap<>();
public Builder document(BexDocumentView document) {
@@ -122,6 +178,10 @@ 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 +236,39 @@ 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;
+ }
+
+ public Builder failureBoundary(BexFailureBoundary failureBoundary) {
+ this.failureBoundary = failureBoundary;
return this;
}
@@ -204,6 +285,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/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/blue-bex-core/src/main/java/blue/bex/api/BexGasLedgerHost.java b/blue-bex-core/src/main/java/blue/bex/api/BexGasLedgerHost.java
new file mode 100644
index 0000000..28fd80e
--- /dev/null
+++ b/blue-bex-core/src/main/java/blue/bex/api/BexGasLedgerHost.java
@@ -0,0 +1,136 @@
+package blue.bex.api;
+
+import blue.bex.gas.BexGasLimitExceededException;
+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;
+
+/**
+ * Parent-runtime bridge for one live BEX named child ledger.
+ *
+ * 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 extends BexGasLedgerLifecycle {
+ BexGasLedgerCapability 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 BexSharedGasBudget 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 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);
+
+ /**
+ * 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(BexGasLedgerCapability 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(BexGasLedgerCapability 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(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(
+ BexGasLedgerCapability ledger,
+ BexHostGasExhaustion exhaustion) {
+ Objects.requireNonNull(ledger, "ledger");
+ throw Objects.requireNonNull(
+ exhaustion, "exhaustion").hostFailure();
+ }
+}
diff --git a/blue-bex-core/src/main/java/blue/bex/api/BexIntrinsicInvocation.java b/blue-bex-core/src/main/java/blue/bex/api/BexIntrinsicInvocation.java
new file mode 100644
index 0000000..5059b51
--- /dev/null
+++ b/blue-bex-core/src/main/java/blue/bex/api/BexIntrinsicInvocation.java
@@ -0,0 +1,114 @@
+package blue.bex.api;
+
+import blue.bex.output.BexAdmittedValue;
+import blue.bex.value.BexValue;
+import blue.bex.value.BexValues;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.LongSupplier;
+import java.util.function.Function;
+
+/**
+ * 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 String gasNamespace;
+ private final Map namedCounterWeights;
+ private final NamedGasCharger gasCharger;
+ private final LongSupplier gasUsed;
+ private final Function exactAdmission;
+
+ 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.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;
+ }
+
+ public BexValue type() {
+ return type;
+ }
+
+ public Map fields() {
+ return fields;
+ }
+
+ public BexValue field(String name) {
+ BexValue value = fields.get(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);
+ }
+
+ /**
+ * Explicitly admits a payload field when this intrinsic's declared
+ * semantics require an exact Blue node.
+ */
+ 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/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/blue-bex-core/src/main/java/blue/bex/api/BexIntrinsicRegistry.java b/blue-bex-core/src/main/java/blue/bex/api/BexIntrinsicRegistry.java
new file mode 100644
index 0000000..f7ee1a8
--- /dev/null
+++ b/blue-bex-core/src/main/java/blue/bex/api/BexIntrinsicRegistry.java
@@ -0,0 +1,424 @@
+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.bex.runtime.BexRuntimeIntrinsics;
+import blue.language.model.TypeBlueId;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Exact intrinsic registry keyed by static Blue type identity.
+ */
+public final class BexIntrinsicRegistry
+ implements BexIntrinsicCatalog, BexRuntimeIntrinsics {
+ private static final BexIntrinsicRegistry EMPTY =
+ new BexIntrinsicRegistry(Collections.emptyMap());
+
+ private final Map registrations;
+ private final Map registeredNamedWeights;
+ private final Map>
+ registeredNamespaceWeights;
+ private final String identity;
+
+ 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() {
+ return EMPTY;
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public BexIntrinsicRegistry with(String blueId,
+ String registryIdentity,
+ Map counterWeights,
+ BexIntrinsicProcessor processor) {
+ return toBuilder()
+ .register(blueId, registryIdentity, counterWeights, processor)
+ .build();
+ }
+
+ 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)
+ .build();
+ }
+
+ @Override
+ public boolean supports(String blueId) {
+ return registrations.containsKey(blueId);
+ }
+
+ public Set supportedBlueIds() {
+ 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,
+ BexGasMeter gas,
+ BexOutputAdmission outputAdmission) {
+ Registration registration = registrations.get(blueId);
+ if (registration == null) {
+ throw new BexException("Unsupported intrinsic BlueId: " + blueId);
+ }
+ 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 String resolveAnnotatedTypeBlueId(Class> typeClass) {
+ TypeBlueId annotation = Objects.requireNonNull(
+ typeClass, "typeClass").getAnnotation(TypeBlueId.class);
+ if (annotation == null) {
+ return null;
+ }
+ if (!annotation.defaultValue().isEmpty()) {
+ return annotation.defaultValue();
+ }
+ for (String blueId : annotation.value()) {
+ if (blueId != null && !blueId.isEmpty()) {
+ return blueId;
+ }
+ }
+ return null;
+ }
+
+ 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 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,
+ String registryIdentity,
+ String namespace,
+ Map counterWeights,
+ BexIntrinsicProcessor processor) {
+ if (blueId == null || blueId.trim().isEmpty()) {
+ throw new IllegalArgumentException("intrinsic blueId is required");
+ }
+ 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,
+ 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");
+ }
+ 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: "
+ + typeClass.getName());
+ }
+ return register(
+ blueId,
+ registryIdentity,
+ counterWeights,
+ processor);
+ }
+
+ public BexIntrinsicRegistry build() {
+ return registrations.isEmpty()
+ ? EMPTY
+ : new BexIntrinsicRegistry(registrations);
+ }
+ }
+}
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/blue-bex-core/src/main/java/blue/bex/api/BexTypeBlueIdResolver.java b/blue-bex-core/src/main/java/blue/bex/api/BexTypeBlueIdResolver.java
new file mode 100644
index 0000000..1661d1a
--- /dev/null
+++ b/blue-bex-core/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/blue-bex-core/src/main/java/blue/bex/api/FrozenBexDocumentView.java
similarity index 76%
rename from src/main/java/blue/bex/api/FrozenBexDocumentView.java
rename to blue-bex-core/src/main/java/blue/bex/api/FrozenBexDocumentView.java
index 030d7d7..651f5f1 100644
--- a/src/main/java/blue/bex/api/FrozenBexDocumentView.java
+++ b/blue-bex-core/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;
@@ -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/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 60%
rename from src/main/java/blue/bex/compile/BexCompiledProgram.java
rename to blue-bex-core/src/main/java/blue/bex/compile/BexCompiledProgram.java
index 819799e..33fbecc 100644
--- a/src/main/java/blue/bex/compile/BexCompiledProgram.java
+++ b/blue-bex-core/src/main/java/blue/bex/compile/BexCompiledProgram.java
@@ -1,15 +1,12 @@
package blue.bex.compile;
import blue.bex.BexException;
-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.gas.BexGasCounter;
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;
@@ -28,21 +25,15 @@ public final class BexCompiledProgram {
private final int rootFrameSize;
private final String programBlueId;
private final Set requiredIntrinsicBlueIds;
-
- public 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) {
+ private final BexCompiledProgramKey compilationKey;
+
+ BexCompiledProgram(CompiledFunction entry,
+ Map functions,
+ Map constants,
+ int rootFrameSize,
+ String programBlueId,
+ Set requiredIntrinsicBlueIds,
+ BexCompiledProgramKey compilationKey) {
this.entry = entry;
this.functions = Collections.unmodifiableMap(new LinkedHashMap<>(functions));
this.constants = Collections.unmodifiableMap(new LinkedHashMap<>(constants));
@@ -51,18 +42,28 @@ public BexCompiledProgram(CompiledFunction entry,
this.requiredIntrinsicBlueIds = Collections.unmodifiableSet(new LinkedHashSet<>(requiredIntrinsicBlueIds != null
? requiredIntrinsicBlueIds
: Collections.emptySet()));
+ this.compilationKey = java.util.Objects.requireNonNull(
+ compilationKey, "compilationKey");
}
- 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; }
+ 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 compilationKey.compileEnvironmentIdentity();
+ }
public BexValue constant(String name) {
BexValue value = constants.get(name);
@@ -75,7 +76,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;
@@ -84,23 +85,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;
}
@@ -115,20 +117,32 @@ 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.metrics().incrementFunctionCalls();
- runtime.gas().charge(runtime.gas().schedule().functionCall);
- CompiledFrame frame = new CompiledFrame(runtime, frameSize, parent);
+ public BexValue invokePrepared(
+ BexExecutionMachine machine,
+ CompiledFrame parent,
+ int[] slots,
+ BexValue[] values) {
+ machine.gas().charge(BexGasCounter.FUNCTION_CALLED);
+ machine.metrics().incrementFunctionCalls();
+ if (parent == null) {
+ machine.metrics().incrementCompiledExecutions();
+ }
+ 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(value, arg.pattern())) {
+ && !machine.matchesType(
+ value,
+ arg.pattern(),
+ parent != null ? parent.sourcePath() : null)) {
throw new BexException("Function " + name
+ " argument " + arg.name()
+ " does not match declared Blue pattern at "
@@ -142,21 +156,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/blue-bex-core/src/main/java/blue/bex/compile/BexCompiledProgramKey.java b/blue-bex-core/src/main/java/blue/bex/compile/BexCompiledProgramKey.java
new file mode 100644
index 0000000..a75bafd
--- /dev/null
+++ b/blue-bex-core/src/main/java/blue/bex/compile/BexCompiledProgramKey.java
@@ -0,0 +1,84 @@
+package blue.bex.compile;
+
+import java.util.Objects;
+
+/**
+ * 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 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(BexCompilationInput.Kind.FULL_PROGRAM, programIdentity, definitionIdentity, entryName,
+ COMPILER_IDENTITY);
+ }
+
+ public BexCompiledProgramKey(BexCompilationInput.Kind kind, String programIdentity, String definitionIdentity, String entryName) {
+ this(kind, programIdentity, definitionIdentity, entryName, COMPILER_IDENTITY);
+ }
+
+ public BexCompiledProgramKey(BexCompilationInput.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(BexCompilationInput source) {
+ return from(source, COMPILER_IDENTITY);
+ }
+
+ public static BexCompiledProgramKey from(BexCompilationInput source,
+ String compileEnvironmentIdentity) {
+ 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 BexCompilationInput.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) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof BexCompiledProgramKey)) {
+ return false;
+ }
+ BexCompiledProgramKey that = (BexCompiledProgramKey) other;
+ return kind == that.kind
+ && Objects.equals(programIdentity, that.programIdentity)
+ && Objects.equals(definitionIdentity, that.definitionIdentity)
+ && Objects.equals(entryName, that.entryName)
+ && Objects.equals(compileEnvironmentIdentity, that.compileEnvironmentIdentity);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(kind, programIdentity, definitionIdentity, entryName,
+ compileEnvironmentIdentity);
+ }
+}
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..f6408eb
--- /dev/null
+++ b/blue-bex-core/src/main/java/blue/bex/compile/BexCompiledProgramRuntimeAccess.java
@@ -0,0 +1,27 @@
+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);
+ }
+
+ /** 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/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 71%
rename from src/main/java/blue/bex/compile/BexContainsCache.java
rename to blue-bex-core/src/main/java/blue/bex/compile/BexContainsCache.java
index 64ec67d..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;
}
@@ -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/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 98%
rename from src/main/java/blue/bex/compile/BexNodeFingerprint.java
rename to blue-bex-core/src/main/java/blue/bex/compile/BexNodeFingerprint.java
index 6ed4bae..47c16c8 100644
--- a/src/main/java/blue/bex/compile/BexNodeFingerprint.java
+++ b/blue-bex-core/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/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 61%
rename from src/main/java/blue/bex/compile/BexOperands.java
rename to blue-bex-core/src/main/java/blue/bex/compile/BexOperands.java
index b77a0c1..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 {
@@ -13,9 +13,44 @@ 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 = Collections.unmodifiableList(
+ new ArrayList<>(segments));
+ }
+
+ String authored() {
+ return authored;
+ }
+
+ String absolute() {
+ return absolute;
+ }
+
+ List segments() {
+ return segments;
+ }
}
final class StaticTextExpr implements TextOperand {
@@ -66,21 +101,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.machine().resolvePointer(authored);
+ return new ResolvedPointer(authored, resolved,
+ frame.machine().parseDynamicPointer(resolved));
}
}
@@ -92,18 +119,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.machine().resolvePointer(authored);
+ return new ResolvedPointer(authored, absolute,
+ frame.machine().parseDynamicPointer(absolute));
}
}
@@ -119,18 +139,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 +159,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.machine().canonicalPointer(authored);
+ return new ResolvedPointer(authored, 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..ea07265
--- /dev/null
+++ b/blue-bex-core/src/main/java/blue/bex/compile/BexProgramCompiler.java
@@ -0,0 +1,365 @@
+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) {
+ BexCompiledProgramKey compilationKey = BexCompiledProgramKey.from(
+ source, 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,
+ compilationKey);
+ }
+ 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,
+ compilationKey);
+ }
+
+ 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 84%
rename from src/main/java/blue/bex/compile/BexStatements.java
rename to blue-bex-core/src/main/java/blue/bex/compile/BexStatements.java
index 8e88d37..1665cb7 100644
--- a/src/main/java/blue/bex/compile/BexStatements.java
+++ b/blue-bex-core/src/main/java/blue/bex/compile/BexStatements.java
@@ -2,15 +2,14 @@
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;
-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;
@@ -18,8 +17,8 @@
abstract class Stmt implements CompiledStatement {
@Override
public final Control exec(CompiledFrame frame) {
- frame.runtime().metrics().incrementStatementExecutions();
- frame.runtime().gas().charge(frame.runtime().gas().schedule().statementBase);
+ BexGasWork.charge(frame, BexGasCounter.STATEMENT_EXECUTED);
+ frame.machine().metrics().incrementStatementExecutions();
try {
return doExec(frame);
} catch (BexException ex) {
@@ -81,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;
}
@@ -118,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
@@ -144,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
@@ -152,13 +153,15 @@ protected Control doExec(CompiledFrame frame) {
BexValue value = input.eval(frame);
if (value.isObject()) {
for (String key : value.keys()) {
- frame.runtime().metrics().incrementLoopIterations();
- frame.runtime().gas().charge(frame.runtime().gas().schedule().forEachItem);
+ BexGasWork.charge(frame, BexGasCounter.COLLECTION_ITEM_VISITED);
+ BexGasWork.charge(frame, BexGasCounter.OBJECT_MEMBER_READ);
+ frame.machine().metrics().incrementLoopIterations();
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++) {
- frame.runtime().metrics().incrementLoopIterations();
- frame.runtime().gas().charge(frame.runtime().gas().schedule().forEachItem);
+ BexGasWork.charge(frame, BexGasCounter.COLLECTION_ITEM_VISITED);
+ BexGasWork.charge(frame, BexGasCounter.LIST_ITEM_READ);
+ frame.machine().metrics().incrementLoopIterations();
frame.set(itemSlot, value.get(String.valueOf(i)));
if (indexSlot >= 0) {
frame.set(indexSlot, BexValues.scalar(BigInteger.valueOf(i)));
@@ -197,16 +201,16 @@ private BexStatementEffects() {
}
static void appendChange(CompiledFrame frame, BexPatchEntry entry) {
- frame.runtime().gas().chargeValue(frame.runtime().gas().schedule().appendChangeBase, entry.val());
- frame.accumulator().appendChange(entry);
+ BexGasWork.charge(frame, BexGasCounter.PATCH_APPENDED);
+ frame.appendChange(entry);
}
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);
- frame.accumulator().appendEvent(value);
+ BexGasWork.charge(frame, BexGasCounter.EVENT_APPENDED);
+ frame.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;
@@ -312,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;
}
}
@@ -331,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 72%
rename from src/main/java/blue/bex/compile/CollectionExpressions.java
rename to blue-bex-core/src/main/java/blue/bex/compile/CollectionExpressions.java
index e9aa1d2..af07d6c 100644
--- a/src/main/java/blue/bex/compile/CollectionExpressions.java
+++ b/blue-bex-core/src/main/java/blue/bex/compile/CollectionExpressions.java
@@ -1,14 +1,16 @@
package blue.bex.compile;
import blue.bex.BexException;
-import blue.bex.runtime.CompiledExpression;
-import blue.bex.runtime.CompiledFrame;
+import blue.bex.gas.BexGasCounter;
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;
@@ -32,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
@@ -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++) {
- frame.runtime().metrics().incrementLoopIterations();
- frame.runtime().gas().charge(frame.runtime().gas().schedule().forEachItem);
+ BexGasWork.charge(frame, BexGasCounter.COLLECTION_ITEM_VISITED);
+ BexGasWork.charge(frame, BexGasCounter.LIST_ITEM_READ);
+ 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);
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);
- frame.runtime().metrics().incrementLoopIterations();
- frame.runtime().gas().charge(frame.runtime().gas().schedule().forEachItem);
+ BexGasWork.charge(frame, BexGasCounter.COLLECTION_ITEM_VISITED);
+ BexGasWork.charge(frame, BexGasCounter.OBJECT_MEMBER_READ);
+ 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);
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++) {
- frame.runtime().metrics().incrementLoopIterations();
- frame.runtime().gas().charge(frame.runtime().gas().schedule().forEachItem);
+ BexGasWork.charge(frame, BexGasCounter.COLLECTION_ITEM_VISITED);
+ BexGasWork.charge(frame, BexGasCounter.LIST_ITEM_READ);
+ frame.machine().metrics().incrementLoopIterations();
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);
- frame.runtime().metrics().incrementLoopIterations();
- frame.runtime().gas().charge(frame.runtime().gas().schedule().forEachItem);
+ BexGasWork.charge(frame, BexGasCounter.COLLECTION_ITEM_VISITED);
+ BexGasWork.charge(frame, BexGasCounter.OBJECT_MEMBER_READ);
+ 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);
@@ -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) {
- this.slots = slots;
- this.values = values;
+ private SlotSnapshot(int[] slots, BexValue[] values, boolean[] initialized) {
+ this.slots = slots.clone();
+ this.values = values.clone();
+ this.initialized = initialized.clone();
}
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++) {
- frame.runtime().metrics().incrementLoopIterations();
- frame.runtime().gas().charge(frame.runtime().gas().schedule().forEachItem);
- if (BexValues.equal(list.get(String.valueOf(i)), val)) {
+ BexGasWork.charge(frame, BexGasCounter.COLLECTION_ITEM_VISITED);
+ BexGasWork.charge(frame, BexGasCounter.LIST_ITEM_READ);
+ frame.machine().metrics().incrementLoopIterations();
+ 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++) {
- frame.runtime().metrics().incrementLoopIterations();
- frame.runtime().gas().charge(frame.runtime().gas().schedule().forEachItem);
+ BexGasWork.charge(frame, BexGasCounter.COLLECTION_ITEM_VISITED);
+ BexGasWork.charge(frame, BexGasCounter.LIST_ITEM_READ);
+ frame.machine().metrics().incrementLoopIterations();
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/blue-bex-core/src/main/java/blue/bex/compile/CompileScope.java b/blue-bex-core/src/main/java/blue/bex/compile/CompileScope.java
new file mode 100644
index 0000000..0d1bae2
--- /dev/null
+++ b/blue-bex-core/src/main/java/blue/bex/compile/CompileScope.java
@@ -0,0 +1,84 @@
+package blue.bex.compile;
+
+import blue.bex.BexException;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/** Compile-time slot allocator. */
+public final class CompileScope {
+ private final CompileScope parent;
+ private final Map slots = new LinkedHashMap<>();
+ private int nextSlot;
+
+ public CompileScope() {
+ this(null);
+ }
+
+ public CompileScope(CompileScope parent) {
+ this.parent = parent;
+ this.nextSlot = parent != null ? parent.frameSize() : 0;
+ }
+
+ public int declareOrGetSlot(String name) {
+ Integer existing = slots.get(name);
+ if (existing != null) {
+ return existing;
+ }
+ if (parent != null && parent.hasSlot(name)) {
+ return parent.resolveSlot(name);
+ }
+ int slot = nextSlot++;
+ slots.put(name, slot);
+ return slot;
+ }
+
+ public int resolveSlot(String name) {
+ Integer slot = slots.get(name);
+ if (slot != null) {
+ return slot;
+ }
+ if (parent != null) {
+ return parent.resolveSlot(name);
+ }
+ throw new BexException("Unknown variable: " + name);
+ }
+
+ public boolean hasSlot(String name) {
+ return slots.containsKey(name)
+ || (parent != null && parent.hasSlot(name));
+ }
+
+ public int frameSize() {
+ return Math.max(
+ nextSlot,
+ parent != null ? parent.frameSize() : 0);
+ }
+
+ /**
+ * 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
+ * allocated slots available to the compiled expression.
+ */
+ 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/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/blue-bex-core/src/main/java/blue/bex/compile/CompiledFrame.java b/blue-bex-core/src/main/java/blue/bex/compile/CompiledFrame.java
new file mode 100644
index 0000000..99a6a59
--- /dev/null
+++ b/blue-bex-core/src/main/java/blue/bex/compile/CompiledFrame.java
@@ -0,0 +1,130 @@
+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 invocation frame for compiled BEX IR. */
+public final class CompiledFrame {
+ private final BexExecutionMachine machine;
+ private final BexValue[] slots;
+ private final CompiledFrame parent;
+ private BexValue returnValue;
+ private BexSourcePath sourcePath;
+
+ public CompiledFrame(
+ BexExecutionMachine machine,
+ int frameSize,
+ CompiledFrame parent) {
+ this.machine = machine;
+ this.slots = new BexValue[frameSize];
+ this.parent = parent;
+ }
+
+ public BexExecutionMachine machine() {
+ return machine;
+ }
+
+ public BexValue get(int slot) {
+ BexValue value = slots[slot];
+ return value != null ? value : BexValues.undefined();
+ }
+
+ /** Reads a declared slot and fails while its initializer is incomplete. */
+ 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();
+ }
+
+ public CompiledFrame parent() {
+ return parent;
+ }
+
+ public BexValue readDocument(
+ String pointer,
+ List precompiledSegments,
+ boolean resolved) {
+ return machine.readDocument(pointer, precompiledSegments, resolved);
+ }
+
+ public BexValue readEvent(List precompiledSegments) {
+ return machine.readEvent(precompiledSegments);
+ }
+
+ public BexValue readProcessingEvent(List precompiledSegments) {
+ return machine.readProcessingEvent(precompiledSegments);
+ }
+
+ public BexValue readCurrentContract(List precompiledSegments) {
+ return machine.readCurrentContract(precompiledSegments);
+ }
+
+ public BexValue readBinding(
+ String name,
+ List precompiledSegments) {
+ return machine.readBinding(name, precompiledSegments);
+ }
+
+ 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() {
+ return returnValue;
+ }
+
+ public void returnValue(BexValue returnValue) {
+ this.returnValue = returnValue;
+ }
+
+ public BexSourcePath sourcePath() {
+ return sourcePath != null
+ ? sourcePath
+ : parent != null ? parent.sourcePath() : null;
+ }
+
+ public BexSourcePath enter(BexSourcePath next) {
+ BexSourcePath previous = sourcePath;
+ sourcePath = next;
+ return previous;
+ }
+
+ public void restore(BexSourcePath previous) {
+ sourcePath = previous;
+ }
+}
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/blue-bex-core/src/main/java/blue/bex/compile/LogicNumericExpressions.java b/blue-bex-core/src/main/java/blue/bex/compile/LogicNumericExpressions.java
new file mode 100644
index 0000000..7382810
--- /dev/null
+++ b/blue-bex-core/src/main/java/blue/bex/compile/LogicNumericExpressions.java
@@ -0,0 +1,228 @@
+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.BigInteger;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+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;
+
+ CompareExpr(List expressions, CompareOp op) {
+ this.expressions = ImmutableExpressionLists.copyOf(expressions);
+ this.op = op;
+ }
+
+ @Override
+ protected BexValue doEval(CompiledFrame frame) {
+ if (expressions.size() != 2) throw new BexException("Comparison expects two operands");
+ BexValue a = expressions.get(0).eval(frame);
+ BexValue b = expressions.get(1).eval(frame);
+ boolean result;
+ if (op == CompareOp.EQ || op == CompareOp.NE) {
+ result = MeteredEquality.equal(frame, a, b);
+ if (op == CompareOp.NE) result = !result;
+ } else {
+ 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);
+ }
+}
+
+final class LogicalExpr extends Expr {
+ private final List expressions;
+ private final boolean and;
+
+ LogicalExpr(List expressions, boolean and) {
+ this.expressions = ImmutableExpressionLists.copyOf(expressions);
+ this.and = and;
+ }
+
+ @Override
+ protected BexValue doEval(CompiledFrame frame) {
+ for (CompiledExpression expression : expressions) {
+ boolean value = BexValues.truthy(expression.eval(frame));
+ if (and && !value) return BexValues.scalar(false);
+ if (!and && value) return BexValues.scalar(true);
+ }
+ return BexValues.scalar(and);
+ }
+}
+
+final class NotExpr extends Expr {
+ private final CompiledExpression expression;
+
+ NotExpr(CompiledExpression expression) {
+ this.expression = expression;
+ }
+
+ @Override
+ protected BexValue doEval(CompiledFrame frame) {
+ return BexValues.scalar(!BexValues.truthy(expression.eval(frame)));
+ }
+}
+
+final class CoalesceExpr extends Expr {
+ private final List expressions;
+
+ CoalesceExpr(List expressions) {
+ this.expressions = ImmutableExpressionLists.copyOf(expressions);
+ }
+
+ @Override
+ protected BexValue doEval(CompiledFrame frame) {
+ for (CompiledExpression expression : expressions) {
+ BexValue value = expression.eval(frame);
+ if (!BexValues.empty(value)) return value;
+ }
+ return BexValues.undefined();
+ }
+}
+
+enum NumericOp { ADD, SUBTRACT, MULTIPLY, DIVIDE }
+
+final class NumericExpr extends Expr {
+ private final List expressions;
+ private final NumericOp op;
+
+ NumericExpr(List expressions, NumericOp op) {
+ this.expressions = ImmutableExpressionLists.copyOf(expressions);
+ this.op = op;
+ }
+
+ @Override
+ protected BexValue doEval(CompiledFrame frame) {
+ if (expressions.isEmpty()) throw new BexException("Numeric operator needs operands");
+ 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 = 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];
+ break;
+ default:
+ throw new BexException("Unknown numeric op");
+ }
+ }
+ 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/blue-bex-core/src/main/java/blue/bex/compile/ObjectResultExpressions.java b/blue-bex-core/src/main/java/blue/bex/compile/ObjectResultExpressions.java
new file mode 100644
index 0000000..c9d8dd7
--- /dev/null
+++ b/blue-bex-core/src/main/java/blue/bex/compile/ObjectResultExpressions.java
@@ -0,0 +1,243 @@
+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.BigInteger;
+import java.util.Collections;
+import java.util.List;
+
+final class ListGetExpr extends Expr {
+ private final CompiledExpression list;
+ private final CompiledExpression index;
+ private final CompiledExpression defaultValue;
+
+ ListGetExpr(CompiledExpression list, CompiledExpression index, CompiledExpression defaultValue) {
+ this.list = list;
+ this.index = index;
+ this.defaultValue = defaultValue;
+ }
+
+ @Override
+ protected BexValue doEval(CompiledFrame frame) {
+ BexValue l = list.eval(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;
+ }
+}
+
+final class ObjectSetExpr extends Expr {
+ private final CompiledExpression object;
+ private final TextOperand key;
+ private final CompiledExpression value;
+
+ ObjectSetExpr(CompiledExpression object, TextOperand key, CompiledExpression value) {
+ this.object = object;
+ this.key = key;
+ this.value = value;
+ }
+
+ @Override
+ protected BexValue doEval(CompiledFrame frame) {
+ BexValue base = object.eval(frame);
+ String evaluatedKey = key.get(frame);
+ BexValue val = value.eval(frame);
+ 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);
+ }
+}
+
+final class PointerGetExpr extends Expr {
+ private final CompiledExpression object;
+ private final PointerOperand pointer;
+ private final CompiledExpression defaultValue;
+
+ PointerGetExpr(CompiledExpression object, PointerOperand pointer, CompiledExpression defaultValue) {
+ this.object = object;
+ this.pointer = pointer;
+ this.defaultValue = defaultValue;
+ }
+
+ @Override
+ protected BexValue doEval(CompiledFrame frame) {
+ BexValue base = object.eval(frame);
+ List segments = pointer.segments(frame);
+ 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 {
+ private final CompiledExpression object;
+ private final TextOperand op;
+ private final PointerOperand pointer;
+ private final CompiledExpression value;
+
+ PointerSetExpr(CompiledExpression object, TextOperand op, PointerOperand pointer, CompiledExpression value) {
+ this.object = object;
+ this.op = op;
+ this.pointer = pointer;
+ this.value = value;
+ }
+
+ @Override
+ protected BexValue doEval(CompiledFrame frame) {
+ BexValue base = object.eval(frame);
+ List