From c01308e19796d08bea2c27e1a37f0fac2c93a383 Mon Sep 17 00:00:00 2001
From: piotr-blue
Date: Wed, 20 May 2026 20:02:44 +0200
Subject: [PATCH 1/4] Add comprehensive Bex workflow processing framework
Introduces a suite of classes to handle Bex-based workflows. This includes support for parsing, evaluating, and managing Bex bindings, expressions, and execution contexts, as well as utilities for metrics tracking and compute step execution.
---
src/main/java/blue/bex/compile/BexCompiler.java | 9 ++++++---
src/main/java/blue/bex/value/BexNodeWriter.java | 3 +++
2 files changed, 9 insertions(+), 3 deletions(-)
diff --git a/src/main/java/blue/bex/compile/BexCompiler.java b/src/main/java/blue/bex/compile/BexCompiler.java
index ee36232..36ba81a 100644
--- a/src/main/java/blue/bex/compile/BexCompiler.java
+++ b/src/main/java/blue/bex/compile/BexCompiler.java
@@ -185,8 +185,11 @@ private List compileStatements(FrozenNode node, CompileScope
}
private CompiledStatement compileStatement(FrozenNode statement, CompileScope scope, String pointer) {
- if (statement == null || statement.getProperties() == null) {
- throw new BexException("Statement must be an operator object");
+ if (statement == null || statement.isEmptyNode()) {
+ return sourceStatement(currentFunction, pointer, "$return", new ReturnStatement(null));
+ }
+ if (statement.getProperties() == null) {
+ throw new BexException("Statement must be an operator object at " + pointer);
}
int count = 0;
String op = null;
@@ -199,7 +202,7 @@ private CompiledStatement compileStatement(FrozenNode statement, CompileScope sc
}
}
if (count != 1 || statement.getProperties().size() != 1) {
- throw new BexException("Statement must have exactly one $ operator");
+ throw new BexException("Statement must have exactly one $ operator at " + pointer);
}
String bodyPointer = pointer + "/" + escape(op);
CompiledStatement compiled;
diff --git a/src/main/java/blue/bex/value/BexNodeWriter.java b/src/main/java/blue/bex/value/BexNodeWriter.java
index 2546d60..6c2371d 100644
--- a/src/main/java/blue/bex/value/BexNodeWriter.java
+++ b/src/main/java/blue/bex/value/BexNodeWriter.java
@@ -19,6 +19,9 @@ public static Node toNode(BexValue value) {
if (value.isNull()) {
return new Node();
}
+ if (value instanceof NodeBexValue || value instanceof FrozenNodeBexValue) {
+ return value.toNode();
+ }
if (value.isScalar()) {
return value.toNode();
}
From 490b8b7350b383c890197474b20e4db88b93cc8c Mon Sep 17 00:00:00 2001
From: piotr-blue
Date: Sat, 23 May 2026 01:38:54 +0200
Subject: [PATCH 2/4] Track compile and execution times in BexMetrics and
integrate meaningful node checks in BexCompiler
---
src/main/java/blue/bex/api/BexEngine.java | 14 ++++++++++-
.../java/blue/bex/compile/BexCompiler.java | 24 ++++++++++++++-----
src/main/java/blue/bex/result/BexMetrics.java | 8 +++++++
3 files changed, 39 insertions(+), 7 deletions(-)
diff --git a/src/main/java/blue/bex/api/BexEngine.java b/src/main/java/blue/bex/api/BexEngine.java
index ab2d0e4..88dec7b 100644
--- a/src/main/java/blue/bex/api/BexEngine.java
+++ b/src/main/java/blue/bex/api/BexEngine.java
@@ -43,6 +43,8 @@ public BexCompiledProgram compile(BexProgramSource source) {
}
private BexCompiledProgram compile(BexProgramSource source, BexMetrics metrics) {
+ long start = System.nanoTime();
+ try {
BexCompiledProgramKey key = key(source);
BexCompiledProgram cached = cache.get(key);
if (cached != null) {
@@ -53,6 +55,9 @@ private BexCompiledProgram compile(BexProgramSource source, BexMetrics metrics)
BexCompiledProgram program = new BexCompiler(metrics).compile(source);
cache.put(key, program);
return program;
+ } finally {
+ metrics.addCompileNanos(System.nanoTime() - start);
+ }
}
public BexExecutionResult execute(BexCompiledProgram program, BexExecutionContext context) {
@@ -63,8 +68,15 @@ public BexExecutionResult execute(BexCompiledProgram program, BexExecutionContex
}
private BexExecutionResult execute(BexCompiledProgram program, BexExecutionContext context, BexMetrics metrics) {
+ long start = System.nanoTime();
BexRuntime runtime = new BexRuntime(program, context, gasSchedule, metrics, pointerCache);
- return runtime.execute();
+ BexExecutionResult result = runtime.execute();
+ metrics.addExecuteNanos(System.nanoTime() - start);
+ return new BexExecutionResult(result.value(),
+ result.changeset(),
+ result.events(),
+ result.gasUsed(),
+ metrics);
}
public BexExecutionResult compileAndExecute(BexProgramSource source, BexExecutionContext context) {
diff --git a/src/main/java/blue/bex/compile/BexCompiler.java b/src/main/java/blue/bex/compile/BexCompiler.java
index 36ba81a..1bfb52f 100644
--- a/src/main/java/blue/bex/compile/BexCompiler.java
+++ b/src/main/java/blue/bex/compile/BexCompiler.java
@@ -49,7 +49,8 @@ public BexCompiledProgram compile(blue.bex.api.BexProgramSource source) {
compiledFunctions.put(name, compileFunction(name, functionNodes.get(name)));
}
- String entryName = source.entry().orElse(text(prop(step, "entry")));
+ FrozenNode stepExpr = meaningful(prop(step, "expr"));
+ String entryName = source.entry().orElse(text(meaningful(prop(step, "entry"))));
BexCompiledProgram.CompiledFunction root;
int rootFrameSize = 0;
if (entryName != null && !entryName.isEmpty()) {
@@ -62,14 +63,14 @@ public BexCompiledProgram compile(blue.bex.api.BexProgramSource source) {
new ReturnStatement(sourceExpr("$root", "/entry/$call", "$call",
new CallExpr(entryName, new String[0], new CompiledExpression[0]))))),
null, 0);
- } else if (prop(step, "expr") != null) {
+ } else if (stepExpr != null) {
currentFunction = "$root";
root = new BexCompiledProgram.CompiledFunction("$root", Collections.emptyList(),
- Collections.emptyList(), compileExpr(prop(step, "expr"), new CompileScope(), "/expr"), 0);
+ Collections.emptyList(), compileExpr(stepExpr, new CompileScope(), "/expr"), 0);
} else {
CompileScope scope = new CompileScope();
currentFunction = "$root";
- List statements = compileStatements(prop(step, "do"), scope, "/do");
+ List statements = compileStatements(meaningful(prop(step, "do")), scope, "/do");
rootFrameSize = scope.frameSize();
root = new BexCompiledProgram.CompiledFunction("$root", Collections.emptyList(), statements, null, rootFrameSize);
}
@@ -90,9 +91,10 @@ private BexCompiledProgram.CompiledFunction compileFunction(String name, FrozenN
scope.declareOrGetSlot(arg);
}
}
- CompiledExpression expression = prop(functionNode, "expr") != null ? compileExpr(prop(functionNode, "expr"), scope, "/functions/" + escape(name) + "/expr") : null;
+ FrozenNode functionExpr = meaningful(prop(functionNode, "expr"));
+ CompiledExpression expression = functionExpr != null ? compileExpr(functionExpr, scope, "/functions/" + escape(name) + "/expr") : null;
List statements = expression == null
- ? compileStatements(prop(functionNode, "do"), scope, "/functions/" + escape(name) + "/do")
+ ? compileStatements(meaningful(prop(functionNode, "do")), scope, "/functions/" + escape(name) + "/do")
: Collections.emptyList();
currentFunction = previousFunction;
return new BexCompiledProgram.CompiledFunction(name, Collections.unmodifiableList(args),
@@ -467,6 +469,16 @@ private FrozenNode prop(FrozenNode node, String key) {
return null;
}
+ private FrozenNode meaningful(FrozenNode node) {
+ if (node == null) {
+ return null;
+ }
+ boolean hasPayload = node.getValue() != null
+ || (node.getItems() != null && !node.getItems().isEmpty())
+ || (node.getProperties() != null && !node.getProperties().isEmpty());
+ return hasPayload ? node : null;
+ }
+
private FrozenNode required(FrozenNode node, String label) {
if (node == null) {
throw new BexException("Missing required field: " + label);
diff --git a/src/main/java/blue/bex/result/BexMetrics.java b/src/main/java/blue/bex/result/BexMetrics.java
index 8d0ae76..b830323 100644
--- a/src/main/java/blue/bex/result/BexMetrics.java
+++ b/src/main/java/blue/bex/result/BexMetrics.java
@@ -41,6 +41,8 @@ public final class BexMetrics {
private long sizeEstimateCacheMisses;
private long frozenWriterNodeFallbacks;
private long frozenWriterChildNodeRoundTrips;
+ private long compileNanos;
+ private long executeNanos;
public BexMetrics copy() {
BexMetrics copy = new BexMetrics();
@@ -77,6 +79,8 @@ public BexMetrics copy() {
copy.sizeEstimateCacheMisses = sizeEstimateCacheMisses;
copy.frozenWriterNodeFallbacks = frozenWriterNodeFallbacks;
copy.frozenWriterChildNodeRoundTrips = frozenWriterChildNodeRoundTrips;
+ copy.compileNanos = compileNanos;
+ copy.executeNanos = executeNanos;
return copy;
}
@@ -113,6 +117,8 @@ public BexMetrics copy() {
public void incrementSizeEstimateCacheMisses() { sizeEstimateCacheMisses++; }
public void incrementFrozenWriterNodeFallbacks() { frozenWriterNodeFallbacks++; }
public void incrementFrozenWriterChildNodeRoundTrips() { frozenWriterChildNodeRoundTrips++; }
+ public void addCompileNanos(long nanos) { compileNanos += Math.max(0L, nanos); }
+ public void addExecuteNanos(long nanos) { executeNanos += Math.max(0L, nanos); }
public long compiledExecutions() { return compiledExecutions; }
public long compileCacheHits() { return compileCacheHits; }
@@ -147,4 +153,6 @@ public BexMetrics copy() {
public long sizeEstimateCacheMisses() { return sizeEstimateCacheMisses; }
public long frozenWriterNodeFallbacks() { return frozenWriterNodeFallbacks; }
public long frozenWriterChildNodeRoundTrips() { return frozenWriterChildNodeRoundTrips; }
+ public long compileNanos() { return compileNanos; }
+ public long executeNanos() { return executeNanos; }
}
From cd74f625a67ffc66fd09e37aa7a45f395e52ba0f Mon Sep 17 00:00:00 2001
From: piotr-blue
Date: Sat, 23 May 2026 04:03:33 +0200
Subject: [PATCH 3/4] Add Blue integration for Bex with type matching, node
conversion, and tests
---
.cz.toml | 2 +-
README.md | 119 +++
src/main/java/blue/bex/api/BexEngine.java | 11 +-
.../blue/bex/compile/BexCompiledProgram.java | 62 +-
.../java/blue/bex/compile/BexCompiler.java | 344 ++++++-
.../blue/bex/compile/BexContainsCache.java | 10 +
src/main/java/blue/bex/compile/CallExpr.java | 23 +-
.../bex/compile/TypeStringExpressions.java | 19 +-
.../java/blue/bex/runtime/BexRuntime.java | 6 +
.../blue/bex/type/BexBlueTypeMatcher.java | 34 +
.../blue/bex/value/BexBlueNodeWriter.java | 257 ++++++
.../java/blue/bex/value/BexFrozenWriter.java | 3 +
.../java/blue/bex/value/BexNodeWriter.java | 31 +-
src/main/java/blue/bex/value/MapBexValue.java | 11 +-
.../java/blue/bex/BexBlueTypeSupportTest.java | 836 ++++++++++++++++++
.../blue/bex/BexEngineConformanceTest.java | 4 +-
16 files changed, 1665 insertions(+), 107 deletions(-)
create mode 100644 src/main/java/blue/bex/type/BexBlueTypeMatcher.java
create mode 100644 src/main/java/blue/bex/value/BexBlueNodeWriter.java
create mode 100644 src/test/java/blue/bex/BexBlueTypeSupportTest.java
diff --git a/.cz.toml b/.cz.toml
index 967930d..24200c5 100644
--- a/.cz.toml
+++ b/.cz.toml
@@ -2,5 +2,5 @@
name = "cz_conventional_commits"
tag_format = "v$version"
version_scheme = "semver"
-version = "0.1.0"
+version = "0.2.0"
update_changelog_on_bump = true
diff --git a/README.md b/README.md
index f916563..f980e14 100644
--- a/README.md
+++ b/README.md
@@ -103,6 +103,102 @@ BexProgramSource source = BexProgramSource.withDefinition(
);
```
+## 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 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`, `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`, and `valueType` are legal Blue language fields, but they are not
+ordinary object properties. An operator may use one of them 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
@@ -188,6 +284,7 @@ BEX operators are Blue objects whose single key starts with `$`.
| Operator | Purpose |
|---|---|
| `$unwrap` | Unwrap Blue scalar wrapper values. |
+| `$is` | Return whether a value matches a Blue pattern. |
| `$text` | Convert to text. |
| `$integer` | Convert to exact integer. |
| `$number` | Convert to exact decimal/number. |
@@ -195,6 +292,16 @@ BEX operators are Blue objects whose single key starts with `$`.
| `$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
+```
+
### Strings
| Operator | Purpose |
@@ -205,6 +312,14 @@ BEX operators are Blue objects whose single key starts with `$`.
| `$startsWith` | Check a prefix. |
| `$sliceAfter` | Return text after a prefix. |
+```yaml
+$join:
+ list:
+ - a
+ - b
+ separator: ":"
+```
+
### Logic And Comparison
| Operator | Purpose |
@@ -254,6 +369,10 @@ BEX operators are Blue objects whose single key starts with `$`.
| `$call` | Call a local function. |
| `$literal` | Return payload without compiling nested operators. |
+`$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`.
+
## Statement Operators
| Statement | Purpose |
diff --git a/src/main/java/blue/bex/api/BexEngine.java b/src/main/java/blue/bex/api/BexEngine.java
index 88dec7b..d8e26ee 100644
--- a/src/main/java/blue/bex/api/BexEngine.java
+++ b/src/main/java/blue/bex/api/BexEngine.java
@@ -10,6 +10,7 @@
import blue.bex.result.BexExecutionResult;
import blue.bex.result.BexMetrics;
import blue.bex.runtime.BexRuntime;
+import blue.language.Blue;
/**
* Public entry point for compiling and executing selected BEX programs.
@@ -20,12 +21,14 @@
* emit events, or perform host actions.
*/
public final class BexEngine {
+ private final Blue blue;
private final BexGasSchedule gasSchedule;
private final BexCompiledProgramCache cache;
private final BexMetricsSink metricsSink;
private final BexPointerCache pointerCache = new BexPointerCache();
private BexEngine(Builder builder) {
+ this.blue = builder.blue;
this.gasSchedule = builder.gasSchedule;
this.cache = builder.cache;
this.metricsSink = builder.metricsSink;
@@ -69,7 +72,7 @@ public BexExecutionResult execute(BexCompiledProgram program, BexExecutionContex
private BexExecutionResult execute(BexCompiledProgram program, BexExecutionContext context, BexMetrics metrics) {
long start = System.nanoTime();
- BexRuntime runtime = new BexRuntime(program, context, gasSchedule, metrics, pointerCache);
+ BexRuntime runtime = new BexRuntime(program, context, blue, gasSchedule, metrics, pointerCache);
BexExecutionResult result = runtime.execute();
metrics.addExecuteNanos(System.nanoTime() - start);
return new BexExecutionResult(result.value(),
@@ -92,10 +95,16 @@ private BexCompiledProgramKey key(BexProgramSource source) {
}
public static final class Builder {
+ private Blue blue = new Blue();
private BexGasSchedule gasSchedule = BexGasSchedule.defaults();
private BexCompiledProgramCache cache = new LruBexCompiledProgramCache();
private BexMetricsSink metricsSink = BexMetricsSink.NOOP;
+ public Builder blue(Blue blue) {
+ this.blue = blue != null ? blue : new Blue();
+ return this;
+ }
+
public Builder gasSchedule(BexGasSchedule gasSchedule) {
this.gasSchedule = gasSchedule;
return this;
diff --git a/src/main/java/blue/bex/compile/BexCompiledProgram.java b/src/main/java/blue/bex/compile/BexCompiledProgram.java
index 0a5b0f8..4ee9062 100644
--- a/src/main/java/blue/bex/compile/BexCompiledProgram.java
+++ b/src/main/java/blue/bex/compile/BexCompiledProgram.java
@@ -1,5 +1,6 @@
package blue.bex.compile;
+import blue.bex.BexException;
import blue.bex.runtime.BexRuntime;
import blue.bex.runtime.CompiledFrame;
import blue.bex.runtime.CompiledStatement;
@@ -7,7 +8,9 @@
import blue.bex.runtime.CompiledExpression;
import blue.bex.value.BexValue;
import blue.bex.value.BexValues;
+import blue.language.snapshot.FrozenNode;
+import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
@@ -55,35 +58,42 @@ public BexValue constant(String name) {
*/
public static final class CompiledFunction {
private final String name;
- private final List args;
- private final Map argSlotByName;
+ private final List args;
+ private final Map argByName;
+ private final ArgSpec[] argBySlot;
private final List statements;
private final CompiledExpression expression;
private final int frameSize;
public CompiledFunction(String name,
- List args,
+ List args,
List statements,
CompiledExpression expression,
int frameSize) {
this.name = name;
- this.args = args;
- LinkedHashMap argSlots = new LinkedHashMap<>();
- for (int i = 0; i < args.size(); i++) {
- argSlots.put(args.get(i), i);
+ this.args = Collections.unmodifiableList(args);
+ LinkedHashMap argSpecs = new LinkedHashMap<>();
+ this.argBySlot = new ArgSpec[frameSize];
+ for (ArgSpec arg : args) {
+ argSpecs.put(arg.name(), arg);
+ if (arg.slot() >= 0 && arg.slot() < argBySlot.length) {
+ argBySlot[arg.slot()] = arg;
+ }
}
- this.argSlotByName = Collections.unmodifiableMap(argSlots);
+ this.argByName = Collections.unmodifiableMap(argSpecs);
this.statements = statements;
this.expression = expression;
this.frameSize = frameSize;
}
public String name() { return name; }
- public List args() { return args; }
+ public Collection args() { return args; }
public int frameSize() { return frameSize; }
+ public boolean hasArg(String name) { return argByName.containsKey(name); }
+ public ArgSpec arg(String name) { return argByName.get(name); }
public int argSlot(String name) {
- Integer slot = argSlotByName.get(name);
- return slot != null ? slot : -1;
+ ArgSpec arg = argByName.get(name);
+ return arg != null ? arg.slot() : -1;
}
public BexValue invokeRoot(BexRuntime runtime) {
@@ -97,6 +107,14 @@ public BexValue invokePrepared(BexRuntime runtime, CompiledFrame parent, int[] s
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())) {
+ throw new BexException("Function " + name
+ + " argument " + arg.name()
+ + " does not match declared Blue pattern at "
+ + arg.sourcePointer());
+ }
frame.set(slots[i], value != null ? value : BexValues.undefined());
}
}
@@ -111,4 +129,26 @@ public BexValue invokePrepared(BexRuntime runtime, CompiledFrame parent, int[] s
return runtime.defaultResultValue();
}
}
+
+ public 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) {
+ this.name = name;
+ this.slot = slot;
+ this.pattern = pattern;
+ this.typed = pattern != null && !pattern.isEmptyNode();
+ this.sourcePointer = sourcePointer;
+ }
+
+ public String name() { return name; }
+ public int slot() { return slot; }
+ public FrozenNode pattern() { return pattern; }
+ public boolean typed() { return typed; }
+ public String sourcePointer() { return sourcePointer; }
+ }
}
diff --git a/src/main/java/blue/bex/compile/BexCompiler.java b/src/main/java/blue/bex/compile/BexCompiler.java
index 1bfb52f..f2ce447 100644
--- a/src/main/java/blue/bex/compile/BexCompiler.java
+++ b/src/main/java/blue/bex/compile/BexCompiler.java
@@ -8,6 +8,8 @@
import blue.bex.value.BexValue;
import blue.bex.value.BexValues;
import blue.bex.result.BexMetrics;
+import blue.language.model.Node;
+import blue.language.model.Schema;
import blue.language.snapshot.FrozenNode;
import java.util.ArrayDeque;
@@ -23,8 +25,11 @@
* Compiler from frozen BEX Blue data to specialized runtime objects.
*/
public final class BexCompiler {
+ private static final Set RESERVED_BLUE_KEYS = reservedBlueKeys();
+
private final BexContainsCache containsCache = new BexContainsCache();
private final BexMetrics metrics;
+ private Map functionSignatures = Collections.emptyMap();
private String currentFunction = "$root";
public BexCompiler(BexMetrics metrics) {
@@ -43,10 +48,11 @@ public BexCompiledProgram compile(blue.bex.api.BexProgramSource source) {
loadFunctions(functionNodes, prop(definition, "functions"));
loadFunctions(functionNodes, prop(step, "functions"));
rejectRecursion(functionNodes);
+ functionSignatures = compileFunctionSignatures(functionNodes);
Map compiledFunctions = new LinkedHashMap<>();
for (String name : functionNodes.keySet()) {
- compiledFunctions.put(name, compileFunction(name, functionNodes.get(name)));
+ compiledFunctions.put(name, compileFunction(name, functionNodes.get(name), functionSignatures.get(name)));
}
FrozenNode stepExpr = meaningful(prop(step, "expr"));
@@ -58,37 +64,37 @@ public BexCompiledProgram compile(blue.bex.api.BexProgramSource source) {
if (entry == null) {
throw new BexException("Unknown entry function: " + entryName);
}
- root = new BexCompiledProgram.CompiledFunction("$root", Collections.emptyList(),
+ 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 String[0], new CompiledExpression[0]))))),
+ new CallExpr(entryName, new int[0], new CompiledExpression[0]))))),
null, 0);
} else if (stepExpr != null) {
currentFunction = "$root";
- root = new BexCompiledProgram.CompiledFunction("$root", Collections.emptyList(),
+ root = new BexCompiledProgram.CompiledFunction("$root", Collections.emptyList(),
Collections.emptyList(), compileExpr(stepExpr, new CompileScope(), "/expr"), 0);
} else {
CompileScope scope = new CompileScope();
currentFunction = "$root";
List statements = compileStatements(meaningful(prop(step, "do")), scope, "/do");
rootFrameSize = scope.frameSize();
- root = new BexCompiledProgram.CompiledFunction("$root", Collections.emptyList(), statements, null, rootFrameSize);
+ root = new BexCompiledProgram.CompiledFunction("$root", Collections.emptyList(), statements, null, rootFrameSize);
}
return new BexCompiledProgram(root, compiledFunctions, constants, rootFrameSize, step.blueId());
}
- private BexCompiledProgram.CompiledFunction compileFunction(String name, FrozenNode functionNode) {
+ private BexCompiledProgram.CompiledFunction compileFunction(String name, FrozenNode functionNode, FunctionSignature signature) {
String previousFunction = currentFunction;
currentFunction = name;
CompileScope scope = new CompileScope();
- List args = new ArrayList<>();
- FrozenNode argsNode = prop(functionNode, "args");
- if (argsNode != null && argsNode.getProperties() != null) {
- args.addAll(argsNode.getProperties().keySet());
- Collections.sort(args);
- for (String arg : args) {
- scope.declareOrGetSlot(arg);
+ 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());
}
}
FrozenNode functionExpr = meaningful(prop(functionNode, "expr"));
@@ -97,11 +103,47 @@ private BexCompiledProgram.CompiledFunction compileFunction(String name, FrozenN
? compileStatements(meaningful(prop(functionNode, "do")), scope, "/functions/" + escape(name) + "/do")
: Collections.emptyList();
currentFunction = previousFunction;
- return new BexCompiledProgram.CompiledFunction(name, Collections.unmodifiableList(args),
+ return new BexCompiledProgram.CompiledFunction(name, signature.args(),
statements, expression, scope.frameSize());
}
+ private Map compileFunctionSignatures(Map functionNodes) {
+ Map signatures = new LinkedHashMap<>();
+ for (Map.Entry entry : functionNodes.entrySet()) {
+ signatures.put(entry.getKey(), compileFunctionSignature(entry.getKey(), entry.getValue()));
+ }
+ return signatures;
+ }
+
+ private FunctionSignature compileFunctionSignature(String name, FrozenNode functionNode) {
+ 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);
+ }
+ }
+ List args = new ArrayList<>();
+ for (int i = 0; i < names.size(); i++) {
+ String arg = names.get(i);
+ FrozenNode pattern = argsNode.getProperties().get(arg);
+ String sourcePointer = "/functions/" + escape(name) + "/args/" + escape(arg);
+ rejectBexAnywhereInStaticPattern(pattern, sourcePointer);
+ args.add(new BexCompiledProgram.ArgSpec(arg, i,
+ pattern,
+ sourcePointer));
+ }
+ return new FunctionSignature(Collections.unmodifiableList(args));
+ }
+
private void loadConstants(Map constants, FrozenNode node) {
+ validatePlainObjectContainer(node, "constants");
if (node == null || node.getProperties() == null) {
return;
}
@@ -111,6 +153,7 @@ private void loadConstants(Map constants, FrozenNode node) {
}
private void loadFunctions(Map functions, FrozenNode node) {
+ validatePlainObjectContainer(node, "functions");
if (node == null || node.getProperties() == null) {
return;
}
@@ -121,7 +164,9 @@ private void rejectRecursion(Map functions) {
Map> calls = new LinkedHashMap<>();
for (String name : functions.keySet()) {
Set targets = new LinkedHashSet<>();
- collectCalls(functions.get(name), targets);
+ FrozenNode function = functions.get(name);
+ collectCalls(meaningful(prop(function, "expr")), targets);
+ collectCalls(meaningful(prop(function, "do")), targets);
calls.put(name, targets);
}
for (String name : calls.keySet()) {
@@ -152,6 +197,10 @@ private void collectCalls(FrozenNode node, Set calls) {
if (isOperator(node, "$literal")) {
return;
}
+ if (isOperator(node, "$is")) {
+ collectCalls(prop(onlyValue(node), "node"), calls);
+ return;
+ }
if (isOperator(node, "$call")) {
FrozenNode body = onlyValue(node);
String function = text(prop(body, "function"));
@@ -255,6 +304,7 @@ private CompiledExpression compileExpr(FrozenNode node, CompileScope scope, Stri
if (node == null) {
return sourceExpr(currentFunction, pointer, null, new LiteralExpr(BexValues.nullValue()));
}
+ rejectBexInStaticBlueDefinitionFields(node, pointer);
if (!containsCache.containsBex(node, metrics)) {
return sourceExpr(currentFunction, pointer, null, new LiteralExpr(BexValues.frozen(node)));
}
@@ -270,18 +320,27 @@ private CompiledExpression compileExpr(FrozenNode node, CompileScope scope, Stri
}
}
}
- if (node.getItems() != null) {
+ 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) {
+ if (node.getProperties() != null || hasLanguageFields(node)) {
Map fields = new LinkedHashMap<>();
addMetadataFields(fields, node, scope, pointer);
- for (Map.Entry entry : node.getProperties().entrySet()) {
- fields.put(entry.getKey(), compileExpr(entry.getValue(), scope, pointer + "/" + escape(entry.getKey())));
+ 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 (Map.Entry entry : node.getProperties().entrySet()) {
+ fields.put(entry.getKey(), compileExpr(entry.getValue(), scope, pointer + "/" + escape(entry.getKey())));
+ }
}
return sourceExpr(currentFunction, pointer, null, new ObjectExpr(fields));
}
@@ -302,6 +361,7 @@ private CompiledExpression compileOperator(String op, FrozenNode body, CompileSc
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);
@@ -309,7 +369,7 @@ private CompiledExpression compileOperator(String op, FrozenNode body, CompileSc
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 ("$join".equals(op)) return new JoinExpr(compileExpr(required(prop(body, "items"), "$join.items"), scope, pointer + "/items"), compileExpr(required(prop(body, "separator"), "$join.separator"), scope, pointer + "/separator"));
+ 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);
@@ -344,6 +404,17 @@ private CompiledExpression compileOperator(String op, FrozenNode body, CompileSc
throw new BexException("Unknown expression operator: " + op);
}
+ private CompiledExpression isExpr(FrozenNode body, CompileScope scope, String pointer) {
+ if (body == null || body.getProperties() == null) {
+ throw new BexException("$is expects an object body");
+ }
+ FrozenNode pattern = required(prop(body, "pattern"), "$is.pattern");
+ rejectBexAnywhereInStaticPattern(pattern, pointer + "/pattern");
+ return new IsExpr(
+ compileExpr(required(prop(body, "node"), "$is.node"), scope, pointer + "/node"),
+ pattern);
+ }
+
private CompiledExpression documentExpr(FrozenNode body, CompileScope scope, String pointer) {
boolean resolved = false;
FrozenNode pointerNode = body;
@@ -392,17 +463,44 @@ private CompiledExpression stepsExpr(FrozenNode body, CompileScope scope, String
private CallExpr compileCall(FrozenNode body, CompileScope scope, String pointer) {
String function = requiredText(prop(body, "function"), "$call.function");
- List argNames = new ArrayList<>();
+ 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 && argsNode.getProperties() != null) {
- for (Map.Entry entry : argsNode.getProperties().entrySet()) {
- argNames.add(entry.getKey());
- argExpressions.add(compileExpr(entry.getValue(), scope, pointer + "/args/" + escape(entry.getKey())));
+ 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 (Map.Entry entry : argsNode.getProperties().entrySet()) {
+ String argName = entry.getKey();
+ BexCompiledProgram.ArgSpec arg = signature.arg(argName);
+ if (arg == null) {
+ throw new BexException("Unknown argument " + argName + " for function " + function);
+ }
+ providedArgs.add(argName);
+ targetSlots.add(arg.slot());
+ argExpressions.add(compileExpr(entry.getValue(), scope, pointer + "/args/" + escape(argName)));
+ }
+ }
+ }
+ 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,
- argNames.toArray(new String[0]),
+ slots,
argExpressions.toArray(new CompiledExpression[0]));
}
@@ -470,13 +568,7 @@ private FrozenNode prop(FrozenNode node, String key) {
}
private FrozenNode meaningful(FrozenNode node) {
- if (node == null) {
- return null;
- }
- boolean hasPayload = node.getValue() != null
- || (node.getItems() != null && !node.getItems().isEmpty())
- || (node.getProperties() != null && !node.getProperties().isEmpty());
- return hasPayload ? node : null;
+ return node == null || node.isEmptyNode() ? null : node;
}
private FrozenNode required(FrozenNode node, String label) {
@@ -519,9 +611,43 @@ private void addMetadataFields(Map fields, FrozenNod
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 LiteralExpr(BexValues.scalar(node.getValue())));
}
+ if (node.getReferenceBlueId() != null) {
+ fields.put("blueId", new LiteralExpr(BexValues.scalar(node.getReferenceBlueId())));
+ }
+ if (node.getBlue() != null) {
+ fields.put("blue", compileExpr(node.getBlue(), scope, pointer + "/blue"));
+ }
+ 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 LiteralExpr(BexValues.scalar(node.getMergePolicy())));
+ }
+ }
+
+ private boolean hasLanguageFields(FrozenNode node) {
+ return node.getName() != null
+ || node.getDescription() != null
+ || node.getType() != null
+ || node.getItemType() != null
+ || node.getKeyType() != null
+ || node.getValueType() != null
+ || node.getReferenceBlueId() != null
+ || node.getBlue() != null
+ || node.getSchema() != null
+ || node.getMergePolicy() != null;
}
private FrozenNode scalarNode(Object value) {
@@ -540,4 +666,156 @@ private String escape(String segment) {
return segment.replace("~", "~0").replace("/", "~1");
}
+ private void validatePlainObjectContainer(FrozenNode node, String label) {
+ if (node == null) {
+ return;
+ }
+ if (node.getValue() != null || node.getItems() != null || node.getReferenceBlueId() != null) {
+ throw new BexException(label + " must be a plain object with non-reserved field names");
+ }
+ if (node.getPreviousBlueId() != null || node.getPosition() != null) {
+ throw new BexException(label + " contains a Blue list-control key; use non-reserved names");
+ }
+ if (node.getName() != null
+ || node.getDescription() != null
+ || node.getType() != null
+ || node.getItemType() != null
+ || node.getKeyType() != null
+ || node.getValueType() != null
+ || node.getBlue() != null
+ || node.getSchema() != null
+ || node.getMergePolicy() != null) {
+ throw new BexException(label + " contains a Blue language key; use non-reserved names");
+ }
+ if (node.getProperties() != null) {
+ for (String key : node.getProperties().keySet()) {
+ if (RESERVED_BLUE_KEYS.contains(key)) {
+ throw new BexException(label + " contains reserved Blue key: " + key);
+ }
+ }
+ }
+ }
+
+ private void rejectBexAnywhereInStaticPattern(FrozenNode pattern, String pointer) {
+ if (pattern != null && containsCache.containsBex(pattern, metrics)) {
+ throw new BexException("BEX expressions inside static Blue patterns are not supported at " + pointer);
+ }
+ rejectBexInStaticBlueDefinitionFields(pattern, pointer);
+ }
+
+ private void rejectBexInStaticBlueDefinitionFields(FrozenNode node, String pointer) {
+ if (node == null) {
+ return;
+ }
+ rejectBexInStaticField(node.getType(), pointer + "/type", "type");
+ rejectBexInStaticField(node.getItemType(), pointer + "/itemType", "itemType");
+ rejectBexInStaticField(node.getKeyType(), pointer + "/keyType", "keyType");
+ rejectBexInStaticField(node.getValueType(), pointer + "/valueType", "valueType");
+ rejectBexInStaticField(node.getBlue(), pointer + "/blue", "blue");
+ if (node.getSchema() != null) {
+ rejectBexInSchema(node.getSchema(), pointer + "/schema");
+ }
+ if (node.getItems() != null) {
+ for (int i = 0; i < node.getItems().size(); i++) {
+ rejectBexInStaticBlueDefinitionFields(node.getItems().get(i), pointer + "/" + i);
+ }
+ }
+ if (node.getProperties() != null) {
+ for (Map.Entry entry : node.getProperties().entrySet()) {
+ rejectBexInStaticBlueDefinitionFields(entry.getValue(), pointer + "/" + escape(entry.getKey()));
+ }
+ }
+ }
+
+ private void rejectBexInStaticField(FrozenNode field, String pointer, String fieldName) {
+ if (field != null && containsCache.containsBex(field, metrics)) {
+ throw new BexException("BEX expressions inside Blue " + fieldName
+ + " fields are not supported at " + pointer);
+ }
+ rejectBexInStaticBlueDefinitionFields(field, pointer);
+ }
+
+ private void rejectBexInSchema(Schema schema, String pointer) {
+ rejectSchemaNode(schema.getRequired(), pointer);
+ rejectSchemaNode(schema.getAllowMultiple(), 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);
+ }
+ }
+ if (schema.getOptions() != null) {
+ for (Node node : schema.getOptions()) {
+ rejectSchemaNode(node, pointer);
+ }
+ }
+ }
+
+ private void rejectSchemaNode(Node node, String pointer) {
+ if (node == null) {
+ return;
+ }
+ FrozenNode frozen = FrozenNode.fromResolvedNode(node);
+ if (containsCache.containsBex(frozen, metrics)) {
+ throw new BexException("BEX expressions inside schema are not supported at " + pointer);
+ }
+ rejectBexInStaticBlueDefinitionFields(frozen, pointer);
+ }
+
+ private static Set reservedBlueKeys() {
+ Set keys = new LinkedHashSet<>();
+ Collections.addAll(keys,
+ "name",
+ "description",
+ "type",
+ "itemType",
+ "keyType",
+ "valueType",
+ "value",
+ "items",
+ "blueId",
+ "blue",
+ "schema",
+ "constraints",
+ "mergePolicy",
+ "properties",
+ "contracts",
+ "$previous",
+ "$pos");
+ return Collections.unmodifiableSet(keys);
+ }
+
+ private static final class FunctionSignature {
+ private final List args;
+ private final Map argsByName;
+
+ private FunctionSignature(List args) {
+ this.args = args;
+ Map byName = new LinkedHashMap<>();
+ for (BexCompiledProgram.ArgSpec arg : args) {
+ byName.put(arg.name(), arg);
+ }
+ this.argsByName = Collections.unmodifiableMap(byName);
+ }
+
+ private List args() {
+ return args;
+ }
+
+ private BexCompiledProgram.ArgSpec arg(String name) {
+ return argsByName.get(name);
+ }
+ }
+
}
diff --git a/src/main/java/blue/bex/compile/BexContainsCache.java b/src/main/java/blue/bex/compile/BexContainsCache.java
index a4b7408..289edf0 100644
--- a/src/main/java/blue/bex/compile/BexContainsCache.java
+++ b/src/main/java/blue/bex/compile/BexContainsCache.java
@@ -47,6 +47,16 @@ public synchronized boolean containsBex(FrozenNode node, BexMetrics metrics) {
}
private boolean scan(FrozenNode node) {
+ if (node == null) {
+ return false;
+ }
+ if (scan(node.getType())
+ || scan(node.getItemType())
+ || scan(node.getKeyType())
+ || scan(node.getValueType())
+ || scan(node.getBlue())) {
+ return true;
+ }
if (node.getProperties() != null) {
if (node.getProperties().size() == 1) {
String key = node.getProperties().keySet().iterator().next();
diff --git a/src/main/java/blue/bex/compile/CallExpr.java b/src/main/java/blue/bex/compile/CallExpr.java
index b21146c..3fe0789 100644
--- a/src/main/java/blue/bex/compile/CallExpr.java
+++ b/src/main/java/blue/bex/compile/CallExpr.java
@@ -1,40 +1,27 @@
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 {
private final String function;
- private final String[] argNames;
+ private final int[] targetSlots;
private final CompiledExpression[] argExpressions;
- private BexCompiledProgram.CompiledFunction cachedFunction;
- private int[] cachedTargetSlots;
- CallExpr(String function, String[] argNames, CompiledExpression[] argExpressions) {
+ CallExpr(String function, int[] targetSlots, CompiledExpression[] argExpressions) {
this.function = function;
- this.argNames = argNames;
+ this.targetSlots = targetSlots;
this.argExpressions = argExpressions;
}
@Override
protected BexValue doEval(CompiledFrame frame) {
- BexCompiledProgram.CompiledFunction compiled = cachedFunction;
- if (compiled == null) {
- compiled = frame.runtime().program().functions().get(function);
- if (compiled == null) throw new BexException("Unknown function: " + function);
- int[] slots = new int[argNames.length];
- for (int i = 0; i < argNames.length; i++) {
- slots[i] = compiled.argSlot(argNames[i]);
- }
- cachedTargetSlots = slots;
- cachedFunction = compiled;
- }
+ BexCompiledProgram.CompiledFunction compiled = frame.runtime().program().functions().get(function);
BexValue[] values = new BexValue[argExpressions.length];
for (int i = 0; i < argExpressions.length; i++) {
values[i] = argExpressions[i].eval(frame);
}
- return compiled.invokePrepared(frame.runtime(), frame, cachedTargetSlots, values);
+ return compiled.invokePrepared(frame.runtime(), frame, targetSlots, values);
}
}
diff --git a/src/main/java/blue/bex/compile/TypeStringExpressions.java b/src/main/java/blue/bex/compile/TypeStringExpressions.java
index fdd0983..24ac18f 100644
--- a/src/main/java/blue/bex/compile/TypeStringExpressions.java
+++ b/src/main/java/blue/bex/compile/TypeStringExpressions.java
@@ -5,6 +5,7 @@
import blue.bex.runtime.CompiledFrame;
import blue.bex.value.BexValue;
import blue.bex.value.BexValues;
+import blue.language.snapshot.FrozenNode;
import java.math.BigInteger;
import java.util.ArrayList;
@@ -75,6 +76,22 @@ protected BexValue doEval(CompiledFrame frame) {
}
}
+final class IsExpr extends Expr {
+ private final CompiledExpression valueExpression;
+ private final FrozenNode pattern;
+
+ IsExpr(CompiledExpression valueExpression, FrozenNode pattern) {
+ this.valueExpression = valueExpression;
+ this.pattern = pattern;
+ }
+
+ @Override
+ protected BexValue doEval(CompiledFrame frame) {
+ BexValue value = valueExpression.eval(frame);
+ return BexValues.scalar(frame.runtime().typeMatcher().matches(value, pattern));
+ }
+}
+
enum VariadicOp { CONCAT, LIST_CONCAT, MERGE }
final class VariadicExpr extends Expr {
@@ -124,7 +141,7 @@ final class JoinExpr extends Expr {
@Override
protected BexValue doEval(CompiledFrame frame) {
BexValue list = items.eval(frame);
- if (!list.isList()) throw new BexException("$join items must be a list");
+ if (!list.isList()) throw new BexException("$join list must be a list");
String sep = separator.eval(frame).asText();
StringBuilder out = new StringBuilder();
for (int i = 0; i < list.size(); i++) {
diff --git a/src/main/java/blue/bex/runtime/BexRuntime.java b/src/main/java/blue/bex/runtime/BexRuntime.java
index 3b2a597..ea2d408 100644
--- a/src/main/java/blue/bex/runtime/BexRuntime.java
+++ b/src/main/java/blue/bex/runtime/BexRuntime.java
@@ -9,8 +9,10 @@
import blue.bex.result.BexExecutionResult;
import blue.bex.result.BexMetrics;
import blue.bex.result.BexResultOverlay;
+import blue.bex.type.BexBlueTypeMatcher;
import blue.bex.value.BexValue;
import blue.bex.value.BexValues;
+import blue.language.Blue;
import blue.language.utils.JsonPointer;
import java.util.LinkedHashMap;
@@ -27,9 +29,11 @@ public final class BexRuntime {
private final BexMetrics metrics;
private final BexPointerCache pointerCache;
private final BexExecutionAccumulator accumulator;
+ private final BexBlueTypeMatcher typeMatcher;
public BexRuntime(BexCompiledProgram program,
BexExecutionContext context,
+ Blue blue,
BexGasSchedule gasSchedule,
BexMetrics metrics,
BexPointerCache pointerCache) {
@@ -39,6 +43,7 @@ public BexRuntime(BexCompiledProgram program,
this.metrics = metrics;
this.pointerCache = pointerCache;
this.accumulator = new BexExecutionAccumulator(new BexResultOverlay(context.document(), metrics));
+ this.typeMatcher = new BexBlueTypeMatcher(blue);
}
public BexExecutionResult execute() {
@@ -53,6 +58,7 @@ public BexExecutionResult execute() {
public BexMetrics metrics() { return metrics; }
public BexPointerCache pointerCache() { return pointerCache; }
public BexExecutionAccumulator accumulator() { return accumulator; }
+ public BexBlueTypeMatcher typeMatcher() { return typeMatcher; }
public BexValue readDocument(String absolutePointer, List precompiledSegments, boolean resolved) {
if (resolved) {
diff --git a/src/main/java/blue/bex/type/BexBlueTypeMatcher.java b/src/main/java/blue/bex/type/BexBlueTypeMatcher.java
new file mode 100644
index 0000000..869d264
--- /dev/null
+++ b/src/main/java/blue/bex/type/BexBlueTypeMatcher.java
@@ -0,0 +1,34 @@
+package blue.bex.type;
+
+import blue.bex.value.BexNodeWriter;
+import blue.bex.value.BexValue;
+import blue.language.Blue;
+import blue.language.model.Node;
+import blue.language.snapshot.FrozenNode;
+
+/**
+ * BEX boundary adapter for Blue's node/type matcher.
+ */
+public final class BexBlueTypeMatcher {
+ private final Blue blue;
+
+ public BexBlueTypeMatcher(Blue blue) {
+ this.blue = blue != null ? blue : new Blue();
+ }
+
+ public boolean matches(BexValue value, FrozenNode pattern) {
+ if (value == null || value.isUndefined()) {
+ return false;
+ }
+ if (pattern == null || pattern.isEmptyNode()) {
+ return true;
+ }
+ try {
+ Node valueNode = BexNodeWriter.toNode(value);
+ Node patternNode = pattern.toNode();
+ return blue.nodeMatchesType(valueNode, patternNode);
+ } catch (RuntimeException ex) {
+ return false;
+ }
+ }
+}
diff --git a/src/main/java/blue/bex/value/BexBlueNodeWriter.java b/src/main/java/blue/bex/value/BexBlueNodeWriter.java
new file mode 100644
index 0000000..c9d8538
--- /dev/null
+++ b/src/main/java/blue/bex/value/BexBlueNodeWriter.java
@@ -0,0 +1,257 @@
+package blue.bex.value;
+
+import blue.bex.BexException;
+import blue.language.model.Node;
+import blue.language.model.Schema;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+
+/**
+ * Boundary writer from BEX values to Blue nodes using Blue language keys.
+ */
+public final class BexBlueNodeWriter {
+ private BexBlueNodeWriter() {
+ }
+
+ public static Node toNode(BexValue value) {
+ if (value.isUndefined()) {
+ throw new BexException("Undefined cannot be emitted as a Blue value");
+ }
+ if (value.isNull()) {
+ return new Node();
+ }
+ if (value instanceof NodeBexValue || value instanceof FrozenNodeBexValue) {
+ return value.toNode();
+ }
+ if (value.isScalar()) {
+ return value.toNode();
+ }
+ if (value.isList()) {
+ return new Node().items(toNodeList(value));
+ }
+ if (!value.isObject()) {
+ return value.toNode();
+ }
+
+ if (!value.get("properties").isUndefined()) {
+ throw new BexException("\"properties\" is an internal Blue field and must not appear in BEX output");
+ }
+ validateBlueIdReferenceShape(value);
+
+ Node node = new Node();
+ LinkedHashMap properties = new LinkedHashMap<>();
+ boolean hasValuePayload = false;
+ boolean hasItemsPayload = false;
+ boolean hasSchema = false;
+ for (String key : value.keys()) {
+ BexValue child = value.get(key);
+ if (child == null || child.isUndefined()) {
+ continue;
+ }
+ if ("name".equals(key)) {
+ node.name(child.isNull() ? null : child.asText());
+ } else if ("description".equals(key)) {
+ node.description(child.isNull() ? null : child.asText());
+ } else if ("type".equals(key)) {
+ node.type(toNode(child));
+ } else if ("itemType".equals(key)) {
+ node.itemType(toNode(child));
+ } else if ("keyType".equals(key)) {
+ node.keyType(toNode(child));
+ } else if ("valueType".equals(key)) {
+ node.valueType(toNode(child));
+ } else if ("mergePolicy".equals(key)) {
+ node.mergePolicy(child.isNull() ? null : child.asText());
+ } else if ("value".equals(key)) {
+ hasValuePayload = true;
+ node.value(scalarValue(child, "value"));
+ } else if ("items".equals(key)) {
+ hasItemsPayload = true;
+ if (!child.isList()) {
+ throw new BexException("Blue items field must be a list");
+ }
+ node.items(toNodeList(child));
+ } else if ("blueId".equals(key)) {
+ node.blueId(child.asText());
+ } else if ("blue".equals(key)) {
+ node.blue(toNode(child));
+ } else if ("schema".equals(key) || "constraints".equals(key)) {
+ if (hasSchema) {
+ throw new BexException("A Blue node cannot contain both schema and constraints");
+ }
+ hasSchema = true;
+ node.schema(toSchema(child));
+ } else if ("$previous".equals(key) || "$pos".equals(key)) {
+ throw new BexException("BEX output does not currently support Blue list-control field " + key);
+ } else if ("properties".equals(key)) {
+ throw new BexException("\"properties\" is an internal Blue field and must not appear in BEX output");
+ } else {
+ properties.put(key, toNode(child));
+ }
+ }
+ int payloadKinds = 0;
+ if (hasValuePayload) {
+ payloadKinds++;
+ }
+ if (hasItemsPayload) {
+ payloadKinds++;
+ }
+ if (!properties.isEmpty()) {
+ payloadKinds++;
+ }
+ if (payloadKinds > 1) {
+ throw new BexException("A Blue node may contain only one payload kind: value, items, or object fields");
+ }
+ if (!properties.isEmpty()) {
+ node.properties(properties);
+ }
+ return node;
+ }
+
+ public static boolean hasLanguageField(BexValue value) {
+ if (value == null || !value.isObject()) {
+ return false;
+ }
+ for (String key : value.keys()) {
+ if (isLanguageField(key)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static boolean isLanguageField(String key) {
+ return "name".equals(key)
+ || "description".equals(key)
+ || "type".equals(key)
+ || "itemType".equals(key)
+ || "keyType".equals(key)
+ || "valueType".equals(key)
+ || "value".equals(key)
+ || "items".equals(key)
+ || "blueId".equals(key)
+ || "blue".equals(key)
+ || "schema".equals(key)
+ || "constraints".equals(key)
+ || "mergePolicy".equals(key)
+ || "properties".equals(key)
+ || "$previous".equals(key)
+ || "$pos".equals(key);
+ }
+
+ private static void validateBlueIdReferenceShape(BexValue value) {
+ if (value.get("blueId").isUndefined()) {
+ return;
+ }
+ for (String key : value.keys()) {
+ if (!"blueId".equals(key)) {
+ throw new BexException("Blue blueId reference node cannot contain sibling field: " + key);
+ }
+ }
+ }
+
+ private static Object scalarValue(BexValue value, String field) {
+ if (value.isNull()) {
+ return null;
+ }
+ if (!value.isScalar()) {
+ throw new BexException("Blue " + field + " field must be a scalar value");
+ }
+ return BexValues.rawScalar(value);
+ }
+
+ private static List toNodeList(BexValue value) {
+ ArrayList items = new ArrayList<>();
+ for (int i = 0; i < value.size(); i++) {
+ items.add(toNode(value.get(String.valueOf(i))));
+ }
+ return items;
+ }
+
+ private static Schema toSchema(BexValue value) {
+ if (value.isNull() || value.isUndefined()) {
+ return null;
+ }
+ Node node = toNode(value);
+ if (node.getSchema() != null && node.getValue() == null
+ && node.getItems() == null && node.getProperties() == null) {
+ return node.getSchema();
+ }
+ if (!value.isObject()) {
+ throw new BexException("Blue schema field must be an object");
+ }
+ Schema schema = new Schema();
+ setSchemaNode(schema, value, "required");
+ setSchemaNode(schema, value, "allowMultiple");
+ setSchemaNode(schema, value, "minLength");
+ setSchemaNode(schema, value, "maxLength");
+ setSchemaNode(schema, value, "minimum");
+ setSchemaNode(schema, value, "maximum");
+ setSchemaNode(schema, value, "exclusiveMinimum");
+ setSchemaNode(schema, value, "exclusiveMaximum");
+ setSchemaNode(schema, value, "multipleOf");
+ setSchemaNode(schema, value, "minItems");
+ setSchemaNode(schema, value, "maxItems");
+ setSchemaNode(schema, value, "uniqueItems");
+ setSchemaNode(schema, value, "minFields");
+ setSchemaNode(schema, value, "maxFields");
+ setSchemaList(schema, value, "enum");
+ setSchemaList(schema, value, "options");
+ return schema;
+ }
+
+ private static void setSchemaNode(Schema schema, BexValue source, String key) {
+ BexValue value = source.get(key);
+ if (value.isUndefined()) {
+ return;
+ }
+ Node node = toNode(value);
+ if ("required".equals(key)) {
+ schema.required(node);
+ } else if ("allowMultiple".equals(key)) {
+ schema.allowMultiple(node);
+ } else if ("minLength".equals(key)) {
+ schema.minLength(node);
+ } else if ("maxLength".equals(key)) {
+ schema.maxLength(node);
+ } else if ("minimum".equals(key)) {
+ schema.minimum(node);
+ } else if ("maximum".equals(key)) {
+ schema.maximum(node);
+ } else if ("exclusiveMinimum".equals(key)) {
+ schema.exclusiveMinimum(node);
+ } else if ("exclusiveMaximum".equals(key)) {
+ schema.exclusiveMaximum(node);
+ } else if ("multipleOf".equals(key)) {
+ schema.multipleOf(node);
+ } else if ("minItems".equals(key)) {
+ schema.minItems(node);
+ } else if ("maxItems".equals(key)) {
+ schema.maxItems(node);
+ } else if ("uniqueItems".equals(key)) {
+ schema.uniqueItems(node);
+ } else if ("minFields".equals(key)) {
+ schema.minFields(node);
+ } else if ("maxFields".equals(key)) {
+ schema.maxFields(node);
+ }
+ }
+
+ private static void setSchemaList(Schema schema, BexValue source, String key) {
+ BexValue value = source.get(key);
+ if (value.isUndefined()) {
+ return;
+ }
+ if (!value.isList()) {
+ throw new BexException("Blue schema " + key + " field must be a list");
+ }
+ List nodes = toNodeList(value);
+ if ("enum".equals(key)) {
+ schema.enumValues(nodes);
+ } else if ("options".equals(key)) {
+ schema.options(nodes);
+ }
+ }
+}
diff --git a/src/main/java/blue/bex/value/BexFrozenWriter.java b/src/main/java/blue/bex/value/BexFrozenWriter.java
index cf55ea6..67f8b29 100644
--- a/src/main/java/blue/bex/value/BexFrozenWriter.java
+++ b/src/main/java/blue/bex/value/BexFrozenWriter.java
@@ -56,6 +56,9 @@ private FrozenNode toFrozenInternal(BexValue value) {
return factory.list(items, metrics);
}
if (value.isObject()) {
+ if (BexBlueNodeWriter.hasLanguageField(value)) {
+ return FrozenNode.fromResolvedNode(BexBlueNodeWriter.toNode(value));
+ }
Map properties = new LinkedHashMap<>();
for (String key : value.keys()) {
BexValue child = value.get(key);
diff --git a/src/main/java/blue/bex/value/BexNodeWriter.java b/src/main/java/blue/bex/value/BexNodeWriter.java
index 6c2371d..c198f97 100644
--- a/src/main/java/blue/bex/value/BexNodeWriter.java
+++ b/src/main/java/blue/bex/value/BexNodeWriter.java
@@ -2,9 +2,6 @@
import blue.language.model.Node;
-import java.util.ArrayList;
-import java.util.LinkedHashMap;
-
/**
* Boundary writer from BEX values to mutable Blue nodes.
*/
@@ -13,32 +10,6 @@ private BexNodeWriter() {
}
public static Node toNode(BexValue value) {
- if (value.isUndefined()) {
- throw new blue.bex.BexException("Undefined cannot be emitted as a Blue value");
- }
- if (value.isNull()) {
- return new Node();
- }
- if (value instanceof NodeBexValue || value instanceof FrozenNodeBexValue) {
- return value.toNode();
- }
- if (value.isScalar()) {
- return value.toNode();
- }
- if (value.isList()) {
- ArrayList items = new ArrayList<>();
- for (int i = 0; i < value.size(); i++) {
- items.add(toNode(value.get(String.valueOf(i))));
- }
- return new Node().items(items);
- }
- LinkedHashMap properties = new LinkedHashMap<>();
- for (String key : value.keys()) {
- BexValue child = value.get(key);
- if (!child.isUndefined()) {
- properties.put(key, toNode(child));
- }
- }
- return new Node().properties(properties);
+ return BexBlueNodeWriter.toNode(value);
}
}
diff --git a/src/main/java/blue/bex/value/MapBexValue.java b/src/main/java/blue/bex/value/MapBexValue.java
index a515780..1ca9ab6 100644
--- a/src/main/java/blue/bex/value/MapBexValue.java
+++ b/src/main/java/blue/bex/value/MapBexValue.java
@@ -50,16 +50,7 @@ public List keys() {
@Override
public Node toNode() {
- Node node = new Node();
- LinkedHashMap properties = new LinkedHashMap<>();
- for (String key : keys()) {
- BexValue value = values.get(key);
- if (value != null && !value.isUndefined()) {
- properties.put(key, value.toNode());
- }
- }
- node.properties(properties);
- return node;
+ return BexNodeWriter.toNode(this);
}
@Override
diff --git a/src/test/java/blue/bex/BexBlueTypeSupportTest.java b/src/test/java/blue/bex/BexBlueTypeSupportTest.java
new file mode 100644
index 0000000..b80de92
--- /dev/null
+++ b/src/test/java/blue/bex/BexBlueTypeSupportTest.java
@@ -0,0 +1,836 @@
+package blue.bex;
+
+import blue.bex.api.BexEngine;
+import blue.bex.api.BexProgramSource;
+import blue.bex.compile.BexCompiledProgram;
+import blue.bex.result.BexExecutionResult;
+import blue.bex.value.BexFrozenWriter;
+import blue.bex.value.BexNodeWriter;
+import blue.bex.value.BexValue;
+import blue.bex.value.BexValues;
+import blue.language.Blue;
+import blue.language.model.Node;
+import blue.language.snapshot.FrozenNode;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigInteger;
+import java.util.Collections;
+
+import static blue.bex.test.BexTestFixtures.bi;
+import static blue.bex.test.BexTestFixtures.defaultContext;
+import static blue.bex.test.BexTestFixtures.list;
+import static blue.bex.test.BexTestFixtures.m;
+import static blue.bex.test.BexTestFixtures.obj;
+import static blue.bex.test.BexTestFixtures.op;
+import static blue.bex.test.BexTestFixtures.simple;
+import static blue.bex.test.BexTestFixtures.stepExpr;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class BexBlueTypeSupportTest {
+ private static final Blue YAML_BLUE = new Blue();
+ private final Blue blue = new Blue(blueId -> {
+ if ("HotelOrderType".equals(blueId)) {
+ return Collections.singletonList(YAML_BLUE.yamlToNode(yaml(
+ "status:",
+ " type: Text")));
+ }
+ if ("RestaurantOrderType".equals(blueId)) {
+ return Collections.singletonList(YAML_BLUE.yamlToNode(yaml(
+ "restaurantStatus:",
+ " type: Text")));
+ }
+ return Collections.emptyList();
+ });
+ private final BexEngine engine = BexEngine.builder().blue(blue).build();
+
+ @Test
+ void functionArgAcceptsMatchingPrimitiveType() {
+ BexExecutionResult result = run(yaml(
+ "type: Blue/BEX Program",
+ "functions:",
+ " f:",
+ " args:",
+ " amount:",
+ " type: Integer",
+ " expr:",
+ " $var: amount",
+ "expr:",
+ " $call:",
+ " function: f",
+ " args:",
+ " amount: 400"));
+
+ assertEquals(BigInteger.valueOf(400), simple(result.value()));
+ }
+
+ @Test
+ void functionArgRejectsWrongPrimitiveTypeAtRuntime() {
+ BexCompiledProgram program = compile(yaml(
+ "type: Blue/BEX Program",
+ "functions:",
+ " f:",
+ " args:",
+ " amount:",
+ " type: Integer",
+ " expr:",
+ " $var: amount",
+ "expr:",
+ " $call:",
+ " function: f",
+ " args:",
+ " amount: \"400\""));
+
+ BexException ex = assertThrows(BexException.class, () -> engine.execute(program, defaultContext()));
+ assertTrue(ex.getMessage().contains("Function f argument amount does not match declared Blue pattern at /functions/f/args/amount"));
+ }
+
+ @Test
+ void functionArgAcceptsStructuralObjectPattern() {
+ BexExecutionResult result = run(yaml(
+ "type: Blue/BEX Program",
+ "functions:",
+ " f:",
+ " args:",
+ " request:",
+ " customerName:",
+ " type: Text",
+ " schema:",
+ " required: true",
+ " nights:",
+ " type: Integer",
+ " schema:",
+ " required: true",
+ " expr:",
+ " $var: request",
+ "expr:",
+ " $call:",
+ " function: f",
+ " args:",
+ " request:",
+ " customerName: Jan",
+ " nights: 2"));
+
+ assertEquals(m("customerName", "Jan", "nights", bi(2)), simple(result.value()));
+ }
+
+ @Test
+ void functionArgRejectsMissingRequiredStructuralField() {
+ BexCompiledProgram program = compile(yaml(
+ "type: Blue/BEX Program",
+ "functions:",
+ " f:",
+ " args:",
+ " request:",
+ " customerName:",
+ " type: Text",
+ " schema:",
+ " required: true",
+ " nights:",
+ " type: Integer",
+ " schema:",
+ " required: true",
+ " expr:",
+ " $var: request",
+ "expr:",
+ " $call:",
+ " function: f",
+ " args:",
+ " request:",
+ " customerName: Jan"));
+
+ assertThrows(BexException.class, () -> engine.execute(program, defaultContext()));
+ }
+
+ @Test
+ void functionArgRejectsUnknownArgAtCompileTime() {
+ assertThrows(BexException.class, () -> compile(yaml(
+ "type: Blue/BEX Program",
+ "functions:",
+ " f:",
+ " args:",
+ " amount:",
+ " type: Integer",
+ " expr:",
+ " $var: amount",
+ "expr:",
+ " $call:",
+ " function: f",
+ " args:",
+ " amount: 400",
+ " extra: true")));
+ }
+
+ @Test
+ void functionArgRejectsMissingArgAtCompileTime() {
+ assertThrows(BexException.class, () -> compile(yaml(
+ "type: Blue/BEX Program",
+ "functions:",
+ " f:",
+ " args:",
+ " amount:",
+ " type: Integer",
+ " expr:",
+ " $var: amount",
+ "expr:",
+ " $call:",
+ " function: f",
+ " args: {}")));
+ }
+
+ @Test
+ void untypedArgStillWorks() {
+ BexExecutionResult result = run(yaml(
+ "type: Blue/BEX Program",
+ "functions:",
+ " f:",
+ " args:",
+ " input: {}",
+ " expr:",
+ " $var: input",
+ "expr:",
+ " $call:",
+ " function: f",
+ " args:",
+ " input:",
+ " anything: works"));
+
+ assertEquals(m("anything", "works"), simple(result.value()));
+ }
+
+ @Test
+ void isUsesNodeFieldInYamlSyntax() {
+ BexExecutionResult result = run(yaml(
+ "type: Blue/BEX Program",
+ "expr:",
+ " $is:",
+ " node: 400",
+ " pattern:",
+ " type: Integer"));
+
+ assertEquals(true, simple(result.value()));
+ }
+
+ @Test
+ void isUsesNodeFieldAndReturnsFalseForWrongPrimitiveInYamlSyntax() {
+ BexExecutionResult result = run(yaml(
+ "type: Blue/BEX Program",
+ "expr:",
+ " $is:",
+ " node: \"400\"",
+ " pattern:",
+ " type: Integer"));
+
+ assertEquals(false, simple(result.value()));
+ }
+
+ @Test
+ void isIntegerPatternExamplesMatchOnlyIntegerNodes() {
+ assertEquals(true, simple(run(yaml(
+ "type: Blue/BEX Program",
+ "expr:",
+ " $is:",
+ " node: 400",
+ " pattern:",
+ " type: Integer")).value()));
+ assertEquals(true, simple(run(yaml(
+ "type: Blue/BEX Program",
+ "expr:",
+ " $is:",
+ " node:",
+ " $integer: \"400\"",
+ " pattern:",
+ " type: Integer")).value()));
+ assertEquals(true, simple(run(yaml(
+ "type: Blue/BEX Program",
+ "expr:",
+ " $is:",
+ " node:",
+ " type: Integer",
+ " value: 400",
+ " pattern:",
+ " type: Integer")).value()));
+ assertEquals(false, simple(run(yaml(
+ "type: Blue/BEX Program",
+ "expr:",
+ " $is:",
+ " node: \"400\"",
+ " pattern:",
+ " type: Integer")).value()));
+ assertEquals(false, simple(run(yaml(
+ "type: Blue/BEX Program",
+ "expr:",
+ " $is:",
+ " node: 4.5",
+ " pattern:",
+ " type: Integer")).value()));
+ assertEquals(false, simple(run(yaml(
+ "type: Blue/BEX Program",
+ "expr:",
+ " $is:",
+ " node: true",
+ " pattern:",
+ " type: Integer")).value()));
+ assertEquals(false, simple(run(yaml(
+ "type: Blue/BEX Program",
+ "expr:",
+ " $is:",
+ " node:",
+ " $document: /missing",
+ " pattern:",
+ " type: Integer")).value()));
+ }
+
+ @Test
+ void oldIsValueShapeIsInvalidBlueYaml() {
+ assertThrows(IllegalArgumentException.class, () -> blue.yamlToNode(yaml(
+ "type: Blue/BEX Program",
+ "expr:",
+ " $is:",
+ " value: 400",
+ " pattern:",
+ " type: Integer")));
+ }
+
+ @Test
+ void joinUsesListFieldInYamlSyntax() {
+ BexExecutionResult result = run(yaml(
+ "type: Blue/BEX Program",
+ "expr:",
+ " $join:",
+ " list:",
+ " - a",
+ " - b",
+ " separator: \":\""));
+
+ assertEquals("a:b", simple(result.value()));
+ }
+
+ @Test
+ void oldJoinItemsShapeIsInvalidBlueYaml() {
+ assertThrows(IllegalArgumentException.class, () -> blue.yamlToNode(yaml(
+ "type: Blue/BEX Program",
+ "expr:",
+ " $join:",
+ " items:",
+ " - a",
+ " - b",
+ " separator: \":\"")));
+ }
+
+ @Test
+ void reservedArgNameFromInternalShapeFailsCompileTime() {
+ BexException ex = assertThrows(BexException.class, () -> engine.compile(BexProgramSource.inline(FrozenNode.fromResolvedNode(obj(
+ "type", "Blue/BEX Program",
+ "functions", obj("f", obj(
+ "args", obj("value", obj()),
+ "expr", op("$var", "value"))),
+ "expr", op("$call", obj(
+ "function", "f",
+ "args", obj("value", 1))))))));
+
+ assertTrue(ex.getMessage().contains("reserved Blue key: value"));
+ }
+
+ @Test
+ void reservedArgNameIsInvalidOrRejectedInYaml() {
+ assertThrows(RuntimeException.class, () -> run(yaml(
+ "type: Blue/BEX Program",
+ "functions:",
+ " f:",
+ " args:",
+ " value: {}",
+ " expr:",
+ " $var: value",
+ "expr:",
+ " $call:",
+ " function: f",
+ " args:",
+ " value: 1")));
+ }
+
+ @Test
+ void contractsIsReservedAsFunctionArgumentName() {
+ BexException ex = assertThrows(BexException.class, () -> engine.compile(BexProgramSource.inline(FrozenNode.fromResolvedNode(obj(
+ "type", "Blue/BEX Program",
+ "functions", obj("f", obj(
+ "args", obj("contracts", obj()),
+ "expr", op("$var", "contracts"))),
+ "expr", op("$call", obj(
+ "function", "f",
+ "args", obj("contracts", 1))))))));
+
+ assertTrue(ex.getMessage().contains("reserved Blue key: contracts"));
+ }
+
+ @Test
+ void functionArgAcceptsMatchingBlueIdPattern() {
+ Node hotelOrderPattern = pattern("blueId: HotelOrderBlueId");
+ BexExecutionResult result = run(obj(
+ "type", "Blue/BEX Program",
+ "functions", obj("f", obj(
+ "args", obj("hotelOrder", hotelOrderPattern),
+ "expr", op("$is", obj(
+ "node", op("$var", "hotelOrder"),
+ "pattern", hotelOrderPattern)))),
+ "expr", op("$call", obj(
+ "function", "f",
+ "args", obj("hotelOrder", hotelOrderPattern)))));
+
+ assertEquals(true, simple(result.value()));
+ }
+
+ @Test
+ void isReturnsTrueForComputedTypedObjectWithBlueIdPattern() {
+ Node hotelOrderPattern = pattern("blueId: HotelOrderType");
+ BexExecutionResult result = run(stepExpr(op("$is", obj(
+ "node", obj(
+ "type", hotelOrderPattern,
+ "status", op("$literal", "confirmed")),
+ "pattern", hotelOrderPattern))));
+
+ assertEquals(true, simple(result.value()));
+ }
+
+ @Test
+ void isReturnsFalseForComputedTypedObjectWithDifferentBlueIdPattern() {
+ BexExecutionResult result = run(stepExpr(op("$is", obj(
+ "node", obj(
+ "type", pattern("blueId: RestaurantOrderType"),
+ "status", op("$literal", "confirmed")),
+ "pattern", pattern("blueId: HotelOrderType")))));
+
+ assertEquals(false, simple(result.value()));
+ }
+
+ @Test
+ void functionArgAcceptsComputedTypedObjectMatchingBlueIdPattern() {
+ Node hotelOrderPattern = pattern("blueId: HotelOrderType");
+ BexExecutionResult result = run(obj(
+ "type", "Blue/BEX Program",
+ "functions", obj("f", obj(
+ "args", obj("hotelOrder", hotelOrderPattern),
+ "expr", op("$is", obj(
+ "node", op("$var", "hotelOrder"),
+ "pattern", hotelOrderPattern)))),
+ "expr", op("$call", obj(
+ "function", "f",
+ "args", obj("hotelOrder", obj(
+ "type", hotelOrderPattern,
+ "status", op("$literal", "confirmed")))))));
+
+ assertEquals(true, simple(result.value()));
+ }
+
+ @Test
+ void functionArgRejectsComputedTypedObjectWithDifferentBlueIdPattern() {
+ BexCompiledProgram program = engine.compile(BexProgramSource.inline(FrozenNode.fromResolvedNode(obj(
+ "type", "Blue/BEX Program",
+ "functions", obj("f", obj(
+ "args", obj("hotelOrder", pattern("blueId: HotelOrderType")),
+ "expr", op("$var", "hotelOrder"))),
+ "expr", op("$call", obj(
+ "function", "f",
+ "args", obj("hotelOrder", obj(
+ "type", pattern("blueId: RestaurantOrderType"),
+ "status", op("$literal", "confirmed")))))))));
+
+ assertThrows(BexException.class, () -> engine.execute(program, defaultContext()));
+ }
+
+ @Test
+ void isReturnsFalseForBlueIdReferenceWithSiblingFields() {
+ BexExecutionResult result = run(stepExpr(op("$is", obj(
+ "node", obj(
+ "blueId", "HotelOrderType",
+ "status", op("$literal", "confirmed")),
+ "pattern", pattern("blueId: HotelOrderType")))));
+
+ assertEquals(false, simple(result.value()));
+ }
+
+ @Test
+ void isReturnsTrueForPrimitiveType() {
+ assertEquals(true, simple(run(stepExpr(op("$is", obj(
+ "node", 400,
+ "pattern", pattern("type: Integer"))))).value()));
+ }
+
+ @Test
+ void isReturnsFalseForWrongPrimitiveType() {
+ assertEquals(false, simple(run(stepExpr(op("$is", obj(
+ "node", "400",
+ "pattern", pattern("type: Integer"))))).value()));
+ }
+
+ @Test
+ void isReturnsTrueForStructuralPattern() {
+ assertEquals(true, simple(run(stepExpr(op("$is", obj(
+ "node", obj("customerName", "Jan", "nights", 2),
+ "pattern", pattern(
+ "customerName:",
+ " type: Text",
+ " schema:",
+ " required: true",
+ "nights:",
+ " type: Integer",
+ " schema:",
+ " required: true"))))).value()));
+ }
+
+ @Test
+ void isReturnsTrueForStructuralPatternWithComputedFields() {
+ assertEquals(true, simple(run(stepExpr(op("$is", obj(
+ "node", obj(
+ "customerName", op("$literal", "Jan"),
+ "nights", op("$integer", "2")),
+ "pattern", pattern(
+ "customerName:",
+ " type: Text",
+ " schema:",
+ " required: true",
+ "nights:",
+ " type: Integer",
+ " schema:",
+ " required: true"))))).value()));
+ }
+
+ @Test
+ void isReturnsFalseForStructuralPatternMismatch() {
+ assertEquals(false, simple(run(stepExpr(op("$is", obj(
+ "node", obj("customerName", "Jan", "nights", "2"),
+ "pattern", pattern(
+ "customerName:",
+ " type: Text",
+ " schema:",
+ " required: true",
+ "nights:",
+ " type: Integer",
+ " schema:",
+ " required: true"))))).value()));
+ }
+
+ @Test
+ void isReturnsFalseForUndefinedValue() {
+ assertEquals(false, simple(run(stepExpr(op("$is", obj(
+ "node", op("$document", "/missing"),
+ "pattern", pattern("type: Text"))))).value()));
+ }
+
+ @Test
+ void computedObjectPreservesValueTypeLanguageField() {
+ BexExecutionResult result = run(stepExpr(obj(
+ "valueType", pattern("type: Integer"),
+ "amount", op("$integer", "2"))));
+ Node node = BexNodeWriter.toNode(result.value());
+
+ assertEquals(BigInteger.valueOf(2), node.getProperties().get("amount").getValue());
+ assertTrue(node.getValueType() != null);
+ assertTrue(node.getValueType().getType() != null);
+ }
+
+ @Test
+ void bexInsideTypeLanguageFieldIsRejected() {
+ assertThrows(BexException.class, () -> run(yaml(
+ "type: Blue/BEX Program",
+ "expr:",
+ " $is:",
+ " node:",
+ " type:",
+ " $choose:",
+ " cond: true",
+ " then: Integer",
+ " else: Text",
+ " value: 10",
+ " pattern:",
+ " type: Integer")));
+ }
+
+ @Test
+ void bexInsideSchemaIsRejectedAtCompileTime() {
+ assertThrows(BexException.class, () -> run(yaml(
+ "type: Blue/BEX Program",
+ "expr:",
+ " $is:",
+ " node: abc",
+ " pattern:",
+ " type: Text",
+ " schema:",
+ " minLength:",
+ " $integer: \"1\"")));
+ }
+
+ @Test
+ void bexInsideIsPatternIsRejectedAtCompileTime() {
+ assertThrows(BexException.class, () -> run(yaml(
+ "type: Blue/BEX Program",
+ "expr:",
+ " $is:",
+ " node: 10",
+ " pattern:",
+ " type:",
+ " $choose:",
+ " cond: true",
+ " then: Integer",
+ " else: Text")));
+ }
+
+ @Test
+ void bexInsideFunctionArgumentPatternIsRejectedAtCompileTime() {
+ assertThrows(BexException.class, () -> compile(yaml(
+ "type: Blue/BEX Program",
+ "functions:",
+ " f:",
+ " args:",
+ " input:",
+ " type:",
+ " $choose:",
+ " cond: true",
+ " then: Integer",
+ " else: Text",
+ " expr:",
+ " $var: input",
+ "expr:",
+ " $call:",
+ " function: f",
+ " args:",
+ " input: abc")));
+ }
+
+ @Test
+ void bexInsideFunctionArgumentSchemaIsRejectedAtCompileTime() {
+ assertThrows(BexException.class, () -> compile(yaml(
+ "type: Blue/BEX Program",
+ "functions:",
+ " f:",
+ " args:",
+ " input:",
+ " type: Text",
+ " schema:",
+ " minLength:",
+ " $integer: \"1\"",
+ " expr:",
+ " $var: input",
+ "expr:",
+ " $call:",
+ " function: f",
+ " args:",
+ " input: abc")));
+ }
+
+ @Test
+ void bexInsideNestedTypeBlueIdIsRejectedAtCompileTime() {
+ Node expr = new Node()
+ .type(obj("blueId", op("$concat", list("Hotel", "OrderType"))))
+ .properties("status", new Node().value("confirmed"));
+
+ assertThrows(BexException.class, () -> run(stepExpr(expr)));
+ }
+
+ @Test
+ void staticAuthoredTypeStillWorks() {
+ BexExecutionResult result = run(yaml(
+ "type: Blue/BEX Program",
+ "expr:",
+ " $is:",
+ " node:",
+ " type: Integer",
+ " value: 10",
+ " pattern:",
+ " type: Integer"));
+
+ assertEquals(true, simple(result.value()));
+ }
+
+ @Test
+ void computedScalarTypeIsNotTreatedAsAuthoredTypeAlias() {
+ BexValue value = BexValues.fromSimple(m(
+ "type", "Integer",
+ "value", BigInteger.TEN));
+ Node node = BexNodeWriter.toNode(value);
+ Node pattern = blue.yamlToNode("type: Integer");
+
+ assertTrue(node.getType() != null);
+ assertEquals("Integer", node.getType().getValue());
+ assertFalse(node.getType().isInlineValue());
+ assertFalse(blue.nodeMatchesType(node, pattern));
+ }
+
+ @Test
+ void nameAndDescriptionAreNeutralForBluePatternMatching() {
+ BexExecutionResult result = run(stepExpr(op("$is", obj(
+ "node", obj(
+ "name", "Runtime name",
+ "description", "Runtime description",
+ "type", pattern("type: Integer"),
+ "value", op("$literal", 10)),
+ "pattern", pattern(
+ "name: Pattern name",
+ "description: Pattern description",
+ "type: Integer")))));
+
+ assertEquals(true, simple(result.value()));
+ }
+
+ @Test
+ void entryFunctionWithArgsFailsAtCompileTime() {
+ BexException ex = assertThrows(BexException.class, () -> compile(yaml(
+ "type: Blue/BEX Program",
+ "entry: f",
+ "functions:",
+ " f:",
+ " args:",
+ " amount:",
+ " type: Integer",
+ " expr:",
+ " $var: amount")));
+
+ assertTrue(ex.getMessage().contains("Entry function f declares arguments but entry invocation provides none"));
+ }
+
+ @Test
+ void staticPatternsRejectBexOperators() {
+ Node recursiveLookingPattern = obj("$call", obj("function", "f"));
+
+ assertThrows(BexException.class, () -> run(obj(
+ "type", "Blue/BEX Program",
+ "functions", obj("f", obj(
+ "args", obj("x", recursiveLookingPattern),
+ "expr", op("$is", obj(
+ "node", op("$literal", recursiveLookingPattern),
+ "pattern", recursiveLookingPattern)))),
+ "expr", op("$call", obj(
+ "function", "f",
+ "args", obj("x", op("$literal", recursiveLookingPattern)))))));
+ }
+
+ @Test
+ void literalPayloadStillRejectsBexInsideTypeDefinitionFields() {
+ assertThrows(BexException.class, () -> run(yaml(
+ "type: Blue/BEX Program",
+ "expr:",
+ " $literal:",
+ " type:",
+ " $choose:",
+ " cond: true",
+ " then: Integer",
+ " else: Text")));
+ }
+
+ @Test
+ void nodeWriterMapsComputedObjectLanguageKeysToBlueNodeFields() {
+ BexValue value = BexValues.fromSimple(m(
+ "type", m("blueId", "HotelOrderType"),
+ "status", "confirmed"));
+
+ Node node = BexNodeWriter.toNode(value);
+
+ assertEquals("HotelOrderType", node.getType().getBlueId());
+ assertEquals("confirmed", node.getProperties().get("status").getValue());
+ assertFalse(node.getProperties().containsKey("type"));
+ }
+
+ @Test
+ void frozenWriterMapsComputedObjectLanguageKeysToBlueNodeFields() {
+ BexValue value = BexValues.fromSimple(m(
+ "type", m("blueId", "HotelOrderType"),
+ "status", "confirmed"));
+
+ FrozenNode node = BexFrozenWriter.toFrozen(value);
+
+ assertEquals("HotelOrderType", node.getType().getReferenceBlueId());
+ assertEquals("confirmed", node.property("status").getValue());
+ assertNull(node.property("type"));
+ }
+
+ @Test
+ void nodeWriterMapsSchemaAndValueLanguageKeysToBlueNodeFields() {
+ BexValue value = BexValues.fromSimple(m(
+ "name", "Amount",
+ "type", m("blueId", "Integer"),
+ "schema", m("required", true),
+ "value", bi(10)));
+
+ Node node = BexNodeWriter.toNode(value);
+
+ assertEquals("Amount", node.getName());
+ assertEquals("Integer", node.getType().getBlueId());
+ assertTrue(node.getSchema().getRequiredValue());
+ assertEquals(bi(10), node.getValue());
+ assertNull(node.getProperties());
+ }
+
+ @Test
+ void nodeWriterRejectsBlueIdReferenceWithSiblingFields() {
+ BexValue value = BexValues.fromSimple(m(
+ "blueId", "HotelOrderType",
+ "status", "confirmed"));
+
+ assertThrows(BexException.class, () -> BexNodeWriter.toNode(value));
+ }
+
+ @Test
+ void nodeWriterRejectsPropertiesInternalField() {
+ BexValue value = BexValues.fromSimple(m(
+ "properties", m("status", "confirmed")));
+
+ assertThrows(BexException.class, () -> BexNodeWriter.toNode(value));
+ assertThrows(BexException.class, () -> BexFrozenWriter.toFrozen(value));
+ }
+
+ @Test
+ void nodeWriterRejectsMixedPayloadKinds() {
+ BexValue value = BexValues.fromSimple(m(
+ "value", "confirmed",
+ "status", "confirmed"));
+
+ assertThrows(BexException.class, () -> BexNodeWriter.toNode(value));
+ }
+
+ @Test
+ void nodeWriterRejectsSchemaAndConstraintsTogether() {
+ BexValue value = BexValues.fromSimple(m(
+ "schema", m("required", true),
+ "constraints", m("minLength", 1)));
+
+ assertThrows(BexException.class, () -> BexNodeWriter.toNode(value));
+ }
+
+ @Test
+ void nodeWriterRejectsListControlFields() {
+ assertThrows(BexException.class, () -> BexNodeWriter.toNode(BexValues.fromSimple(m(
+ "$previous", "abc"))));
+ assertThrows(BexException.class, () -> BexNodeWriter.toNode(BexValues.fromSimple(m(
+ "$pos", 1))));
+ assertThrows(BexException.class, () -> BexFrozenWriter.toFrozen(BexValues.fromSimple(m(
+ "$previous", "abc"))));
+ }
+
+ private BexExecutionResult run(String yaml) {
+ return engine.compileAndExecute(source(yaml), defaultContext());
+ }
+
+ private BexExecutionResult run(Node step) {
+ return engine.compileAndExecute(BexProgramSource.inline(FrozenNode.fromResolvedNode(step)), defaultContext());
+ }
+
+ private BexCompiledProgram compile(String yaml) {
+ return engine.compile(source(yaml));
+ }
+
+ private BexProgramSource source(String yaml) {
+ Node node = blue.yamlToNode(yaml);
+ return BexProgramSource.inline(FrozenNode.fromResolvedNode(node));
+ }
+
+ private static String yaml(String... lines) {
+ return String.join("\n", lines);
+ }
+
+ private Node pattern(String... lines) {
+ return blue.yamlToNode(yaml(lines));
+ }
+}
diff --git a/src/test/java/blue/bex/BexEngineConformanceTest.java b/src/test/java/blue/bex/BexEngineConformanceTest.java
index dbfbd07..abf5025 100644
--- a/src/test/java/blue/bex/BexEngineConformanceTest.java
+++ b/src/test/java/blue/bex/BexEngineConformanceTest.java
@@ -70,7 +70,7 @@ Collection expressionOperators() {
cases.add(c("object undefined", op("$object", op("$document", "/none")), m()));
cases.add(c("list undefined", op("$list", op("$document", "/none")), l()));
cases.add(c("concat", op("$concat", list("a", op("$document", "/status"))), "aactive"));
- cases.add(c("join", op("$join", obj("items", list("a", "b"), "separator", ":")), "a:b"));
+ cases.add(c("join", op("$join", obj("list", list("a", "b"), "separator", ":")), "a:b"));
cases.add(c("split basic", op("$split", obj("text", "a:b:c", "separator", ":")), l("a", "b", "c")));
cases.add(c("split empty segment", op("$split", obj("text", "a::c", "separator", ":")), l("a", "", "c")));
cases.add(c("split trailing empty", op("$split", obj("text", "a:", "separator", ":")), l("a", "")));
@@ -229,7 +229,7 @@ void functionArgsUseSlotPassingWithoutMapAllocation() {
"functions", obj("sum", sum, "missing", missing),
"do", list(
op("$let", obj("name", "first", "expr", op("$call", obj("function", "sum", "args", obj("a", 1, "b", 2, "c", 3))))),
- op("$let", obj("name", "second", "expr", op("$call", obj("function", "missing", "args", obj("a", "set"))))),
+ op("$let", obj("name", "second", "expr", op("$call", obj("function", "missing", "args", obj("a", "set", "b", op("$document", "/missing")))))),
op("$return", obj("first", op("$var", "first"), "second", op("$var", "second")))
)
);
From 454b418d04d72f098682b028c828d05ecdfcb737 Mon Sep 17 00:00:00 2001
From: piotr-blue
Date: Sat, 23 May 2026 04:09:22 +0200
Subject: [PATCH 4/4] Refactor pointer handling and execution context lookups
in Bex views
---
.../blue/bex/api/FrozenBexDocumentView.java | 2 +-
...cessorExecutionContextBexDocumentView.java | 24 ++++++++-----------
.../blue/bex/value/FrozenNodeBexValue.java | 2 +-
3 files changed, 12 insertions(+), 16 deletions(-)
diff --git a/src/main/java/blue/bex/api/FrozenBexDocumentView.java b/src/main/java/blue/bex/api/FrozenBexDocumentView.java
index e3a4cb1..030d7d7 100644
--- a/src/main/java/blue/bex/api/FrozenBexDocumentView.java
+++ b/src/main/java/blue/bex/api/FrozenBexDocumentView.java
@@ -57,7 +57,7 @@ public String currentScopePath() {
private BexValue read(FrozenNode root, String pointer) {
List segments = JsonPointer.split(pointer);
- FrozenNode selected = root.at(segments);
+ FrozenNode selected = root.at(JsonPointer.toPointer(segments));
if (selected != null) {
return BexValues.frozen(selected);
}
diff --git a/src/main/java/blue/bex/api/ProcessorExecutionContextBexDocumentView.java b/src/main/java/blue/bex/api/ProcessorExecutionContextBexDocumentView.java
index 771d18e..f7724bb 100644
--- a/src/main/java/blue/bex/api/ProcessorExecutionContextBexDocumentView.java
+++ b/src/main/java/blue/bex/api/ProcessorExecutionContextBexDocumentView.java
@@ -2,8 +2,8 @@
import blue.bex.value.BexValue;
import blue.bex.value.BexValues;
+import blue.language.model.Node;
import blue.language.processor.ProcessorExecutionContext;
-import blue.language.snapshot.FrozenNode;
import blue.language.utils.JsonPointer;
import java.util.Objects;
@@ -25,26 +25,22 @@ public String resolvePointer(String authoredPointer) {
@Override
public BexValue canonicalAt(String absolutePointer) {
- FrozenNode selected = context.canonicalFrozenAt(absolutePointer);
- if (selected != null) {
- return BexValues.frozen(selected);
- }
- FrozenNode root = context.canonicalFrozenAt("/");
- return root != null ? BexValues.frozen(root).at(JsonPointer.split(absolutePointer)) : BexValues.undefined();
+ return documentAt(absolutePointer);
}
@Override
public BexValue resolvedAt(String absolutePointer) {
- FrozenNode selected = context.resolvedFrozenAt(absolutePointer);
- if (selected != null) {
- return BexValues.frozen(selected);
- }
- FrozenNode root = context.resolvedFrozenAt("/");
- return root != null ? BexValues.frozen(root).at(JsonPointer.split(absolutePointer)) : BexValues.undefined();
+ return documentAt(absolutePointer);
}
@Override
public String currentScopePath() {
- return context.scopePath();
+ String pointer = context.resolvePointer("");
+ return pointer != null ? JsonPointer.canonicalize(pointer) : "/";
+ }
+
+ private BexValue documentAt(String absolutePointer) {
+ Node selected = context.documentAt(absolutePointer);
+ return selected != null ? BexValues.nodeSnapshot(selected) : BexValues.undefined();
}
}
diff --git a/src/main/java/blue/bex/value/FrozenNodeBexValue.java b/src/main/java/blue/bex/value/FrozenNodeBexValue.java
index bc00e31..78b19a9 100644
--- a/src/main/java/blue/bex/value/FrozenNodeBexValue.java
+++ b/src/main/java/blue/bex/value/FrozenNodeBexValue.java
@@ -82,7 +82,7 @@ public BexValue get(String key) {
@Override
public BexValue at(List pointerSegments) {
- FrozenNode selected = node.at(pointerSegments);
+ FrozenNode selected = node.at(JsonPointer.toPointer(pointerSegments));
if (selected != null) {
return BexValues.frozen(selected);
}