finos / finos/morphir-scala

Elm langkit: resolve operator fixity from package dependencies (conformance gap G6)

Open
#930 1 comment 0 reactions 0 assignees View on GitHub
bug
Dominant language
Scala
Stars
17
Forks
28
Avg merge
3h 45m
Merged PRs (30d)
52

Description

The Elm langkit cannot shape an operator chain when the operator's `infix` declaration lives in a package whose source it does not have. It reports `ELM-P005` rather than guessing, which is the right call — a guessed fixity silently mis-groups the expression around it — but it means valid Elm does not parse.

This is the last open row (**G6**) in the module's conformance tracker, [`morphir/langkit/elm/conformance.html`](../blob/main/morphir/langkit/elm/conformance.html). It came out of the work in #929, which closed G1–G5 and G7.

## Why it exists

Elm has no built-in operators. Every one is declared:

```elm
infix left 5 (|=) = keeper
```

That line lives in `elm/parser`'s source. A module that merely *uses* `|=` says nothing about its precedence or associativity, so the fixity has to come from somewhere else.

Three layers cover it today, in order:

1. `OperatorTable.wellKnown` — bundled fixities for the official packages that declare operators: `elm/core` (`Basics`, `List`), `elm/parser`, `elm/url`.
2. `ElmProject.parse` — resolves across a project. If the declaring module is one of the sources handed in, its `infix` declarations are read and used.
3. `ElmParseOptions.operators` — the caller supplies a table.

An operator outside all three is unresolvable, and that is the gap.

## Reproduction

Any operator from a third-party package. `(|:)` from `elm-community/json-extra` is a real instance; the shape is:

```elm
module Use exposing (..)

import Json.Decode.Extra exposing ((|:))

decoder =
succeed Person
|: field "name" string
|: field "age" int
```

Actual — `ELM-P005`:

```
I do not know the precedence or associativity of (|:).

Without them I cannot tell how to group the expression around it.

Hint: Declare it with an `infix` declaration in this module, or supply its fixity through ElmParseOptions.
```

Expected: parses, grouped by the fixity `Json.Decode.Extra` declares.

A self-contained version, with no third-party dependency, is in `OperatorPrecedenceSpec`:

```scala
// "an operator nothing declares is rejected"
Elm.parseCst("module M exposing (..)\n\nmain = a <%> b") // ELM-P005
```

Both work today if the fixity is supplied by hand:

```scala
Elm.parseCst(source, ElmParseOptions.elm.withOperators(table))
```

## Why it is not a parser fix

Closing this means resolving imports to *package* sources: reading `elm.json`, locating the dependency in `~/.elm//packages/...`, parsing its modules for `infix` declarations, and caching the result. That is package-manager work sitting above the parser, and the parser is the wrong place for it — it takes one module's text and has no business knowing where packages live on disk.

`ElmProject` is the seam this plugs into. It already does the two-pass shape — collect every `infix` declaration, then parse each module against a table built from the modules it imports — with the table fed from the sources the caller passed. Dependency resolution feeds the same table from somewhere else.

## Acceptance criteria

- A resolver that, given an Elm project directory, reads `elm.json` and produces an `OperatorTable` from the `infix` declarations of its direct and indirect package dependencies.
- `ElmProject` accepts that table, so a module using `(|:)` parses with the fixity `elm-community/json-extra` declares, and groups accordingly.
- The package cache location is configurable and the resolver degrades honestly: a dependency whose source is absent leaves its operators unresolved and reports which package was wanted, rather than failing the whole project or silently guessing.
- Resolution is cached per package and version rather than re-parsing dependency sources for every module.
- Tests assert **tree shape**, not parse success — a wrong fixity produces a tree that parses and groups incorrectly, which a success assertion cannot catch. `ElmProjectSpec` has the pattern.
- A real-world case in the corpus: a package with its own operators added to `ElmPackages.corpus` and parsed through the resolver, in `RealWorldCorpusTest`.
- `ELM-P005` still fires for an operator that genuinely cannot be resolved, and the message says which package was expected to declare it where that is known.
- The G6 row is removed from `morphir/langkit/elm/conformance.html` in the same change that closes it, per the ledger rule in the module's `CONTRIBUTING.md`.

## References

- Tracker: `morphir/langkit/elm/conformance.html`
- Fixity resolution: `OperatorTable`, `OperatorReassociator`, `ElmProject`
- Diagnostic: `ParseDiagnostic.unknownOperator`, `DiagnosticCode.UnknownOperator`
- Upstream: `elm/compiler` `Canonicalize/Expression.hs` resolves binops after imports are canonicalised, which is the same ordering this asks for.

Contributor guide

Open the contributing guide

Research direction

Start with OperatorTable, ElmProject, and the existing ElmProjectSpec pattern for tree-shape assertions. Read how ElmProject collects infix declarations and how ElmParseOptions supplies operators, then inspect the conformance tracker and ElmPackages.corpus entry points. Done means package dependencies provide cached fixities, missing sources remain honest, real-world parsing asserts grouping, and the G6 row is removed.

Written by the indexing model from the issue text.

Assessment

Tech stack
elm, scala
Domain
compilers
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.