From 6896a7147e2221bed458e803c32cd61471ae76cf Mon Sep 17 00:00:00 2001 From: Bartosz Hanc Date: Wed, 22 Jul 2026 05:57:07 +0200 Subject: [PATCH 1/4] initial new schema implementation --- .../src/core/schema.ts | 203 ++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 packages/react-native-executorch/src/core/schema.ts diff --git a/packages/react-native-executorch/src/core/schema.ts b/packages/react-native-executorch/src/core/schema.ts new file mode 100644 index 0000000000..ff990f75a8 --- /dev/null +++ b/packages/react-native-executorch/src/core/schema.ts @@ -0,0 +1,203 @@ +import { type DType } from './tensor'; + +export type Range = { min: number; max: number; step?: number }; + +export type TypedDim = + | { readonly kind: 'static'; readonly symbol: string } + | { readonly kind: 'dynamic'; readonly symbol: string; readonly range?: Range } + | { readonly kind: 'constant'; readonly value: number }; + +export type TypedShape = readonly TypedDim[]; + +export type TensorConstraint = { + readonly kind: 'Tensor'; + readonly dtype?: DType; + readonly shapes?: readonly TypedShape[]; +}; + +export type ValueConstraint = + | TensorConstraint + | { readonly kind: 'None' } + | { readonly kind: 'Int' } + | { readonly kind: 'Double' } + | { readonly kind: 'Bool' } + | { readonly kind: 'String' }; + +export type MethodSchema = { + inputs: ValueConstraint[]; + outputs: ValueConstraint[]; +}; + +export type SymbolicShape = readonly (number | string | TypedDim)[]; + +export const SymbolicTensor = (dtype?: DType, ...shapes: readonly SymbolicShape[]) => { + const toTypedShape = (shape: SymbolicShape): TypedShape => { + return shape.map((dim) => { + if (typeof dim === 'number') return { kind: 'constant', value: dim } as TypedDim; + if (typeof dim === 'string') return { kind: 'static', symbol: dim } as TypedDim; + return dim; + }); + }; + return { kind: 'Tensor', dtype, shapes: shapes.map(toTypedShape) }; +}; + +type SymbolMap = Map; + +function isRangeCovered(expRange?: Range, actRange?: Range): boolean { + // Expected takes whatever range actual offers + if (!expRange) return true; + // Expected requires a specific range, but actual range is unknown + if (!actRange) return false; + // Range containment check + if (actRange.min > expRange.min || actRange.max < expRange.max) return false; + // Step and grid alignment + if (actRange.step !== undefined) { + if (expRange.step === undefined) return false; + if (expRange.step % actRange.step !== 0) return false; + if (Math.abs(expRange.min - actRange.min) % actRange.step !== 0) return false; + } + return true; +} + +export function matchDim(exp: TypedDim, act: TypedDim, env: SymbolMap): SymbolMap | null { + const bindSymbol = (key: string, value: string | number) => { + if (env.has(key)) return env.get(key) === value ? env : null; + return new Map(env).set(key, value); + }; + + if (exp.kind === 'constant' && act.kind === 'constant') { + return exp.value === act.value ? env : null; + } + + if (exp.kind === 'constant' && act.kind === 'dynamic') { + if (!act.range) return null; // Unknown model range cannot guarantee exp.value + + if (exp.value < act.range.min || exp.value > act.range.max) return null; + + if (act.range.step !== undefined) { + if (Math.abs(exp.value - act.range.min) % act.range.step !== 0) return null; + } + + return env; + } + + if (exp.kind === 'static' && act.kind === 'constant') { + return bindSymbol(exp.symbol, act.value); + } + + if (exp.kind === 'static' && act.kind === 'static') { + return bindSymbol(exp.symbol, act.symbol); + } + + if (exp.kind === 'dynamic' && act.kind === 'dynamic') { + if (!isRangeCovered(exp.range, act.range)) return null; + return bindSymbol(exp.symbol, act.symbol); + } + + // All remaining combinations fail + return null; +} + +function constraintToString(constraint: ValueConstraint): string { + if (constraint.kind !== 'Tensor') { + return constraint.kind; + } + + const dimToString = (dim: TypedDim) => { + if (dim.kind === 'constant') return `${dim.value}`; + if (dim.kind === 'static') return dim.symbol; + if (dim.range) { + const stepStr = dim.range.step !== undefined ? `:${dim.range.step}` : ''; + return `${dim.symbol}[${dim.range.min}..${dim.range.max}${stepStr}]`; + } + return `Dynamic(${dim.symbol})`; + }; + + const parts: string[] = []; + if (constraint.dtype !== undefined) { + parts.push(`dtype=${constraint.dtype}`); + } + if (constraint.shapes && constraint.shapes.length > 0) { + const shapesStr = constraint.shapes + .map((shape) => `[${shape.map(dimToString).join(', ')}]`) + .join(' | '); + parts.push(`shapes=${shapesStr}`); + } + + return parts.length > 0 ? `Tensor(${parts.join(', ')})` : 'Tensor'; +} + +export function schemaToString(schema: MethodSchema): string { + const inputsStr = schema.inputs.map(constraintToString).join(', '); + const outputsStr = schema.outputs.map(constraintToString).join(', '); + return `(${inputsStr}) -> (${outputsStr})`; +} + +export function validateSchema(expected: MethodSchema, actual: MethodSchema) { + const formatError = (msg: string) => + `${msg}\n` + + `Expected schema: ${schemaToString(expected)}\n` + + `Actual schema : ${schemaToString(actual)}`; + + if (expected.inputs.length !== actual.inputs.length) { + throw new Error(formatError('Number of inputs does not match!')); + } + if (expected.outputs.length !== actual.outputs.length) { + throw new Error(formatError('Number of outputs does not match!')); + } + + const expList = [...expected.inputs, ...expected.outputs]; + const actList = [...actual.inputs, ...actual.outputs]; + + const match = (index: number, map: SymbolMap): boolean => { + if (index === expList.length) return true; + + const exp = expList[index]!; + const act = actList[index]!; + + if (exp.kind !== act.kind) return false; + if (exp.kind !== 'Tensor') return match(index + 1, map); + + const expTensor = exp as TensorConstraint; + const actTensor = act as TensorConstraint; + + // DType Sub-typing + if (expTensor.dtype !== undefined && actTensor.dtype !== expTensor.dtype) { + return false; + } + + // Shape Sub-typing + if (expTensor.shapes && expTensor.shapes.length > 0) { + if (!actTensor.shapes || actTensor.shapes.length === 0) { + return false; + } + + for (const expShape of expTensor.shapes) { + for (const actShape of actTensor.shapes) { + if (expShape.length !== actShape.length) continue; + + let currentMap: SymbolMap | null = map; + for (let dim = 0; dim < expShape.length; dim++) { + currentMap = matchDim(expShape[dim]!, actShape[dim]!, currentMap); + if (!currentMap) break; + } + + if (currentMap && match(index + 1, currentMap)) { + return true; // Found a globally consistent path! + } + } + } + + return false; // No shape overload satisfied constraints + } + + // Expected doesn't restrict shapes; any actual shape is valid + return match(index + 1, map); + }; + + if (!match(0, new Map())) { + throw new Error(formatError('No possible consistent matching!')); + } + + return actual; +} From 5f99930c715383d789c86df7fe2cae92b54c04ea Mon Sep 17 00:00:00 2001 From: Bartosz Hanc Date: Thu, 23 Jul 2026 18:13:58 +0200 Subject: [PATCH 2/4] wip --- .../src/core/schema.ts | 359 +++++++++++------- 1 file changed, 231 insertions(+), 128 deletions(-) diff --git a/packages/react-native-executorch/src/core/schema.ts b/packages/react-native-executorch/src/core/schema.ts index ff990f75a8..9eb71e9089 100644 --- a/packages/react-native-executorch/src/core/schema.ts +++ b/packages/react-native-executorch/src/core/schema.ts @@ -1,18 +1,16 @@ import { type DType } from './tensor'; -export type Range = { min: number; max: number; step?: number }; +export type Range = { min: number; max: number; step: number }; export type TypedDim = | { readonly kind: 'static'; readonly symbol: string } | { readonly kind: 'dynamic'; readonly symbol: string; readonly range?: Range } | { readonly kind: 'constant'; readonly value: number }; -export type TypedShape = readonly TypedDim[]; - export type TensorConstraint = { readonly kind: 'Tensor'; readonly dtype?: DType; - readonly shapes?: readonly TypedShape[]; + readonly shape?: readonly TypedDim[]; }; export type ValueConstraint = @@ -23,181 +21,286 @@ export type ValueConstraint = | { readonly kind: 'Bool' } | { readonly kind: 'String' }; -export type MethodSchema = { - inputs: ValueConstraint[]; - outputs: ValueConstraint[]; -}; +export type MethodSignature = { inputs: ValueConstraint[]; outputs: ValueConstraint[] }; + +export type MethodOverloads = MethodSignature[]; + +export type ModelSchema = Record; export type SymbolicShape = readonly (number | string | TypedDim)[]; -export const SymbolicTensor = (dtype?: DType, ...shapes: readonly SymbolicShape[]) => { - const toTypedShape = (shape: SymbolicShape): TypedShape => { - return shape.map((dim) => { - if (typeof dim === 'number') return { kind: 'constant', value: dim } as TypedDim; - if (typeof dim === 'string') return { kind: 'static', symbol: dim } as TypedDim; - return dim; - }); - }; - return { kind: 'Tensor', dtype, shapes: shapes.map(toTypedShape) }; +export type SymbolBinding = { + readonly exp: Exclude; + readonly act: TypedDim; }; -type SymbolMap = Map; - -function isRangeCovered(expRange?: Range, actRange?: Range): boolean { - // Expected takes whatever range actual offers - if (!expRange) return true; - // Expected requires a specific range, but actual range is unknown - if (!actRange) return false; - // Range containment check - if (actRange.min > expRange.min || actRange.max < expRange.max) return false; - // Step and grid alignment - if (actRange.step !== undefined) { - if (expRange.step === undefined) return false; - if (expRange.step % actRange.step !== 0) return false; - if (Math.abs(expRange.min - actRange.min) % actRange.step !== 0) return false; - } - return true; +export type SymbolBindings = readonly SymbolBinding[]; + +export type MatchResult = + | { readonly ok: true; readonly bindings: SymbolBindings } + | { readonly ok: false; readonly error: string }; + +const ok = (bindings: SymbolBindings): MatchResult => ({ ok: true, bindings }); +const err = (error: string): MatchResult => ({ ok: false, error }); + +function validateRange(range: Range): void { + if (range.min < 0) { + throw new Error(`Invalid range min (${range.min}): dimension minimum cannot be negative.`); + } + if (range.max < range.min) { + throw new Error(`Invalid range [${range.min}, ${range.max}]: max cannot be less than min.`); + } + if (range.step <= 0) { + throw new Error(`Invalid range step (${range.step}): step must be a positive integer.`); + } } -export function matchDim(exp: TypedDim, act: TypedDim, env: SymbolMap): SymbolMap | null { - const bindSymbol = (key: string, value: string | number) => { - if (env.has(key)) return env.get(key) === value ? env : null; - return new Map(env).set(key, value); - }; +export const StaticDim = (symbol: string) => { + return { kind: 'static', symbol } as TypedDim; +}; - if (exp.kind === 'constant' && act.kind === 'constant') { - return exp.value === act.value ? env : null; +export const DynamicDim = (symbol: string, range?: Range) => { + if (range) validateRange(range); + return { kind: 'dynamic', symbol, range } as TypedDim; +}; + +export const ConstantDim = (value: number) => { + if (value <= 0 || !Number.isInteger(value)) { + throw new Error(`Invalid value (${value}): must be a positive integer.`); } + return { kind: 'constant', value } as TypedDim; +}; - if (exp.kind === 'constant' && act.kind === 'dynamic') { - if (!act.range) return null; // Unknown model range cannot guarantee exp.value +export const SymbolicTensor = (dtype?: DType, shape?: SymbolicShape) => { + const typedShape = shape?.map((dim) => { + if (typeof dim === 'string') return StaticDim(dim); + if (typeof dim === 'number') return ConstantDim(dim); + return dim; + }); + return { kind: 'Tensor', dtype, shape: typedShape } as TensorConstraint; +}; - if (exp.value < act.range.min || exp.value > act.range.max) return null; +function rangesEqual(range1?: Range, range2?: Range): boolean { + if (range1 === range2) return true; + if (!range1 || !range2) return false; + return range1.min === range2.min && range1.max === range2.max && range1.step === range2.step; +} + +function dimsEqual(dim1: TypedDim, dim2: TypedDim): boolean { + if (dim1.kind === 'static' && dim2.kind === 'static') { + return dim1.symbol === dim2.symbol; + } + if (dim1.kind === 'dynamic' && dim2.kind === 'dynamic') { + return dim1.symbol === dim2.symbol && rangesEqual(dim1.range, dim2.range); + } + if (dim1.kind === 'constant' && dim2.kind === 'constant') { + return dim1.value === dim2.value; + } + return false; +} - if (act.range.step !== undefined) { - if (Math.abs(exp.value - act.range.min) % act.range.step !== 0) return null; +function matchDim(exp: TypedDim, act: TypedDim, bindings: SymbolBindings, ctx: string) { + if ('symbol' in act) { + for (const b of bindings) { + if ('symbol' in b.act && act.symbol === b.act.symbol && !dimsEqual(act, b.act)) { + return err(`${ctx}: Actual '${act.symbol}' is used inconsistently across signatures.`); + } } + } - return env; + if ('symbol' in exp) { + for (const b of bindings) { + if (exp.symbol === b.exp.symbol && !dimsEqual(exp, b.exp)) { + return err(`${ctx}: Expected '${exp.symbol}' is used inconsistently across signatures.`); + } + } } - if (exp.kind === 'static' && act.kind === 'constant') { - return bindSymbol(exp.symbol, act.value); + // [constant, constant] + if (exp.kind === 'constant' && act.kind === 'constant') { + if (exp.value !== act.value) { + return err(`${ctx}: Constant dimension value mismatch.`); + } + return ok(bindings); } - if (exp.kind === 'static' && act.kind === 'static') { - return bindSymbol(exp.symbol, act.symbol); + // [static, *] + if (exp.kind === 'static') { + const existing = bindings.find((b) => b.exp.symbol === exp.symbol); + if (existing) { + if (!dimsEqual(existing.act, act)) { + return err(`${ctx}: Static '${exp.symbol}' has inconsistent bindings.`); + } + return ok(bindings); + } + return ok([...bindings, { exp, act }]); } - if (exp.kind === 'dynamic' && act.kind === 'dynamic') { - if (!isRangeCovered(exp.range, act.range)) return null; - return bindSymbol(exp.symbol, act.symbol); + // [constant, dynamic] + if (exp.kind === 'constant' && act.kind === 'dynamic') { + if (!act.range) { + return err(`${ctx}: Actual dynamic dimension '${act.symbol}' has an unspecified range.`); + } + if (exp.value < act.range.min || act.range.max < exp.value) { + return err(`${ctx}: Constant ${exp.value} falls outside dynamic range.`); + } + if ((exp.value - act.range.min) % act.range.step !== 0) { + return err(`${ctx}: Constant ${exp.value} does not align with range step.`); + } + return ok(bindings); } - // All remaining combinations fail - return null; -} + // [dynamic, dynamic] + if (exp.kind === 'dynamic' && act.kind === 'dynamic') { + const existing = bindings.find((b) => b.exp.symbol === exp.symbol); + if (existing) { + if (!dimsEqual(existing.act, act)) { + return err(`${ctx}: Dynamic '${exp.symbol}' has inconsistent bindings.`); + } + return ok(bindings); + } + + if (exp.range) { + if (!act.range) { + return err(`${ctx}: Actual dynamic '${act.symbol}' has an unspecified range.`); + } + if (exp.range.min < act.range.min || exp.range.max > act.range.max) { + return err(`${ctx}: Expected range exceeds accepted actual range.`); + } + if ((exp.range.min - act.range.min) % act.range.step !== 0) { + return err(`${ctx}: Expected min does not align with actual range step.`); + } + if (exp.range.step % act.range.step !== 0) { + return err(`${ctx}: Expected step is not a multiple of accepted step.`); + } + } -function constraintToString(constraint: ValueConstraint): string { - if (constraint.kind !== 'Tensor') { - return constraint.kind; + return ok([...bindings, { exp, act }]); } - const dimToString = (dim: TypedDim) => { - if (dim.kind === 'constant') return `${dim.value}`; - if (dim.kind === 'static') return dim.symbol; - if (dim.range) { - const stepStr = dim.range.step !== undefined ? `:${dim.range.step}` : ''; - return `${dim.symbol}[${dim.range.min}..${dim.range.max}${stepStr}]`; - } - return `Dynamic(${dim.symbol})`; - }; + return err(`${ctx}: cannot match expected ${exp.kind} with actual ${act.kind}.`); +} - const parts: string[] = []; - if (constraint.dtype !== undefined) { - parts.push(`dtype=${constraint.dtype}`); +function matchSignature( + expSgn: MethodSignature, + actSgn: MethodSignature, + initialBindings: SymbolBindings = [], + ctx: string = '' +): MatchResult { + if (expSgn.inputs.length !== actSgn.inputs.length) { + return err(`${ctx}: Input count mismatch.`); } - if (constraint.shapes && constraint.shapes.length > 0) { - const shapesStr = constraint.shapes - .map((shape) => `[${shape.map(dimToString).join(', ')}]`) - .join(' | '); - parts.push(`shapes=${shapesStr}`); + if (expSgn.outputs.length !== actSgn.outputs.length) { + return err(`${ctx}: Output count mismatch.`); } - return parts.length > 0 ? `Tensor(${parts.join(', ')})` : 'Tensor'; -} + const expConstraints = [...expSgn.inputs, ...expSgn.outputs]; + const actConstraints = [...actSgn.inputs, ...actSgn.outputs]; + const numConstraints = expConstraints.length; -export function schemaToString(schema: MethodSchema): string { - const inputsStr = schema.inputs.map(constraintToString).join(', '); - const outputsStr = schema.outputs.map(constraintToString).join(', '); - return `(${inputsStr}) -> (${outputsStr})`; -} + let currentBindings = initialBindings; -export function validateSchema(expected: MethodSchema, actual: MethodSchema) { - const formatError = (msg: string) => - `${msg}\n` + - `Expected schema: ${schemaToString(expected)}\n` + - `Actual schema : ${schemaToString(actual)}`; + for (let c = 0; c < numConstraints; ++c) { + const isInput = c < expSgn.inputs.length; + const constraintIdx = isInput ? c : c - expSgn.inputs.length; + const constraintCtx = `${ctx} ${isInput ? 'input' : 'output'} #${constraintIdx}`; - if (expected.inputs.length !== actual.inputs.length) { - throw new Error(formatError('Number of inputs does not match!')); - } - if (expected.outputs.length !== actual.outputs.length) { - throw new Error(formatError('Number of outputs does not match!')); - } + const expConstr = expConstraints[c]!; + const actConstr = actConstraints[c]!; - const expList = [...expected.inputs, ...expected.outputs]; - const actList = [...actual.inputs, ...actual.outputs]; + if (expConstr.kind !== actConstr.kind) { + return err(`${constraintCtx}: Constraint kind mismatch.`); + } - const match = (index: number, map: SymbolMap): boolean => { - if (index === expList.length) return true; + if (expConstr.kind !== 'Tensor' && actConstr.kind !== 'Tensor') continue; - const exp = expList[index]!; - const act = actList[index]!; + const expTensor = expConstr as TensorConstraint; + const actTensor = actConstr as TensorConstraint; - if (exp.kind !== act.kind) return false; - if (exp.kind !== 'Tensor') return match(index + 1, map); + // Unspecified expTensor.dtype means "whatever dtype actual gives me" + if (expTensor.dtype && expTensor.dtype !== actTensor.dtype) { + return err(`${constraintCtx}: DType mismatch.`); + } - const expTensor = exp as TensorConstraint; - const actTensor = act as TensorConstraint; + // Unspecified expTensor.shape means "whatever shape actual gives me" + if (!expTensor.shape) continue; - // DType Sub-typing - if (expTensor.dtype !== undefined && actTensor.dtype !== expTensor.dtype) { - return false; + // Unspecified actTensor.shape means "we don't know", throws because there is no way to check it + if (!actTensor.shape) { + return err(`${constraintCtx}: Actual tensor shape is missing.`); } - // Shape Sub-typing - if (expTensor.shapes && expTensor.shapes.length > 0) { - if (!actTensor.shapes || actTensor.shapes.length === 0) { - return false; + if (expTensor.shape.length !== actTensor.shape.length) { + return err(`${constraintCtx}: Rank mismatch.`); + } + + const rank = expTensor.shape.length; + for (let d = 0; d < rank; ++d) { + const expDim = expTensor.shape[d]!; + const actDim = actTensor.shape[d]!; + const dimCtx = `${constraintCtx} Tensor dim #${d}`; + const dimResult = matchDim(expDim, actDim, currentBindings, dimCtx); + if (!dimResult.ok) { + return dimResult; } + currentBindings = dimResult.bindings; + } + } - for (const expShape of expTensor.shapes) { - for (const actShape of actTensor.shapes) { - if (expShape.length !== actShape.length) continue; + return ok(currentBindings); +} - let currentMap: SymbolMap | null = map; - for (let dim = 0; dim < expShape.length; dim++) { - currentMap = matchDim(expShape[dim]!, actShape[dim]!, currentMap); - if (!currentMap) break; - } +export function matchSchema(expectedSchema: ModelSchema, actualSchema: ModelSchema): MatchResult { + for (const methodName in expectedSchema) { + if (!actualSchema[methodName]) { + return err(`Method '${methodName}' not found in actual schema.`); + } + } + + const methodNames = Object.keys(expectedSchema); - if (currentMap && match(index + 1, currentMap)) { - return true; // Found a globally consistent path! + const match = (idx: number, bindings: SymbolBindings): MatchResult => { + if (idx >= methodNames.length) return ok(bindings); + + const methodName = methodNames[idx]!; + const expOverloads = expectedSchema[methodName]!; + const actOverloads = actualSchema[methodName]!; + + const errors: string[] = []; + + for (let e = 0; e < expOverloads.length; ++e) { + for (let a = 0; a < actOverloads.length; ++a) { + const sigCtx = `Method '${methodName}' (exp overload #${e}, act overload #${a})`; + const sigResult = matchSignature(expOverloads[e]!, actOverloads[a]!, bindings, sigCtx); + + if (sigResult.ok) { + const nextResult = match(idx + 1, sigResult.bindings); + if (nextResult.ok) { + return nextResult; } + errors.push(nextResult.error); + } else { + errors.push(sigResult.error); } } - - return false; // No shape overload satisfied constraints } - // Expected doesn't restrict shapes; any actual shape is valid - return match(index + 1, map); + return err(`Method '${methodName}' failed overload matching:\n - ` + errors.join('\n - ')); }; - if (!match(0, new Map())) { - throw new Error(formatError('No possible consistent matching!')); + return match(0, []); +} + +export function validateSchema(expectedVariants: ModelSchema[], actualSchema: ModelSchema) { + const errors: string[] = []; + + for (let i = 0; i < expectedVariants.length; ++i) { + const result = matchSchema(expectedVariants[i]!, actualSchema); + if (result.ok) { + return actualSchema; + } + errors.push(`Variant ${i}:\n${result.error}`); } - return actual; + throw new Error(`Model schema doesn't match any of the provided variants:\n` + errors.join('\n')); } From d0c53ddef76da32ce7396ff4432e1653e4706efc Mon Sep 17 00:00:00 2001 From: Bartosz Hanc Date: Fri, 24 Jul 2026 02:46:11 +0200 Subject: [PATCH 3/4] ts side kinda finished --- .../src/core/schema.ts | 365 ++++++++---------- 1 file changed, 169 insertions(+), 196 deletions(-) diff --git a/packages/react-native-executorch/src/core/schema.ts b/packages/react-native-executorch/src/core/schema.ts index 9eb71e9089..9213e79b7d 100644 --- a/packages/react-native-executorch/src/core/schema.ts +++ b/packages/react-native-executorch/src/core/schema.ts @@ -1,305 +1,278 @@ -import { type DType } from './tensor'; +import type { DType } from './tensor'; +import type { ExecuTorchTag } from './model'; export type Range = { min: number; max: number; step: number }; -export type TypedDim = +export type ConcreteDim = + | { readonly kind: 'constant'; readonly value: number } + | { readonly kind: 'range'; readonly range: Range }; + +export type SymbolicDim = + | ConcreteDim | { readonly kind: 'static'; readonly symbol: string } - | { readonly kind: 'dynamic'; readonly symbol: string; readonly range?: Range } - | { readonly kind: 'constant'; readonly value: number }; + | { readonly kind: 'dynamic'; readonly symbol: string }; -export type TensorConstraint = { +export type TensorConstraint = { readonly kind: 'Tensor'; - readonly dtype?: DType; - readonly shape?: readonly TypedDim[]; + readonly dtype: DType; + readonly shape: readonly D[]; }; -export type ValueConstraint = - | TensorConstraint - | { readonly kind: 'None' } - | { readonly kind: 'Int' } - | { readonly kind: 'Double' } - | { readonly kind: 'Bool' } - | { readonly kind: 'String' }; +export type ValueConstraint = + | TensorConstraint + | { readonly kind: Exclude }; -export type MethodSignature = { inputs: ValueConstraint[]; outputs: ValueConstraint[] }; +export type MethodSignature = { + inputs: ValueConstraint[]; + outputs: ValueConstraint[]; +}; -export type MethodOverloads = MethodSignature[]; +export type MethodOverloads = readonly MethodSignature[]; -export type ModelSchema = Record; +export type ModelSchema = Record>; -export type SymbolicShape = readonly (number | string | TypedDim)[]; +export type SymbolicShape = readonly (number | string | SymbolicDim)[]; -export type SymbolBinding = { - readonly exp: Exclude; - readonly act: TypedDim; +export const S = (symbol: string): SymbolicDim => { + return { kind: 'static', symbol }; }; -export type SymbolBindings = readonly SymbolBinding[]; - -export type MatchResult = - | { readonly ok: true; readonly bindings: SymbolBindings } - | { readonly ok: false; readonly error: string }; +export const D = (symbol: string): SymbolicDim => { + return { kind: 'dynamic', symbol }; +}; -const ok = (bindings: SymbolBindings): MatchResult => ({ ok: true, bindings }); -const err = (error: string): MatchResult => ({ ok: false, error }); +export const C = (value: number): ConcreteDim => { + if (value <= 0 || !Number.isInteger(value)) { + throw new Error(`Invalid value (${value}): must be a positive integer.`); + } + return { kind: 'constant', value }; +}; -function validateRange(range: Range): void { +export const R = (range: Range): ConcreteDim => { if (range.min < 0) { - throw new Error(`Invalid range min (${range.min}): dimension minimum cannot be negative.`); + throw new Error(`Invalid range: dimension minimum cannot be negative.`); } if (range.max < range.min) { throw new Error(`Invalid range [${range.min}, ${range.max}]: max cannot be less than min.`); } - if (range.step <= 0) { - throw new Error(`Invalid range step (${range.step}): step must be a positive integer.`); + if (!Number.isInteger(range.min)) { + throw new Error(`Invalid range min (${range.min}): must be a non-negative integer.`); } -} - -export const StaticDim = (symbol: string) => { - return { kind: 'static', symbol } as TypedDim; -}; - -export const DynamicDim = (symbol: string, range?: Range) => { - if (range) validateRange(range); - return { kind: 'dynamic', symbol, range } as TypedDim; -}; - -export const ConstantDim = (value: number) => { - if (value <= 0 || !Number.isInteger(value)) { - throw new Error(`Invalid value (${value}): must be a positive integer.`); + if (!Number.isInteger(range.max)) { + throw new Error(`Invalid range max (${range.max}): must be a non-negative integer.`); + } + if (range.step <= 0 || !Number.isInteger(range.step)) { + throw new Error(`Invalid range step (${range.step}): must be a positive integer.`); } - return { kind: 'constant', value } as TypedDim; + return { kind: 'range', range }; }; -export const SymbolicTensor = (dtype?: DType, shape?: SymbolicShape) => { - const typedShape = shape?.map((dim) => { - if (typeof dim === 'string') return StaticDim(dim); - if (typeof dim === 'number') return ConstantDim(dim); +export const SymbolicTensor = (dtype: DType, shape: SymbolicShape) => { + const typedShape = shape.map((dim) => { + if (typeof dim === 'string') return S(dim); + if (typeof dim === 'number') return C(dim); return dim; }); - return { kind: 'Tensor', dtype, shape: typedShape } as TensorConstraint; + return { kind: 'Tensor', dtype, shape: typedShape } as TensorConstraint; }; -function rangesEqual(range1?: Range, range2?: Range): boolean { - if (range1 === range2) return true; - if (!range1 || !range2) return false; - return range1.min === range2.min && range1.max === range2.max && range1.step === range2.step; -} +type SymbolBinding = { symbolic: Exclude; concrete: ConcreteDim }; +type SymbolBindings = readonly SymbolBinding[]; +type MatchResult = + | { readonly ok: true; readonly bindings: SymbolBindings } + | { readonly ok: false; readonly error: string }; -function dimsEqual(dim1: TypedDim, dim2: TypedDim): boolean { - if (dim1.kind === 'static' && dim2.kind === 'static') { - return dim1.symbol === dim2.symbol; - } - if (dim1.kind === 'dynamic' && dim2.kind === 'dynamic') { - return dim1.symbol === dim2.symbol && rangesEqual(dim1.range, dim2.range); - } - if (dim1.kind === 'constant' && dim2.kind === 'constant') { - return dim1.value === dim2.value; - } - return false; -} +const ok = (bindings: SymbolBindings): MatchResult => ({ ok: true, bindings }); +const err = (error: string): MatchResult => ({ ok: false, error }); +const rangesEqual = (r1: Range, r2: Range) => { + return r1.min === r2.min && r1.max === r2.max && r1.step === r2.step; +}; -function matchDim(exp: TypedDim, act: TypedDim, bindings: SymbolBindings, ctx: string) { - if ('symbol' in act) { - for (const b of bindings) { - if ('symbol' in b.act && act.symbol === b.act.symbol && !dimsEqual(act, b.act)) { - return err(`${ctx}: Actual '${act.symbol}' is used inconsistently across signatures.`); - } +function matchDim(sDim: SymbolicDim, cDim: ConcreteDim, bindings: SymbolBindings, ctx: string) { + if ('symbol' in sDim) { + if (bindings.some((b) => b.symbolic.symbol === sDim.symbol && b.symbolic.kind !== sDim.kind)) { + // returning err(...) here would treat a self-contradictory schema as a + // simple match failure and move on to the next variant. + throw new Error( + `Invalid schema ${ctx}: Symbol ${sDim.symbol} is used as both 'static' and 'dynamic'` + ); } } - if ('symbol' in exp) { - for (const b of bindings) { - if (exp.symbol === b.exp.symbol && !dimsEqual(exp, b.exp)) { - return err(`${ctx}: Expected '${exp.symbol}' is used inconsistently across signatures.`); - } + if (sDim.kind === 'constant' && cDim.kind === 'constant') { + if (sDim.value !== cDim.value) { + return err(`${ctx}: Constant dimension value mismatch.`); } + return ok(bindings); } - // [constant, constant] - if (exp.kind === 'constant' && act.kind === 'constant') { - if (exp.value !== act.value) { - return err(`${ctx}: Constant dimension value mismatch.`); + if (sDim.kind === 'range' && cDim.kind === 'range') { + if (!rangesEqual(sDim.range, cDim.range)) { + return err(`${ctx}: Range dimension mismatch`); } return ok(bindings); } - // [static, *] - if (exp.kind === 'static') { - const existing = bindings.find((b) => b.exp.symbol === exp.symbol); + if (sDim.kind === 'static' && cDim.kind === 'constant') { + const existing = bindings.find((binding) => binding.symbolic.symbol === sDim.symbol); if (existing) { - if (!dimsEqual(existing.act, act)) { - return err(`${ctx}: Static '${exp.symbol}' has inconsistent bindings.`); + if (existing.concrete.kind === 'constant' && cDim.value !== existing.concrete.value) { + return err(`${ctx}: Symbol '${sDim.symbol}' has inconsistent bindings`); } return ok(bindings); } - return ok([...bindings, { exp, act }]); - } - - // [constant, dynamic] - if (exp.kind === 'constant' && act.kind === 'dynamic') { - if (!act.range) { - return err(`${ctx}: Actual dynamic dimension '${act.symbol}' has an unspecified range.`); - } - if (exp.value < act.range.min || act.range.max < exp.value) { - return err(`${ctx}: Constant ${exp.value} falls outside dynamic range.`); - } - if ((exp.value - act.range.min) % act.range.step !== 0) { - return err(`${ctx}: Constant ${exp.value} does not align with range step.`); - } - return ok(bindings); + return ok([...bindings, { symbolic: sDim, concrete: cDim }]); } - // [dynamic, dynamic] - if (exp.kind === 'dynamic' && act.kind === 'dynamic') { - const existing = bindings.find((b) => b.exp.symbol === exp.symbol); + if (sDim.kind === 'dynamic' && cDim.kind === 'range') { + const existing = bindings.find((binding) => binding.symbolic.symbol === sDim.symbol); if (existing) { - if (!dimsEqual(existing.act, act)) { - return err(`${ctx}: Dynamic '${exp.symbol}' has inconsistent bindings.`); + if (existing.concrete.kind === 'range' && !rangesEqual(cDim.range, existing.concrete.range)) { + return err(`${ctx}: Symbol '${sDim.symbol}' has inconsistent bindings`); } return ok(bindings); } - - if (exp.range) { - if (!act.range) { - return err(`${ctx}: Actual dynamic '${act.symbol}' has an unspecified range.`); - } - if (exp.range.min < act.range.min || exp.range.max > act.range.max) { - return err(`${ctx}: Expected range exceeds accepted actual range.`); - } - if ((exp.range.min - act.range.min) % act.range.step !== 0) { - return err(`${ctx}: Expected min does not align with actual range step.`); - } - if (exp.range.step % act.range.step !== 0) { - return err(`${ctx}: Expected step is not a multiple of accepted step.`); - } - } - - return ok([...bindings, { exp, act }]); + return ok([...bindings, { symbolic: sDim, concrete: cDim }]); } - return err(`${ctx}: cannot match expected ${exp.kind} with actual ${act.kind}.`); + return err(`${ctx}: Cannot match symbolic ${sDim.kind} with concrete ${cDim.kind}.`); } function matchSignature( - expSgn: MethodSignature, - actSgn: MethodSignature, - initialBindings: SymbolBindings = [], - ctx: string = '' + symbolicSgn: MethodSignature, + concreteSgn: MethodSignature, + bindings: SymbolBindings, + ctx: string ): MatchResult { - if (expSgn.inputs.length !== actSgn.inputs.length) { + if (symbolicSgn.inputs.length !== concreteSgn.inputs.length) { return err(`${ctx}: Input count mismatch.`); } - if (expSgn.outputs.length !== actSgn.outputs.length) { + if (symbolicSgn.outputs.length !== concreteSgn.outputs.length) { return err(`${ctx}: Output count mismatch.`); } - const expConstraints = [...expSgn.inputs, ...expSgn.outputs]; - const actConstraints = [...actSgn.inputs, ...actSgn.outputs]; - const numConstraints = expConstraints.length; + const symbolicConstraints = [...symbolicSgn.inputs, ...symbolicSgn.outputs]; + const concreteConstraints = [...concreteSgn.inputs, ...concreteSgn.outputs]; + const numConstraints = symbolicConstraints.length; - let currentBindings = initialBindings; + let currentBindings = bindings; for (let c = 0; c < numConstraints; ++c) { - const isInput = c < expSgn.inputs.length; - const constraintIdx = isInput ? c : c - expSgn.inputs.length; + const isInput = c < symbolicSgn.inputs.length; + const constraintIdx = isInput ? c : c - concreteSgn.inputs.length; const constraintCtx = `${ctx} ${isInput ? 'input' : 'output'} #${constraintIdx}`; - const expConstr = expConstraints[c]!; - const actConstr = actConstraints[c]!; + const symbolicConstr = symbolicConstraints[c]!; + const concreteConstr = concreteConstraints[c]!; - if (expConstr.kind !== actConstr.kind) { + if (symbolicConstr.kind !== concreteConstr.kind) { return err(`${constraintCtx}: Constraint kind mismatch.`); } - if (expConstr.kind !== 'Tensor' && actConstr.kind !== 'Tensor') continue; + if (symbolicConstr.kind !== 'Tensor' && concreteConstr.kind !== 'Tensor') continue; - const expTensor = expConstr as TensorConstraint; - const actTensor = actConstr as TensorConstraint; + const symbolicTensor = symbolicConstr as TensorConstraint; + const concreteTensor = concreteConstr as TensorConstraint; - // Unspecified expTensor.dtype means "whatever dtype actual gives me" - if (expTensor.dtype && expTensor.dtype !== actTensor.dtype) { + if (symbolicTensor.dtype !== concreteTensor.dtype) { return err(`${constraintCtx}: DType mismatch.`); } - // Unspecified expTensor.shape means "whatever shape actual gives me" - if (!expTensor.shape) continue; - - // Unspecified actTensor.shape means "we don't know", throws because there is no way to check it - if (!actTensor.shape) { - return err(`${constraintCtx}: Actual tensor shape is missing.`); - } - - if (expTensor.shape.length !== actTensor.shape.length) { + if (symbolicTensor.shape.length !== concreteTensor.shape.length) { return err(`${constraintCtx}: Rank mismatch.`); } - const rank = expTensor.shape.length; + const rank = symbolicTensor.shape.length; + for (let d = 0; d < rank; ++d) { - const expDim = expTensor.shape[d]!; - const actDim = actTensor.shape[d]!; + const sDim = symbolicTensor.shape[d]!; + const cDim = concreteTensor.shape[d]!; const dimCtx = `${constraintCtx} Tensor dim #${d}`; - const dimResult = matchDim(expDim, actDim, currentBindings, dimCtx); - if (!dimResult.ok) { - return dimResult; - } - currentBindings = dimResult.bindings; + + const result = matchDim(sDim, cDim, currentBindings, dimCtx); + if (!result.ok) return result; + currentBindings = result.bindings; } } return ok(currentBindings); } -export function matchSchema(expectedSchema: ModelSchema, actualSchema: ModelSchema): MatchResult { - for (const methodName in expectedSchema) { - if (!actualSchema[methodName]) { - return err(`Method '${methodName}' not found in actual schema.`); +export function matchSchema( + symbolicSchema: ModelSchema, + concreteSchema: ModelSchema, + ctx: string +): MatchResult { + const methodNames = Object.keys(symbolicSchema); + for (const methodName of methodNames) { + if (!concreteSchema[methodName]) { + return err(`${ctx}: Method '${methodName}' not found in actual schema.`); } } - const methodNames = Object.keys(expectedSchema); + const match = (nameIdx: number, sOverloadIdx: number, bindings: SymbolBindings): MatchResult => { + if (nameIdx >= methodNames.length) { + return ok(bindings); + } + + const methodName = methodNames[nameIdx]!; + const sOverloads = symbolicSchema[methodName]!; - const match = (idx: number, bindings: SymbolBindings): MatchResult => { - if (idx >= methodNames.length) return ok(bindings); + if (sOverloadIdx >= sOverloads.length) { + return match(nameIdx + 1, 0, bindings); + } - const methodName = methodNames[idx]!; - const expOverloads = expectedSchema[methodName]!; - const actOverloads = actualSchema[methodName]!; + const cOverloads = concreteSchema[methodName]!; + const sOverload = sOverloads[sOverloadIdx]!; const errors: string[] = []; - for (let e = 0; e < expOverloads.length; ++e) { - for (let a = 0; a < actOverloads.length; ++a) { - const sigCtx = `Method '${methodName}' (exp overload #${e}, act overload #${a})`; - const sigResult = matchSignature(expOverloads[e]!, actOverloads[a]!, bindings, sigCtx); - - if (sigResult.ok) { - const nextResult = match(idx + 1, sigResult.bindings); - if (nextResult.ok) { - return nextResult; - } - errors.push(nextResult.error); - } else { - errors.push(sigResult.error); - } + for (const [idx, cOverload] of cOverloads.entries()) { + const overloadCtx = `${ctx}: Method '${methodName}' (symbolic overload #${sOverloadIdx}, concrete overload #${idx})`; + + const sgnResult = matchSignature(sOverload, cOverload, bindings, overloadCtx); + if (!sgnResult.ok) { + errors.push(sgnResult.error); + continue; } + + const matchResult = match(nameIdx, sOverloadIdx + 1, sgnResult.bindings); + if (!matchResult.ok) { + errors.push(matchResult.error); + continue; + } + + return matchResult; } - return err(`Method '${methodName}' failed overload matching:\n - ` + errors.join('\n - ')); + return err(`${ctx}: Method '${methodName}' failed matching:\n - ` + errors.join('\n - ')); }; - return match(0, []); + return match(0, 0, []); } -export function validateSchema(expectedVariants: ModelSchema[], actualSchema: ModelSchema) { +export function validateSchema( + symbolicVariants: ModelSchema[], + concreteSchema: ModelSchema +) { const errors: string[] = []; - for (let i = 0; i < expectedVariants.length; ++i) { - const result = matchSchema(expectedVariants[i]!, actualSchema); - if (result.ok) { - return actualSchema; + for (const [idx, symbolicSchema] of symbolicVariants.entries()) { + const result = matchSchema(symbolicSchema, concreteSchema, `Variant ${idx}`); + if (!result.ok) { + errors.push(result.error); + continue; } - errors.push(`Variant ${i}:\n${result.error}`); + return new Map( + result.bindings.map((binding) => { + switch (binding.concrete.kind) { + case 'constant': + return [binding.symbolic.symbol, binding.concrete.value]; + case 'range': + return [binding.symbolic.symbol, binding.concrete.range]; + } + }) + ); } throw new Error(`Model schema doesn't match any of the provided variants:\n` + errors.join('\n')); From b337cbf1e09232607655bf7ef20f4ed83adff939 Mon Sep 17 00:00:00 2001 From: Bartosz Hanc Date: Fri, 24 Jul 2026 03:12:39 +0200 Subject: [PATCH 4/4] small fix --- .../src/core/schema.ts | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/packages/react-native-executorch/src/core/schema.ts b/packages/react-native-executorch/src/core/schema.ts index 9213e79b7d..8c0794323d 100644 --- a/packages/react-native-executorch/src/core/schema.ts +++ b/packages/react-native-executorch/src/core/schema.ts @@ -20,7 +20,7 @@ export type TensorConstraint = { export type ValueConstraint = | TensorConstraint - | { readonly kind: Exclude }; + | { readonly kind: Exclude }; export type MethodSignature = { inputs: ValueConstraint[]; @@ -84,17 +84,22 @@ type MatchResult = const ok = (bindings: SymbolBindings): MatchResult => ({ ok: true, bindings }); const err = (error: string): MatchResult => ({ ok: false, error }); -const rangesEqual = (r1: Range, r2: Range) => { +const rangesEqual = (r1: Range, r2: Range): boolean => { return r1.min === r2.min && r1.max === r2.max && r1.step === r2.step; }; -function matchDim(sDim: SymbolicDim, cDim: ConcreteDim, bindings: SymbolBindings, ctx: string) { +function matchDim( + sDim: SymbolicDim, + cDim: ConcreteDim, + bindings: SymbolBindings, + ctx: string +): MatchResult { if ('symbol' in sDim) { if (bindings.some((b) => b.symbolic.symbol === sDim.symbol && b.symbolic.kind !== sDim.kind)) { // returning err(...) here would treat a self-contradictory schema as a // simple match failure and move on to the next variant. throw new Error( - `Invalid schema ${ctx}: Symbol ${sDim.symbol} is used as both 'static' and 'dynamic'` + `Invalid schema ${ctx}: ${sDim.symbol} is used as both 'static' and 'dynamic'` ); } } @@ -116,7 +121,10 @@ function matchDim(sDim: SymbolicDim, cDim: ConcreteDim, bindings: SymbolBindings if (sDim.kind === 'static' && cDim.kind === 'constant') { const existing = bindings.find((binding) => binding.symbolic.symbol === sDim.symbol); if (existing) { - if (existing.concrete.kind === 'constant' && cDim.value !== existing.concrete.value) { + if (existing.concrete.kind !== 'constant') { + throw new Error(`Matcher error ${ctx}: 'static' was matched to 'range'`); + } + if (cDim.value !== existing.concrete.value) { return err(`${ctx}: Symbol '${sDim.symbol}' has inconsistent bindings`); } return ok(bindings); @@ -127,7 +135,10 @@ function matchDim(sDim: SymbolicDim, cDim: ConcreteDim, bindings: SymbolBindings if (sDim.kind === 'dynamic' && cDim.kind === 'range') { const existing = bindings.find((binding) => binding.symbolic.symbol === sDim.symbol); if (existing) { - if (existing.concrete.kind === 'range' && !rangesEqual(cDim.range, existing.concrete.range)) { + if (existing.concrete.kind !== 'range') { + throw new Error(`Matcher error ${ctx}: 'dynamic' was matched to 'constant'`); + } + if (!rangesEqual(cDim.range, existing.concrete.range)) { return err(`${ctx}: Symbol '${sDim.symbol}' has inconsistent bindings`); } return ok(bindings); @@ -198,7 +209,7 @@ function matchSignature( return ok(currentBindings); } -export function matchSchema( +function matchSchema( symbolicSchema: ModelSchema, concreteSchema: ModelSchema, ctx: string @@ -254,7 +265,7 @@ export function matchSchema( export function validateSchema( symbolicVariants: ModelSchema[], concreteSchema: ModelSchema -) { +): Map { const errors: string[] = []; for (const [idx, symbolicSchema] of symbolicVariants.entries()) {