diff --git a/ports/javascript/index.mjs b/ports/javascript/index.mjs index f58f86bf5..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 &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..c34a83c08 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()); @@ -2652,7 +2696,7 @@ using DispatchHandler = bool (*)( template // Must have same order as InstructionIndex // NOLINTNEXTLINE(modernize-avoid-c-arrays) -static constexpr DispatchHandler handlers[100] = { +static constexpr DispatchHandler handlers[101] = { AssertionFail, AssertionDefines, AssertionDefinesStrict, @@ -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",