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
86 changes: 61 additions & 25 deletions ports/javascript/index.mjs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
const JSON_VERSION = 1;
const JSON_VERSION = 2;
const DEPTH_LIMIT = 300;
const ANNOTATION_EMIT = 44;
const ANNOTATION_TO_PARENT = 45;
const ANNOTATION_BASENAME_TO_PARENT = 46;
const CONTROL_GROUP_START = 85;
const CONTROL_EVALUATE_END = 89;
const CONTROL_GROUP_START = 86;
const CONTROL_EVALUATE_END = 90;
const URI_REGEX = /^[a-zA-Z][a-zA-Z0-9+\-.]*:[^\s]*$/;

function buildJsonPointer(tokens, length) {
Expand Down Expand Up @@ -150,7 +150,7 @@ function prepareInstruction(instruction) {
function resolveJumpTargets(instructions, targets) {
for (let index = 0; index < instructions.length; index++) {
const instruction = instructions[index];
if (instruction[0] === 91) {
if (instruction[0] === 92) {
const targetIndex = instruction[5];
if (targetIndex < targets.length) {
instruction[5] = targets[targetIndex];
Expand Down Expand Up @@ -185,7 +185,7 @@ function collectAnchorNames(targets, result) {
function collectAnchorNamesFromInstructions(instructions, result) {
for (let index = 0; index < instructions.length; index++) {
const instruction = instructions[index];
if (instruction[0] === 90 && typeof instruction[5] === 'string') {
if (instruction[0] === 91 && typeof instruction[5] === 'string') {
result.add(instruction[5]);
}
if (instruction[6]) {
Expand Down Expand Up @@ -321,14 +321,14 @@ function compileInstructionToCode(instruction, captures, visited, budget) {
case 79: { var r=R('t'); return r?r+'if(!Array.isArray(t))return true;for(var j=0;j<t.length;j++){var a=_jt(t[j]);if(a!=='+value+'&&!('+value+'===2&&_ii(t[j])))return false;}return true;':null; }
case 80: { var r=R('t'); return r?r+'if(!Array.isArray(t))return true;for(var j=0;j<t.length;j++){if(_es(t[j])!=='+value+')return false;}return true;':null; }
case 81: { var r=R('t'); return r?r+'if(!Array.isArray(t))return true;for(var j=0;j<t.length;j++){if(('+value+'&(1<<_es(t[j])))===0)return false;}return true;':null; }
case 82: return fb(82); case 83: return fb(83); case 84: return fb(84);
case 85: { if(!children||children.length===0)return 'return true;'; var c=''; for(var j=0;j<children.length;j++){var r2=compileInstructionToCode(children[j],captures,visited,budget); if(r2===null){var ci=captures.length;captures.push(children[j]);c+='if(!_e(_c['+ci+'],i,d+1,_t,_v))return false;';}else{budget[0]-=r2.length;c+='if(!(function(i,d,_t,_v){'+r2+'})(i,d+1,_t,_v))return false;';}} return c+'return true;'; }
case 86: { var r=R('t'); if(!r)return null; var c=r+TO+'return true;if(!Object.hasOwn(t,'+JSON.stringify(value)+'))return true;'; if(children&&children.length>0)c+=seq(children,'i'); return c+'return true;'; }
case 87: { var c=IO+'if(!Object.hasOwn(i,'+JSON.stringify(value)+'))return true;'; if(children&&children.length>0)c+=seq(children,'i'); return c+'return true;'; }
case 88: { var c='if(_jt(i)!=='+value+')return true;'; if(children&&children.length>0)c+=seq(children,'i'); return c+'return true;'; }
case 89: return 'return true;';
case 90: return fb(90);
case 91: { if(!value)return 'return true;'; if(visited&&visited.has(instruction))return fb(91); if(!visited)visited=new Set(); visited.add(instruction); var r=R('t'); if(!r)return fb(91); var c=r; for(var j=0;j<value.length;j++){var r2=compileInstructionToCode(value[j],captures,visited,budget); if(r2===null){var ci=captures.length;captures.push(value[j]);c+='if(!_e(_c['+ci+'],t,d+1,_t,_v))return false;';}else{budget[0]-=r2.length;c+='if(!(function(i,d,_t,_v){'+r2+'})(t,d+1,_t,_v))return false;';}} return c+'return true;'; }
case 82: return fb(82); case 83: return fb(83); case 84: return fb(84); case 85: return fb(85);
case 86: { if(!children||children.length===0)return 'return true;'; var c=''; for(var j=0;j<children.length;j++){var r2=compileInstructionToCode(children[j],captures,visited,budget); if(r2===null){var ci=captures.length;captures.push(children[j]);c+='if(!_e(_c['+ci+'],i,d+1,_t,_v))return false;';}else{budget[0]-=r2.length;c+='if(!(function(i,d,_t,_v){'+r2+'})(i,d+1,_t,_v))return false;';}} return c+'return true;'; }
case 87: { var r=R('t'); if(!r)return null; var c=r+TO+'return true;if(!Object.hasOwn(t,'+JSON.stringify(value)+'))return true;'; if(children&&children.length>0)c+=seq(children,'i'); return c+'return true;'; }
case 88: { var c=IO+'if(!Object.hasOwn(i,'+JSON.stringify(value)+'))return true;'; if(children&&children.length>0)c+=seq(children,'i'); return c+'return true;'; }
case 89: { var c='if(_jt(i)!=='+value+')return true;'; if(children&&children.length>0)c+=seq(children,'i'); return c+'return true;'; }
case 90: return 'return true;';
case 91: return fb(91);
case 92: { if(!value)return 'return true;'; if(visited&&visited.has(instruction))return fb(92); if(!visited)visited=new Set(); visited.add(instruction); var r=R('t'); if(!r)return fb(92); var c=r; for(var j=0;j<value.length;j++){var r2=compileInstructionToCode(value[j],captures,visited,budget); if(r2===null){var ci=captures.length;captures.push(value[j]);c+='if(!_e(_c['+ci+'],t,d+1,_t,_v))return false;';}else{budget[0]-=r2.length;c+='if(!(function(i,d,_t,_v){'+r2+'})(t,d+1,_t,_v))return false;';}} return c+'return true;'; }
default: return null;
}
}
Expand Down Expand Up @@ -623,7 +623,7 @@ function evaluateInstructionTracked(instruction, instance, depth, template, eval
if (!handler) return true;

const type = instruction[0];
if (type < 85 || type > 89) {
if (type < 86 || type > 90) {
if (evaluator.trackMode) {
evaluator.pushPath(instruction[1]);
}
Expand Down Expand Up @@ -2288,6 +2288,28 @@ function LoopItemsPropertiesExactlyTypeStrictHash(instruction, instance, depth,
if (evaluator.callbackMode) evaluator.callbackPop(instruction, true);
return true;
};

function LoopItemsIntegerBounded(instruction, instance, depth, template, evaluator) {
const target = resolveInstance(instance, instruction[2]);
if (!Array.isArray(target) || target.length === 0) return true;
if (evaluator.callbackMode) evaluator.callbackPush(instruction);
const minimum = instruction[5][0];
const maximum = instruction[5][1];
for (let index = 0; index < target.length; index++) {
const element = target[index];
if (typeof element !== 'number') {
if (evaluator.callbackMode) evaluator.callbackPop(instruction, false);
return false;
}
if (element < minimum || element > maximum) {
if (evaluator.callbackMode) evaluator.callbackPop(instruction, false);
return false;
}
}
if (evaluator.callbackMode) evaluator.callbackPop(instruction, true);
return true;
};

function LoopContains(instruction, instance, depth, template, evaluator) {
const target = resolveInstance(instance, instruction[2]);
if (!Array.isArray(target)) return true;
Expand Down Expand Up @@ -2516,14 +2538,15 @@ const handlers = [
LoopItemsTypeStrictAny, // 81
LoopItemsPropertiesExactlyTypeStrictHash, // 82
LoopItemsPropertiesExactlyTypeStrictHash, // 83
LoopContains, // 84
ControlGroup, // 85
ControlGroupWhenDefines, // 86
ControlGroupWhenDefinesDirect, // 87
ControlGroupWhenType, // 88
ControlEvaluate, // 89
ControlDynamicAnchorJump, // 90
ControlJump // 91
LoopItemsIntegerBounded, // 84
LoopContains, // 85
ControlGroup, // 86
ControlGroupWhenDefines, // 87
ControlGroupWhenDefinesDirect, // 88
ControlGroupWhenType, // 89
ControlEvaluate, // 90
ControlDynamicAnchorJump, // 91
ControlJump // 92
];

function AssertionTypeArrayBounded_fast(instruction, instance, depth, template, evaluator) {
Expand Down Expand Up @@ -3622,6 +3645,18 @@ function ControlDynamicAnchorJump_fast(instruction, instance, depth, template, e
return false;
}

function LoopItemsIntegerBounded_fast(instruction, instance, depth, template, evaluator) {
const target = resolveInstance(instance, instruction[2]);
if (!Array.isArray(target) || target.length === 0) return true;
const minimum = instruction[5][0];
const maximum = instruction[5][1];
for (let index = 0; index < target.length; index++) {
const element = target[index];
if (typeof element !== 'number' || element < minimum || element > maximum) return false;
}
return true;
}

const fastHandlers = handlers.slice();
fastHandlers[15] = AssertionTypeArrayBounded_fast;
fastHandlers[81] = LoopItemsTypeStrictAny_fast;
Expand All @@ -3631,7 +3666,7 @@ fastHandlers[4] = AssertionDefinesAllStrict_fast;
fastHandlers[26] = AssertionEqual_fast;
fastHandlers[59] = LoopPropertiesMatch_fast;
fastHandlers[50] = LogicalOr_fast;
fastHandlers[91] = ControlJump_fast;
fastHandlers[92] = ControlJump_fast;
fastHandlers[28] = AssertionEqualsAnyStringHash_fast;
fastHandlers[52] = LogicalXor_fast;
fastHandlers[2] = AssertionDefinesStrict_fast;
Expand All @@ -3649,7 +3684,7 @@ fastHandlers[1] = AssertionDefines_fast;
fastHandlers[54] = LogicalWhenType_fast;
fastHandlers[55] = LogicalWhenDefines_fast;
fastHandlers[0] = AssertionFail_fast;
fastHandlers[84] = LoopContains_fast;
fastHandlers[85] = LoopContains_fast;
fastHandlers[48] = LogicalNot_fast;
fastHandlers[79] = LoopItemsType_fast;
fastHandlers[80] = LoopItemsTypeStrict_fast;
Expand Down Expand Up @@ -3709,6 +3744,7 @@ fastHandlers[77] = LoopItemsFrom_fast;
fastHandlers[78] = LoopItemsUnevaluated_fast;
fastHandlers[82] = LoopItemsPropertiesExactlyTypeStrictHash_fast;
fastHandlers[83] = LoopItemsPropertiesExactlyTypeStrictHash_fast;
fastHandlers[90] = ControlDynamicAnchorJump_fast;
fastHandlers[84] = LoopItemsIntegerBounded_fast;
fastHandlers[91] = ControlDynamicAnchorJump_fast;

export { Blaze };
6 changes: 3 additions & 3 deletions ports/javascript/test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -116,15 +116,15 @@ for (const [subdirectory, blacklist] of Object.entries(BLACKLISTS)) {

describe('version', () => {
it('rejects a template with an unsupported version', () => {
const template = [2, false, false, [[]], []];
const template = [3, false, false, [[]], []];
assert.throws(() => new Blaze(template), {
message: 'Only version 1 of the compiled template is supported by this version of the evaluator'
message: 'Only version 2 of the compiled template is supported by this version of the evaluator'
});
});

it('rejects a template that is not an array', () => {
assert.throws(() => new Blaze({}), {
message: 'Only version 1 of the compiled template is supported by this version of the evaluator'
message: 'Only version 2 of the compiled template is supported by this version of the evaluator'
});
});
});
17 changes: 9 additions & 8 deletions ports/javascript/trace.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -95,14 +95,15 @@ const INSTRUCTION_NAMES = {
"LoopItemsTypeStrictAny": 81,
"LoopItemsPropertiesExactlyTypeStrictHash": 82,
"LoopItemsPropertiesExactlyTypeStrictHash3": 83,
"LoopContains": 84,
"ControlGroup": 85,
"ControlGroupWhenDefines": 86,
"ControlGroupWhenDefinesDirect": 87,
"ControlGroupWhenType": 88,
"ControlEvaluate": 89,
"ControlDynamicAnchorJump": 90,
"ControlJump": 91,
"LoopItemsIntegerBounded": 84,
"LoopContains": 85,
"ControlGroup": 86,
"ControlGroupWhenDefines": 87,
"ControlGroupWhenDefinesDirect": 88,
"ControlGroupWhenType": 89,
"ControlEvaluate": 90,
"ControlDynamicAnchorJump": 91,
"ControlJump": 92,
"Annotation": -1
};

Expand Down
74 changes: 74 additions & 0 deletions src/compiler/default_compiler_draft4.h
Original file line number Diff line number Diff line change
Expand Up @@ -1474,6 +1474,64 @@ auto compiler_draft4_applicator_items_array(
}
}

auto is_number_type_check(const Instruction &instruction) -> bool {
Copy link
Copy Markdown

@augmentcode augmentcode Bot Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

src/compiler/default_compiler_draft4.h:1477 — is_number_type_check currently returns true for AssertionTypeStrict/AssertionType when the schema type is just integer, and for AssertionTypeStrictAny when the type-set includes integer+real (even if other types are also allowed). That can make is_integer_bounded_pattern fire for type: "integer" (or unions like ["number","string"]), causing LoopItemsIntegerBounded to reject values the original schema would accept / accept values (reals) the original schema would reject.

Severity: high

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

if (instruction.type != InstructionIndex::AssertionTypeStrictAny) {
return false;
}

const auto &value{std::get<ValueTypes>(instruction.value)};
const auto numeric_count{
static_cast<std::size_t>(value.test(
std::to_underlying(sourcemeta::core::JSON::Type::Integer))) +
static_cast<std::size_t>(
value.test(std::to_underlying(sourcemeta::core::JSON::Type::Real))) +
static_cast<std::size_t>(value.test(
std::to_underlying(sourcemeta::core::JSON::Type::Decimal)))};
return numeric_count >= 2 && value.count() == numeric_count;
}

auto is_integer_bounded_pattern(const Instructions &children) -> bool {
if (children.size() != 3) {
return false;
}

bool has_type{false};
bool has_min{false};
bool has_max{false};
for (const auto &child : children) {
if (is_number_type_check(child)) {
has_type = true;
} else if (child.type == InstructionIndex::AssertionGreaterEqual) {
if (!std::get<ValueJSON>(child.value).is_integer()) {
return false;
}
has_min = true;
} else if (child.type == InstructionIndex::AssertionLessEqual) {
if (!std::get<ValueJSON>(child.value).is_integer()) {
return false;
}
has_max = true;
}
}

return has_type && has_min && has_max;
}

auto extract_integer_bounds(const Instructions &children)
-> ValueIntegerBounds {
std::int64_t minimum{0};
std::int64_t maximum{0};
for (const auto &child : children) {
if (child.type == InstructionIndex::AssertionGreaterEqual) {
minimum = std::get<ValueJSON>(child.value).to_integer();
} else if (child.type == InstructionIndex::AssertionLessEqual) {
maximum = std::get<ValueJSON>(child.value).to_integer();
}
}

return {minimum, maximum};
}

auto compiler_draft4_applicator_items_with_options(
const Context &context, const SchemaContext &schema_context,
const DynamicContext &dynamic_context, const bool annotate,
Expand Down Expand Up @@ -1539,6 +1597,13 @@ auto compiler_draft4_applicator_items_with_options(
return {};
}

if (context.mode == Mode::FastValidation && children.size() == 3 &&
is_integer_bounded_pattern(children)) {
return {make(sourcemeta::blaze::InstructionIndex::LoopItemsIntegerBounded,
context, schema_context, dynamic_context,
extract_integer_bounds(children))};
}

if (context.mode == Mode::FastValidation && children.size() == 1) {
if (children.front().type == InstructionIndex::AssertionTypeStrict) {
return {make(sourcemeta::blaze::InstructionIndex::LoopItemsTypeStrict,
Expand Down Expand Up @@ -1614,6 +1679,15 @@ auto compiler_draft4_applicator_additionalitems_from_cursor(
Instructions children;

if (!subchildren.empty()) {
if (context.mode == Mode::FastValidation && cursor == 0 && !annotate &&
!track_evaluation && is_integer_bounded_pattern(subchildren)) {
children.push_back(
make(sourcemeta::blaze::InstructionIndex::LoopItemsIntegerBounded,
context, schema_context, dynamic_context,
extract_integer_bounds(subchildren)));
return children;
}

children.push_back(make(sourcemeta::blaze::InstructionIndex::LoopItemsFrom,
context, schema_context, dynamic_context,
ValueUnsignedInteger{cursor},
Expand Down
4 changes: 3 additions & 1 deletion src/compiler/default_compiler_draft6.h
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,9 @@ auto compiler_draft6_validation_type(const Context &context,
LoopItemsPropertiesExactlyTypeStrictHash ||
current.back().type ==
sourcemeta::blaze::InstructionIndex::
LoopItemsPropertiesExactlyTypeStrictHash3) &&
LoopItemsPropertiesExactlyTypeStrictHash3 ||
current.back().type ==
sourcemeta::blaze::InstructionIndex::LoopItemsIntegerBounded) &&
current.back().relative_instance_location ==
to_pointer(dynamic_context.base_instance_location)) {
return {};
Expand Down
6 changes: 6 additions & 0 deletions src/evaluator/evaluator_describe.cc
Original file line number Diff line number Diff line change
Expand Up @@ -775,6 +775,12 @@ auto describe(const bool valid, const Instruction &step,
return message.str();
}

if (step.type ==
sourcemeta::blaze::InstructionIndex::LoopItemsIntegerBounded) {
return "Every item in the array was expected to be a number within the "
"given range";
}

if (step.type == sourcemeta::blaze::InstructionIndex::LoopPropertiesType) {
std::ostringstream message;
message << "The object properties were expected to be of type "
Expand Down
7 changes: 7 additions & 0 deletions src/evaluator/evaluator_json.cc
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ auto value_from_json(const sourcemeta::core::JSON &wrapper)
case 19: return sourcemeta::core::from_json<ValueTypedProperties>(value);
case 20: return sourcemeta::core::from_json<ValueStringHashes>(value);
case 21: return sourcemeta::core::from_json<ValueTypedHashes>(value);
case 22:
if (value.is_array() && value.array_size() == 2 &&
value.at(0).is_integer() && value.at(1).is_integer()) {
return ValueIntegerBounds{value.at(0).to_integer(),
value.at(1).to_integer()};
}
return std::nullopt;
// clang-format on
default:
std::unreachable();
Expand Down
2 changes: 1 addition & 1 deletion src/evaluator/include/sourcemeta/blaze/evaluator.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ struct Template {
};

/// @ingroup evaluator
constexpr std::size_t JSON_VERSION{1};
constexpr std::size_t JSON_VERSION{2};

/// @ingroup evaluator
/// Parse a template from JSON
Expand Down
Loading
Loading