Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: checked cast with TypedMatcher #8394

Merged
merged 7 commits into from
Jul 12, 2024
Merged
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
1 change: 1 addition & 0 deletions .prettierrc.json5
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
'packages/store/**/*.js',
'packages/smart-wallet/**/*.js',
'packages/vats/**/*.js',
'packages/vat-data/**/*.js',
],
options: {
plugins: ['prettier-plugin-jsdoc'],
Expand Down
2 changes: 1 addition & 1 deletion packages/ERTP/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,6 @@
"access": "public"
},
"typeCoverage": {
"atLeast": 91.22
"atLeast": 91.23
}
}
6 changes: 4 additions & 2 deletions packages/ERTP/src/paymentLedger.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ import { BrandI, makeIssuerInterfaces } from './typeGuards.js';
/**
* @import {Amount, AssetKind, DisplayInfo, PaymentLedger, Payment, Brand, RecoverySetsOption, Purse, Issuer, Mint} from './types.js'
* @import {ShutdownWithFailure} from '@agoric/swingset-vat'
* @import {Key} from '@endo/patterns';
* @import {TypedPattern} from '@agoric/internal';
*/

/**
* @template {AssetKind} K
Copy link
Member

Choose a reason for hiding this comment

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

Generally, which I've written code like this template declaration, I revise it to

Suggested change
* @template {AssetKind} K
* @template {AssetKind} [K=AssetKind]

since the supertype constraint is usually also a fine default.

Copy link
Member Author

Choose a reason for hiding this comment

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

In this case there's no way the to omit the parameter because it's positional

* @param {Brand} brand
* @param {AssetKind} assetKind
* @param {K} assetKind
* @param {Pattern} elementShape
* @returns {TypedPattern<Amount<K>>}
*/
const amountShapeFromElementShape = (brand, assetKind, elementShape) => {
let valueShape;
Expand Down
2 changes: 1 addition & 1 deletion packages/SwingSet/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,6 @@
"access": "public"
},
"typeCoverage": {
"atLeast": 75.02
"atLeast": 75.1
}
}
2 changes: 1 addition & 1 deletion packages/agoric-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,6 @@
"workerThreads": false
},
"typeCoverage": {
"atLeast": 76.99
"atLeast": 77.3
}
}
2 changes: 1 addition & 1 deletion packages/async-flow/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,6 @@
"workerThreads": false
},
"typeCoverage": {
"atLeast": 77.32
"atLeast": 76.95
}
}
2 changes: 1 addition & 1 deletion packages/base-zone/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,6 @@
"workerThreads": false
},
"typeCoverage": {
"atLeast": 91.11
"atLeast": 91.4
}
}
2 changes: 1 addition & 1 deletion packages/boot/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,6 @@
"workerThreads": false
},
"typeCoverage": {
"atLeast": 87.28
"atLeast": 86.66
}
}
2 changes: 1 addition & 1 deletion packages/builders/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,6 @@
"workerThreads": false
},
"typeCoverage": {
"atLeast": 74.36
"atLeast": 76.03
}
}
2 changes: 1 addition & 1 deletion packages/casting/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,6 @@
"workerThreads": false
},
"typeCoverage": {
"atLeast": 88.94
"atLeast": 88.92
}
}
2 changes: 1 addition & 1 deletion packages/cosmic-swingset/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,6 @@
"timeout": "20m"
},
"typeCoverage": {
"atLeast": 80.49
"atLeast": 80.6
}
}
2 changes: 1 addition & 1 deletion packages/deploy-script-support/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,6 @@
"access": "public"
},
"typeCoverage": {
"atLeast": 81.63
"atLeast": 82.44
}
}
2 changes: 1 addition & 1 deletion packages/governance/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,6 @@
"access": "public"
},
"typeCoverage": {
"atLeast": 89.31
"atLeast": 89.35
}
}
5 changes: 2 additions & 3 deletions packages/inter-protocol/src/auction/auctionBook.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
/**
* @import {Baggage} from '@agoric/vat-data';
* @import {PriceAuthority, PriceDescription, PriceQuote, PriceQuoteValue, PriceQuery,} from '@agoric/zoe/tools/types.js';
* @import {TypedPattern} from '@agoric/internal';
*/

const { makeEmpty } = AmountMath;
Expand Down Expand Up @@ -172,9 +173,7 @@ export const prepareAuctionBook = (baggage, zcf, makeRecorderKit) => {

const bookDataKit = makeRecorderKit(
node,
/** @type {import('@agoric/zoe/src/contractSupport/recorder.js').TypedPattern<BookDataNotification>} */ (
M.any()
),
/** @type {TypedPattern<BookDataNotification>} */ (M.any()),
);

return {
Expand Down
5 changes: 2 additions & 3 deletions packages/inter-protocol/src/auction/auctioneer.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { makeScheduler } from './scheduler.js';
import { AuctionState } from './util.js';

/**
* @import {TypedPattern} from '@agoric/internal';
* @import {Baggage} from '@agoric/vat-data';
* @import {PriceAuthority, PriceDescription, PriceQuote, PriceQuoteValue, PriceQuery,} from '@agoric/zoe/tools/types.js';
*/
Expand Down Expand Up @@ -440,9 +441,7 @@ export const start = async (zcf, privateArgs, baggage) => {
const scheduleKit = makeERecorderKit(
E(privateArgs.storageNode).makeChildNode('schedule'),
/**
* @type {import('@agoric/zoe/src/contractSupport/recorder.js').TypedPattern<
* import('./scheduler.js').ScheduleNotification
* >}
* @type {TypedPattern<import('./scheduler.js').ScheduleNotification>}
*/ (M.any()),
);

Expand Down
13 changes: 6 additions & 7 deletions packages/inter-protocol/src/price/fluxAggregatorKit.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ import { Far } from '@endo/marshal';
import { prepareOracleAdminKit } from './priceOracleKit.js';
import { prepareRoundsManagerKit } from './roundsManager.js';

/** @import {PriceAuthority, PriceDescription, PriceQuote, PriceQuoteValue, PriceQuery,} from '@agoric/zoe/tools/types.js'; */
/**
* @import {TypedPattern} from '@agoric/internal';
* @import {PriceAuthority, PriceDescription, PriceQuote, PriceQuoteValue, PriceQuery,} from '@agoric/zoe/tools/types.js';
*/

const trace = makeTracer('FlxAgg', true);

Expand Down Expand Up @@ -144,18 +147,14 @@ export const prepareFluxAggregatorKit = async (
priceKit: () =>
makeRecorderKit(
storageNode,
/** @type {import('@agoric/zoe/src/contractSupport/recorder.js').TypedPattern<PriceDescription>} */ (
M.any()
),
/** @type {TypedPattern<PriceDescription>} */ (M.any()),
),
latestRoundKit: () =>
E.when(E(storageNode).makeChildNode('latestRound'), node =>
makeRecorderKit(
node,
/**
* @type {import('@agoric/zoe/src/contractSupport/recorder.js').TypedPattern<
* import('./roundsManager.js').LatestRound
* >}
* @type {TypedPattern<import('./roundsManager.js').LatestRound>}
*/ (M.any()),
),
),
Expand Down
9 changes: 5 additions & 4 deletions packages/inter-protocol/src/psm/psm.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,10 @@ import { makeNatAmountShape } from '../contractSupport.js';
* given by this contract
*/

/** @import {Baggage} from '@agoric/vat-data' */
/**
* @import {TypedPattern} from '@agoric/internal';
* @import {Baggage} from '@agoric/vat-data'
*/

/** @type {ContractMeta} */
export const meta = {
Expand Down Expand Up @@ -174,9 +177,7 @@ export const start = async (zcf, privateArgs, baggage) => {
E.when(E(privateArgs.storageNode).makeChildNode('metrics'), node =>
makeRecorderKit(
node,
/** @type {import('@agoric/zoe/src/contractSupport/recorder.js').TypedPattern<MetricsNotification>} */ (
M.any()
),
/** @type {TypedPattern<MetricsNotification>} */ (M.any()),
),
),
});
Expand Down
8 changes: 5 additions & 3 deletions packages/inter-protocol/src/reserve/assetReserveKit.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ import { UnguardedHelperI } from '@agoric/internal/src/typeGuards.js';

const trace = makeTracer('ReserveKit', true);

/**
* @import {TypedPattern} from '@agoric/internal';
*/

/**
* @typedef {object} MetricsNotification
* @property {AmountKeywordRecord} allocations
Expand Down Expand Up @@ -88,9 +92,7 @@ export const prepareAssetReserveKit = async (
keywordForBrand,
metricsKit: makeRecorderKit(
metricsNode,
/** @type {import('@agoric/zoe/src/contractSupport/recorder.js').TypedPattern<MetricsNotification>} */ (
M.any()
),
/** @type {TypedPattern<MetricsNotification>} */ (M.any()),
),
totalFeeMinted: emptyAmount,
totalFeeBurned: emptyAmount,
Expand Down
8 changes: 5 additions & 3 deletions packages/inter-protocol/src/vaultFactory/vaultDirector.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ import {
provideAndStartVaultManagerKits,
} from './vaultManager.js';

/**
* @import {TypedPattern} from '@agoric/internal';
*/

const trace = makeTracer('VD', true);

/**
Expand Down Expand Up @@ -131,9 +135,7 @@ const prepareVaultDirector = (

const metricsKit = makeERecorderKit(
metricsNode,
/** @type {import('@agoric/zoe/src/contractSupport/recorder.js').TypedPattern<MetricsNotification>} */ (
M.any()
),
/** @type {TypedPattern<MetricsNotification>} */ (M.any()),
);

const managersNode = E(storageNode).makeChildNode('managers');
Expand Down
3 changes: 2 additions & 1 deletion packages/internal/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"jessie.js": "^0.3.4"
},
"devDependencies": {
"@endo/exo": "^1.5.0",
"@endo/init": "^1.1.2",
"ava": "^5.3.0",
"tsd": "^0.31.1"
Expand All @@ -56,6 +57,6 @@
"access": "public"
},
"typeCoverage": {
"atLeast": 93.89
"atLeast": 93.78
}
}
1 change: 1 addition & 0 deletions packages/internal/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export * from './debug.js';
export * from './errors.js';
export * from './utils.js';
export * from './method-tools.js';
export * from './typeCheck.js';
export * from './typeGuards.js';

// eslint-disable-next-line import/export -- just types
Expand Down
23 changes: 23 additions & 0 deletions packages/internal/src/typeCheck.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// @ts-check
import { mustMatch as typelessMustMatch } from '@endo/patterns';
Copy link
Member

Choose a reason for hiding this comment

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

Should we eventually move this into @endo/patterns so the only mustMatch is the typed one, and the typeless one is never exported? (Modulo the transition costs, which I expect will be painful but tolerable.)

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, I think once we've had some stress testing of it so we know it won't change much.


/**
* @import {MustMatch, PatternType, TypedPattern} from './types.js';
*/

/** @type {MustMatch} */
export const mustMatch = typelessMustMatch;

/**
* @template M
* @param {unknown} specimen
* @param {TypedPattern<M>} patt
* @returns {M}
*/
export const cast = (specimen, patt) => {
// mustMatch throws if they don't, which means that `cast` also narrows the
// type but a function can't both narrow and return a type. That is by design:
// https://github.com/microsoft/TypeScript/issues/34636#issuecomment-545025916
mustMatch(specimen, patt);
return specimen;
};
30 changes: 30 additions & 0 deletions packages/internal/src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,33 @@ export type Remote<Primary, Local = DataOnly<Primary>> =
export type FarRef<Primary, Local = DataOnly<Primary>> = ERef<
Remote<Primary, Local>
>;

/*
* Stop-gap until https://github.com/Agoric/agoric-sdk/issues/6160
* explictly specify the type that the Pattern will verify through a match.
*
* TODO move all this pattern typing stuff to @endo/patterns
*/
declare const validatedType: unique symbol;
/**
* Tag a pattern with the static type it represents.
*/
export type TypedPattern<T> = Pattern & { [validatedType]?: T };

export declare type PatternType<TM extends TypedPattern<any>> =
TM extends TypedPattern<infer T> ? T : never;

// TODO make Endo's mustMatch do this
/**
* Returning normally indicates success. Match failure is indicated by
* throwing.
*
* Note: remotables can only be matched as "remotable", not the specific kind.
*
* @see {import('@endo/patterns').mustMatch} for the implementation. This one has a type annotation to narrow if the pattern is a TypedPattern.
*/
export declare type MustMatch = <P extends Pattern>(
specimen: unknown,
pattern: P,
label?: string | number,
) => asserts specimen is P extends TypedPattern<any> ? PatternType<P> : unknown;
Copy link
Member

Choose a reason for hiding this comment

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

Now that I understand it, OMG this is cool!

Copy link
Member

Choose a reason for hiding this comment

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

Should this eventually migrate into endo? Into the @endo/patterns package? I hope so. If you agree, would be good to comment on that expected migration here, or somewhere relevant.

Similarly, should #6160 have been an endo issue rather than an agoric-sdk issue, with the eventual fix also eventually being in endo?

Copy link
Member Author

Choose a reason for hiding this comment

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

If you agree, would be good to comment on that expected migration here, or somewhere relevant.

I expect it'll happen when there's some trigger.

should #6160 have been an endo issue rather than an agoric-sdk issue, with the eventual fix also eventually being in endo?

Could have been. Though it's much faster to being useful by making it work in agoric-sdk and moving it to Endo when it's stable.

55 changes: 55 additions & 0 deletions packages/internal/test/typeCheck.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// @ts-check
import test from 'ava';

import { makeExo } from '@endo/exo';
import { M } from '@endo/patterns';
import { cast, mustMatch } from '../src/typeCheck.js';

/**
* @import {TypedPattern} from '@agoric/internal';
* @import {RemotableObject} from '@endo/marshal';
*/

const Mstring = /** @type {TypedPattern<string>} */ (M.string());
const MremotableFoo = /** @type {TypedPattern<RemotableObject<'Foo'>>} */ (
M.remotable('Foo')
);
const MremotableBar = /** @type {TypedPattern<RemotableObject<'Bar'>>} */ (
M.remotable('Bar')
);

const unknownString = /** @type {unknown} */ ('');

test('cast', t => {
// @ts-expect-error unknown type
unknownString.length;
// @ts-expect-error not any
cast(unknownString, Mstring).missing;
cast(unknownString, Mstring).length;
t.pass();
});

test('mustMatch', t => {
// @ts-expect-error unknown type
unknownString.length;
mustMatch(unknownString, Mstring);
unknownString.length;
t.pass();
});

test('remotable', t => {
const maybeFoo = makeExo(`Remotable1`, undefined, {});
mustMatch(maybeFoo, MremotableFoo);
maybeFoo; // narrowed to Foo

const maybeBar = makeExo(`Remotable2`, undefined, {});
mustMatch(maybeBar, MremotableBar);
maybeBar; // narrowed to Bar

mustMatch(maybeFoo, MremotableBar);
maybeFoo; // further narrowed to never
mustMatch(maybeBar, MremotableFoo);
maybeBar; // further narrowed to never

t.pass();
});
2 changes: 1 addition & 1 deletion packages/network/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,6 @@
"workerThreads": false
},
"typeCoverage": {
"atLeast": 89.7
"atLeast": 90.69
}
}
Loading
Loading