From 635ff2919715f5a2e7e6e67b86a123200c63dd59 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Fri, 30 Oct 2026 17:00:38 -0300 Subject: [PATCH] Fuse `items` of size-bounded typed arrays into one instruction Signed-off-by: Juan Cruz Viotti --- ports/javascript/index.mjs | 84 ++++++++++--- ports/javascript/official.test.mjs | 6 +- ports/javascript/opcodes.mjs | 18 +-- src/compiler/default_compiler_draft3.h | 53 ++++++++ src/evaluator/evaluator_describe.cc | 6 + src/evaluator/evaluator_json.cc | 1 + .../include/sourcemeta/blaze/evaluator.h | 2 +- .../sourcemeta/blaze/evaluator_dispatch.h | 38 +++++- .../sourcemeta/blaze/evaluator_instruction.h | 2 + .../sourcemeta/blaze/evaluator_value.h | 6 +- test/compiler/compiler_json_test.cc | 73 +++++++++-- test/evaluator/evaluator_test.cc | 119 ++++++++++++++++++ 12 files changed, 362 insertions(+), 46 deletions(-) diff --git a/ports/javascript/index.mjs b/ports/javascript/index.mjs index d01cd7775..fdc1edf59 100644 --- a/ports/javascript/index.mjs +++ b/ports/javascript/index.mjs @@ -7,7 +7,7 @@ import { CONTROL_JUMP, CONTROL_DYNAMIC_ANCHOR_JUMP } from './opcodes.mjs'; -const JSON_VERSION = 10; +const JSON_VERSION = 11; const DEPTH_LIMIT = 300; const URI_REGEX = /^[a-zA-Z][a-zA-Z0-9+\-.]*:[^\s]*$/; @@ -338,13 +338,14 @@ function compileInstructionToCode(instruction, captures, visited, budget) { case 90: { 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 98: { 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 99: { var c='if(_jt(i)!=='+value+')return true;'; if(children&&children.length>0)c+=seq(children,'i'); return c+'return true;'; } - case 100: return 'return true;'; - case 101: return fb(101); - case 102: { if(!value)return 'return true;'; if(visited&&visited.has(instruction))return fb(102); if(!visited)visited=new Set(); visited.add(instruction); var r=R('t'); if(!r)return fb(102); var c=r; for(var j=0;j0)c+=seq(children,'i'); return c+'return true;'; } + case 99: { 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 100: { var c='if(_jt(i)!=='+value+')return true;'; if(children&&children.length>0)c+=seq(children,'i'); return c+'return true;'; } + case 101: return 'return true;'; + case 102: return fb(102); + case 103: { if(!value)return 'return true;'; if(visited&&visited.has(instruction))return fb(103); if(!visited)visited=new Set(); visited.add(instruction); var r=R('t'); if(!r)return fb(103); var c=r; for(var j=0;j range[1])) { + if (evaluator.callbackMode) evaluator.callbackPop(instruction, false); + return false; + } + for (let nested = 0; nested < item.length; nested++) { + if (!typeSetTest(bitmask, effectiveTypeStrictReal(item[nested]))) { + 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; @@ -2794,14 +2819,15 @@ const handlers = [ LoopItemsPropertiesExactlyTypeStrictHash, // 92 LoopItemsIntegerBounded, // 93 LoopItemsIntegerBoundedSized, // 94 - LoopContains, // 95 - ControlGroup, // 96 - ControlGroupWhenDefines, // 97 - ControlGroupWhenDefinesDirect, // 98 - ControlGroupWhenType, // 99 - ControlEvaluate, // 100 - ControlDynamicAnchorJump, // 101 - ControlJump // 102 + LoopItemsArrayBoundedTypeStrictAny, // 95 + LoopContains, // 96 + ControlGroup, // 97 + ControlGroupWhenDefines, // 98 + ControlGroupWhenDefinesDirect, // 99 + ControlGroupWhenType, // 100 + ControlEvaluate, // 101 + ControlDynamicAnchorJump, // 102 + ControlJump // 103 ]; function AssertionTypeArrayBounded_fast(instruction, instance, depth, template, evaluator) { @@ -3962,6 +3988,25 @@ function LoopItemsIntegerBounded_fast(instruction, instance, depth, template, ev return true; } +function LoopItemsArrayBoundedTypeStrictAny_fast(instruction, instance, depth, template, evaluator) { + const relInstance = instruction[2]; + const target = relInstance.length === 0 ? instance : resolveInstance(instance, relInstance); + if (!Array.isArray(target)) return true; + const bitmask = instruction[6][0]; + const range = instruction[6][1]; + const minimum = range[0]; + const maximum = range[1]; + for (let index = 0; index < target.length; index++) { + const item = target[index]; + if (!Array.isArray(item) || item.length < minimum || + (maximum !== null && maximum !== undefined && item.length > maximum)) return false; + for (let nested = 0; nested < item.length; nested++) { + if (!typeSetTest(bitmask, effectiveTypeStrictReal(item[nested]))) return false; + } + } + return true; +} + function LoopItemsIntegerBoundedSized_fast(instruction, instance, depth, template, evaluator) { const value = instruction[6]; const minimum = value[0][0]; @@ -4025,7 +4070,7 @@ fastHandlers[4] = AssertionDefinesAllStrict_fast; fastHandlers[27] = AssertionEqual_fast; fastHandlers[68] = LoopPropertiesMatch_fast; fastHandlers[59] = LogicalOr_fast; -fastHandlers[102] = ControlJump_fast; +fastHandlers[103] = ControlJump_fast; fastHandlers[29] = AssertionEqualsAnyStringHash_fast; fastHandlers[61] = LogicalXor_fast; fastHandlers[2] = AssertionDefinesStrict_fast; @@ -4043,7 +4088,8 @@ fastHandlers[1] = AssertionDefines_fast; fastHandlers[63] = LogicalWhenType_fast; fastHandlers[64] = LogicalWhenDefines_fast; fastHandlers[0] = AssertionFail_fast; -fastHandlers[95] = LoopContains_fast; +fastHandlers[95] = LoopItemsArrayBoundedTypeStrictAny_fast; +fastHandlers[96] = LoopContains_fast; fastHandlers[57] = LogicalNot_fast; fastHandlers[88] = LoopItemsType_fast; fastHandlers[89] = LoopItemsTypeStrict_fast; @@ -4114,7 +4160,7 @@ fastHandlers[91] = LoopItemsPropertiesExactlyTypeStrictHash_fast; fastHandlers[92] = LoopItemsPropertiesExactlyTypeStrictHash_fast; fastHandlers[93] = LoopItemsIntegerBounded_fast; fastHandlers[94] = LoopItemsIntegerBoundedSized_fast; -fastHandlers[101] = ControlDynamicAnchorJump_fast; +fastHandlers[102] = ControlDynamicAnchorJump_fast; import { describe } from './describe.mjs'; diff --git a/ports/javascript/official.test.mjs b/ports/javascript/official.test.mjs index 6577dca22..ef09b8596 100644 --- a/ports/javascript/official.test.mjs +++ b/ports/javascript/official.test.mjs @@ -133,15 +133,15 @@ describe('reviver', () => { describe('version', () => { it('rejects a template with an unsupported version', () => { - const template = [9, false, false, [[]], [], []]; + const template = [10, false, false, [[]], [], []]; assert.throws(() => new Blaze(template), { - message: 'Only version 10 of the compiled template is supported by this version of the evaluator' + message: 'Only version 11 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 10 of the compiled template is supported by this version of the evaluator' + message: 'Only version 11 of the compiled template is supported by this version of the evaluator' }); }); }); diff --git a/ports/javascript/opcodes.mjs b/ports/javascript/opcodes.mjs index 54a1eccf4..1ed1d42a5 100644 --- a/ports/javascript/opcodes.mjs +++ b/ports/javascript/opcodes.mjs @@ -93,14 +93,15 @@ export const LOOP_ITEMS_PROPERTIES_EXACTLY_TYPE_STRICT_HASH = 91; export const LOOP_ITEMS_PROPERTIES_EXACTLY_TYPE_STRICT_HASH3 = 92; export const LOOP_ITEMS_INTEGER_BOUNDED = 93; export const LOOP_ITEMS_INTEGER_BOUNDED_SIZED = 94; -export const LOOP_CONTAINS = 95; -export const CONTROL_GROUP = 96; -export const CONTROL_GROUP_WHEN_DEFINES = 97; -export const CONTROL_GROUP_WHEN_DEFINES_DIRECT = 98; -export const CONTROL_GROUP_WHEN_TYPE = 99; -export const CONTROL_EVALUATE = 100; -export const CONTROL_DYNAMIC_ANCHOR_JUMP = 101; -export const CONTROL_JUMP = 102; +export const LOOP_ITEMS_ARRAY_BOUNDED_TYPE_STRICT_ANY = 95; +export const LOOP_CONTAINS = 96; +export const CONTROL_GROUP = 97; +export const CONTROL_GROUP_WHEN_DEFINES = 98; +export const CONTROL_GROUP_WHEN_DEFINES_DIRECT = 99; +export const CONTROL_GROUP_WHEN_TYPE = 100; +export const CONTROL_EVALUATE = 101; +export const CONTROL_DYNAMIC_ANCHOR_JUMP = 102; +export const CONTROL_JUMP = 103; export const INSTRUCTION_NAMES = { "AssertionFail": ASSERTION_FAIL, @@ -198,6 +199,7 @@ export const INSTRUCTION_NAMES = { "LoopItemsPropertiesExactlyTypeStrictHash3": LOOP_ITEMS_PROPERTIES_EXACTLY_TYPE_STRICT_HASH3, "LoopItemsIntegerBounded": LOOP_ITEMS_INTEGER_BOUNDED, "LoopItemsIntegerBoundedSized": LOOP_ITEMS_INTEGER_BOUNDED_SIZED, + "LoopItemsArrayBoundedTypeStrictAny": LOOP_ITEMS_ARRAY_BOUNDED_TYPE_STRICT_ANY, "LoopContains": LOOP_CONTAINS, "ControlGroup": CONTROL_GROUP, "ControlGroupWhenDefines": CONTROL_GROUP_WHEN_DEFINES, diff --git a/src/compiler/default_compiler_draft3.h b/src/compiler/default_compiler_draft3.h index 267a023f2..7ebbf8a43 100644 --- a/src/compiler/default_compiler_draft3.h +++ b/src/compiler/default_compiler_draft3.h @@ -1942,6 +1942,51 @@ auto is_integer_bounded_pattern(const Instructions &children) -> bool { return has_type && has_min && has_max; } +auto is_bounded_array_items_pattern(const Instructions &children) -> bool { + if (children.size() != 2) { + return false; + } + + bool has_types{false}; + bool has_bounds{false}; + for (const auto &child : children) { + // A child that looks anywhere other than the item itself is not part of + // the shape, whatever its type says + if (!child.relative_instance_location.empty()) { + return false; + } + + if (child.type == InstructionIndex::LoopItemsTypeStrictAny) { + has_types = true; + } else if (child.type == InstructionIndex::AssertionTypeArrayBounded) { + // The fused instruction stops at the first item that does not match, + // which an exhaustive bound does not allow + if (std::get<2>(std::get(child.value))) { + return false; + } + + has_bounds = true; + } + } + + return has_types && has_bounds; +} + +auto extract_bounded_array_items(const Instructions &children) + -> ValueTypesWithSize { + ValueTypes types; + ValueRange bounds; + for (const auto &child : children) { + if (child.type == InstructionIndex::LoopItemsTypeStrictAny) { + types = std::get(child.value); + } else if (child.type == InstructionIndex::AssertionTypeArrayBounded) { + bounds = std::get(child.value); + } + } + + return {types, bounds}; +} + auto compiler_draft3_applicator_items_with_options( const Context &context, const SchemaContext &schema_context, const DynamicContext &dynamic_context, const bool annotate, @@ -2038,6 +2083,14 @@ auto compiler_draft3_applicator_items_with_options( extract_integer_bounds(children))}; } + if (context.mode == Mode::FastValidation && children.size() == 2 && + is_bounded_array_items_pattern(children)) { + return {make(sourcemeta::blaze::InstructionIndex:: + LoopItemsArrayBoundedTypeStrictAny, + context, schema_context, dynamic_context, + extract_bounded_array_items(children))}; + } + if (context.mode == Mode::FastValidation && children.size() == 1) { if (children.front().type == InstructionIndex::AssertionTypeStrict) { return {make(sourcemeta::blaze::InstructionIndex::LoopItemsTypeStrict, diff --git a/src/evaluator/evaluator_describe.cc b/src/evaluator/evaluator_describe.cc index 4e08bffec..8c06033c6 100644 --- a/src/evaluator/evaluator_describe.cc +++ b/src/evaluator/evaluator_describe.cc @@ -1168,6 +1168,12 @@ auto describe(const bool valid, const Instruction &step, "given range"; } + if (step.type == + sourcemeta::blaze::InstructionIndex::LoopItemsArrayBoundedTypeStrictAny) { + return "Every item in the array was expected to be an array of the given " + "size whose items were of the given types"; + } + if (step.type == sourcemeta::blaze::InstructionIndex::AssertionTypeIntegerBounded || step.type == sourcemeta::blaze::InstructionIndex:: diff --git a/src/evaluator/evaluator_json.cc b/src/evaluator/evaluator_json.cc index 7f159f9f1..8aab64a44 100644 --- a/src/evaluator/evaluator_json.cc +++ b/src/evaluator/evaluator_json.cc @@ -45,6 +45,7 @@ auto value_from_json(const sourcemeta::core::JSON &wrapper) case 23: return sourcemeta::core::from_json(value); case 24: return sourcemeta::core::from_json(value); case 25: return sourcemeta::core::from_json(value); + case 26: return sourcemeta::core::from_json(value); // clang-format on default: std::unreachable(); diff --git a/src/evaluator/include/sourcemeta/blaze/evaluator.h b/src/evaluator/include/sourcemeta/blaze/evaluator.h index ad32518be..86a8f562c 100644 --- a/src/evaluator/include/sourcemeta/blaze/evaluator.h +++ b/src/evaluator/include/sourcemeta/blaze/evaluator.h @@ -46,7 +46,7 @@ struct Template { }; /// @ingroup evaluator -constexpr std::size_t JSON_VERSION{10}; +constexpr std::size_t JSON_VERSION{11}; /// @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 1174deb08..04d599830 100644 --- a/src/evaluator/include/sourcemeta/blaze/evaluator_dispatch.h +++ b/src/evaluator/include/sourcemeta/blaze/evaluator_dispatch.h @@ -2918,6 +2918,41 @@ INSTRUCTION_HANDLER(LoopItemsIntegerBoundedSized) { EVALUATE_END(LoopItemsIntegerBoundedSized); } +// `items` whose subschema is itself an `items` with a size bound compiles to a +// loop whose body is two instructions that both only look at the item, which +// means a dispatch per item of the outer array. Nesting the two loops here +// decides the whole shape in one instruction instead +INSTRUCTION_HANDLER(LoopItemsArrayBoundedTypeStrictAny) { + EVALUATE_BEGIN_NON_STRING(LoopItemsArrayBoundedTypeStrictAny, + target.is_array()); + const auto &value{assume_value(instruction.value)}; + const auto types{value.first}; + const auto minimum{std::get<0>(value.second)}; + const auto &maximum{std::get<1>(value.second)}; + assert(types.any()); + // Require early breaking + assert(!std::get<2>(value.second)); + result = true; + for (const auto &item : target.as_array()) { + if (item.type() != JSON::Type::Array || item.array_size() < minimum || + (maximum.has_value() && item.array_size() > maximum.value())) + [[unlikely]] { + result = false; + EVALUATE_END(LoopItemsArrayBoundedTypeStrictAny); + } + + for (const auto &entry : item.as_array()) { + if (!types.test(std::to_underlying(effective_type_strict_real(entry)))) + [[unlikely]] { + result = false; + EVALUATE_END(LoopItemsArrayBoundedTypeStrictAny); + } + } + } + + EVALUATE_END(LoopItemsArrayBoundedTypeStrictAny); +} + INSTRUCTION_HANDLER(LoopContains) { EVALUATE_BEGIN_NON_STRING(LoopContains, target.is_array()); // The subschema may compile to no children, in which case every array @@ -2991,7 +3026,7 @@ using DispatchHandler = bool (*)( template // Must have same order as InstructionIndex // NOLINTNEXTLINE(modernize-avoid-c-arrays) -static constexpr DispatchHandler HANDLERS[103] = { +static constexpr DispatchHandler HANDLERS[104] = { AssertionFail, AssertionDefines, AssertionDefinesStrict, @@ -3087,6 +3122,7 @@ static constexpr DispatchHandler HANDLERS[103] = { LoopItemsPropertiesExactlyTypeStrictHash3, LoopItemsIntegerBounded, LoopItemsIntegerBoundedSized, + LoopItemsArrayBoundedTypeStrictAny, LoopContains, ControlGroup, ControlGroupWhenDefines, diff --git a/src/evaluator/include/sourcemeta/blaze/evaluator_instruction.h b/src/evaluator/include/sourcemeta/blaze/evaluator_instruction.h index 8e08a4ed6..bae48a553 100644 --- a/src/evaluator/include/sourcemeta/blaze/evaluator_instruction.h +++ b/src/evaluator/include/sourcemeta/blaze/evaluator_instruction.h @@ -115,6 +115,7 @@ enum class InstructionIndex : std::uint8_t { LoopItemsPropertiesExactlyTypeStrictHash3, LoopItemsIntegerBounded, LoopItemsIntegerBoundedSized, + LoopItemsArrayBoundedTypeStrictAny, LoopContains, ControlGroup, ControlGroupWhenDefines, @@ -223,6 +224,7 @@ constexpr std::string_view INSTRUCTION_NAMES[] = { "LoopItemsPropertiesExactlyTypeStrictHash3", "LoopItemsIntegerBounded", "LoopItemsIntegerBoundedSized", + "LoopItemsArrayBoundedTypeStrictAny", "LoopContains", "ControlGroup", "ControlGroupWhenDefines", diff --git a/src/evaluator/include/sourcemeta/blaze/evaluator_value.h b/src/evaluator/include/sourcemeta/blaze/evaluator_value.h index a9a5943fd..cea5bedc6 100644 --- a/src/evaluator/include/sourcemeta/blaze/evaluator_value.h +++ b/src/evaluator/include/sourcemeta/blaze/evaluator_value.h @@ -193,6 +193,10 @@ using ValueIntegerBounds = std::pair; /// Represents integer bounds combined with array size range using ValueIntegerBoundsWithSize = std::pair; +/// @ingroup evaluator +/// Represents a set of types combined with an array size range +using ValueTypesWithSize = std::pair; + /// @ingroup evaluator /// Represents a list of object property entries: (name, hash, required) using ValueObjectProperties = std::vector< @@ -211,7 +215,7 @@ using Value = ValuePropertyFilter, ValueIndexPair, ValuePointer, ValueTypedProperties, ValueStringHashes, ValueTypedHashes, ValueIntegerBounds, ValueIntegerBoundsWithSize, - ValueObjectProperties, ValueNumberType>; + ValueObjectProperties, ValueNumberType, ValueTypesWithSize>; } // namespace sourcemeta::blaze diff --git a/test/compiler/compiler_json_test.cc b/test/compiler/compiler_json_test.cc index a68c3a6ab..953aac771 100644 --- a/test/compiler/compiler_json_test.cc +++ b/test/compiler/compiler_json_test.cc @@ -34,7 +34,7 @@ TEST(example_1) { const sourcemeta::core::JSON expected{sourcemeta::core::parse_json(R"JSON( [ - 10, + 11, false, false, [ @@ -74,7 +74,7 @@ TEST(example_2) { const sourcemeta::core::JSON expected{sourcemeta::core::parse_json(R"JSON( [ - 10, + 11, false, false, [ @@ -134,7 +134,7 @@ TEST(example_3) { const sourcemeta::core::JSON expected{sourcemeta::core::parse_json(R"JSON( [ - 10, + 11, false, false, [ @@ -176,7 +176,7 @@ TEST(example_4) { const sourcemeta::core::JSON expected{sourcemeta::core::parse_json(R"JSON( [ - 10, + 11, false, false, [ @@ -262,7 +262,7 @@ TEST(example_5) { const sourcemeta::core::JSON expected{sourcemeta::core::parse_json(R"JSON( [ - 10, + 11, false, true, [ @@ -311,7 +311,7 @@ TEST(example_6) { const sourcemeta::core::JSON expected{sourcemeta::core::parse_json(R"JSON( [ - 10, + 11, false, true, [ @@ -405,7 +405,7 @@ TEST(properties_as_a_loop_without_a_table_of_contents) { const sourcemeta::core::JSON expected{sourcemeta::core::parse_json(R"JSON( [ - 10, + 11, false, false, [ @@ -489,7 +489,7 @@ TEST(properties_as_a_loop_without_a_table_of_contents) { ], [ [ - 96, + 97, [], [], "#/properties", @@ -519,7 +519,7 @@ TEST(properties_as_a_loop_without_a_table_of_contents) { ] ], [ - 96, + 97, [], [], "#/properties", @@ -549,7 +549,7 @@ TEST(properties_as_a_loop_without_a_table_of_contents) { ] ], [ - 96, + 97, [], [], "#/properties", @@ -579,7 +579,7 @@ TEST(properties_as_a_loop_without_a_table_of_contents) { ] ], [ - 96, + 97, [], [], "#/properties", @@ -609,7 +609,7 @@ TEST(properties_as_a_loop_without_a_table_of_contents) { ] ], [ - 96, + 97, [], [], "#/properties", @@ -639,7 +639,7 @@ TEST(properties_as_a_loop_without_a_table_of_contents) { ] ], [ - 96, + 97, [], [], "#/properties", @@ -867,3 +867,50 @@ TEST(annotations_whitelist_unused_matches_untweaked) { EXPECT_EQ(sourcemeta::blaze::to_json(with_whitelist), sourcemeta::blaze::to_json(without_tweaks)); } + +TEST(items_fused_as_a_bounded_array_type_loop) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-07/schema#", + "items": { + "type": "array", + "items": { "type": "number" }, + "minItems": 2 + } + })JSON")}; + + const auto schema_template{ + sourcemeta::blaze::compile(schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, + sourcemeta::blaze::default_schema_compiler)}; + + const sourcemeta::core::JSON expected{sourcemeta::core::parse_json(R"JSON( + [ + 11, + false, + false, + [ + [ + [ + 95, + [ "items" ], + [], + "#/items", + 0, + 1, + [ + 26, + [ + 140, + [ 2, null, false ] + ] + ] + ] + ] + ], + [], + [ "http://json-schema.org/draft-07/schema#" ] + ] + )JSON")}; + + EXPECT_BIDIRECTIONAL_JSON(schema_template, expected); +} diff --git a/test/evaluator/evaluator_test.cc b/test/evaluator/evaluator_test.cc index 111040582..7ab627d7c 100644 --- a/test/evaluator/evaluator_test.cc +++ b/test/evaluator/evaluator_test.cc @@ -804,3 +804,122 @@ TEST(properties_as_a_loop_with_an_inlined_deeper_assertion_failure) { const auto result{evaluator.validate(compiled_schema, instance)}; EXPECT_FALSE(result); } + +TEST(items_as_a_bounded_array_of_typed_items) { + // The outer `items` and the size-bounded inner `items` fuse into a single + // instruction, so the nested loop runs without a dispatch per item + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-07/schema#", + "items": { + "type": "array", + "items": { "type": "number" }, + "minItems": 2 + } + })JSON")}; + + const auto compiled_schema{ + sourcemeta::blaze::compile(schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, + sourcemeta::blaze::default_schema_compiler, + sourcemeta::blaze::Mode::FastValidation)}; + + sourcemeta::blaze::Evaluator evaluator; + const sourcemeta::core::JSON instance{ + sourcemeta::core::parse_json(R"JSON([ [ 1, 2 ], [ 3, 4, 5 ] ])JSON")}; + const auto result{evaluator.validate(compiled_schema, instance)}; + EXPECT_TRUE(result); +} + +TEST(items_as_a_bounded_array_of_typed_items_failure_on_an_inner_type) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-07/schema#", + "items": { + "type": "array", + "items": { "type": "number" }, + "minItems": 2 + } + })JSON")}; + + const auto compiled_schema{ + sourcemeta::blaze::compile(schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, + sourcemeta::blaze::default_schema_compiler, + sourcemeta::blaze::Mode::FastValidation)}; + + sourcemeta::blaze::Evaluator evaluator; + const sourcemeta::core::JSON instance{ + sourcemeta::core::parse_json(R"JSON([ [ 1, 2 ], [ 3, "x" ] ])JSON")}; + const auto result{evaluator.validate(compiled_schema, instance)}; + EXPECT_FALSE(result); +} + +TEST(items_as_a_bounded_array_of_typed_items_failure_on_the_size) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-07/schema#", + "items": { + "type": "array", + "items": { "type": "number" }, + "minItems": 2 + } + })JSON")}; + + const auto compiled_schema{ + sourcemeta::blaze::compile(schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, + sourcemeta::blaze::default_schema_compiler, + sourcemeta::blaze::Mode::FastValidation)}; + + sourcemeta::blaze::Evaluator evaluator; + const sourcemeta::core::JSON instance{ + sourcemeta::core::parse_json(R"JSON([ [ 1, 2 ], [ 3 ] ])JSON")}; + const auto result{evaluator.validate(compiled_schema, instance)}; + EXPECT_FALSE(result); +} + +TEST(items_as_a_bounded_array_of_typed_items_failure_on_a_scalar_item) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-07/schema#", + "items": { + "type": "array", + "items": { "type": "number" }, + "minItems": 2 + } + })JSON")}; + + const auto compiled_schema{ + sourcemeta::blaze::compile(schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, + sourcemeta::blaze::default_schema_compiler, + sourcemeta::blaze::Mode::FastValidation)}; + + sourcemeta::blaze::Evaluator evaluator; + const sourcemeta::core::JSON instance{ + sourcemeta::core::parse_json(R"JSON([ 5 ])JSON")}; + const auto result{evaluator.validate(compiled_schema, instance)}; + EXPECT_FALSE(result); +} + +TEST(items_as_a_bounded_array_of_typed_items_on_a_non_array) { + // The instruction passes trivially when the instance is not an array, as its + // own precondition makes it + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-07/schema#", + "items": { + "type": "array", + "items": { "type": "number" }, + "minItems": 2 + } + })JSON")}; + + const auto compiled_schema{ + sourcemeta::blaze::compile(schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, + sourcemeta::blaze::default_schema_compiler, + sourcemeta::blaze::Mode::FastValidation)}; + + sourcemeta::blaze::Evaluator evaluator; + const sourcemeta::core::JSON instance{ + sourcemeta::core::parse_json(R"JSON({ "a": 1 })JSON")}; + const auto result{evaluator.validate(compiled_schema, instance)}; + EXPECT_TRUE(result); +}