-
Notifications
You must be signed in to change notification settings - Fork 2.3k
Conversation
(I opened #4248 in case the answer to this question is "yes") |
- Create new method cache.getCachedFileNames() in order to remove coupling between VersionRange and compilerCachePath
As a potential stopgap: 1. Blindly replace all `noVersion` / `noWhatever` strings with analogous `NoVersionError` classes, putting them in new errors bucket module 2. For each such new error class, if the class is used by only one loading strategy, move the class to that module.
- Stop doing a separate `this.loadParserSolc()` inside `supplier.load()` - Remove `parserSolc` from the return value of `supplier.load()`, since the invoker of this method can just load a specific parser itself, and forcing this cleans up the CompilerSupplier interface / logic flow - Add the parserSolc selection logic to the only place in Truffle that uses the now-gone return value. (!! thus breaking @truffle/compile-solidity !!)
- Define new unified method for listing versions, which returns { latestRelease, prereleases, releases}, where `prereleases` and `releases` may both be AsyncIterableIterators - Align VersionRange with new interface for this method - Replace naïve `docker.getDockerTags()`, which only returns the first page of results from the Hub API: - Implement API page traversal as an AsyncIterableIterator of all tags individually. - Add retry logic to axios to perform exponential backoff to get around Docker Hub's 429 Too Many Requests responses - Use iter-tools to fork/filter/conform unified stream into `{ latestRelease, prereleases, releases }` - Add warning not to do `truffle compile --list=docker --all`
- Rename cache.fileIsCached() to cache.has() - Rename cache.addFileToCache() to cache.add() - Rename cache.resolveCache() to cache.resolve() - Rename cache.getCachedFileNames() to cache.list()
Rename `getSolcByUrlAndCache` to `getAndCacheSolcByUrl`
375a5e1
to
5499b2e
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, further comments on this. I remain really wary of the whole thing due to the complexity of the types; I'm still not sure that this is necessary, and that you couldn't get most of the effect a simpler (if looser) way.
Also, reviewing these would have been a lot easier had I read from the bottom up! Each thing is defined in terms of things below it, and those components below it aren't easily-blackboxed abstractions, but rather preliminaries that need to be understood first for the whole thing to make sense.
const strategyName = determineStrategy(options); | ||
const Strategy = strategyConstructors[strategyName]; | ||
|
||
// @ts-ignore since we can't figure out N from inside |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using ts-ignore
in ordinary code makes me really wary. Can't this just be a coercion instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm of the mind now that @ts-ignore
is safer than coercions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I strongly disagree with this point of view. I do not think we should be using ts-ignore
... basically at all. We used it in that one place in encoder
only because the configuration inconsistency (where the same code had to compile with strictNullChecks
both on and off) left us with basically no other choice.
In general, we should try to work within TS, not ignore it. Obviously coercions are not fully "working within TS", but they're closer. TS does for instance perform rudimentary checks on coercions to make you're not sure you're doing anything too stupid with them, for instance; it obviously doesn't do that with ts-ignore.
You have elsewhere, although not here, made arguments as to why coercion is unsafe. I do not see that any of these arguments apply here. Indeed, I'm not convinced those arguments apply much of anywhere, as you've never provided any concrete examples, and the ones you've vaguely described seem to me to rely on what I would consider to be other bad practices; I would say those are the problems, not the coercion.
If you want to convince me on this point you are going to need to provide much more in the way of argument.
I would strongly discourage the use of ts-ignore both here and everywhere.
* name. | ||
*/ | ||
strategies: { | ||
[strategyName: string]: FreeStrategy.Specification; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Weren't you going to force results agreement here? I don't see anything here that forces that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately, there's not a good way to do this enforcement here; since this is the base specification type, there's not a good way to enforce the cross-linkage.
The enforcement happens elsewhere: forDefinition
will fail to type-check if given an S
whose strategies' results
types do not agree with each other.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, hm. Well, if you can't enforce it in the type system proper, I'd suggest at least adding a comment to indicate that that's how you should use it. (Comments: The poor programmer's type system! :P )
*/ | ||
export type Supplier< | ||
S extends Supplier.Specification, | ||
N extends Supplier.StrategyName<S> = Supplier.StrategyName<S> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the difference between
N extends Supplier.StrategyName<S> = Supplier.StrategyName<S>
and just
N extends Supplier.StrategyName<S>
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The former allows the parameter to be omitted, so you can do Supplier<S>
instead of Supplier<S, N>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh! It's a default value. I see, I didn't get that. Perhaps a comment indicating that would be useful?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I revised the docstring, hopefully it's clear enough from that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh yeah, revising the docstring is a better idea. What I was confused about was the syntax, so I was thinking of just adding a comment to clear up the syntax, but revising the docstring is actually probably the better idea.
Unfortunately the new material that you added to the docstring is not entirely clear. When you write "When N
is omitted, Supplier<S>
represents a supplier that uses any specified strategy"... "any specified strategy" is a terribly unclear phrase. No strategy was specified! I eventually figured out that you meant "any of the strategies appearing in the specification S
".
packages/supplier/src/supplier.ts
Outdated
export type Supplier< | ||
S extends Supplier.Specification, | ||
N extends Supplier.StrategyName<S> = Supplier.StrategyName<S> | ||
> = Supplier.StrategyName<S> extends N |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do not understand what this ternary is doing. Or rather, I think understand what it's doing, but if so I don't understand why.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If N extends T
(enforced by the generic param) and T extends N
, then N
equals T
. So this distinguishes supplier types for any strategy name vs. a specific strategy name.
I'll add a comment for this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still don't understand this, sorry. Admittedly you haven't written your comment yet. But what you've said only explains the condition of the ternary. That doesn't explain why you would use BaseStrategy
in the one case and FreeStrategy
in the other. The thing I don't understand about the ternary is that the effect of the condition seems, as best I can tell, to be unrelated to the condition.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added the comment - but maybe not enough explanation of the "why"... I'll take a look and see what I can do about that
|
||
export namespace Strategies { | ||
/** | ||
* Type-level specification of all strategies for a specified supplier |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Specifically, this is the mapping from names to strategies, yes?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mapping from names to strategy specifications, yep.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I should be more explicit. I think the comment should state that.
- Remove unused colors - Move ts-mixer to devDependencies
- Ensure generic parameters are documented - Extend optionality of N through a few types where appropriate - Define a standalone `Supplier.Strategy<S, N>` type
If you recall our discussion about this, it seems that the question of this directionality hinges on the open question of "which exports are truly external?" My stance is that the exports towards the bottom of the file are inherently more internal, and thus should not be at the top. Do you disagree and think that these types are intrinsically an externally-facing concern?
So, the question is: is the additional type safety here worth the cognitive load? My thinking (obv.) is yes: I want the compiler to tell people who are building supplier strategies that they must know upfront (e.g.) if their strategy allows listing versions. I want new suppliers and new strategies for existing suppliers to require as little duplication as possible, while still making sure that all resultant I posit, thus, that this is a good trade-off: the cognitive load when implementing a new supplier is that you must figure out how to write a specification type; in return, you won't have to worry about figuring out what type something is anywhere else; the compiler will tell you when you mess up, and in the future, changes to that supplier won't run the risk of being incomplete. (In case I'm not getting my full argument across: by counterexample, think of all the places in the code that one needs to touch just to do something as simple as add a field to @truffle/contract-schema. That's the sort of thing I'm trying to avoid with these specification types.) |
The idea of some things as internal and some things as external relies on the idea that interface can be separated from implementation, that the user can focus on certain things and ignore their internals. Like, if you define a function, the user shouldn't need to look at what other functions that function calls to understand it; they shouldn't need to read the body at all. You black-box the implementation and just focus on the interface. The problem is, with types, there is very little distinction between a type's "interface" and it's "implementation". Frankly I have little idea how I'd separate the two. To understand a type, you generally need to have some understanding of what each of its components means. This is how you come to understand a type -- by reading each of the components and the comments on them. The whole thing is both interface and implementation; there's no separate body you can ignore! Now often you don't need to understand all the components immediately and fully, so you can sensibly work top-down... but this relies on the components making some sort of a priori intuitive sense, so that you can have some reasonable provisional idea about what they are. That doesn't apply here, because none of these types are intuitive. My reaction on reading these isn't "oh OK I guess that's a blah, I can check the details of it later", it's, "Huh? what the hell is a blah? Guess I'd better stop what I'm reading and go jump to that and see". And now I'm trying to figure out two confusing types. Basically I don't think much of any of this is internal, because, well... it's types. I don't know how one could be confident that they could black-box much of any of this. ...OK I got kind of pulled away before replying to the rest of this, I think to make sure this at least goes up I'm going to just hit post and can reply to the rest after in a separate comment... |
Again, I think you could still get most of the type safety without such complication. But I think there's another problem here: What new suppliers and new strategies? How many of these are actually going to get written? My understanding was that the point of this was to allow the writing of a Vyper CompilerSupplier. Note, that's specifically a second complier supplier; it doesn't require a more general "supplier" notion. And, um, do we expect to be writing any more suppliers than that...? I guess what I'm realizing now is that part of my objection is that this is a lot of abstraction that is hardly ever going to get used. You're trying to abstract things that we simply have no plan for. Better to just make an abstraction that covers the things we actually plan to do. Actually, this makes me realize there's some bits of the design I hadn't properly processed and criticized before, but I'll post that in a separate comment.
I don't think "you won't have to worry about figuring out what type something is anywhere else" is a good goal. Like, you should always have types on your functions.
What makes you think that anything like that situation would arise without this mechanism? |
OK, so, here's something I only just realized; Why do strategies have names? In the existing way, strategies don't have names. This whole strategy-name thing is invented here. Like, imagine we stuck closer to the original code. Then instead of having The whole strategy-name thing seems like a needless complication. What if that were cut out? (Although, perhaps it's impossible to do all the type things you want to do if that were cut out?) |
* | ||
* When specifying a strategy, this must be `true` or `false` explicitly. | ||
*/ | ||
allowsLoadingSpecificVersion: boolean; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are these supposed to be methods? The mixin specifies a function that returns the boolean.
73511bb
to
f5adb64
Compare
Closing this for cleanup. If we want to continue on this front, I think it might be worth starting from tabula rasa |
To build a supplier for a given software component (e.g. the solc CompilerSupplier), @truffle/supplier now exposes
forDefinition({ determineStrategy, strategyConstructors })
, which returns acreateSupplier
function as a result.createSupplier
functions take a language-specific set of options, determine which of the defined strategies to use based on those options, then instantiate the determined strategy with those options.This new package provides type-safety support by way of
forDefinition
's being generic to a particular Supplier Specification. Defining an explicit Supplier specification (and corresponding Specifications for each Strategy) will ensure thatsupplier.load(version?: string)
andsupplier.list()
throws errors unless the type-checker can guarantee the strategy implements those methods, using type guards, e.g.:Question: should this be moved to a new package (maybe @truffle/supplier)? There's nothing specific to compilers about it, really, and if we want to move common code for multiple languages here later, we might find ourselves adding dependencies to @truffle/compile-common that we don't want.(doing this)