Skip to content

Commit

Permalink
Clean up finalizer
Browse files Browse the repository at this point in the history
  • Loading branch information
DZakh committed Jul 7, 2024
1 parent 787611c commit 5943e49
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 108 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG_NEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,8 @@
- Update operation type to be more detailed and feature it in the error message.
- S.union still doesn't support schemas with async, but treats them differently. Please don't try to use them, since the behavior is not predictable.
- Added `S.assertOrRaiseWith` or `schema.assert` for js/ts users. It doesn't return parsed value, but that makes the function 2-3 times faster, depending on the schema.

// TODO:

- Test type check for recursive schema
- Test GenType compatibility with d.ts
2 changes: 1 addition & 1 deletion IDEAS.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ let trimContract: S.contract<string => string> = S.contract(s => {

## v8

- Change operation to include AsyncParse and simplify init functions (throw when asyncTransfor applied for SyncParse)
- Make S.serializeToJsonString super fast
- Rename `InvalidJsonStruct` error, since after `rescript-struct`->`rescript-schema` it became misleading
- Add S.bigint
- Check only number of fields for strict object schema when fields are not optional

## v???

Expand Down
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,12 @@ Besides the individual bundle size, the overall size of the library is also sign

At the same time **rescript-schema** is the fastest composable validation library in the entire JavaScript ecosystem. This is achieved because of the JIT approach when an ultra optimized validator is created using `eval`.

| | [email protected] | [email protected] | [email protected] |
| ----------------------------------------- | --------------------- | --------------- | -------------- |
| **Total size** (minified + gzipped) | 9.82 kB | 14.6 kB | 9.88 kB |
| **Example size** (minified + gzipped) | 4.98 kB | 12.9 kB | 1.22 B |
| **Nested object parsing** | 156,244 ops/ms | 1,304 ops/ms | 3,822 ops/ms |
| **Create schema + Nested object parsing** | 56 ops/ms | 112 ops/ms | 2,475 ops/ms |
| **Eval-free** ||||
| **Codegen-free** (Doesn't need compiler) ||||
| **Ecosystem** | ⭐️ | ⭐️⭐️⭐️⭐️⭐️ | ⭐️⭐️⭐️ |
| | [email protected] | [email protected] | [email protected] |
| ---------------------------------------- | --------------------- | --------------- | -------------- |
| **Total size** (minified + gzipped) | 9.82 kB | 14.6 kB | 9.88 kB |
| **Example size** (minified + gzipped) | 4.98 kB | 12.9 kB | 1.22 B |
| **Nested object parsing** | 156,244 ops/ms | 1,304 ops/ms | 3,822 ops/ms |
| **Create schema + parse once** | 56 ops/ms | 112 ops/ms | 2,475 ops/ms |
| **Eval-free** ||||
| **Codegen-free** (Doesn't need compiler) ||||
| **Ecosystem** | ⭐️ | ⭐️⭐️⭐️⭐️⭐️ | ⭐️⭐️⭐️ |
79 changes: 43 additions & 36 deletions src/S_Core.bs.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ function noopOperation(i) {
return i;
}

function noopFinalizer(_b, param, output) {
function noopFinalizer(_b, param, param$1, output) {
return output;
}

Expand All @@ -399,7 +399,7 @@ function build(builder, schema, operation, finalizer) {
if (b.l !== "") {
b.c = "let " + b.l + ";" + b.c;
}
var output$1 = finalizer(b, input, output);
var output$1 = finalizer(b, schema, input, output);
if (b.c === "" && output$1 === input) {
return noopOperation;
}
Expand Down Expand Up @@ -608,17 +608,19 @@ function parse(any) {
return parseInternal(any);
}

function isAsyncParseFinalizer(b, schema, input, output) {
schema.i = output.a;
b.c = "";
return input;
}

function isAsyncParse(schema) {
var v = schema.i;
if (typeof v === "boolean") {
return v;
}
try {
build(schema.p, schema, "ParseAsync", (function (b, input, output) {
schema.i = output.a;
b.c = "";
return input;
}));
build(schema.p, schema, "ParseAsync", isAsyncParseFinalizer);
return schema.i;
}
catch (raw_exn){
Expand Down Expand Up @@ -785,30 +787,34 @@ function parseJsonStringWith(jsonString, schema) {
}
}

function parseFinalizer(b, schema, input, output) {
var typeFilter = schema.f;
if (typeFilter !== undefined) {
b.c = typeFilterCode(b, typeFilter, schema, input, "") + b.c;
}
schema.i = false;
return output;
}

function initialParseOrRaise(unknown) {
var schema = this;
var operation = build(schema.p, schema, "Parse", (function (b, input, output) {
var typeFilter = schema.f;
if (typeFilter !== undefined) {
b.c = typeFilterCode(b, typeFilter, schema, input, "") + b.c;
}
schema.i = false;
return output;
}));
var operation = build(schema.p, schema, "Parse", parseFinalizer);
schema.parseOrThrow = operation;
return operation(unknown);
}

function parseAsyncFinalizer(b, schema, input, output) {
var typeFilter = schema.f;
if (typeFilter !== undefined) {
b.c = typeFilterCode(b, typeFilter, schema, input, "") + b.c;
}
schema.i = output.a;
return output;
}

function initialParseAsyncOrRaise(unknown) {
var schema = this;
var operation = build(schema.p, schema, "ParseAsync", (function (b, input, output) {
var typeFilter = schema.f;
if (typeFilter !== undefined) {
b.c = typeFilterCode(b, typeFilter, schema, input, "") + b.c;
}
schema.i = output.a;
return output;
}));
var operation = build(schema.p, schema, "ParseAsync", parseAsyncFinalizer);
var isAsync = schema.i;
var operation$1 = isAsync ? operation : (function (input) {
var syncValue = operation(input);
Expand All @@ -820,16 +826,18 @@ function initialParseAsyncOrRaise(unknown) {
return operation$1(unknown);
}

function assertFinalizer(b, schema, input, param) {
var typeFilter = schema.f;
if (typeFilter !== undefined) {
b.c = typeFilterCode(b, typeFilter, schema, input, "") + b.c;
}
schema.i = false;
return val(b, "void 0");
}

function initialAssertOrRaise(unknown) {
var schema = this;
var operation = build(schema.p, schema, "Assert", (function (b, input, param) {
var typeFilter = schema.f;
if (typeFilter !== undefined) {
b.c = typeFilterCode(b, typeFilter, schema, input, "") + b.c;
}
schema.i = false;
return val(b, "void 0");
}));
var operation = build(schema.p, schema, "Assert", assertFinalizer);
schema.assert = operation;
return operation(unknown);
}
Expand Down Expand Up @@ -998,18 +1006,17 @@ function recursive(fn) {
}));
}
});
var operation = build(builder, selfSchema, b.g.o, noopFinalizer);
if (isAsync) {
selfSchema.a = operation;
selfSchema.a = build(builder, selfSchema, b.g.o, parseAsyncFinalizer);
} else {
selfSchema.parseOrThrow = operation;
selfSchema.parseOrThrow = build(builder, selfSchema, b.g.o, parseFinalizer);
}
selfSchema.p = builder;
return withPathPrepend(b, input, path, undefined, (function (b, input, param) {
if (isAsync) {
return embedAsyncOperation(b, input, operation);
return embedAsyncOperation(b, input, selfSchema.a);
} else {
return embedSyncOperation(b, input, operation);
return embedSyncOperation(b, input, selfSchema.parseOrThrow);
}
}));
});
Expand Down
124 changes: 62 additions & 62 deletions src/S_Core.res
Original file line number Diff line number Diff line change
Expand Up @@ -724,7 +724,7 @@ module Builder = {
@inline
let intitialInputVar = "i"

let noopFinalizer = (_b, ~input as _, ~output) => output
let noopFinalizer = (_b, ~schema as _, ~input as _, ~output) => output

let build = (builder, ~schema, ~operation, finalizer) => {
let b = {
Expand All @@ -745,7 +745,7 @@ module Builder = {
b.code = `let ${b.varsAllocation};${b.code}`
}

let output = finalizer(b, ~input, ~output)
let output = finalizer(b, ~schema, ~input, ~output)

if b.code === "" && output === input {
noopOperation
Expand Down Expand Up @@ -1040,22 +1040,25 @@ module Literal = {
}
}

let isAsyncParseFinalizer = (b, ~schema, ~input, ~output) => {
schema.isAsyncParse = Value(output.isAsync)

// Avoid new Function call
b.code = ""
input
}

let isAsyncParse = schema => {
let schema = schema->toUnknown
switch schema.isAsyncParse {
| Unknown =>
try {
let _ = schema.parseOperationBuilder->Builder.build(~schema, ~operation=ParseAsync, (
b,
~input,
~output,
) => {
schema.isAsyncParse = Value(output.isAsync)

// Avoid new Function call
b.code = ""
input
})
let _ =
schema.parseOperationBuilder->Builder.build(
~schema,
~operation=ParseAsync,
isAsyncParseFinalizer,
)
schema.isAsyncParse->(Obj.magic: isAsyncParse => bool)
} catch {
| exn => {
Expand Down Expand Up @@ -1175,40 +1178,38 @@ let parseJsonStringWith = (jsonString: string, schema: t<'value>): result<'value
}
}

let parseFinalizer = (b, ~schema, ~input, ~output) => {
switch schema.maybeTypeFilter {
| Some(typeFilter) =>
b.code = b->B.typeFilterCode(~schema, ~typeFilter, ~input, ~path=Path.empty) ++ b.code
| None => ()
}
schema.isAsyncParse = Value(false)
output
}

let initialParseOrRaise = unknown => {
let schema = %raw(`this`)
let operation = schema.parseOperationBuilder->Builder.build(~schema, ~operation=Parse, (
b,
~input,
~output,
) => {
switch schema.maybeTypeFilter {
| Some(typeFilter) =>
b.code = b->B.typeFilterCode(~schema, ~typeFilter, ~input, ~path=Path.empty) ++ b.code
| None => ()
}
schema.isAsyncParse = Value(false)
output
})
let operation =
schema.parseOperationBuilder->Builder.build(~schema, ~operation=Parse, parseFinalizer)
schema.parseOrRaise = operation
operation(unknown)
}

let parseAsyncFinalizer = (b, ~schema, ~input, ~output) => {
switch schema.maybeTypeFilter {
| Some(typeFilter) =>
b.code = b->B.typeFilterCode(~schema, ~typeFilter, ~input, ~path=Path.empty) ++ b.code
| None => ()
}
schema.isAsyncParse = Value(output.isAsync)
output
}

let initialParseAsyncOrRaise = unknown => {
let schema = %raw(`this`)
let operation = schema.parseOperationBuilder->Builder.build(~schema, ~operation=ParseAsync, (
b,
~input,
~output,
) => {
switch schema.maybeTypeFilter {
| Some(typeFilter) =>
b.code = b->B.typeFilterCode(~schema, ~typeFilter, ~input, ~path=Path.empty) ++ b.code
| None => ()
}
schema.isAsyncParse = Value(output.isAsync)
output
})
let operation =
schema.parseOperationBuilder->Builder.build(~schema, ~operation=ParseAsync, parseAsyncFinalizer)
let isAsync = schema.isAsyncParse->(Obj.magic: isAsyncParse => bool)
// FIXME: Get rid of this
let operation = isAsync
Expand All @@ -1221,21 +1222,20 @@ let initialParseAsyncOrRaise = unknown => {
operation(unknown)
}

let assertFinalizer = (b, ~schema, ~input, ~output as _) => {
switch schema.maybeTypeFilter {
| Some(typeFilter) =>
b.code = b->B.typeFilterCode(~schema, ~typeFilter, ~input, ~path=Path.empty) ++ b.code
| None => ()
}
schema.isAsyncParse = Value(false)
b->B.val("void 0")
}

let initialAssertOrRaise = unknown => {
let schema = %raw(`this`)
let operation = schema.parseOperationBuilder->Builder.build(~schema, ~operation=Assert, (
b,
~input,
~output as _,
) => {
switch schema.maybeTypeFilter {
| Some(typeFilter) =>
b.code = b->B.typeFilterCode(~schema, ~typeFilter, ~input, ~path=Path.empty) ++ b.code
| None => ()
}
schema.isAsyncParse = Value(false)
b->B.val("void 0")
})
let operation =
schema.parseOperationBuilder->Builder.build(~schema, ~operation=Assert, assertFinalizer)
schema.assertOrRaise = operation
operation(unknown)
}
Expand Down Expand Up @@ -1426,26 +1426,26 @@ let recursive = fn => {
}
})

let operation =
builder->Builder.build(
~schema=selfSchema,
~operation=b.global.operation,
Builder.noopFinalizer,
)
if isAsync {
selfSchema.parseAsyncOrRaise = operation
selfSchema.parseAsyncOrRaise =
builder->Builder.build(
~schema=selfSchema,
~operation=b.global.operation,
parseAsyncFinalizer,
)
} else {
// TODO: Use init function
selfSchema.parseOrRaise = operation
selfSchema.parseOrRaise =
builder->Builder.build(~schema=selfSchema, ~operation=b.global.operation, parseFinalizer)
}

selfSchema.parseOperationBuilder = builder

b->B.withPathPrepend(~input, ~path, (b, ~input, ~path as _) =>
if isAsync {
b->B.embedAsyncOperation(~input, ~fn=operation)
b->B.embedAsyncOperation(~input, ~fn=selfSchema.parseAsyncOrRaise)
} else {
b->B.embedSyncOperation(~input, ~fn=operation)
b->B.embedSyncOperation(~input, ~fn=selfSchema.parseOrRaise)
}
)
})
Expand Down

2 comments on commit 5943e49

@github-actions
Copy link

Choose a reason for hiding this comment

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

Benchmark

Benchmark suite Current: 5943e49 Previous: 06df40b Ratio
Parse string 818550561 ops/sec (±0.11%) 818372259 ops/sec (±0.13%) 1.00
Serialize string 819984051 ops/sec (±0.08%) 819110535 ops/sec (±0.05%) 1.00
Advanced object schema factory 459876 ops/sec (±0.52%) 449156 ops/sec (±0.56%) 0.98
Parse advanced object 51294469 ops/sec (±0.68%) 45100200 ops/sec (±0.16%) 0.88
Assert advanced object 172961466 ops/sec (±0.21%)
Create and parse advanced object 92136 ops/sec (±0.18%) 35041 ops/sec (±1.18%) 0.38
Parse advanced strict object 24268119 ops/sec (±0.20%) 22748126 ops/sec (±0.24%) 0.94
Assert advanced strict object 28481628 ops/sec (±0.74%)
Serialize advanced object 66198466 ops/sec (±4.18%) 806595235 ops/sec (±0.11%) 12.18

This comment was automatically generated by workflow using github-action-benchmark.

@github-actions
Copy link

Choose a reason for hiding this comment

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

⚠️ Performance Alert ⚠️

Possible performance regression was detected for benchmark.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold 1.50.

Benchmark suite Current: 5943e49 Previous: 06df40b Ratio
Serialize advanced object 66198466 ops/sec (±4.18%) 806595235 ops/sec (±0.11%) 12.18

This comment was automatically generated by workflow using github-action-benchmark.

Please sign in to comment.