Skip to content

Import from another App#263

Closed
brprice wants to merge 12 commits intomainfrom
brprice/module-import
Closed

Import from another App#263
brprice wants to merge 12 commits intomainfrom
brprice/module-import

Conversation

@brprice
Copy link
Copy Markdown
Contributor

@brprice brprice commented Feb 22, 2022

We introduce a way to import code from some other App. This is essentially automated copy-and-paste. Currently there is no notion in primer of a "module", and no tracking of where code originally came from -- this is left as future work.

Since primitive constructors are hard-wired into the Expr type, they are
always in scope. However, the corresponding primitive type definitions
may not be in scope. Since we have a hard-wired map `primConName` that
gives the type any PrimCon constructs we could infer a non-existant type
for primitive constructors!

Previously, we would infer a type of `Char` for `'a'`, even when the
type `Char` did not exist in the current program, or even if it had been
arbitrarily defined by the user.

We now check that the type-name given by the hard-wired map exists in
the current program, and is a primitive type. Note that this design
relies on the fact that primitive types cannot be renamed.
Otherwise it will generate ill-typed terms in the empty context: it will
generate primitve constructors but they are not considered to be in
scope as their corresponding types are not.
When we successfully synthesise a type for some term, that type should
kind-check at the kind KType. I.e. the types we synthesise are actually
recognised as types by our kind checker.
We split the module in two, to enable creation of a Primer.Import
module which depends on some basic definitions in Primer.App, but should
provide a new handler for Primer.App to use. If we did not do this
refactor, we would create a import cycle.
@brprice brprice changed the base branch from main to brprice/primitive-type-scope February 22, 2022 19:17
@brprice
Copy link
Copy Markdown
Contributor Author

brprice commented Feb 22, 2022

Reviewer notes:
There are some bulky commits in here, but most of it is either a refactor, or mostly test cases.
This mostly implements the first point of the module proposal: imports are clones. We don't add a notion of "module" quantifier to names (future work). We somewhat generalise by allowing arbitrary renamings rather than just renaming-of-module-prefixes (this is very little extra work, although it makes giving nice errors a bit more hairy). When we (in a follow-up) add a "module" notion, it will be simple to expose a simpler api.

Comment on lines +146 to +193
-- Not allowed to both import and dep-rename some type
for_ (Map.intersection iacImportRenamingTypes iacDepsTypes) $ throwError' . ImportRenameType . fst
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I could combine the maps and get this by construction, but think that is less ergonomic to use. Perhaps I should given that this is a low-level API which will be wrapped by simpler "import module A, noting that A depends on B which is already imported as C"

Comment thread primer/src/Primer/Core.hs
Comment on lines +468 to 495
_typeDefName :: Lens' TypeDef Name
_typeDefName = typeDefLens #primTypeDefName #astTypeDefName

typeDefName :: TypeDef -> Name
typeDefName = \case
TypeDefPrim t -> primTypeDefName t
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Given that I am defining lenses for these anyway, perhaps I should re-define the current accessors to use the lens?
I have not done this, as I was a bit worried about performance of generic-lens. I wanted to test it with inspection-testing, but that is broken with haskell.nix. However, typing this comment gave me the impetus to just dump the core and eyeball it -- it looks like everything does optimise away as expected, but I am not sure whether it gets worker-wrapper transformed in exactly the same way as the manual definition.

If we aren't too worried about this issue (and I expect we are not), it is easy to refactor this.

Copy link
Copy Markdown
Member

@dhess dhess Feb 22, 2022

Choose a reason for hiding this comment

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

You know, this is a good point that we've lost track of — we had planned to write some benchmark tests so that we could start tracking regressions. Perhaps we should get that set up before we look into writing more lenses. (I'm in favor of that in general, BTW.)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Or indeed, given that typeDefName would just be view _typeDefName, we might then be better off just inlining it.

Side note - in #143 we agreed:

Don't use leading underscores _ for lens names, unless it's necessary to avoid a name clash.

If we inline, we free up the name typeDefName for the lens.

Comment thread primer/src/Primer/Core.hs
Comment on lines +364 to +365
_defID :: Lens' Def ID
_defID = defLens #primDefID #astDefID
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Same comment here about lenses as for types

Comment on lines +150 to +154
, iacImportRenamingTerms :: Map ID Name
, iacDepsTerms :: Map ID ID
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Same comment here about combining the maps as for types

@brprice brprice force-pushed the brprice/module-import branch 2 times, most recently from 2f88af4 to 14d603e Compare February 22, 2022 21:17
The reason that srcProg and primSrcProg have been put into an
Applicative context is that later we will want to import terms, and to
test we will define some terms in these programs. This will need a
MonadFresh context. To avoid changing all use sites later, we will write
it monadically now.
The point of this trivial rejigging is to set up for importing terms
(which will change these fields's type), so that commit will be less
cluttered.
Similarly to the previous commit, set up for tests of importing terms.
We need to return the ID of the new definitions, but this requires lots
of knock-on changes in the tests. We do those now rather than in the
commit that adds the ability to import terms.
I.e. that they look through explicit mutual recursion.

When implementing importing terms, I worried whether these combinators
would recuse into branches of a case expression, as that requires going
from an Expr to a CaseBranch back to an Expr, and we very much need to
replace names inside branches. These functions do indeed recurse into
and out of other mutually recursive types in this manner.
Since the left hand side of a pattern match (the "C" in
  case x of
    C -> e
) is a Name in a CaseBranch, rather than a Con, it was not being
updated.
Since we allow rewiring imported type dependencies to any compatible
existing type, with any compatible bijection of their constructors, we
need to handle the case where the constructors get reordered. This is
important since the typechecker enforces that the branches of a pattern
match are in the order that constructors are declared in the
corresponding type definition.

For example, if we imported and rewired the type
  data T = A | B
to
  data R = C | D
via A -> D and B -> C, then we would need to reorder the import of
  case x of
    A -> t
    B -> some_other_rhs
to
  case x of
    C -> some_other_rhs
    D -> t

(Note that much of this commit only changes indentation)
@brprice brprice force-pushed the brprice/module-import branch from 55336ae to ab3ad49 Compare February 22, 2022 21:24
@brprice brprice requested a review from a team February 22, 2022 21:29
RewriteCtorTgtNotExist Name Name
| -- | This type was referenced in the source, but we have neither imported it nor rewritten it
ReferencedTypeNotHandled Name
| -- | @RewrittenCtorTypeDiffers ty1 con1 ty2 con2@: we tried to rewrite @ty1@ into @ty2@, mapping @con1@ to @con2@, but their types do not match
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't mind long lines and certainly don't subscribe to any kind of antiquated 80-column line limit, but this one and a few below are probably a bit much. Is there a way to enforce a reasonable limit in Fourmolu?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

These are timely: we should consider how these import failures should be represented in the UI. Using our current category 1-5 classification, would these be category 2 (action failure)?

To answer that we'll need to think about the import workflow and UI.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Just curious: assuming that once we have a proper module system with namespaced imports, we remove the "import-via-copy" functionality implemented in this PR, how many of these import errors will remain?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is there a way to enforce a reasonable limit in Fourmolu?

There's an unmerged PR adding support. IIRC, it has some issues around idempotence, which I'm a little uncomfortable with.

I am actually hoping to spend some long-overdue time on Fourmolu this weekend. But then again, I thought that last weekend...

deriving (FromJSON, ToJSON) via VJSON ImportError

-- | How to rename a type, and each of its ctors
type TypeImportDetails = Map Name (Name, Map Name Name)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can this type renaming code be re-used in our upcoming editable types work?

-- - It needs to be injective for the translation, as previously distinguished
-- cases are now identified
-- - It needs to be surjective for the translated match to be exhaustive)
--
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is a helpful and informative comment, but perhaps it should be an internal comment not made visible to Haddock?


-- | Test whether an term contains any holes
-- (empty or non-empty, or inside a type)
noHolesTm :: (Data a, Data b) => Expr' a b -> Bool
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Hmm, this is an interesting consequence of our "every program is valid" approach, and raises a question that I think is truly unique to Primer. Do we support importing code that contains holes? Should we?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

My inclination is that we should. A student might, for example, want to run some code from their own partially-incomplete module, and it would be frustrating if that were artificially restricted.

Perhaps we could have some kind of warning against "publishing" (whatever that means) a module containing holes.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think I agree on both points.

@dhess
Copy link
Copy Markdown
Member

dhess commented Feb 22, 2022

Thanks. I appreciate the very helpful organization of these successive commits, as it makes it much easier to review.

Speaking of review, I'll defer to @georgefst on the technical bits.

In addition to my localized comments above, I do have two more points:

  • There's a typo in the commit for 50adfaa: "When implementing importing terms, I worried whether these combinators would recuse [sic] into branches"
  • I confess that this appears to be a lot more code than I would have guessed would be required to implement this first pass, naïve approach to imports. What's your estimate of how much of this will need to be thrown out or substantially rewritten once we have support for proper (non-parameterized) modules, assuming we decide not to keep the naïve implementation around?

@georgefst
Copy link
Copy Markdown
Contributor

I am slightly concerned about the overall approach. It feels like this would make it impossible to update a dependency. The first comment here mentions "tracking of where code originally came from" - it's not obvious to me how we get there from here.

While I was writing this comment, @brprice has just said that he also has some concerns about the approach, so I'll hold off on reviewing further for now.

Base automatically changed from brprice/primitive-type-scope to main February 24, 2022 15:46
@brprice
Copy link
Copy Markdown
Contributor Author

brprice commented Mar 10, 2022

We decided to take a different approach in the end. Closing in favour of #293, but I'll leave the branch around for a while, as it does some renaming codemodding that may be useful to look at when implementing #267

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants