Skip to content

Commit

Permalink
Tagged helper for StartFunction (#9315)
Browse files Browse the repository at this point in the history
refs: #9283

## Description

#9283 was [failing
integration](https://github.com/Agoric/agoric-sdk/actions/runs/8927087379/job/24519667094?pr=9283)
with,
```
Error: src/proposals/econ-behaviors.js(4,1): error TS9006: Declaration emit for this file requires using private name 'StartFunction' from module '"/home/runner/work/agoric-sdk/agoric-sdk/packages/zoe/src/zoeService/utils"'. An explicit type annotation may unblock declaration emit.
```

I've been meaning to adopt the `Tagged` helper from types-fest so I took
this opportunity to try it and it solved this problem. So this PR adds
it to `@agoric/internal` and uses it for the `Installation` and
`Instance` types that need a tag. It doesn't use if for the three other
`declare const` we have in the repo.

While debugging I also noticed we could cut a few ambient runtime
imports so I did that. One required moving ManualTimer type into its
impl module.

### Security Considerations
I vendored the file instead of importing from NPM to not take the
external dep

<!-- Does this change introduce new assumptions or dependencies that, if
violated, could introduce security vulnerabilities? How does this PR
change the boundaries between mutually-suspicious components? What new
authorities are introduced by this change, perhaps by new API calls?
-->

### Scaling Considerations
n/a, types
<!-- Does this change require or encourage significant increase in
consumption of CPU cycles, RAM, on-chain storage, message exchanges, or
other scarce resources? If so, can that be prevented or mitigated? -->

### Documentation Considerations
should not be developer-facing
<!-- Give our docs folks some hints about what needs to be described to
downstream users.

Backwards compatibility: what happens to existing data or deployments
when this code is shipped? Do we need to instruct users to do something
to upgrade their saved data? If there is no upgrade path possible, how
bad will that be for users?

-->

### Testing Considerations
CI

<!-- Every PR should of course come with tests of its own functionality.
What additional tests are still needed beyond those unit tests? How does
this affect CI, other test automation, or the testnet?
-->

### Upgrade Considerations
n/a, types
<!-- What aspects of this PR are relevant to upgrading live production
systems, and how should they be addressed? -->
  • Loading branch information
mergify[bot] authored May 2, 2024
2 parents 5a4779a + 80d0479 commit 2e2466e
Show file tree
Hide file tree
Showing 11 changed files with 191 additions and 34 deletions.
2 changes: 0 additions & 2 deletions packages/governance/src/types.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import '@agoric/zoe/src/zoeService/types-ambient.js';

export {};

/** @import {ContractStartFunction} from '@agoric/zoe/src/zoeService/utils.js' */
Expand Down
2 changes: 0 additions & 2 deletions packages/inter-protocol/src/proposals/econ-behaviors.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
2 changes: 2 additions & 0 deletions packages/inter-protocol/test/psm/setupPsm.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof setUpZoeForTest>} FarZoeKit */

/**
Expand Down
2 changes: 2 additions & 0 deletions packages/inter-protocol/test/reserve/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import {
voteForOpenQuestion,
} from './contexts.js';

/** @import {ManualTimer} from '@agoric/zoe/tools/manualTimer.js'; */

/**
* @typedef {Awaited<ReturnType<typeof makeDefaultTestContext>> & {
* consume: import('@agoric/inter-protocol/src/proposals/econ-behaviors.js').EconomyBootstrapPowers['consume'];
Expand Down
155 changes: 155 additions & 0 deletions packages/internal/src/tagged.d.ts
Original file line number Diff line number Diff line change
@@ -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<Token> = {
readonly [tag]: Token;
};

type Tag<Token extends PropertyKey, TagMetadata> = 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<number, 'AccountNumber'>;
type AccountBalance = Tagged<number, 'AccountBalance'>;
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<string, 'URL'>;
type SpecialCacheKey = Tagged<Url, 'SpecialCacheKey'>;
// 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<string, 'URL' | 'SpecialCacheKey'>;
```
@category Type

Check warning on line 73 in packages/internal/src/tagged.d.ts

View workflow job for this annotation

GitHub Actions / lint-rest

Invalid JSDoc tag name "category"
*/
export type Tagged<
Type,
TagName extends PropertyKey,
TagMetadata = never,
> = Type & Tag<TagName, TagMetadata>;

/**
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<string, 'JSON'>` 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<T>` 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<T> = Tagged<string, 'JSON', T>;
function stringify<T>(it: T) {
return JSON.stringify(it) as JsonOf<T>;
}
function parse<T extends JsonOf<unknown>>(it: T) {
return JSON.parse(it) as GetTagMetadata<T, 'JSON'>;
}
const x = stringify({ hello: 'world' });
const parsed = parse(x); // The type of `parsed` is { hello: string }
```
@category Type

Check warning on line 106 in packages/internal/src/tagged.d.ts

View workflow job for this annotation

GitHub Actions / lint-rest

Invalid JSDoc tag name "category"
*/
export type GetTagMetadata<
Type extends Tag<TagName, unknown>,
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<UnwrapTagged<AccountType>, 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<string>;
```
@category Type

Check warning on line 139 in packages/internal/src/tagged.d.ts

View workflow job for this annotation

GitHub Actions / lint-rest

Invalid JSDoc tag name "category"
*/
export type UnwrapTagged<TaggedType extends Tag<PropertyKey, any>> =
RemoveAllTags<TaggedType>;

type RemoveAllTags<T> =
T extends Tag<PropertyKey, any>
? {
[ThisTag in keyof T[typeof tag]]: T extends Tagged<
infer Type,
ThisTag,
T[typeof tag][ThisTag]
>
? RemoveAllTags<Type>
: never;
}[keyof T[typeof tag]]
: T;
27 changes: 15 additions & 12 deletions packages/zoe/src/zoeService/utils.d.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -13,22 +14,24 @@ type ContractFacet<T extends {} = {}> = {
/**
* Installation of a contract, typed by its start function.
*/
declare const StartFunction: unique symbol;
export type Installation<SF> = {
getBundle: () => SourceBundle;
getBundleLabel: () => string;
// because TS is structural, without this the generic is ignored
[StartFunction]: SF;
};
export type Instance<SF> = Handle<'Instance'> & {
// because TS is structural, without this the generic is ignored
[StartFunction]: SF;
};
export type Installation<SF extends ContractStartFunction | unknown> = Tagged<
{
getBundle: () => SourceBundle;
getBundleLabel: () => string;
},
'StartFunction',
SF
>;
export type Instance<SF extends ContractStartFunction | unknown> = Tagged<
Handle<'Instance'>,
'StartFunction',
SF
>;

export type InstallationStart<I> =
I extends Installation<infer SF> ? SF : never;

type ContractStartFunction = (
export type ContractStartFunction = (
zcf?: ZCF,
privateArgs?: {},
baggage?: Baggage,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {

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

/**
Expand Down
16 changes: 0 additions & 16 deletions packages/zoe/tools/internal-types.js

This file was deleted.

15 changes: 14 additions & 1 deletion packages/zoe/tools/manualTimer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -19,6 +19,19 @@ const { Fail } = assert;

const nolog = (..._args) => {};

/**
* @typedef {object} ManualTimerAdmin
* @property {(msg?: string) => void | Promise<void>} 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<void>} 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
Expand Down
1 change: 0 additions & 1 deletion packages/zoe/typedoc.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
}

0 comments on commit 2e2466e

Please sign in to comment.