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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,18 @@ BexProgramSource source = BexProgramSource.withDefinition(
);
```

## 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
Expand Down Expand Up @@ -262,6 +274,44 @@ Hosts often provide common bindings such as `event`, `steps`, and

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.

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 `$`.
Expand Down Expand Up @@ -307,6 +357,7 @@ $is:
| 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. |
Expand Down Expand Up @@ -402,6 +453,14 @@ BEX-looking operators inside Blue type-definition fields such as `type`,
BEX computes these values only. The host decides whether patches are applied,
events are emitted, or accumulators are treated as ordinary data.

`$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,
Expand Down
5 changes: 4 additions & 1 deletion src/main/java/blue/bex/compile/BexCompiledProgram.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,10 @@ public BexValue execute(BexRuntime runtime) {

public BexValue constant(String name) {
BexValue value = constants.get(name);
return value != null ? value : BexValues.undefined();
if (value == null) {
throw new BexException("Unknown constant: " + name);
}
return value;
}

/**
Expand Down
29 changes: 19 additions & 10 deletions src/main/java/blue/bex/compile/BexCompiler.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public final class BexCompiler {
private final BexContainsCache containsCache = new BexContainsCache();
private final BexMetrics metrics;
private Map<String, FunctionSignature> functionSignatures = Collections.emptyMap();
private Map<String, BexValue> constants = Collections.emptyMap();
private String currentFunction = "$root";

public BexCompiler(BexMetrics metrics) {
Expand All @@ -40,9 +41,10 @@ public BexCompiledProgram compile(blue.bex.api.BexProgramSource source) {
FrozenNode step = source.programNode();
FrozenNode definition = source.definitionNode().orElse(null);

Map<String, BexValue> constants = new LinkedHashMap<>();
loadConstants(constants, prop(definition, "constants"));
loadConstants(constants, prop(step, "constants"));
Map<String, BexValue> loadedConstants = new LinkedHashMap<>();
loadConstants(loadedConstants, prop(definition, "constants"));
loadConstants(loadedConstants, prop(step, "constants"));
constants = Collections.unmodifiableMap(new LinkedHashMap<>(loadedConstants));

Map<String, FrozenNode> functionNodes = new LinkedHashMap<>();
loadFunctions(functionNodes, prop(definition, "functions"));
Expand Down Expand Up @@ -274,7 +276,7 @@ private CompiledStatement compileStatement(FrozenNode statement, CompileScope sc
compiled = new ForEachStatement(compileExpr(required(prop(body, "in"), "$forEach.in"), scope, bodyPointer + "/in"),
slot, compileStatements(prop(body, "do"), scope, bodyPointer + "/do"));
} else if ("$appendChange".equals(op)) {
compiled = new AppendChangeStatement(textOrExpr(prop(body, "op"), scope, "replace", bodyPointer + "/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)) {
Expand Down Expand Up @@ -355,7 +357,13 @@ private CompiledExpression compileOperator(String op, FrozenNode body, CompileSc
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 new VarExpr(scope.resolveSlot(requiredText(body, "$var")));
if ("$const".equals(op)) return new ConstExpr(requiredText(body, "$const"));
if ("$const".equals(op)) {
String name = requiredText(body, "$const");
if (!constants.containsKey(name)) {
throw new BexException("Unknown constant: " + name);
}
return new ConstExpr(name);
}
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();
Expand All @@ -369,6 +377,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 ("$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);
Expand Down Expand Up @@ -397,8 +406,8 @@ private CompiledExpression compileOperator(String op, FrozenNode body, CompileSc
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"), pointerOperand(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"), pointerOperand(required(prop(body, "path"), "$pointerSet.path"), scope, pointer + "/path"), prop(body, "val") != null ? compileExpr(prop(body, "val"), scope, pointer + "/val") : null);
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 ("$choose".equals(op)) return new ChooseExpr(compileExpr(required(prop(body, "cond"), "$choose.cond"), scope, pointer + "/cond"), compileExpr(required(prop(body, "then"), "$choose.then"), scope, pointer + "/then"), prop(body, "else") != null ? compileExpr(prop(body, "else"), scope, pointer + "/else") : new LiteralExpr(BexValues.undefined()));
if ("$call".equals(op)) return compileCall(body, scope, pointer);
throw new BexException("Unknown expression operator: " + op);
Expand Down Expand Up @@ -427,7 +436,7 @@ private CompiledExpression documentExpr(FrozenNode body, CompileScope scope, Str
}

private CompiledExpression contextPointerExpr(FrozenNode body, CompileScope scope, ContextKind kind, String pointer) {
return new ContextPointerExpr(pointerOperand(body, scope, pointer), kind);
return new ContextPointerExpr(valuePointerOperand(body, scope, pointer), kind);
}

private CompiledExpression bindingExpr(FrozenNode body, CompileScope scope, String pointer) {
Expand Down Expand Up @@ -455,10 +464,10 @@ private CompiledExpression stepsExpr(FrozenNode body, CompileScope scope, String
int dot = selector.indexOf('.');
String step = dot >= 0 ? selector.substring(0, dot) : selector;
String path = dot >= 0 ? "/" + selector.substring(dot + 1).replace('.', '/') : "/";
return new StepsExpr(new StaticTextExpr(step), StaticPointerOperand.absolute(path));
return new StepsExpr(new StaticTextExpr(step), StaticValuePointerOperand.of(path));
}
return new StepsExpr(textOrExpr(required(prop(body, "step"), "$steps.step"), scope, null, pointer + "/step"),
pointerOperand(prop(body, "path") != null ? prop(body, "path") : scalarNode("/"), scope, pointer + "/path"));
valuePointerOperand(prop(body, "path") != null ? prop(body, "path") : scalarNode("/"), scope, pointer + "/path"));
}

private CallExpr compileCall(FrozenNode body, CompileScope scope, String pointer) {
Expand Down
18 changes: 16 additions & 2 deletions src/main/java/blue/bex/compile/BexOperands.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package blue.bex.compile;

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.List;

Expand Down Expand Up @@ -89,7 +91,7 @@ final class DynamicPointerOperand implements PointerOperand {

@Override
public String authored(CompiledFrame frame) {
return expr.eval(frame).asText();
return PointerOperands.pointerText(expr.eval(frame));
}

@Override
Expand Down Expand Up @@ -146,7 +148,7 @@ final class DynamicValuePointerOperand implements PointerOperand {

@Override
public String authored(CompiledFrame frame) {
return StaticValuePointerOperand.normalize(expr.eval(frame).asText());
return StaticValuePointerOperand.normalize(PointerOperands.pointerText(expr.eval(frame)));
}

@Override
Expand All @@ -159,3 +161,15 @@ public List<String> segments(CompiledFrame frame) {
return frame.runtime().parseDynamicPointer(absolute(frame));
}
}

final class PointerOperands {
private PointerOperands() {
}

static String pointerText(BexValue value) {
if (value == null || value.isUndefined() || value.isNull()) {
throw new BexException("Pointer operand cannot be null or undefined");
}
return value.asText();
}
}
60 changes: 60 additions & 0 deletions src/main/java/blue/bex/compile/BexPatchEntryParser.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package blue.bex.compile;

import blue.bex.BexException;
import blue.bex.result.BexPatchEntry;
import blue.bex.runtime.CompiledFrame;
import blue.bex.value.BexValue;
import blue.bex.value.BexValues;

final class BexPatchEntryParser {
private BexPatchEntryParser() {
}

static BexPatchEntry fromFields(CompiledFrame frame,
String op,
String authoredPath,
boolean hasVal,
BexValue val) {
validateOp(op);
if (authoredPath == null) {
throw new BexException("Patch path is required");
}
BexValue normalizedVal = BexValues.undefined();
if ("add".equals(op) || "replace".equals(op)) {
if (!hasVal || val == null || val.isUndefined()) {
throw new BexException("Patch op " + op + " requires val");
}
normalizedVal = val;
}
String absolute = frame.runtime().resolvePointer(authoredPath);
return new BexPatchEntry(op, authoredPath, absolute, normalizedVal);
}

static boolean requiresValue(String op) {
validateOp(op);
return !"remove".equals(op);
}

static BexPatchEntry fromValue(CompiledFrame frame, BexValue entry) {
if (entry == null || !entry.isObject()) {
throw new BexException("Patch entry must be an object");
}
String op = requiredText(entry.get("op"), "patch.op");
String path = requiredText(entry.get("path"), "patch.path");
BexValue val = entry.get("val");
return fromFields(frame, op, path, !val.isUndefined(), val);
}

private static void validateOp(String op) {
if (!"add".equals(op) && !"replace".equals(op) && !"remove".equals(op)) {
throw new BexException("Unsupported patch op: " + op);
}
}

private static String requiredText(BexValue value, String label) {
if (value == null || value.isUndefined() || value.isNull()) {
throw new BexException("Missing required text field: " + label);
}
return value.asText();
}
}
53 changes: 31 additions & 22 deletions src/main/java/blue/bex/compile/BexStatements.java
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,24 @@ protected Control doExec(CompiledFrame frame) {
}
}

final class BexStatementEffects {
private BexStatementEffects() {
}

static void appendChange(CompiledFrame frame, BexPatchEntry entry) {
frame.runtime().gas().chargeValue(frame.runtime().gas().schedule().appendChangeBase, entry.val());
frame.accumulator().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);
}
}

final class AppendChangeStatement extends Stmt {
private final TextOperand op;
private final PointerOperand pointer;
Expand All @@ -157,16 +175,16 @@ final class AppendChangeStatement extends Stmt {
@Override
protected Control doExec(CompiledFrame frame) {
String operation = op.get(frame);
if (!"add".equals(operation) && !"replace".equals(operation) && !"remove".equals(operation)) {
throw new BexException("Unsupported patch op: " + operation);
}
BexValue value = "remove".equals(operation) ? BexValues.undefined() : val.eval(frame);
if (!"remove".equals(operation) && value.isUndefined()) {
throw new BexException("Undefined cannot be emitted as a patch value");
boolean requiresVal = BexPatchEntryParser.requiresValue(operation);
BexValue value = BexValues.undefined();
if (requiresVal) {
if (val == null) {
throw new BexException("Patch op " + operation + " requires val");
}
value = val.eval(frame);
}
String absolute = pointer.absolute(frame);
frame.runtime().gas().charge(frame.runtime().gas().schedule().appendChangeBase + frame.runtime().gas().estimatedSize(value) / 64);
frame.accumulator().appendChange(new BexPatchEntry(operation, pointer.authored(frame), absolute, value));
BexStatementEffects.appendChange(frame,
BexPatchEntryParser.fromFields(frame, operation, pointer.authored(frame), val != null, value));
return Control.CONTINUE;
}
}
Expand All @@ -183,14 +201,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++) {
BexValue entry = list.get(String.valueOf(i));
String op = entry.get("op").asText();
String path = entry.get("path").asText();
BexValue val = "remove".equals(op) ? BexValues.undefined() : entry.get("val");
if (!"remove".equals(op) && val.isUndefined()) {
throw new BexException("Undefined cannot be emitted as a patch value");
}
frame.accumulator().appendChange(new BexPatchEntry(op, path, frame.runtime().resolvePointer(path), val));
BexStatementEffects.appendChange(frame,
BexPatchEntryParser.fromValue(frame, list.get(String.valueOf(i))));
}
return Control.CONTINUE;
}
Expand All @@ -205,10 +217,7 @@ final class AppendEventStatement extends Stmt {

@Override
protected Control doExec(CompiledFrame frame) {
BexValue value = expr.eval(frame);
if (value.isUndefined()) throw new BexException("Undefined cannot be emitted as an event");
frame.runtime().gas().charge(frame.runtime().gas().schedule().appendEventBase + frame.runtime().gas().estimatedSize(value) / 64);
frame.accumulator().appendEvent(value);
BexStatementEffects.appendEvent(frame, expr.eval(frame));
return Control.CONTINUE;
}
}
Expand All @@ -225,7 +234,7 @@ 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++) {
frame.accumulator().appendEvent(list.get(String.valueOf(i)));
BexStatementEffects.appendEvent(frame, list.get(String.valueOf(i)));
}
return Control.CONTINUE;
}
Expand Down
Loading
Loading