Skip to content
Closed
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
101 changes: 83 additions & 18 deletions ports/javascript/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -331,13 +331,14 @@ function compileInstructionToCode(instruction, captures, visited, budget) {
case 86: { 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 87: { 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 88: return fb(88); case 89: return fb(89); case 90: return fb(90); case 91: return fb(91); case 92: return fb(92);
case 93: { 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 94: { 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 95: { 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 96: { var c='if(_jt(i)!=='+value+')return true;'; if(children&&children.length>0)c+=seq(children,'i'); return c+'return true;'; }
case 97: return 'return true;';
case 98: return fb(98);
case 99: { if(!value)return 'return true;'; if(visited&&visited.has(instruction))return fb(99); if(!visited)visited=new Set(); visited.add(instruction); var r=R('t'); if(!r)return fb(99); 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 93: return fb(93);
case 94: { 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 95: { 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 96: { 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 97: { var c='if(_jt(i)!=='+value+')return true;'; if(children&&children.length>0)c+=seq(children,'i'); return c+'return true;'; }
case 98: return 'return true;';
case 99: return fb(99);
case 100: { if(!value)return 'return true;'; if(visited&&visited.has(instruction))return fb(100); if(!visited)visited=new Set(); visited.add(instruction); var r=R('t'); if(!r)return fb(100); 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 @@ -1561,6 +1562,44 @@ function AssertionObjectPropertiesSimple(instruction, instance, depth, template,
return true;
};

function LoopItemsObjectProperties(instruction, instance, depth, template, evaluator) {
const target = resolveInstance(instance, instruction[2]);
if (evaluator.callbackMode) evaluator.callbackPush(instruction);
if (!Array.isArray(target)) {
if (evaluator.callbackMode) evaluator.callbackPop(instruction, false);
return false;
}
const value = instruction[5];
const children = instruction[6];
for (let elementIndex = 0; elementIndex < target.length; elementIndex++) {
const element = target[elementIndex];
if (!isObject(element)) {
if (evaluator.callbackMode) evaluator.callbackPop(instruction, false);
return false;
}
for (let index = 0; index < value.length; index++) {
const entry = value[index];
const name = entry[0];
const required = entry[2];
if (!Object.hasOwn(element, name)) {
if (required) {
if (evaluator.callbackMode) evaluator.callbackPop(instruction, false);
return false;
}
continue;
}
if (index < children.length) {
if (!evaluateInstructionFast(children[index], element[name], depth + 1, template, evaluator)) {
if (evaluator.callbackMode) evaluator.callbackPop(instruction, false);
return false;
}
}
}
}
if (evaluator.callbackMode) evaluator.callbackPop(instruction, true);
return true;
}

function AnnotationEmit(instruction, instance, depth, template, evaluator) {
if (evaluator.callbackMode) evaluator.callbackAnnotation(instruction);
return true;
Expand Down Expand Up @@ -2707,14 +2746,15 @@ const handlers = [
LoopItemsPropertiesExactlyTypeStrictHash, // 89
LoopItemsIntegerBounded, // 90
LoopItemsIntegerBoundedSized, // 91
LoopContains, // 92
ControlGroup, // 93
ControlGroupWhenDefines, // 94
ControlGroupWhenDefinesDirect, // 95
ControlGroupWhenType, // 96
ControlEvaluate, // 97
ControlDynamicAnchorJump, // 98
ControlJump // 99
LoopItemsObjectProperties, // 92
LoopContains, // 93
ControlGroup, // 94
ControlGroupWhenDefines, // 95
ControlGroupWhenDefinesDirect, // 96
ControlGroupWhenType, // 97
ControlEvaluate, // 98
ControlDynamicAnchorJump, // 99
ControlJump // 100
];

function AssertionTypeArrayBounded_fast(instruction, instance, depth, template, evaluator) {
Expand Down Expand Up @@ -3461,6 +3501,30 @@ function AssertionObjectPropertiesSimple_fast(instruction, instance, depth, temp
return true;
}

function LoopItemsObjectProperties_fast(instruction, instance, depth, template, evaluator) {
const target = resolveInstance(instance, instruction[2]);
if (!Array.isArray(target)) return false;
const value = instruction[5];
const children = instruction[6];
for (let elementIndex = 0; elementIndex < target.length; elementIndex++) {
const element = target[elementIndex];
if (!isObject(element)) return false;
for (let index = 0; index < value.length; index++) {
const entry = value[index];
const name = entry[0];
const required = entry[2];
if (!Object.hasOwn(element, name)) {
if (required) return false;
continue;
}
if (index < children.length) {
if (!evaluateInstructionFast(children[index], element[name], depth + 1, template, evaluator)) return false;
}
}
}
return true;
}

function AnnotationEmit_fast() { return true; }
function AnnotationToParent_fast() { return true; }
function AnnotationBasenameToParent_fast() { return true; }
Expand Down Expand Up @@ -3905,7 +3969,7 @@ fastHandlers[4] = AssertionDefinesAllStrict_fast;
fastHandlers[27] = AssertionEqual_fast;
fastHandlers[65] = LoopPropertiesMatch_fast;
fastHandlers[56] = LogicalOr_fast;
fastHandlers[99] = ControlJump_fast;
fastHandlers[100] = ControlJump_fast;
fastHandlers[29] = AssertionEqualsAnyStringHash_fast;
fastHandlers[58] = LogicalXor_fast;
fastHandlers[2] = AssertionDefinesStrict_fast;
Expand All @@ -3923,7 +3987,7 @@ fastHandlers[1] = AssertionDefines_fast;
fastHandlers[60] = LogicalWhenType_fast;
fastHandlers[61] = LogicalWhenDefines_fast;
fastHandlers[0] = AssertionFail_fast;
fastHandlers[92] = LoopContains_fast;
fastHandlers[93] = LoopContains_fast;
fastHandlers[54] = LogicalNot_fast;
fastHandlers[85] = LoopItemsType_fast;
fastHandlers[86] = LoopItemsTypeStrict_fast;
Expand Down Expand Up @@ -3991,7 +4055,8 @@ fastHandlers[88] = LoopItemsPropertiesExactlyTypeStrictHash_fast;
fastHandlers[89] = LoopItemsPropertiesExactlyTypeStrictHash_fast;
fastHandlers[90] = LoopItemsIntegerBounded_fast;
fastHandlers[91] = LoopItemsIntegerBoundedSized_fast;
fastHandlers[98] = ControlDynamicAnchorJump_fast;
fastHandlers[92] = LoopItemsObjectProperties_fast;
fastHandlers[99] = ControlDynamicAnchorJump_fast;

import { describe } from './describe.mjs';

Expand Down
18 changes: 10 additions & 8 deletions ports/javascript/opcodes.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -90,14 +90,15 @@ export const LOOP_ITEMS_PROPERTIES_EXACTLY_TYPE_STRICT_HASH = 88;
export const LOOP_ITEMS_PROPERTIES_EXACTLY_TYPE_STRICT_HASH3 = 89;
export const LOOP_ITEMS_INTEGER_BOUNDED = 90;
export const LOOP_ITEMS_INTEGER_BOUNDED_SIZED = 91;
export const LOOP_CONTAINS = 92;
export const CONTROL_GROUP = 93;
export const CONTROL_GROUP_WHEN_DEFINES = 94;
export const CONTROL_GROUP_WHEN_DEFINES_DIRECT = 95;
export const CONTROL_GROUP_WHEN_TYPE = 96;
export const CONTROL_EVALUATE = 97;
export const CONTROL_DYNAMIC_ANCHOR_JUMP = 98;
export const CONTROL_JUMP = 99;
export const LOOP_ITEMS_OBJECT_PROPERTIES = 92;
export const LOOP_CONTAINS = 93;
export const CONTROL_GROUP = 94;
export const CONTROL_GROUP_WHEN_DEFINES = 95;
export const CONTROL_GROUP_WHEN_DEFINES_DIRECT = 96;
export const CONTROL_GROUP_WHEN_TYPE = 97;
export const CONTROL_EVALUATE = 98;
export const CONTROL_DYNAMIC_ANCHOR_JUMP = 99;
export const CONTROL_JUMP = 100;

export const INSTRUCTION_NAMES = {
"AssertionFail": ASSERTION_FAIL,
Expand Down Expand Up @@ -192,6 +193,7 @@ export const INSTRUCTION_NAMES = {
"LoopItemsPropertiesExactlyTypeStrictHash3": LOOP_ITEMS_PROPERTIES_EXACTLY_TYPE_STRICT_HASH3,
"LoopItemsIntegerBounded": LOOP_ITEMS_INTEGER_BOUNDED,
"LoopItemsIntegerBoundedSized": LOOP_ITEMS_INTEGER_BOUNDED_SIZED,
"LoopItemsObjectProperties": LOOP_ITEMS_OBJECT_PROPERTIES,
"LoopContains": LOOP_CONTAINS,
"ControlGroup": CONTROL_GROUP,
"ControlGroupWhenDefines": CONTROL_GROUP_WHEN_DEFINES,
Expand Down
59 changes: 59 additions & 0 deletions src/compiler/postprocess.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ inline auto is_noop_without_children(const InstructionIndex type) noexcept
case InstructionIndex::LoopItems:
case InstructionIndex::LoopItemsFrom:
case InstructionIndex::LoopItemsUnevaluated:
case InstructionIndex::LoopItemsObjectProperties:

@augmentcode augmentcode Bot Jun 30, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

In src/compiler/postprocess.h:43, treating InstructionIndex::LoopItemsObjectProperties as a no-op when children.empty() looks incorrect: the instruction still enforces that the target is an array and each item is an object, and it can also enforce required properties encoded in value even with zero children.

Severity: high

Fix This in Augment

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

case InstructionIndex::LoopContains:
case InstructionIndex::ControlGroupWhenDefines:
case InstructionIndex::ControlGroupWhenDefinesDirect:
Expand Down Expand Up @@ -417,7 +418,65 @@ inline auto postprocess(std::vector<Instructions> &targets,
}
}

std::size_t loop_items_object_candidate{SIZE_MAX};
std::size_t type_array_candidate{SIZE_MAX};
for (std::size_t scan = 0; scan < current->size(); scan++) {
const auto &scan_instruction{(*current)[scan]};
if ((scan_instruction.type == InstructionIndex::LoopItems ||
(scan_instruction.type == InstructionIndex::LoopItemsFrom &&
std::get<ValueUnsignedInteger>(scan_instruction.value) == 0)) &&
scan_instruction.children.size() == 1 &&
scan_instruction.children.front().type ==
InstructionIndex::AssertionObjectPropertiesSimple) {
loop_items_object_candidate = scan;
}

if ((scan_instruction.type == InstructionIndex::AssertionTypeStrict ||
scan_instruction.type == InstructionIndex::AssertionType ||
scan_instruction.type ==
InstructionIndex::AssertionPropertyTypeStrict ||
scan_instruction.type ==
InstructionIndex::AssertionPropertyType) &&
std::get<ValueType>(scan_instruction.value) ==
sourcemeta::core::JSON::Type::Array) {
type_array_candidate = scan;
}
}

const bool fuse_loop_items_object{loop_items_object_candidate !=

@augmentcode augmentcode Bot Jun 30, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

In src/compiler/postprocess.h:421-448, the fusion logic tracks only one loop_items_object_candidate and one type_array_candidate (the last matches) and doesn’t verify they share the same relative_instance_location, which could fuse/remove the wrong type assertion if multiple array-related instructions exist in the same instruction list.

Severity: medium

Fix This in Augment

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

SIZE_MAX &&
type_array_candidate != SIZE_MAX};

for (auto &instruction : *current) {
if (fuse_loop_items_object) {
if (&instruction == &(*current)[type_array_candidate]) {
changed = true;
continue;
}

if (&instruction == &(*current)[loop_items_object_candidate]) {
auto &child{instruction.children.front()};
const auto new_extra_index{extra.size()};
auto &parent_meta{extra[instruction.extra_index]};
auto &child_meta{extra[child.extra_index]};
extra.push_back(
{.relative_schema_location =
parent_meta.relative_schema_location.concat(
child_meta.relative_schema_location),
.keyword_location = std::move(child_meta.keyword_location),
.schema_resource = child_meta.schema_resource});
result.push_back(Instruction{
.type = InstructionIndex::LoopItemsObjectProperties,
.relative_instance_location =
std::move(instruction.relative_instance_location),
.value = std::move(child.value),
.children = std::move(child.children),
.extra_index = new_extra_index});
changed = true;
continue;
}
}

if (!fusion_covered_properties.empty()) {
switch (instruction.type) {
case InstructionIndex::AssertionDefinesAllStrict:
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 @@ -916,6 +916,12 @@ auto describe(const bool valid, const Instruction &step,
"property subschemas";
}

if (step.type ==
sourcemeta::blaze::InstructionIndex::LoopItemsObjectProperties) {
return "Every item in the array value was expected to be an object "
"validating against the defined property subschemas";
}

if (step.type == sourcemeta::blaze::InstructionIndex::LoopPropertiesType) {
std::ostringstream message;
message << "The object properties were expected to be of type "
Expand Down
47 changes: 46 additions & 1 deletion src/evaluator/include/sourcemeta/blaze/evaluator_dispatch.h
Original file line number Diff line number Diff line change
Expand Up @@ -2582,6 +2582,50 @@ INSTRUCTION_HANDLER(LoopItemsIntegerBoundedSized) {
EVALUATE_END(LoopItemsIntegerBoundedSized);
}

INSTRUCTION_HANDLER(LoopItemsObjectProperties) {
EVALUATE_BEGIN_NON_STRING(LoopItemsObjectProperties, true);
if (!target.is_array()) {
EVALUATE_END(LoopItemsObjectProperties);
}

const auto &value{assume_value<ValueObjectProperties>(instruction.value)};
assert(value.size() >= instruction.children.size());
result = true;

for (const auto &element : target.as_array()) {
if (!element.is_object()) [[unlikely]] {
result = false;
EVALUATE_END(LoopItemsObjectProperties);
}

for (std::size_t index = 0; index < value.size(); index++) {
const auto &entry{value[index]};
const auto &name{std::get<0>(entry)};
const auto hash{std::get<1>(entry)};
const auto is_required{std::get<2>(entry)};
const auto *property_value{element.try_at(name, hash)};
if (!property_value) {
if (is_required) [[unlikely]] {
result = false;
EVALUATE_END(LoopItemsObjectProperties);
}

continue;
}

if (index < instruction.children.size() &&
!evaluate_instruction_without_callback(
instruction.children[index], *property_value, depth + 1, context))
[[unlikely]] {
result = false;
EVALUATE_END(LoopItemsObjectProperties);
}
}
}

EVALUATE_END(LoopItemsObjectProperties);
}

INSTRUCTION_HANDLER(LoopContains) {
EVALUATE_BEGIN_NON_STRING(LoopContains, target.is_array());
assert(!instruction.children.empty());
Expand Down Expand Up @@ -2652,7 +2696,7 @@ using DispatchHandler = bool (*)(
template <bool Track, bool Dynamic, bool HasCallback>
// Must have same order as InstructionIndex
// NOLINTNEXTLINE(modernize-avoid-c-arrays)
static constexpr DispatchHandler<Track, Dynamic, HasCallback> handlers[100] = {
static constexpr DispatchHandler<Track, Dynamic, HasCallback> handlers[101] = {
AssertionFail,
AssertionDefines,
AssertionDefinesStrict,
Expand Down Expand Up @@ -2745,6 +2789,7 @@ static constexpr DispatchHandler<Track, Dynamic, HasCallback> handlers[100] = {
LoopItemsPropertiesExactlyTypeStrictHash3,
LoopItemsIntegerBounded,
LoopItemsIntegerBoundedSized,
LoopItemsObjectProperties,
LoopContains,
ControlGroup,
ControlGroupWhenDefines,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ enum class InstructionIndex : std::uint8_t {
LoopItemsPropertiesExactlyTypeStrictHash3,
LoopItemsIntegerBounded,
LoopItemsIntegerBoundedSized,
LoopItemsObjectProperties,
LoopContains,
ControlGroup,
ControlGroupWhenDefines,
Expand Down Expand Up @@ -217,6 +218,7 @@ constexpr std::string_view InstructionNames[] = {
"LoopItemsPropertiesExactlyTypeStrictHash3",
"LoopItemsIntegerBounded",
"LoopItemsIntegerBoundedSized",
"LoopItemsObjectProperties",
"LoopContains",
"ControlGroup",
"ControlGroupWhenDefines",
Expand Down
Loading