diff --git a/ports/javascript/index.mjs b/ports/javascript/index.mjs index a070777d3..3480b75df 100644 --- a/ports/javascript/index.mjs +++ b/ports/javascript/index.mjs @@ -1,10 +1,10 @@ -const JSON_VERSION = 4; +const JSON_VERSION = 5; const DEPTH_LIMIT = 300; const ANNOTATION_EMIT = 49; const ANNOTATION_TO_PARENT = 50; const ANNOTATION_BASENAME_TO_PARENT = 51; -const CONTROL_GROUP_START = 92; -const CONTROL_EVALUATE_END = 96; +const CONTROL_GROUP_START = 93; +const CONTROL_EVALUATE_END = 97; const URI_REGEX = /^[a-zA-Z][a-zA-Z0-9+\-.]*:[^\s]*$/; function buildJsonPointer(tokens, length) { @@ -152,7 +152,7 @@ function prepareInstruction(instruction) { function resolveJumpTargets(instructions, targets) { for (let index = 0; index < instructions.length; index++) { const instruction = instructions[index]; - if (instruction[0] === 98) { + if (instruction[0] === 99) { const targetIndex = instruction[5]; if (targetIndex < targets.length) { instruction[5] = targets[targetIndex]; @@ -187,7 +187,7 @@ function collectAnchorNames(targets, result) { function collectAnchorNamesFromInstructions(instructions, result) { for (let index = 0; index < instructions.length; index++) { const instruction = instructions[index]; - if (instruction[0] === 97 && typeof instruction[5] === 'string') { + if (instruction[0] === 98 && typeof instruction[5] === 'string') { result.add(instruction[5]); } if (instruction[6]) { @@ -326,14 +326,14 @@ function compileInstructionToCode(instruction, captures, visited, budget) { case 84: { 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 94: { 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 95: { var c='if(_jt(i)!=='+value+')return true;'; if(children&&children.length>0)c+=seq(children,'i'); return c+'return true;'; } - case 96: return 'return true;'; - case 97: return fb(97); - case 98: { if(!value)return 'return true;'; if(visited&&visited.has(instruction))return fb(98); if(!visited)visited=new Set(); visited.add(instruction); var r=R('t'); if(!r)return fb(98); var c=r; 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;j 96) { + if (type < 93 || type > 97) { if (evaluator.trackMode) { evaluator.pushPath(instruction[1]); } @@ -1543,6 +1543,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; @@ -2688,14 +2726,15 @@ const handlers = [ LoopItemsPropertiesExactlyTypeStrictHash, // 88 LoopItemsIntegerBounded, // 89 LoopItemsIntegerBoundedSized, // 90 - LoopContains, // 91 - ControlGroup, // 92 - ControlGroupWhenDefines, // 93 - ControlGroupWhenDefinesDirect, // 94 - ControlGroupWhenType, // 95 - ControlEvaluate, // 96 - ControlDynamicAnchorJump, // 97 - ControlJump // 98 + LoopItemsObjectProperties, // 91 + LoopContains, // 92 + ControlGroup, // 93 + ControlGroupWhenDefines, // 94 + ControlGroupWhenDefinesDirect, // 95 + ControlGroupWhenType, // 96 + ControlEvaluate, // 97 + ControlDynamicAnchorJump, // 98 + ControlJump // 99 ]; function AssertionTypeArrayBounded_fast(instruction, instance, depth, template, evaluator) { @@ -3436,6 +3475,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; } @@ -3880,7 +3943,7 @@ fastHandlers[4] = AssertionDefinesAllStrict_fast; fastHandlers[26] = AssertionEqual_fast; fastHandlers[64] = LoopPropertiesMatch_fast; fastHandlers[55] = LogicalOr_fast; -fastHandlers[98] = ControlJump_fast; +fastHandlers[99] = ControlJump_fast; fastHandlers[28] = AssertionEqualsAnyStringHash_fast; fastHandlers[57] = LogicalXor_fast; fastHandlers[2] = AssertionDefinesStrict_fast; @@ -3898,7 +3961,7 @@ fastHandlers[1] = AssertionDefines_fast; fastHandlers[59] = LogicalWhenType_fast; fastHandlers[60] = LogicalWhenDefines_fast; fastHandlers[0] = AssertionFail_fast; -fastHandlers[91] = LoopContains_fast; +fastHandlers[92] = LoopContains_fast; fastHandlers[53] = LogicalNot_fast; fastHandlers[84] = LoopItemsType_fast; fastHandlers[85] = LoopItemsTypeStrict_fast; @@ -3965,6 +4028,7 @@ fastHandlers[87] = LoopItemsPropertiesExactlyTypeStrictHash_fast; fastHandlers[88] = LoopItemsPropertiesExactlyTypeStrictHash_fast; fastHandlers[89] = LoopItemsIntegerBounded_fast; fastHandlers[90] = LoopItemsIntegerBoundedSized_fast; -fastHandlers[97] = ControlDynamicAnchorJump_fast; +fastHandlers[91] = LoopItemsObjectProperties_fast; +fastHandlers[98] = ControlDynamicAnchorJump_fast; export { Blaze }; diff --git a/ports/javascript/test.mjs b/ports/javascript/test.mjs index 4f6206aea..44dfcbb8f 100644 --- a/ports/javascript/test.mjs +++ b/ports/javascript/test.mjs @@ -133,15 +133,15 @@ describe('reviver', () => { describe('version', () => { it('rejects a template with an unsupported version', () => { - const template = [5, false, false, [[]], []]; + const template = [6, false, false, [[]], []]; assert.throws(() => new Blaze(template), { - message: 'Only version 4 of the compiled template is supported by this version of the evaluator' + message: 'Only version 5 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 4 of the compiled template is supported by this version of the evaluator' + message: 'Only version 5 of the compiled template is supported by this version of the evaluator' }); }); }); diff --git a/ports/javascript/trace.mjs b/ports/javascript/trace.mjs index 4911d80f9..bff69d3cb 100644 --- a/ports/javascript/trace.mjs +++ b/ports/javascript/trace.mjs @@ -103,14 +103,15 @@ const INSTRUCTION_NAMES = { "LoopItemsPropertiesExactlyTypeStrictHash3": 88, "LoopItemsIntegerBounded": 89, "LoopItemsIntegerBoundedSized": 90, - "LoopContains": 91, - "ControlGroup": 92, - "ControlGroupWhenDefines": 93, - "ControlGroupWhenDefinesDirect": 94, - "ControlGroupWhenType": 95, - "ControlEvaluate": 96, - "ControlDynamicAnchorJump": 97, - "ControlJump": 98, + "LoopItemsObjectProperties": 91, + "LoopContains": 92, + "ControlGroup": 93, + "ControlGroupWhenDefines": 94, + "ControlGroupWhenDefinesDirect": 95, + "ControlGroupWhenType": 96, + "ControlEvaluate": 97, + "ControlDynamicAnchorJump": 98, + "ControlJump": 99, "Annotation": -1 }; diff --git a/src/compiler/postprocess.h b/src/compiler/postprocess.h index a959457ae..857d60589 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: @@ -414,7 +415,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 99a2556fc..043b18e06 100644 --- a/src/evaluator/evaluator_describe.cc +++ b/src/evaluator/evaluator_describe.cc @@ -807,6 +807,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.h b/src/evaluator/include/sourcemeta/blaze/evaluator.h index 87822b065..b125cd70c 100644 --- a/src/evaluator/include/sourcemeta/blaze/evaluator.h +++ b/src/evaluator/include/sourcemeta/blaze/evaluator.h @@ -44,7 +44,7 @@ struct Template { }; /// @ingroup evaluator -constexpr std::size_t JSON_VERSION{4}; +constexpr std::size_t JSON_VERSION{5}; /// @ingroup evaluator /// Parse a template from JSON diff --git a/src/evaluator/include/sourcemeta/blaze/evaluator_dispatch.h b/src/evaluator/include/sourcemeta/blaze/evaluator_dispatch.h index 10bc20f75..ba617d4e8 100644 --- a/src/evaluator/include/sourcemeta/blaze/evaluator_dispatch.h +++ b/src/evaluator/include/sourcemeta/blaze/evaluator_dispatch.h @@ -2371,6 +2371,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()); @@ -2441,7 +2485,7 @@ using DispatchHandler = bool (*)( template // Must have same order as InstructionIndex // NOLINTNEXTLINE(modernize-avoid-c-arrays) -static constexpr DispatchHandler handlers[99] = { +static constexpr DispatchHandler handlers[100] = { AssertionFail, AssertionDefines, AssertionDefinesStrict, @@ -2533,6 +2577,7 @@ static constexpr DispatchHandler handlers[99] = { 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 f9c351a15..00539ed49 100644 --- a/src/evaluator/include/sourcemeta/blaze/evaluator_instruction.h +++ b/src/evaluator/include/sourcemeta/blaze/evaluator_instruction.h @@ -111,6 +111,7 @@ enum class InstructionIndex : std::uint8_t { LoopItemsPropertiesExactlyTypeStrictHash3, LoopItemsIntegerBounded, LoopItemsIntegerBoundedSized, + LoopItemsObjectProperties, LoopContains, ControlGroup, ControlGroupWhenDefines, @@ -215,6 +216,7 @@ constexpr std::string_view InstructionNames[] = { "LoopItemsPropertiesExactlyTypeStrictHash3", "LoopItemsIntegerBounded", "LoopItemsIntegerBoundedSized", + "LoopItemsObjectProperties", "LoopContains", "ControlGroup", "ControlGroupWhenDefines", diff --git a/test/compiler/compiler_json_test.cc b/test/compiler/compiler_json_test.cc index e93223824..1342ea89f 100644 --- a/test/compiler/compiler_json_test.cc +++ b/test/compiler/compiler_json_test.cc @@ -31,7 +31,7 @@ TEST(Compiler_JSON, example_1) { sourcemeta::blaze::default_schema_compiler)}; const sourcemeta::core::JSON expected{sourcemeta::core::parse_json(R"JSON([ - 4, + 5, false, false, [ @@ -67,7 +67,7 @@ TEST(Compiler_JSON, example_2) { sourcemeta::blaze::default_schema_compiler)}; const sourcemeta::core::JSON expected{sourcemeta::core::parse_json(R"JSON([ - 4, + 5, false, false, [ @@ -118,7 +118,7 @@ TEST(Compiler_JSON, example_3) { sourcemeta::blaze::default_schema_compiler)}; const sourcemeta::core::JSON expected{sourcemeta::core::parse_json(R"JSON([ - 4, + 5, false, false, [ @@ -156,7 +156,7 @@ TEST(Compiler_JSON, example_4) { sourcemeta::blaze::default_schema_compiler)}; const sourcemeta::core::JSON expected{sourcemeta::core::parse_json(R"JSON([ - 4, + 5, false, false, [ @@ -233,7 +233,7 @@ TEST(Compiler_JSON, example_5) { sourcemeta::blaze::Mode::Exhaustive)}; const sourcemeta::core::JSON expected{sourcemeta::core::parse_json(R"JSON([ - 4, + 5, false, true, [ @@ -278,7 +278,7 @@ TEST(Compiler_JSON, example_6) { sourcemeta::blaze::Mode::Exhaustive)}; const sourcemeta::core::JSON expected{sourcemeta::core::parse_json(R"JSON([ - 4, + 5, false, true, [ @@ -408,7 +408,7 @@ TEST(Compiler_JSON, invalid_1) { TEST(Compiler_JSON, invalid_version) { const auto input{sourcemeta::core::parse_json(R"JSON([ - 5, + 6, false, false, [