Skip to content

Implicit parameters for Unison — RFC + working prototype#6238

Open
runarorama wants to merge 21 commits into
trunkfrom
prototype/implicits-rfc
Open

Implicit parameters for Unison — RFC + working prototype#6238
runarorama wants to merge 21 commits into
trunkfrom
prototype/implicits-rfc

Conversation

@runarorama

@runarorama runarorama commented May 25, 2026

Copy link
Copy Markdown
Contributor

This PR proposes adding Scala-3-style implicit parameters (given / =>) to Unison and includes a working prototype. It exists to anchor RFC discussion: read the design, run the examples below, push back on anything that surprises you.

The end-to-end example from the design discussion typechecks, elaborates, runs, and round-trips through view:

class Monoid m = { op : m -> m -> m, zero : m }

given Monoid.nat : Monoid Nat = Monoid (Nat.+) 0

foldLeft : (b -> a -> b) -> b -> [a] -> b
foldLeft f z xs =
  match xs with
    [] -> z
    h +: t -> foldLeft f (f z h) t

foldMap : (Monoid m) => (a -> m) -> [a] -> m
foldMap f = foldLeft (acc a -> Monoid.op acc (f a)) Monoid.zero

foo : Nat
foo = foldMap Text.size ["one", "two", "three"]

> foo

UCM after add:

+ type Monoid m
+ foldLeft    : (b ->{g1} a ->{g} b) -> b -> [a] ->{g, g1} b
+ foldMap     : Monoid m => (a ->{g} m) -> [a] ->{g} m
+ foo         : Nat
+ Monoid.nat  : Monoid Nat
+ Monoid.op   : Monoid m => m -> m -> m
+ Monoid.zero : Monoid m => m

> foo
    ⧩
    11

A polymorphic-with-premise given also works end-to-end:

class Show s = { show : s -> Text }

given Show.nat : Show Nat = Show Nat.toText

given Show.list : forall a. Show a => Show [a] =
  Show (xs -> "[" ++ textJoin ", " (List.map Show.show xs) ++ "]")

foo : Text
foo = Show.show [1, 2, 3]

> foo
    ⧩
    "[1, 2, 3]"

Show.list's premise Show a is discharged against the function's own lexical => parameter while its body is checked, and at foo's call site the resolver chains Show.list (premise Show a) → Show.nat to build Show [Nat]. The chosen dictionaries are wired into foo's hash, and view foo round-trips to exactly what the user wrote.

dependencies foo lists Show.nat and Show.list — the resolved givens are permanently wired into foo's hash. The same scratch works split across files: drop class Show / given Show.nat into one file, add, then write the rest in a new scratch and resolution finds the givens from the namespace automatically.


How implicits/givens fit into Unison

Five principles are load-bearing:

  1. Givens are sugar for ordinary parameters. After elaboration, a constraint is
    an ordinary positional argument. Runtime, codegen, ABT, and term hashing learn
    nothing new — the entire feature lives in the elaborator.

  2. Resolution is baked into the term hash. A term that referenced Show Nat
    freezes the chosen dictionary's hash at typecheck time. Old terms never change
    instance. Unison gets something Scala and Haskell can't: implicits that are
    immune to "did adding this instance silently change my program?" — already-elaborated
    terms are pinned by content addressing.

  3. Namespace = instance set. No orphan rules, no global registry. Two libraries
    that both define given Show.nat produce ambiguity at any call site that sees
    both, surfaced through the same machinery TDNR uses today.

  4. Givenness is namespace metadata, not a term-level concept. Marking a
    definition given updates the namespace; term hashes are unchanged. The branch
    (causal) hash reflects the change. Aliasing, importing, forking work without
    rehashing terms.

  5. Givens extend TDNR rather than replace it. TDNR resolves "which named
    definition fits this type"; given resolution adds "which value of this type
    exists, with chaining." Same elaborator pass, same ambiguity reporting.

Surface syntax

A class declaration generates getter accessors whose dictionary parameter is an => arrow (no setters, no modifiers):

class Show a = { show : a -> Text }
class Functor f = { fmap : forall a b. (a -> b) -> f a -> f b }

Plain records are still available with type Show a = Show (a -> Text); the difference is that class accessors carry the constraint implicitly, so callers write show x instead of Show.show dict x.

Declaring givens:

given Show.nat : Show Nat = Show Nat.toText

given Show.list : Show a => Show (List a) =
  Show (xs -> "[" ++ Text.join ", " (List.map Show.show xs) ++ "]")

given Functor.optional : Functor Optional = Functor Optional.map

The given keyword tags the binding so that, on add/update, namespace metadata is automatically updated — no follow-up mark.given needed (but the explicit command is still there if you want to flip an existing definition).

Consuming givens:

print : Show a => a ->{IO} ()
print x = printLine (Show.show x)

mapM : (Monad m, Traversable t) => (a ->{e} m b) -> t a ->{e} m (t b)

The summon builtin retrieves the unique resolvable dictionary at the surrounding context's expected type. Its declared type is forall a. a => a — the leading => makes the typechecker emit a constraint goal at every reference, which the resolver fills from lexical / ambient givens. At runtime summon is the identity function. Use it when you want to bind a dictionary explicitly:

sortReversed : Ord a => [a] -> [a]
sortReversed xs =
  given local : Ord a = Ord.flip (summon : Ord a)
  sort xs

summon is an ordinary term reference, so user code may shadow it.

The give keyword is the dual: give f demotes every leading => in f's declared type to ->, so the dictionaries become ordinary positional arguments at the call site. Use it for testing, mocking, or any time you want to bypass the resolver entirely:

combined : Show Nat => Monoid Nat => Nat -> Nat -> Text
combined a b = …

combined 3 4                        -- resolver picks Show.nat + Monoid.nat
give combined mockShow mulMonoid 3 4 -- both dicts supplied positionally

give has to be a keyword (not a builtin) because the type transformation can't be expressed as a regular polymorphic signature — the existing implicit-resolution rule would fire first on a builtin reference and the dictionary would be auto-filled before give could see it. Partial overrides (override one dict, let the resolver pick the rest) use the existing let given shadowing.

Resolution

For each constraint hole the elaborator searches visible givens whose conclusion
unifies with the goal type, recursively resolving each candidate's premises.
Ambiguity surfaces as the existing TDNR-style error. Cycles fail per-branch (the
candidate, not the whole search); a global depth limit (default 50) catches
runaway chains. Resolutions are memoized per top-level invocation, caching
successes and failures.

Specificity: A is strictly more specific than B iff some σ satisfies
σ(B's declared conclusion) = A's declared conclusion, with σ binding only B's
quantified variables. Local given beats outer given. Otherwise → ambiguity.

Coherence by content-addressing

The interplay between resolution and Unison's hashes is what makes this design
distinctive. A term containing a constraint hole elaborates to one containing
the chosen dictionary's hash. If the namespace later gains a second matching
given, the elaborated term is unchanged — its hash already commits to the
original. Only new terms see ambiguity.

This is stronger than Haskell's coherence (depends on global instance discovery
and orphan rules) and stronger than Scala's (can silently change on recompile).
Unison's content addressing makes coherence a non-issue for already-elaborated
code, by construction.

What's not in v1

  • Functional dependencies / associated types
  • Implicit conversions
  • Default parameters
  • deriving / macro derivation
  • Kind-polymorphic givens (Unison's kind system has only Type, Ability, and
    arrow kinds)

What's in this PR

  • Parser=> constraints, given declarations, let given,
    class declarations, @-positional explicit overrides.
  • Namespace##Builtin.Given and ##Builtin.Class sentinels +
    MdValues plumbing on the term and type Stars respectively
    (Unison.Codebase.Givens / Unison.Codebase.Classes); UCM commands
    mark.given, unmark.given, givens; sharing-API round-trip;
    unison-merge conflict resolution for given-set diffs.
    given-declared bindings and class-declared types are auto-marked
    on add/update, and the slurp summary surfaces the class /
    given keyword in place of type / the plain term signature.
  • TypecheckerType.F extended with ImplicitArrow; lexical given
    environment threaded through every binder; constraint goals emitted at apply
    sites and at any Var/Ref use of a function with leading =>; =>I rule
    recognises the parser-injected leading lambda for a binding's implicit
    parameters and registers it as a lexical given for the body, so a polymorphic
    function's own =>-param resolves constraints raised inside its own body
    (this is what makes foldMap and polymorphic Show.list above work).
  • Top-level givens promoted to the ambient pool — file-local given-tagged
    bindings are exposed to every goal in the file via the resolver's ambient
    pool, sidestepping minimize's reordering of independent SCCs (which could
    otherwise leave a sibling given invisible to an earlier user). Lexical
    registration is skipped for top-level givens so they don't tie with a
    function's own =>-parameter on the Lexical 0 scope tag.
  • Speculative-pass info notes suppressedannotateLetRecBindings's
    annotation-less redundancy pass and noteTopLevelType's redundancy-check
    synthesis both would leak ConstraintGoal notes with incomplete lexical
    scopes (because =>I never fires without the user's annotation). A
    discardInfoNotes combinator wraps both passes so only the
    official-typecheck constraint goals reach the resolver.
  • summon builtinsummon : forall a. a => a, runtime identity. The
    => on its declared type drives implicit resolution through the normal
    machinery; the user writes (summon : T) (or relies on context) and gets
    back the unique resolvable given of type T.
  • give keywordgive f is a prefix syntactic transformation that
    demotes every leading => in f's declared type to ->. Tagged with
    Ann.Lowered at parse time; the typechecker swaps peelLeadingImplicits
    for lowerLeadingImplicit at lowered references; GivenApply.rewriteApply
    pre-demotes the type — descending through any leading forall quantifiers
    first — before interleave sees it, so the decision queue stays aligned
    even for generalized signatures like forall a. C a => a -> a.
    Type.existentializeArrows no longer inserts an Effect row between
    adjacent =>s — implicit arrows carry no abilities. This was breaking
    the =>I checkWanted pattern on chained constraints like
    C1 => C2 => T.
  • Resolver — full algorithm above, integrated as a post-pass on the
    TDNR fixed point; resolved decisions applied in the term as ordinary App
    nodes against dictionary hashes. Decisions are looked up by source location
    to keep multi-binding files coherent. decomposeGivenType strips the empty
    effect wrapper that generalization leaves around a constraint's conclusion,
    so a generalized polymorphic given unifies with the plain-shape goal at the
    call site.
  • Errors — categorised NoGiven / Ambiguous / DepthExceeded /
    Cycle rendering. Resolution failures now surface through the file
    loader (previously the file silently failed to add).
  • LSP — hover and goto-definition on synthesized implicit args;
    diagnostics and code actions for resolution failures.
  • Pretty printer=> round-trips through view / edit /
    update. Elide-mode default: dictionary arguments inserted by the
    elaborator are not shown in surface output (so view foo matches
    what the user wrote); the corresponding leading \_implicit_*
    lambdas on the binding are also elided. The hash dependency is
    preserved — dependencies foo still lists the chosen given.
    view for class-tagged types emits the class keyword
    and renders the constructor with { field : type, ... } record syntax
    (driven by an IsClassRef predicate threaded through DeclPrinter,
    and hashClassFieldAccessors which reproduces the =>-bearing
    accessor hashes to recover field names from the namespace).
    view for given-tagged definitions emits the given keyword;
    view for terms that used give emits the give keyword with the
    user-supplied dictionary preserved (detected by walking each apply
    chain whose head has a =>-prefixed type and checking whether the
    dictionary argument is a namespace-tagged given or a local lexical
    given — anything else is treated as user-supplied via give and gets
    a sentinel Type.giveMarkerRef ascription that the surface printer
    recognises). The dependents-update path (prettyUnisonFile) consumes
    the file's classBindings so a class re-printed for re-typechecking
    keeps the class keyword and re-parses to the same hash.
  • Tests — parser snapshot suite (60+ tests across => / given /
    summon / @); printer round-trip property tests; typechecker
    integration; full resolver test matrix including diamond dependencies
    and HKT; source-level end-to-end test pinning the parser → resolver
    wiring; UCM transcripts covering the update-semantics scenarios
    (updating a given changes the dependent's hash; breaking a given
    leaves old code valid; a new ambiguous given doesn't disturb old
    code).

stack build --fast clean. stack test --fast unison-parser-typechecker passes
(452/452).

Not ready yet

  • given and give keyword migration — the prototype hard-breaks identifiers using
    the given and give names. The deprecation cycle is a release-engineering concern.
  • unison-merge pipeline integration — the given-set conflict module
    is sidecar; wiring it into the merge engine is a follow-on.
  • Resolver polish — cycle/metavar semantics, NearMiss substitution rendering,
    override-leaf parens diagnostic, partial-application handling, and more.

runarorama and others added 8 commits May 25, 2026 16:27
Polish on top of the implicits RFC commit so the full
`class Monoid m = { op, zero }` / `foldMap` example from the design
discussion typechecks, elaborates, runs, and round-trips through
`view`.

Surface syntax
--------------
* New `class T a = { f1 : T1, … }` keyword. Sugar over a record-style
  `unique type`: generates only getters (no setters / modifiers), and
  each getter's signature carries the class as an implicit (`=>`)
  parameter so a call like `Monoid.op acc x` lets implicit
  resolution thread the dictionary in.

Typechecker
-----------
* New `=>I` rule in `checkWanted`: when a `Lam'` is checked against
  an `ImplicitArrow'`, bind the lambda variable and register it as a
  lexical given for the body. Together with a parser pre-pass that
  injects `\_implicit_<name>_<i> ->` for each leading `=>` on a
  top-level (or `given`) binding, polymorphic constraints emitted
  inside the body now resolve against the function's own
  `=>`-parameter instead of failing for lack of an ambient instance.
* `peelLeadingImplicits` eagerly emits a `ConstraintGoal` per leading
  `=>` when synthesising a `Var`/`Ref`. The matching dictionary
  insertion is done by `GivenApply.wrapImplicitLeaves`, which
  consults a per-location decision map (so multi-binding files
  elaborate without bindings stealing each other's decisions). The
  resolver's `UnresolvedMetavarInGoal` short-circuit is dropped:
  goal-side inference variables are treated as flexible in
  head-unification.
* `GivenApply.buildDictionary` substitutes a `Term.var` when the
  chosen given is a file-local one (`Local.given.<name>`) — closes
  the runtime gap where the synthetic builtin reference couldn't be
  evaluated.

Codebase plumbing
-----------------
* `ambientGivensFromBranch` now walks sub-branches recursively so
  nested given-tagged refs (e.g. `Monoid.nat` under a `Monoid`
  namespace) are found by the L1 ambient pool.
* `Result.UnresolvedImplicit` is surfaced by the file loader as
  `Output.UnresolvedImplicits` with a rendered diagnostic. Previously
  the file silently failed to load.
* `TypecheckedUnisonFile` carries the parser's `givenBindings` set
  through to `update`/`add`, which auto-marks each `given`-tagged
  binding in namespace metadata — no follow-up `mark.given` needed.

Pretty-printer (ADR-015 elide-mode)
-----------------------------------
* New `Ann.Synthetic` variant flags elaborator-inserted terms in
  memory; `stripSyntheticArgs` removes them before watch-output
  printing.
* For `view`, where annotations have been stripped by codebase
  storage, `stripImplicitArgsByType` derives implicit slots from
  the function's declared type, and `stripLeadingImplicitLambdas`
  removes the parser-injected `\_implicit_*` lambdas from the
  binding term. Result: `view foo` renders exactly as written, but
  `dependencies foo` still lists `Monoid.nat` — the hash of the
  chosen given remains baked into `foo`'s hash.

Tests
-----
* The override-detection test is flipped: a compound-with-widened
  annotation is no longer treated as an `@`-override (it was
  conflicting with list literals and other natural-widening forms;
  the parser-emitted `@`-override path remains gated by the leaf
  predicate).
* The resolver's `UnresolvedMetavarInGoal` test is reworked to
  assert successful resolution against a unique candidate.
* The let-given-shadows-ambient test no longer expects the ambient
  given to appear elsewhere in the decision list — the spurious
  goal at the definition site that produced that decision is gone
  now that `=>I` consumes the binding's leading lambda.
…essors

The class-accessor annotation was using the field type as it appeared in
the raw 'SynDataDecl' (parser output), where external references like
'Text' are still 'Var's. Checking the generated accessor body against
that annotation would fail with

    I don't know about the type Text

Fix: pull each field's type out of the resolved 'DataDeclaration' in
'UF.datas env' instead. 'environmentFor' has already run 'bindNames'
over those constructor types, so every external reference is bound to
its namespace ref. Records have exactly one constructor whose type is
'forall tyvars. T1 -> ... -> Tn -> Self'; the input chain gives the
field types in declaration order, which we zip back against the
parsed field names.
Surface additions:

  * `give f dict args` lowers f's leading `=>` arrows to `->` at the
    use site so the caller supplies dictionaries positionally instead
    of via implicit resolution. Carried through the typechecker as
    an `Ann.Lowered` marker on the head and demoted in synthesis via
    `lowerLeadingImplicit`.

  * `class T tyvars = { f1 : T1, ... }` parses identically to a
    record-style data declaration but the parser also records the
    type name in a side channel; `add` / `update` then marks the
    type's namespace metadata with the `##Builtin.Class` sentinel.
    `view` walks the sentinel, emits the `class` keyword, and renders
    the constructor with `{ field : type, ... }` syntax. Hash
    invariance is preserved: re-parsing the rendered output produces
    the same hash.

Notable pieces:

  * `Unison.Codebase.Classes` (new module) mirrors `Givens` for the
    `types_` namespace Star.
  * `DeclPrinter.IsClassRef` predicate + `hashClassFieldAccessors` /
    `getClassFieldNames` discover class accessors by reconstructing
    the `=>`-bearing accessor type and looking up its hash.
  * `Output.Typechecked` carries a `Set Name` of class bindings so
    the slurp summary renders `+ class T` rather than `+ type T`.
  * `GivenApply.rewriteApply`'s `lowerAll` now descends through
    `Forall` quantifiers; previously a head with type
    `forall a. C a => …` fell through unchanged and `interleave`
    popped a sibling binding's locally-bound dictionary off the FIFO
    queue, leaking it as a free var and crashing `hashTermComponents`.

  * `prettyUnisonFile` consumes the file's `classBindings` so the
    dependents-update re-render path keeps the `class` keyword.
Catches up `unison-src/transcripts/idempotent/` files that drifted
from their `.output.md` rendering since the implicits prototype
landed. The drift is the disappearing transient region message
("Okay, I'm searching the branch…") and the `-- given` comment
scaffold being replaced by the now-emitted `given` keyword.

Verified idempotent on a second pass.
The implicits prototype accumulated a lot of in-source pointers
to design ADRs, phase/chunk tracking, and historical implementation
state. None of it is useful to a reviewer reading the code today;
all of it is now removed.

  * Deleted `docs/architecture/decisions/` (ADR-001 through ADR-023,
    README, TEMPLATE) and the planning docs `docs/implicits-plan.md`,
    `docs/implicits-phase-2-chunks.md`.
  * Stripped `ADR-NNN`, `chunk Xn`, `Phase-2 chunk`, and `chunks.md`
    pointers from comments throughout `parser-typechecker`,
    `unison-cli`, `unison-syntax`, `unison-core`, `unison-runtime`,
    `codebase2`, and the test suite. Substantive technical claims
    that the pointer was attached to are kept; pointer-only comments
    are dropped.
  * Removed implementation-diary commentary ("previously", "this
    used to", "before X landed", "spike port", "carry-over from
    chunk N").
@runarorama runarorama force-pushed the prototype/implicits-rfc branch from 93a648d to fbe07a3 Compare May 25, 2026 20:32
runarorama and others added 8 commits May 25, 2026 19:21
  * `class` and `give` are reserved keywords for parsing source code,
    but a definition stored in the namespace may still have a name
    that collides with a keyword (e.g. an `@unison/base` term named
    `class`). `Name.parseTextEither` / `HashQualified.parseText` now
    fall back to escaping reserved-word segments with backticks
    before retrying, so namespace deserialization succeeds for these
    names.

  * `unison-src/transcripts/idempotent/pattern-match-coverage.md`
    defined an ability constructor `give : a -> {Give a} Unit`; that
    name is now reserved, so renamed to `giv` throughout the file.

  * `unison-syntax/test/Unison/Test/Unison.hs`: `summon` is no longer
    a reserved keyword (it's a regular builtin term reference), so
    the lexer test expectations are updated to expect a wordy
    identifier instead of `Reserved \"summon\"`.

  * Re-ran all idempotent transcripts via the `transcripts` binary,
    which writes corrected output in-place. Several transcripts had
    drifted from their current rendering and are now caught up.
A function declared with an implicit parameter — e.g.
`mpower : Monoid a => Nat -> a -> a` — could not call itself
recursively: typechecking would report "no matching given for Monoid a"
at the recursive call site even though the surrounding binding's own
`=>` slot was in scope.

Four interacting fixes were required.

1. `Unison.Typechecker.Context.subtype` had no clauses for
   `ImplicitArrow'`, so any subsumption involving a `=>` arrow fell
   through to the catchall `TypeMismatch`. Three clauses now handle
   contravariant input + covariant output (parallel to the existing
   `Arrow'` clause) plus the two "drop a leading implicit on either
   side" cases for subtyping `(C => T) <: T`.

2. `GivenResolver.matchHead`'s flex set previously contained only the
   candidate's freshened type variables plus goal-side `Var.Inference`
   metavars. A lexical given like `_implicit_ident_0 : Box a` —
   registered by `=>I` inside the surrounding binding — has `a` free
   in its conclusion referring to an outer universal, but when the
   recursive call site emits its goal, surrounding inference may
   resolve that universal through a *different* freshening, so the
   two `a`s have distinct ids even though they refer to the same
   logical variable. Adding the candidate's outer-scope free vars to
   flex lets the unifier bind them to whatever the goal exposes.
   Ambient givens are unaffected: their universally quantified
   tyvars are already in `fresh`.

3. Goal-side typechecker existentials weren't reaching the resolver.
   `Context.InfoNote.ConstraintGoal` carries a `TypeVar`-wrapped goal
   type, but `TypeVar.lowerType` strips the `Existential`/`Universal`
   distinction before the resolver sees it. `extractConstraintGoals`
   now collects the existential vars *before* lowering and threads
   them through `PendingGoal.pgExistentials` to a new
   `resolveWithExistentials` entry point, which adds them to
   `matchHead`'s flex set so the unifier can bind them.

4. When `wrapImplicitParams` injects `\_implicit_<name>_0 -> body`
   for a binding with an explicit signature, the term reaching
   `checkWanted` is `Ann (Lam _ ...) (forall .. => ..)`. After
   `checkScoped` strips the `Forall`, the type is `ImplicitArrow'`
   but the term is still `Ann`-wrapped, so the `Lam' +
   ImplicitArrow'` `=>I` clause does not fire and the goal is emitted
   by the apply-shaped fallback with an empty lexical scope — the
   lambda binder is never registered as a local given. A new narrow
   clause strips the redundant annotation specifically for
   `Ann (Lam _ ...) _` against `ImplicitArrow' _ _`, letting `=>I`
   fire and register the binder.
The previous commit added the candidate's outer-scope free type
variables to the unifier's flex set in 'matchHead', motivated by the
self-recursive '=>' case where the lexical given's universal had a
different fresh id than the goal's universal. That change is too
permissive: in a multi-constraint binding like
'Monoid.tuple : forall a b. (Monoid a, Monoid b) => Monoid (a,b)',
both '_implicit_Monoid.tuple_0 : Monoid a' and
'_implicit_Monoid.tuple_1 : Monoid b' end up matching any 'Monoid X'
goal, so resolving the binding's body's 'Semigroup'-derived 'Monoid'
premise becomes ambiguous.

The other three pieces of the previous commit — the subtype clauses
for 'ImplicitArrow'', the 'pgExistentials' threading, and the
'Ann' (Lam _ ...)' strip in 'checkWanted' — turn out to be enough on
their own. The Ann'-strip in particular eliminates the double-ForallI
that was causing the goal's universal id to drift away from the
lexical given's, so structural unification now succeeds in the
self-recursive case without the resolver having to be made
permissive.

Both the self-recursive case ('mpower : Monoid a => Nat -> a -> a')
and the multi-constraint algebra library typecheck cleanly with this
revert.
@Baccata

Baccata commented Jun 4, 2026

Copy link
Copy Markdown

The ambiguity errors need a bit of work : once several given instances of the same type are added to the codebase, they are referred to by hash instead of by name in the candidate list :

  I found multiple given candidates for Monoid Nat ; the choice is ambiguous.
  
      3 | main = do foldMap id [1, 2, 3, 4]
  
  
  Candidates:
    - #6lli3jv4to6spb4fh3fisr0olmoihuobg6i369s7drjj0d4emavdf8p6jlhacrf8mdotaak7lq97f3jp8dkdujbj0h5v3mnc4li7gq8
    - #eb24ogtms6mk9gldc3l1f37nrcc7l0lb3qicfjeoshmd8372huqd9m9bcoguc28pd4aad93hmj29vu9pffg7eqj71mg4modgddu88a8

Additionally, if I have a given instance in my codebase and an identical given instance in my scratch file, the elaborator seems to be unable to disambiguate the two :

-- this one was already added to the codebase
given Monoid.nat : Monoid Nat = Monoid (+) 0

main : 'Nat
main = do foldMap id [1, 2, 3, 4]
  I found multiple given candidates for Monoid Nat ; the choice is ambiguous.
  
      4 | main = do foldMap id [1, 2, 3, 4]
  
  
  Candidates:
  
    - ##Local.given.Monoid.nat
    - #eb24ogtms6mk9gldc3l1f37nrcc7l0lb3qicfjeoshmd8372huqd9m9bcoguc28pd4aad93hmj29vu9pffg7eqj71mg4modgddu88a8

runarorama and others added 5 commits June 4, 2026 08:39
Two bugs visible to users of the implicits feature:

1. 'view' on a 'given' binding printed two stanzas — a signature
   line ('given name : T') followed by a separate binding line
   ('name = body') — which is invalid surface syntax. The parser's
   'givenBindingBody' accepts only the single-declaration form
   'given name : T = body', so the printed output failed to
   re-parse with "I was surprised to find an end of stanza here".

   'Unison.Syntax.TermPrinter.prettyGivenBinding' now produces the
   single-stanza form. It strips any leading synthetic
   '\\_implicit_*' lambdas (inserted by 'wrapImplicitParams' for
   bindings whose declared type contains '=>' arrows) before
   rendering the body, so what comes out matches what the user
   originally typed.

2. Hovering on a function call inside a 'given' binding's body
   showed the type of the parser-injected '_implicit_<base>_<i>'
   binder instead of the function being hovered. Those binders
   wrap the entire body and so intersect every cursor position in
   the body, shadowing real in-scope variables in the LSP's
   per-position interval map.

   'Unison.LSP.FileAnalysis' now skips 'VarBinding' / 'VarMention'
   notes for variables whose name begins with '_implicit_', and
   'Unison.LSP.Hover' refuses to surface them even if they're
   reached via 'nodeAtPositionMatching'. The hover for the actual
   function being called is restored.
Two related rough edges in given resolution that were surfaced by
real use:

1. The 'Ambiguous' diagnostic rendered each candidate as the raw
   hash of its reference rather than the surface name. The
   resolver carries 'Reference' values that don't have a name on
   their own; pass them through 'PPE.termName' so users see
   'Monoid.nat' (or its hash-prefixed fallback) instead of the
   bare unsuffixifiable hash.

2. When a scratch file redefined a 'given' that already existed
   in the codebase — i.e. the user was effectively running 'update'
   on an instance in place — both definitions ended up in the
   resolver's candidate pool and the typechecker reported an
   ambiguous resolution. A file-level definition should always
   shadow the codebase definition of the same name during the
   typecheck, just as ordinary term names do.

   'AmbientGiven' now carries the candidate's surface 'Name' (when
   the caller can produce one). 'ambientGivensFromBranch' tracks
   the namespace path while walking and records the full name for
   each given. 'computeTypecheckingEnvironment' filters the
   ambient pool to drop any entry whose name matches a top-level
   'given' binding in the scratch 'UnisonFile' before threading
   the pool to the typechecker.
@Baccata Baccata mentioned this pull request Jun 5, 2026
3 tasks
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.

2 participants