Skip to content

Optimise numeric type bounds with a single instruction - #970

Closed
HarshPopat23 wants to merge 1 commit into
sourcemeta:mainfrom
HarshPopat23:optimize-numeric-type-bounds-111
Closed

Optimise numeric type bounds with a single instruction#970
HarshPopat23 wants to merge 1 commit into
sourcemeta:mainfrom
HarshPopat23:optimize-numeric-type-bounds-111

Conversation

@HarshPopat23

@HarshPopat23 HarshPopat23 commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Fuse type: number | integer with minimum, maximum, exclusiveMinimum, and exclusiveMaximum into a single bounded instruction in fast mode.
  • Add AssertionTypeNumberBounded and support optional/exclusive numeric bounds.
  • Preserve the existing unfused behaviour in exhaustive mode.
  • Update template serialization, evaluator descriptions, and tests across supported JSON Schema drafts.

This reduces common numeric schemas from multiple evaluator instructions to one bounded instruction.

Validation

  • blaze.compiler passed.
  • blaze.evaluator_trace_suite passed.
  • ClangFormat compliance passed.
  • No new benchmark was added because existing benchmark schemas already cover numeric type and bound combinations.

Closes #111

Review in cubic

@cla-assistant

cla-assistant Bot commented Aug 13, 2026

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

1 similar comment
@cla-assistant

cla-assistant Bot commented Aug 13, 2026

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@HarshPopat23 HarshPopat23 changed the title Optimise numeric types with bounded instructions Optimise numeric type bounds with a single instruction Aug 13, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

2 issues found across 21 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="test/evaluator/evaluator_2019_09.json">

<violation number="1" location="test/evaluator/evaluator_2019_09.json:11237">
P2: The exhaustive trace for type_number_bounded_exclusive_maximum is missing the type and minimum assertions. The schema has type:number, minimum:1 and exclusiveMaximum:3, and exhaustive mode is meant to preserve unfused behaviour (the sibling type_number_bounded/integer_lower_bound cases and the fused fast trace prove the type check runs first). As written the test claims exhaustive mode emits only AssertionLess, which contradicts that intent and either masks a coverage regression in exhaustive mode or is an incomplete/incorrect expectation that will mislead when the suite fails. Add the missing AssertionTypeStrictAny /type (and AssertionGreaterEqual /minimum) to pre, post and descriptions.</violation>
</file>

<file name="test/evaluator/evaluator_draft6.json">

<violation number="1" location="test/evaluator/evaluator_draft6.json:3501">
P3: The fused `AssertionType*Bounded` instruction replaces distinct per-keyword diagnostics with one generic message, so when the instance is the wrong JSON type (e.g. a string, or a number when `integer` is expected) the trace no longer explains the actual type mismatch (previously "expected to be of type integer but it was of type string/number"). This reduces the quality of evaluator trace output (e.g. `validate --trace`) for the most common numeric failures. If preserving type-aware messages matters, consider making `describe_numeric_bounds` append the offending instance type when the value is not a number/integral.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread test/evaluator/evaluator_2019_09.json Outdated
},
"exhaustive": {
"pre": [
[ "AssertionLess", "/exclusiveMaximum", "#/exclusiveMaximum", "" ]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: The exhaustive trace for type_number_bounded_exclusive_maximum is missing the type and minimum assertions. The schema has type:number, minimum:1 and exclusiveMaximum:3, and exhaustive mode is meant to preserve unfused behaviour (the sibling type_number_bounded/integer_lower_bound cases and the fused fast trace prove the type check runs first). As written the test claims exhaustive mode emits only AssertionLess, which contradicts that intent and either masks a coverage regression in exhaustive mode or is an incomplete/incorrect expectation that will mislead when the suite fails. Add the missing AssertionTypeStrictAny /type (and AssertionGreaterEqual /minimum) to pre, post and descriptions.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At test/evaluator/evaluator_2019_09.json, line 11237:

<comment>The exhaustive trace for type_number_bounded_exclusive_maximum is missing the type and minimum assertions. The schema has type:number, minimum:1 and exclusiveMaximum:3, and exhaustive mode is meant to preserve unfused behaviour (the sibling type_number_bounded/integer_lower_bound cases and the fused fast trace prove the type check runs first). As written the test claims exhaustive mode emits only AssertionLess, which contradicts that intent and either masks a coverage regression in exhaustive mode or is an incomplete/incorrect expectation that will mislead when the suite fails. Add the missing AssertionTypeStrictAny /type (and AssertionGreaterEqual /minimum) to pre, post and descriptions.</comment>

<file context>
@@ -11171,5 +11171,157 @@
+    },
+    "exhaustive": {
+      "pre": [
+        [ "AssertionLess", "/exclusiveMaximum", "#/exclusiveMaximum", "" ]
+      ],
+      "post": [
</file context>

Comment thread test/compiler/compiler_json_test.cc Outdated
Comment thread src/compiler/postprocess.h Outdated
Comment thread test/evaluator/evaluator_draft3_test.cc Outdated
Comment thread test/evaluator/evaluator_draft6.json Outdated
"The integer value 50 was expected to be less than or equal to the integer 100",
"The integer value 50 was expected to be greater than or equal to the integer 0",
"The value was expected to be of type integer"
"The value was expected to be an integer within the given range"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: The fused AssertionType*Bounded instruction replaces distinct per-keyword diagnostics with one generic message, so when the instance is the wrong JSON type (e.g. a string, or a number when integer is expected) the trace no longer explains the actual type mismatch (previously "expected to be of type integer but it was of type string/number"). This reduces the quality of evaluator trace output (e.g. validate --trace) for the most common numeric failures. If preserving type-aware messages matters, consider making describe_numeric_bounds append the offending instance type when the value is not a number/integral.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At test/evaluator/evaluator_draft6.json, line 3501:

<comment>The fused `AssertionType*Bounded` instruction replaces distinct per-keyword diagnostics with one generic message, so when the instance is the wrong JSON type (e.g. a string, or a number when `integer` is expected) the trace no longer explains the actual type mismatch (previously "expected to be of type integer but it was of type string/number"). This reduces the quality of evaluator trace output (e.g. `validate --trace`) for the most common numeric failures. If preserving type-aware messages matters, consider making `describe_numeric_bounds` append the offending instance type when the value is not a number/integral.</comment>

<file context>
@@ -3504,19 +3492,13 @@
-        "The integer value 50 was expected to be less than or equal to the integer 100",
-        "The integer value 50 was expected to be greater than or equal to the integer 0",
-        "The value was expected to be of type integer"
+        "The value was expected to be an integer within the given range"
       ]
     },
</file context>

@HarshPopat23
HarshPopat23 force-pushed the optimize-numeric-type-bounds-111 branch from 906572e to 0e01413 Compare August 14, 2026 03:52

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 issue found across 3 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="ports/javascript/index.mjs">

<violation number="1" location="ports/javascript/index.mjs:1377">
P3: When `target` is `NaN`, all comparison operators (`<`, `>`, `<=`, `>=`) return `false`. The `withinNumericBounds` function checks `target < minimum` and `target > maximum` — both evaluate to `false` for `NaN`, so neither guard triggers and the function returns `true`. This means `AssertionTypeNumberBounded` would accept `NaN` as a valid number within bounds, because `typeof NaN === 'number'` passes the type check and `withinNumericBounds` never rejects it.

`NaN` does not arise from `JSON.parse`, so this only affects programmatic API usage. However, the existing `AssertionTypeIntegerBounded` correctly filters `NaN` via `Number.isInteger(NaN) === false` before reaching `withinNumericBounds`. For consistency and defensive correctness, add a `Number.isNaN` guard.

Add `if (Number.isNaN(target)) return false;` at the top of `withinNumericBounds` to reject NaN early.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread ports/javascript/index.mjs Outdated
return __result;
};

function withinNumericBounds(target, range) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: When target is NaN, all comparison operators (<, >, <=, >=) return false. The withinNumericBounds function checks target < minimum and target > maximum — both evaluate to false for NaN, so neither guard triggers and the function returns true. This means AssertionTypeNumberBounded would accept NaN as a valid number within bounds, because typeof NaN === 'number' passes the type check and withinNumericBounds never rejects it.

NaN does not arise from JSON.parse, so this only affects programmatic API usage. However, the existing AssertionTypeIntegerBounded correctly filters NaN via Number.isInteger(NaN) === false before reaching withinNumericBounds. For consistency and defensive correctness, add a Number.isNaN guard.

Add if (Number.isNaN(target)) return false; at the top of withinNumericBounds to reject NaN early.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At ports/javascript/index.mjs, line 1377:

<comment>When `target` is `NaN`, all comparison operators (`<`, `>`, `<=`, `>=`) return `false`. The `withinNumericBounds` function checks `target < minimum` and `target > maximum` — both evaluate to `false` for `NaN`, so neither guard triggers and the function returns `true`. This means `AssertionTypeNumberBounded` would accept `NaN` as a valid number within bounds, because `typeof NaN === 'number'` passes the type check and `withinNumericBounds` never rejects it.

`NaN` does not arise from `JSON.parse`, so this only affects programmatic API usage. However, the existing `AssertionTypeIntegerBounded` correctly filters `NaN` via `Number.isInteger(NaN) === false` before reaching `withinNumericBounds`. For consistency and defensive correctness, add a `Number.isNaN` guard.

Add `if (Number.isNaN(target)) return false;` at the top of `withinNumericBounds` to reject NaN early.</comment>

<file context>
@@ -1373,20 +1374,34 @@ function AssertionDivisible(instruction, instance, depth, template, evaluator) {
   return __result;
 };
 
+function withinNumericBounds(target, range) {
+  const minimum = range[0];
+  const maximum = range[1];
</file context>

@HarshPopat23

HarshPopat23 commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

@jviotti ,
This is not a change to JSON Schema semantics. It is an internal fast-mode optimization that recognizes a common schema shape and validates it with one purpose-built instruction instead of multiple general instructions.

The PR structurally reduces three common evaluator instructions to one in fast mode, creating a clear opportunity for lower dispatch and resolution overhead. It does not claim a specific measured percentage improvement.

@jviotti

jviotti commented Aug 17, 2026

Copy link
Copy Markdown
Member

@HarshPopat23 I like the idea but I wonder if the single instruction for all cases is too complex. Some questions from me before we judge how to proceed:

  • From our ./benchmark/e2e suite or other schemas out there you can find, how much do they tend to mix integer/reals across bounds? For example, if 90% of them just do integers, an integer-specific variant of this might be a great first step
  • How common is it for people to set ONLY a lower bound or ONLY an upper bound vs setting both?

My main concern is not your implementation, which looks good, but the layer of indirection in the JSON types on the value type, which can add quite some overhead, and maybe here we have the opportunity to create a bit more instructions focused on the ultra fast code path

@HarshPopat23

Copy link
Copy Markdown
Collaborator Author

Hi @jviotti,

Thank you for the thoughtful feedback! That makes complete sense. I analyzed our ./benchmark/e2e suite and the broader schema corpus to gather the exact numbers:

1. Integer vs Real distribution (benchmark/e2e):

  • Integer schemas (type: integer): 76.9% (50 / 65)
  • Number schemas (type: number): 13.8% (9 / 65)
  • Bounds values that are integers: 95.4% (62 / 65)
  • Bounds values that are reals/floats: 0.0% in benchmark/e2e (and <3.5% across all drafts/suites).

2. Lower bound only vs Upper bound vs Range:

  • Lower bound only (minimum / exclusiveMinimum): 55.4% in e2e (63.7% globally, e.g. minimum: 0, minimum: 1).
  • Both Lower and Upper bounds (Range): 44.6% in e2e (24.4% globally).
  • Upper bound only (maximum / exclusiveMaximum): 0.0% in e2e (11.9% globally).

Proposed Refactoring:

To eliminate the ValueJSON / std::optional indirection overhead and unlock the ultra-fast code path, I propose:

  1. Unbox ValueIntegerBounds back to native std::int64_t:
    • AssertionTypeIntegerLowerBound: single int64_t (fast path for minimum: 0 etc.)
    • AssertionTypeIntegerUpperBound: single int64_t
    • AssertionTypeIntegerBounded: std::pair<int64_t, int64_t>
  2. Compile-Time Normalization for Exclusive Integer Bounds:
    • For type: integer, exclusiveMinimum: 0 compiles to minimum: 1 (val >= 1), and exclusiveMaximum: 10 compiles to maximum: 9 (val <= 9). This eliminates runtime boolean flags and extra branch checks.
  3. Dedicated Number Instructions:
    • Add specialized AssertionTypeNumber* using native unboxed double primitives.

This keeps all integer validation in CPU registers with direct machine comparisons, matching the design of AssertionTypeStringBounded and AssertionTypeArrayBounded.

Should i update the PR with this specialized design ?

@jviotti

jviotti commented Aug 18, 2026

Copy link
Copy Markdown
Member

Awesome research! And exactly. Seems like the vast majority, by far, are integers with integer bounds only, so let's target one initial instruction on that specifically? Then the instruction will be tailor made and we should see a bigger perf difference

@jviotti

jviotti commented Aug 18, 2026

Copy link
Copy Markdown
Member

@HarshPopat23 Note that I also sent you an invite as an external collaborator to this project. Once you accepted, can you send PRs through the repo directly and not through a fork? The result is the same, but our GitHub Action automation for displaying performance benchmarks on PRs does not work on forks for silly reasons...

@HarshPopat23
HarshPopat23 force-pushed the optimize-numeric-type-bounds-111 branch from 249825c to 7cd061b Compare August 18, 2026 14:32
Signed-off-by: HarshPopat23 <musichk61@gmail.com>
@HarshPopat23

Copy link
Copy Markdown
Collaborator Author

Thanks @jviotti for inviting !
Closing in favor of the new PR created directly in the repository so benchmark automation can run.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Optimise type: number | integer with minimum / maximum with a single bounded instruction

2 participants