From ebb06e1984cf5958d0292991a645c259e50b670f Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Wed, 15 Apr 2026 20:45:46 -0400 Subject: [PATCH 1/2] [WIP] Prototype a new `LoopItemsObjectProperties` instruction Signed-off-by: Juan Cruz Viotti --- ports/javascript/index.mjs | 86 ++++++++++++++++--- ports/javascript/opcodes.mjs | 18 ++-- src/compiler/postprocess.h | 59 +++++++++++++ src/evaluator/evaluator_describe.cc | 6 ++ .../sourcemeta/blaze/evaluator_dispatch.h | 45 ++++++++++ .../sourcemeta/blaze/evaluator_instruction.h | 2 + 6 files changed, 197 insertions(+), 19 deletions(-) diff --git a/ports/javascript/index.mjs b/ports/javascript/index.mjs index f58f86bf5..00be834fc 100644 --- a/ports/javascript/index.mjs +++ b/ports/javascript/index.mjs @@ -1561,6 +1561,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; @@ -2707,14 +2745,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) { @@ -3461,6 +3500,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; } @@ -3905,7 +3968,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; @@ -3923,7 +3986,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; @@ -3991,7 +4054,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'; diff --git a/ports/javascript/opcodes.mjs b/ports/javascript/opcodes.mjs index 958382973..a52236adb 100644 --- a/ports/javascript/opcodes.mjs +++ b/ports/javascript/opcodes.mjs @@ -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, @@ -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, diff --git a/src/compiler/postprocess.h b/src/compiler/postprocess.h index 70504705a..147bfb226 100644 --- a/src/compiler/postprocess.h +++ b/src/compiler/postprocess.h @@ -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: case InstructionIndex::LoopContains: case InstructionIndex::ControlGroupWhenDefines: case InstructionIndex::ControlGroupWhenDefinesDirect: @@ -417,7 +418,65 @@ inline auto postprocess(std::vector &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(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(scan_instruction.value) == + sourcemeta::core::JSON::Type::Array) { + type_array_candidate = scan; + } + } + + const bool fuse_loop_items_object{loop_items_object_candidate != + 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: diff --git a/src/evaluator/evaluator_describe.cc b/src/evaluator/evaluator_describe.cc index cd108e419..586a2f17d 100644 --- a/src/evaluator/evaluator_describe.cc +++ b/src/evaluator/evaluator_describe.cc @@ -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 " diff --git a/src/evaluator/include/sourcemeta/blaze/evaluator_dispatch.h b/src/evaluator/include/sourcemeta/blaze/evaluator_dispatch.h index 9ef01708b..02155adc1 100644 --- a/src/evaluator/include/sourcemeta/blaze/evaluator_dispatch.h +++ b/src/evaluator/include/sourcemeta/blaze/evaluator_dispatch.h @@ -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(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()); @@ -2745,6 +2789,7 @@ static constexpr DispatchHandler handlers[100] = { LoopItemsPropertiesExactlyTypeStrictHash3, LoopItemsIntegerBounded, LoopItemsIntegerBoundedSized, + LoopItemsObjectProperties, LoopContains, ControlGroup, ControlGroupWhenDefines, diff --git a/src/evaluator/include/sourcemeta/blaze/evaluator_instruction.h b/src/evaluator/include/sourcemeta/blaze/evaluator_instruction.h index e3d39c345..554ccef7b 100644 --- a/src/evaluator/include/sourcemeta/blaze/evaluator_instruction.h +++ b/src/evaluator/include/sourcemeta/blaze/evaluator_instruction.h @@ -112,6 +112,7 @@ enum class InstructionIndex : std::uint8_t { LoopItemsPropertiesExactlyTypeStrictHash3, LoopItemsIntegerBounded, LoopItemsIntegerBoundedSized, + LoopItemsObjectProperties, LoopContains, ControlGroup, ControlGroupWhenDefines, @@ -217,6 +218,7 @@ constexpr std::string_view InstructionNames[] = { "LoopItemsPropertiesExactlyTypeStrictHash3", "LoopItemsIntegerBounded", "LoopItemsIntegerBoundedSized", + "LoopItemsObjectProperties", "LoopContains", "ControlGroup", "ControlGroupWhenDefines", From ea00550183ef5ac44a1be2971605f03a4643dec4 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Wed, 15 Apr 2026 21:24:13 -0400 Subject: [PATCH 2/2] Fix Signed-off-by: Juan Cruz Viotti --- ports/javascript/index.mjs | 15 ++++++++------- .../include/sourcemeta/blaze/evaluator_dispatch.h | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/ports/javascript/index.mjs b/ports/javascript/index.mjs index 00be834fc..eea0311a7 100644 --- a/ports/javascript/index.mjs +++ b/ports/javascript/index.mjs @@ -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;j0)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;j0)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 // Must have same order as InstructionIndex // NOLINTNEXTLINE(modernize-avoid-c-arrays) -static constexpr DispatchHandler handlers[100] = { +static constexpr DispatchHandler handlers[101] = { AssertionFail, AssertionDefines, AssertionDefinesStrict,