Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions docs/maintainers/value-contract-fixtures.v3.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"revision": 3,
"integerCases": [
{
"logicalDecimal": "0",
"envelope": { "__tywrap__": "integer", "codecVersion": 2, "encoding": "decimal", "value": "0" }
},
{
"logicalDecimal": "9007199254740991",
"envelope": { "__tywrap__": "integer", "codecVersion": 2, "encoding": "decimal", "value": "9007199254740991" }
},
{
"logicalDecimal": "18446744073709551617",
"envelope": { "__tywrap__": "integer", "codecVersion": 2, "encoding": "decimal", "value": "18446744073709551617" }
},
{
"logicalDecimal": "-18446744073709551617",
"envelope": {
"__tywrap__": "integer",
"codecVersion": 2,
"encoding": "decimal",
"value": "-18446744073709551617"
}
}
],
"rejectedDecimals": ["-0", "+1", "01", "1.0", "1e3", "١", ""],
"floatNegativeZero": {
"envelope": { "__tywrap__": "float", "codecVersion": 2, "encoding": "negative-zero" },
"decodedAs": "negative zero"
},
"migration": {
"default": "Python int remains a safe JavaScript number under revision 2",
"opted": "Python int becomes bigint at every declared integer node under revision 3",
"caller": "bigint arithmetic and JSON persistence require explicit handling"
}
}
56 changes: 28 additions & 28 deletions docs/maintainers/value-contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,35 +46,35 @@ These checks do not walk Arrow payloads again for schema validation.

## Exact bigint extension for #337

This extension is a design sketch. The 0.11 policy still rejects unsafe plain
Python integers.

Choose an explicit callable option, `integerMode: 'bigint'`. When enabled, map
Python `int` to TypeScript `bigint` for both parameters and returns. The return
validator must require `typeof value === 'bigint'`, including every record and
sequence member. Keep `bool` separate. Calls without the option keep `number`
and the safe range.

Add a version 2 integer value envelope with a canonical signed decimal string. A
sample value is
The [revision-3 policy](value-contracts.v3.json) is a bounded prototype. It pins
the revision-2 policy and adds one `integer-exact` leaf. Revision 2 remains the
default safe-number policy. No default bridge supports bigint calls yet.

The internal `EXACT_INTEGER_VALUE_CONVERSION` selects revision 3 for a fixture
module. It maps Python `int` to TypeScript `bigint` in parameters and returns.
The generated validator requires `bigint` at nested integer nodes. Python `bool`
and finite `float` keep their revision-2 rules. The compiler requires an
explicit `exact-integer-adapter` capability. It is unavailable by default. The
prototype has no public per-callable option for mixed modules.

The opted call uses a version-2 integer envelope with a canonical signed decimal
string. A sample value is
`{"__tywrap__":"integer","codecVersion":2,"encoding":"decimal","value":"18446744073709551617"}`.
The request encoder and Python request decoder must validate the same grammar
and digit limit. The response encoder and TypeScript decoder must do the same.
Use `BigInt` only after validation. Keep Arrow int64 columns on their current
Arrow path.

The bridge must advertise exact integer support during capability negotiation. A
generated bigint wrapper must reject an older bridge before its first call. The
new bridge must emit version 2 integer envelopes only when the call opts in. An
older client therefore continues to receive the 0.11 safe integer policy.
Unknown integer envelopes, mixed versions, oversized strings, and malformed
decimal values must fail with a path. Do not guess a conversion.

Acceptance evidence still needed: a bounded prototype must round trip positive
and negative values beyond 64 bits through nested records and arrays. It must
test bigint inputs, malformed tags, payload limits, and both mixed-version
directions. An independent review must approve the wire grammar, capability
rule, generated types, and migration cost before #337 can close.
The companion test adapter must check the grammar and 4096-digit limit in both
directions. It must use `BigInt` only after validation. Arrow int64 keeps its
current path. Contract revision 3, envelope `codecVersion: 2`, per-call policy
`bigint-v2`, and RPC protocol `tywrap/1` are different version labels.

The bound test provider must see `exactIntegerDecimalV2` in bridge metadata
before it sends a tagged request. It sets `params.valuePolicy.integer` to
`bigint-v2`. Calls without that policy keep revision-2 behavior. The production
bridge does not negotiate this capability. Unknown versions and malformed
decimal values must fail with a path.

The [revision-3 fixtures](value-contract-fixtures.v3.json) record exact values,
negative zero, and migration cost. The generated proof still needs integrated CI
and independent review before #337 can close. A prototype does not enable bigint
by default or authorize a release.

## Dataclass return extension for #339

Expand Down
51 changes: 51 additions & 0 deletions docs/maintainers/value-contracts.v3.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"revision": 3,
"status": "bounded-internal-prototype",
"base": {
"file": "value-contracts.v2.json",
"revision": 2,
"hashCanonicalization": "UTF-8 with LF line endings",
"sha256": "554f84b75126513250a7eb773294347fe4dc42262e4ac52f86e7aa19cc65bb4e"
},
"transport": "tywrap/1 value RPC",
"exactIntegerEnvelopeCodecVersion": 2,
"callPolicy": "bigint-v2",
"requiredCapability": "exactIntegerDecimalV2",
"rules": {
"integer-exact": {
"logicalKind": "Python int, excluding bool, on an opted call",
"contract": {
"kind": "integer-exact",
"wire": "decimal-string-envelope",
"decodedAs": "bigint",
"constraint": "exact-integer"
},
"wire": {
"__tywrap__": "integer",
"codecVersion": 2,
"encoding": "decimal",
"value": "canonical signed decimal string"
},
"decimalGrammar": "^(?:0|[1-9][0-9]*|-[1-9][0-9]*)$",
"maximumDigitsWithoutSign": 4096,
"composition": "apply the exact integer rule at every selected sequence, tuple, union, and record node"
},
"float-negative-zero": {
"logicalKind": "Python float equal to negative zero on an opted call",
"wire": {
"__tywrap__": "float",
"codecVersion": 2,
"encoding": "negative-zero"
},
"decodedAs": "JavaScript negative zero",
"otherFiniteFloats": "JSON number under the inherited finite float rule"
}
},
"compatibility": {
"revision2Default": "unchanged safe-number integer rule",
"unknownRevision": "reject",
"missingCapability": "reject before sending a tagged request",
"oldClientNewProvider": "revision 2 behavior unless the call opts in",
"arrowInt64": "keep the existing Arrow rule"
}
}
125 changes: 95 additions & 30 deletions src/contracts/value-contract.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
/** Internal value policy consumed by codecs and callable-contract generation. */
export const VALUE_CONTRACT_REVISION = 2 as const;

/** Exact integer mode uses a separate semantic contract revision. */
export const VALUE_CONTRACT_V3_REVISION = 3 as const;

export type ValueContractRevision =
| typeof VALUE_CONTRACT_REVISION
| typeof VALUE_CONTRACT_V3_REVISION;

/** JSON numbers can carry Python integers exactly only within this range. */
export const MAX_SAFE_JSON_INTEGER = 2 ** 53 - 1;

export type ValueContract =
type CommonValueContractLeaf =
| { readonly kind: 'null'; readonly wire: 'json'; readonly decodedAs: 'null' }
| { readonly kind: 'boolean'; readonly wire: 'json'; readonly decodedAs: 'boolean' }
| {
Expand All @@ -21,36 +28,11 @@ export type ValueContract =
}
| { readonly kind: 'string'; readonly wire: 'json'; readonly decodedAs: 'string' }
| { readonly kind: 'bytes'; readonly wire: 'base64-envelope'; readonly decodedAs: 'Uint8Array' }
| {
readonly kind: 'sequence';
readonly wire: 'json';
readonly decodedAs: 'array';
readonly item: ValueContract;
}
| {
readonly kind: 'tuple';
readonly wire: 'json';
readonly decodedAs: 'array';
readonly items: readonly ValueContract[];
}
| {
readonly kind: 'union';
readonly wire: 'selected-option';
readonly decodedAs: 'selected-option';
readonly options: readonly [ValueContract, ValueContract, ...ValueContract[]];
}
| {
readonly kind: 'record';
readonly wire: 'json';
readonly decodedAs: 'object';
readonly fields: readonly ValueContractField[];
readonly additionalValues?: ValueContract;
}
| {
readonly kind: 'ndarray-float16';
readonly wire: 'arrow' | 'json-fallback';
readonly decodedAs: 'nested-array-or-scalar';
readonly element: Extract<ValueContract, { kind: 'float' }>;
readonly element: Extract<CommonValueContractLeaf, { kind: 'float' }>;
readonly dtype: 'float16';
readonly rank?: number;
}
Expand All @@ -59,7 +41,7 @@ export type ValueContract =
readonly wire: 'ndarray-envelope';
readonly decodedAs: 'tensor-record';
readonly dtype: 'torch.float16';
readonly value: Extract<ValueContract, { kind: 'ndarray-float16' }>;
readonly value: Extract<CommonValueContractLeaf, { kind: 'ndarray-float16' }>;
}
| {
readonly kind: 'unsupported';
Expand All @@ -69,12 +51,95 @@ export type ValueContract =
readonly guidance: string;
};

export interface ValueContractField {
interface SequenceContract<TValue> {
readonly kind: 'sequence';
readonly wire: 'json';
readonly decodedAs: 'array';
readonly item: TValue;
}

interface TupleContract<TValue> {
readonly kind: 'tuple';
readonly wire: 'json';
readonly decodedAs: 'array';
readonly items: readonly TValue[];
}

interface UnionContract<TValue> {
readonly kind: 'union';
readonly wire: 'selected-option';
readonly decodedAs: 'selected-option';
readonly options: readonly [TValue, TValue, ...TValue[]];
}

interface RecordContract<TValue> {
readonly kind: 'record';
readonly wire: 'json';
readonly decodedAs: 'object';
readonly fields: readonly ValueContractField<TValue>[];
readonly additionalValues?: TValue;
}

interface V2SequenceContract extends SequenceContract<ValueContract> {}
interface V2TupleContract extends TupleContract<ValueContract> {}
interface V2UnionContract extends UnionContract<ValueContract> {}
interface V2RecordContract extends RecordContract<ValueContract> {}

/** Revision 2 remains the default safe-number value contract. */
export type ValueContract =
| CommonValueContractLeaf
| V2SequenceContract
| V2TupleContract
| V2UnionContract
| V2RecordContract;

export interface ExactIntegerValueContract {
readonly kind: 'integer-exact';
readonly wire: 'decimal-string-envelope';
readonly decodedAs: 'bigint';
readonly constraint: 'exact-integer';
}

interface V3SequenceContract extends SequenceContract<ValueContractV3> {}
interface V3TupleContract extends TupleContract<ValueContractV3> {}
interface V3UnionContract extends UnionContract<ValueContractV3> {}
interface V3RecordContract extends RecordContract<ValueContractV3> {}

/** Revision 3 adds exact integers without changing revision 2. */
export type ValueContractV3 =
| CommonValueContractLeaf
| ExactIntegerValueContract
| V3SequenceContract
| V3TupleContract
| V3UnionContract
| V3RecordContract;

export interface ValueContractField<TValue = ValueContract> {
readonly name: string;
readonly value: ValueContract;
readonly value: TValue;
readonly required: boolean;
}

export function containsExactInteger(value: ValueContractV3): boolean {
switch (value.kind) {
case 'integer-exact':
return true;
case 'sequence':
return containsExactInteger(value.item);
case 'tuple':
return value.items.some(containsExactInteger);
case 'union':
return value.options.some(containsExactInteger);
case 'record':
return (
value.fields.some(field => containsExactInteger(field.value)) ||
(value.additionalValues !== undefined && containsExactInteger(value.additionalValues))
);
default:
return false;
}
}

export function isSafeJsonInteger(value: number): boolean {
return Number.isInteger(value) && Math.abs(value) <= MAX_SAFE_JSON_INTEGER;
}
Loading
Loading