From bd32e8dae9bdd8d59c99fc15d1bb10229b42461c Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Thu, 2 May 2024 13:48:50 -0700 Subject: [PATCH 1/3] refactor(types): ManualTimer export --- packages/inter-protocol/test/psm/setupPsm.js | 2 ++ packages/inter-protocol/test/reserve/setup.js | 2 ++ .../test/smartWallet/test-oracle-integration.js | 2 ++ .../unitTests/contracts/test-priceAggregator.js | 1 + packages/zoe/tools/internal-types.js | 16 ---------------- packages/zoe/tools/manualTimer.js | 15 ++++++++++++++- packages/zoe/typedoc.json | 1 - 7 files changed, 21 insertions(+), 18 deletions(-) delete mode 100644 packages/zoe/tools/internal-types.js diff --git a/packages/inter-protocol/test/psm/setupPsm.js b/packages/inter-protocol/test/psm/setupPsm.js index 089ec7da275..b6ecfd3a9b2 100644 --- a/packages/inter-protocol/test/psm/setupPsm.js +++ b/packages/inter-protocol/test/psm/setupPsm.js @@ -22,6 +22,8 @@ import { startPSM, startEconCharter } from '../../src/proposals/startPSM.js'; const psmRoot = './src/psm/psm.js'; // package relative const charterRoot = './src/econCommitteeCharter.js'; // package relative +/** @import {ManualTimer} from '@agoric/zoe/tools/manualTimer.js'; */ + /** @typedef {ReturnType} FarZoeKit */ /** diff --git a/packages/inter-protocol/test/reserve/setup.js b/packages/inter-protocol/test/reserve/setup.js index a76b0080e41..4877b92c723 100644 --- a/packages/inter-protocol/test/reserve/setup.js +++ b/packages/inter-protocol/test/reserve/setup.js @@ -14,6 +14,8 @@ import { } from '../supports.js'; import { startEconomicCommittee } from '../../src/proposals/startEconCommittee.js'; +/** @import {ManualTimer} from '@agoric/zoe/tools/manualTimer.js'; */ + const reserveRoot = './src/reserve/assetReserve.js'; // package relative const faucetRoot = './test/vaultFactory/faucet.js'; diff --git a/packages/inter-protocol/test/smartWallet/test-oracle-integration.js b/packages/inter-protocol/test/smartWallet/test-oracle-integration.js index 07daf70542b..0317e5b4bc9 100644 --- a/packages/inter-protocol/test/smartWallet/test-oracle-integration.js +++ b/packages/inter-protocol/test/smartWallet/test-oracle-integration.js @@ -20,6 +20,8 @@ import { voteForOpenQuestion, } from './contexts.js'; +/** @import {ManualTimer} from '@agoric/zoe/tools/manualTimer.js'; */ + /** * @typedef {Awaited> & { * consume: import('@agoric/inter-protocol/src/proposals/econ-behaviors.js').EconomyBootstrapPowers['consume']; diff --git a/packages/zoe/test/unitTests/contracts/test-priceAggregator.js b/packages/zoe/test/unitTests/contracts/test-priceAggregator.js index 9ac5e8b75d9..d1ddc92455b 100644 --- a/packages/zoe/test/unitTests/contracts/test-priceAggregator.js +++ b/packages/zoe/test/unitTests/contracts/test-priceAggregator.js @@ -32,6 +32,7 @@ import { /** * @import {PriceAuthority, PriceDescription, PriceQuote, PriceQuoteValue, PriceQuery,} from '@agoric/zoe/tools/types.js'; + * @import {ManualTimer} from '../../../tools/manualTimer.js'; */ /** diff --git a/packages/zoe/tools/internal-types.js b/packages/zoe/tools/internal-types.js deleted file mode 100644 index c2e1e266e06..00000000000 --- a/packages/zoe/tools/internal-types.js +++ /dev/null @@ -1,16 +0,0 @@ -// @jessie-check - -/** @import {TimerService} from '@agoric/time' */ - -/** - * @typedef {object} ManualTimerAdmin - * @property {(msg?: string) => void | Promise} tick Advance the timer by one tick. - * DEPRECATED: use `await tickN(1)` instead. `tick` function errors might be - * thrown synchronously, even though success is signaled by returning anything - * other than a rejected promise. - * @property {(nTimes: number, msg?: string) => Promise} tickN - */ - -/** - * @typedef {TimerService & ManualTimerAdmin} ManualTimer - */ diff --git a/packages/zoe/tools/manualTimer.js b/packages/zoe/tools/manualTimer.js index 40717e0bcc9..44c9f3a639b 100644 --- a/packages/zoe/tools/manualTimer.js +++ b/packages/zoe/tools/manualTimer.js @@ -3,7 +3,7 @@ import { bindAllMethods } from '@agoric/internal'; import { buildManualTimer as build } from '@agoric/swingset-vat/tools/manual-timer.js'; import { TimeMath } from '@agoric/time'; -import './internal-types.js'; +/** @import {TimerService} from '@agoric/time' */ const { Fail } = assert; @@ -19,6 +19,19 @@ const { Fail } = assert; const nolog = (..._args) => {}; +/** + * @typedef {object} ManualTimerAdmin + * @property {(msg?: string) => void | Promise} tick Advance the timer by one tick. + * DEPRECATED: use `await tickN(1)` instead. `tick` function errors might be + * thrown synchronously, even though success is signaled by returning anything + * other than a rejected promise. + * @property {(nTimes: number, msg?: string) => Promise} tickN + */ + +/** + * @typedef {TimerService & ManualTimerAdmin} ManualTimer + */ + /** * A fake TimerService, for unit tests that do not use a real * kernel. You can make time pass by calling `advanceTo(when)`, or one diff --git a/packages/zoe/typedoc.json b/packages/zoe/typedoc.json index f9eee1c3b4e..f5de7d0924e 100644 --- a/packages/zoe/typedoc.json +++ b/packages/zoe/typedoc.json @@ -11,7 +11,6 @@ "src/zoeService/types-ambient.js", "src/zoeService/utils.d.ts", "src/zoeService/zoe.js", - "tools/internal-types.js", "tools/manualTimer.js" ] } From 32cb9614906bc509da5fdfa805a8d14390b6182e Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Thu, 2 May 2024 13:48:09 -0700 Subject: [PATCH 2/3] chore(types): cleanup ambients --- packages/governance/src/types.js | 2 -- packages/inter-protocol/src/proposals/econ-behaviors.js | 2 -- 2 files changed, 4 deletions(-) diff --git a/packages/governance/src/types.js b/packages/governance/src/types.js index cced1b34245..23c8e3ddec7 100644 --- a/packages/governance/src/types.js +++ b/packages/governance/src/types.js @@ -1,5 +1,3 @@ -import '@agoric/zoe/src/zoeService/types-ambient.js'; - export {}; /** @import {ContractStartFunction} from '@agoric/zoe/src/zoeService/utils.js' */ diff --git a/packages/inter-protocol/src/proposals/econ-behaviors.js b/packages/inter-protocol/src/proposals/econ-behaviors.js index ed220917ee4..2ffaf3ddb14 100644 --- a/packages/inter-protocol/src/proposals/econ-behaviors.js +++ b/packages/inter-protocol/src/proposals/econ-behaviors.js @@ -1,8 +1,6 @@ // @jessie-check // XXX ambient types runtime imports until https://github.com/Agoric/agoric-sdk/issues/6512 -import '../../exported.js'; -import '@agoric/governance/exported.js'; import '@agoric/vats/src/core/types-ambient.js'; import { AmountMath } from '@agoric/ertp'; From 80d04790429765e81053d45f6f7b17fb7b06b7c6 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Thu, 2 May 2024 13:47:18 -0700 Subject: [PATCH 3/3] feat(types): Tagged --- packages/internal/src/tagged.d.ts | 155 +++++++++++++++++++++++++ packages/zoe/src/zoeService/utils.d.ts | 27 +++-- 2 files changed, 170 insertions(+), 12 deletions(-) create mode 100644 packages/internal/src/tagged.d.ts diff --git a/packages/internal/src/tagged.d.ts b/packages/internal/src/tagged.d.ts new file mode 100644 index 00000000000..f5bb5389aae --- /dev/null +++ b/packages/internal/src/tagged.d.ts @@ -0,0 +1,155 @@ +/** @file adapted from https://raw.githubusercontent.com/sindresorhus/type-fest/main/source/opaque.d.ts */ + +declare const tag: unique symbol; + +export type TagContainer = { + readonly [tag]: Token; +}; + +type Tag = TagContainer<{ + [K in Token]: TagMetadata; +}>; + +/** +Attach a "tag" to an arbitrary type. This allows you to create distinct types, that aren't assignable to one another, for distinct concepts in your program that should not be interchangeable, even if their runtime values have the same type. (See examples.) + +A type returned by `Tagged` can be passed to `Tagged` again, to create a type with multiple tags. + +[Read more about tagged types.](https://medium.com/@KevinBGreene/surviving-the-typescript-ecosystem-branding-and-type-tagging-6cf6e516523d) + +A tag's name is usually a string (and must be a string, number, or symbol), but each application of a tag can also contain an arbitrary type as its "metadata". See {@link GetTagMetadata} for examples and explanation. + +A type `A` returned by `Tagged` is assignable to another type `B` returned by `Tagged` if and only if: + - the underlying (untagged) type of `A` is assignable to the underlying type of `B`; + - `A` contains at least all the tags `B` has; + - and the metadata type for each of `A`'s tags is assignable to the metadata type of `B`'s corresponding tag. + +There have been several discussions about adding similar features to TypeScript. Unfortunately, nothing has (yet) moved forward: + - [Microsoft/TypeScript#202](https://github.com/microsoft/TypeScript/issues/202) + - [Microsoft/TypeScript#4895](https://github.com/microsoft/TypeScript/issues/4895) + - [Microsoft/TypeScript#33290](https://github.com/microsoft/TypeScript/pull/33290) + +@example +``` +import type {Tagged} from 'type-fest'; + +type AccountNumber = Tagged; +type AccountBalance = Tagged; + +function createAccountNumber(): AccountNumber { + // As you can see, casting from a `number` (the underlying type being tagged) is allowed. + return 2 as AccountNumber; +} + +function getMoneyForAccount(accountNumber: AccountNumber): AccountBalance { + return 4 as AccountBalance; +} + +// This will compile successfully. +getMoneyForAccount(createAccountNumber()); + +// But this won't, because it has to be explicitly passed as an `AccountNumber` type! +// Critically, you could not accidentally use an `AccountBalance` as an `AccountNumber`. +getMoneyForAccount(2); + +// You can also use tagged values like their underlying, untagged type. +// I.e., this will compile successfully because an `AccountNumber` can be used as a regular `number`. +// In this sense, the underlying base type is not hidden, which differentiates tagged types from opaque types in other languages. +const accountNumber = createAccountNumber() + 2; +``` + +@example +``` +import type {Tagged} from 'type-fest'; + +// You can apply multiple tags to a type by using `Tagged` repeatedly. +type Url = Tagged; +type SpecialCacheKey = Tagged; + +// You can also pass a union of tag names, so this is equivalent to the above, although it doesn't give you the ability to assign distinct metadata to each tag. +type SpecialCacheKey2 = Tagged; +``` + +@category Type + */ +export type Tagged< + Type, + TagName extends PropertyKey, + TagMetadata = never, +> = Type & Tag; + +/** +Given a type and a tag name, returns the metadata associated with that tag on that type. + +In the example below, one could use `Tagged` to represent "a string that is valid JSON". That type might be useful -- for instance, it communicates that the value can be safely passed to `JSON.parse` without it throwing an exception. However, it doesn't indicate what type of value will be produced on parse (which is sometimes known). `JsonOf` solves this; it represents "a string that is valid JSON and that, if parsed, would produce a value of type T". The type T is held in the metadata associated with the `'JSON'` tag. + +This article explains more about [how tag metadata works and when it can be useful](https://medium.com/@ethanresnick/advanced-typescript-tagged-types-improved-with-type-level-metadata-5072fc125fcf). + +@example +``` +import type {Tagged} from 'type-fest'; + +type JsonOf = Tagged; + +function stringify(it: T) { + return JSON.stringify(it) as JsonOf; +} + +function parse>(it: T) { + return JSON.parse(it) as GetTagMetadata; +} + +const x = stringify({ hello: 'world' }); +const parsed = parse(x); // The type of `parsed` is { hello: string } +``` + +@category Type + */ +export type GetTagMetadata< + Type extends Tag, + TagName extends PropertyKey, +> = Type[typeof tag][TagName]; + +/** +Revert a tagged type back to its original type by removing all tags. + +Why is this necessary? + +1. Use a `Tagged` type as object keys +2. Prevent TS4058 error: "Return type of exported function has or is using name X from external module Y but cannot be named" + +@example +``` +import type {Tagged, UnwrapTagged} from 'type-fest'; + +type AccountType = Tagged<'SAVINGS' | 'CHECKING', 'AccountType'>; + +const moneyByAccountType: Record, number> = { + SAVINGS: 99, + CHECKING: 0.1 +}; + +// Without UnwrapTagged, the following expression would throw a type error. +const money = moneyByAccountType.SAVINGS; // TS error: Property 'SAVINGS' does not exist + +// Attempting to pass an non-Tagged type to UnwrapTagged will raise a type error. +type WontWork = UnwrapTagged; +``` + +@category Type + */ +export type UnwrapTagged> = + RemoveAllTags; + +type RemoveAllTags = + T extends Tag + ? { + [ThisTag in keyof T[typeof tag]]: T extends Tagged< + infer Type, + ThisTag, + T[typeof tag][ThisTag] + > + ? RemoveAllTags + : never; + }[keyof T[typeof tag]] + : T; diff --git a/packages/zoe/src/zoeService/utils.d.ts b/packages/zoe/src/zoeService/utils.d.ts index a94b16b491b..6a31f5973a0 100644 --- a/packages/zoe/src/zoeService/utils.d.ts +++ b/packages/zoe/src/zoeService/utils.d.ts @@ -1,4 +1,5 @@ import type { Callable } from '@agoric/internal/src/utils.js'; +import type { Tagged } from '@agoric/internal/src/tagged.js'; import type { VatUpgradeResults } from '@agoric/swingset-vat'; import type { Baggage } from '@agoric/swingset-liveslots'; import type { Issuer } from '@agoric/ertp/exported.js'; @@ -13,22 +14,24 @@ type ContractFacet = { /** * Installation of a contract, typed by its start function. */ -declare const StartFunction: unique symbol; -export type Installation = { - getBundle: () => SourceBundle; - getBundleLabel: () => string; - // because TS is structural, without this the generic is ignored - [StartFunction]: SF; -}; -export type Instance = Handle<'Instance'> & { - // because TS is structural, without this the generic is ignored - [StartFunction]: SF; -}; +export type Installation = Tagged< + { + getBundle: () => SourceBundle; + getBundleLabel: () => string; + }, + 'StartFunction', + SF +>; +export type Instance = Tagged< + Handle<'Instance'>, + 'StartFunction', + SF +>; export type InstallationStart = I extends Installation ? SF : never; -type ContractStartFunction = ( +export type ContractStartFunction = ( zcf?: ZCF, privateArgs?: {}, baggage?: Baggage,