Skip to content

Add gradual-typing elaboration to Laurel resolution#1422

Closed
julesmt wants to merge 8 commits into
reviewed-kbd-will-merge-to-mainfrom
native-types/laurel
Closed

Add gradual-typing elaboration to Laurel resolution#1422
julesmt wants to merge 8 commits into
reviewed-kbd-will-merge-to-mainfrom
native-types/laurel

Conversation

@julesmt

@julesmt julesmt commented Jun 26, 2026

Copy link
Copy Markdown
Member

Adds an optional GradualConfig so resolution keeps native operators on native operands and routes Any operands through prelude operators, inserting box/unbox/toBool casts. Inert without a config; strict typing
unchanged. Foundation for the stacked native typed-Python PR.

julesmt added 5 commits June 25, 2026 12:22
Synth.primitiveOp inlined two match arms -- one for the arithmetic operators,
one for the boolean / comparison / equality / concatenation families. Extract
them into standalone nativeArith and nativeOther helpers (with an isArithmeticOp
predicate and a joinAll LUB fold), and have Synth.primitiveOp resolve its
operands once and dispatch to them.

Pure refactor, no behavior change. Gives the native typing rules a single home
so a later commit can reuse them as the fallback for gradual elaboration.
For datatype constructors, getCallInfo returned an empty parameter-type list, so
constructor arguments were never checked against their declared types. Return
ctor.args.map (.type) so a constructor call is type-checked like any other call.

No behavior change for existing programs (their arguments already matched); this
lets a later commit coerce constructor arguments under gradual typing.
Add an optional GradualConfig (a frontend's dynamic type, its boxing
constructors, checked downcast accessors, per-operator prelude functions, and
which procedures to elaborate). A gradualActive flag, set only inside a
frontend's own procedures, gates a single dispatch in Synth.primitiveOp: when
active, route operands through synthArith / synthOther, which keep a native
operator when the operands are native and switch to the Any-prelude operator
(inserting box / unbox / Any_to_bool coercions at boundaries), falling back to
the native nativeArith / nativeOther rules; when inactive the native rules apply
unchanged. Inert until a frontend supplies a config.
Add gradualConfig to LaurelTranslateOptions and pass it from
LaurelCompilationPipeline into resolve by named argument. Defaults to none, so
pipelines that supply no config are unaffected.
filterPrelude runs before elaboration, so the prelude calls it may insert are
not yet referenced by the user program. Add extraSeeds (retained unconditionally)
and a generic opSeed (Operation -> Option String) so an operator's prelude
function is retained only when that operator appears. Inert without seeds.
@julesmt
julesmt requested a review from a team as a code owner June 26, 2026 18:22
@julesmt
julesmt requested a review from leo-leesco June 26, 2026 18:23

@leo-leesco leo-leesco left a comment

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.

Great PR ! This goes really helps moving forward to lower Dyn type into Laurel
I wrote a few design considerations that we can discuss and asked for some modifications

Comment on lines +144 to +147
/-- The dynamic type whose values are boxed (`TCore "Any"` for Python). -/
dynamic : HighType
/-- Whether a type is the dynamic type (may have several spellings). -/
isDynamic : HighType → Bool

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.

Design consideration : instead of threading this Dyn type in the resolution, can we imagine referencing it throughout the program?
In other words, do we expect Dyn to be source-language dependent ? This looks like a dependency inversion to me, if we were to add a new type to express the Dyn type, we could probably defer translating the source-language Dyn to Laurel's Dyn to the corresponding frontend

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

sure, like a canonical HighType.Dyn constructor in LaurelAST.lean?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

maybe another PR no ?

Comment thread Strata/Languages/Laurel/Resolution.lean Outdated
Comment thread Strata/Languages/Laurel/Resolution.lean Outdated
Comment on lines +758 to +763
/-- LUB of the operand types, or `none` if inconsistent. -/
private def joinAll (ctx : TypeLattice) (argTypes : List HighTypeMd)
(source : Option FileRange) : Option HighTypeMd :=
argTypes.foldl
(fun acc t => match acc with | some l => join ctx l t | none => none)
(some { val := .Unknown, source := source })

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.

just a warning : this is not yet an actual LUB (please update doc) as it does not walk any inheritance tree

join simply erases any Unknown if tried to merge with another static type. Can you look at join and see if it needs updating using your GradualConfig ?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

good catch

Comment thread Strata/Languages/Laurel/Resolution.lean Outdated
Comment on lines +780 to +792
/-- A `MultiValuedExpr` operand (a multi-output call used in value position) is
an internal pseudo-type with no Core lowering, so it must never reach an
operator slot — letting it through crashes a later pass as a `StrataBug`.
Emit the position-oriented diagnostic and return `true` so the caller
short-circuits to the operator's result type, suppressing cascades. -/
private def reportMultiValued (a : StmtExprMd) (aTy : HighTypeMd) : ResolveM Bool := do
match aTy.val with
| .MultiValuedExpr _ =>
let diag := diagnosticFromSource a.source
"multi-output call cannot be used as a value here; it returns multiple values. Unpack it into separate variables first"
modify fun s => { s with errors := s.errors.push diag }
pure true
| _ => pure false

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.

I might be wrong, but I seem to remember the typechecker was already making the same kind of check ; if not we should move this helper function with the rest of the typechecker

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I'll ask kiro to check if you have something similar to extract a helper !

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

at least I didn't find anything close, nor did Kiro, could you look ?

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.

Looks like this is the same function : #1422 (comment)

-- and return `true` so the caller short-circuits to the operator's natural
-- result type, suppressing the per-family check (and its cascading error)
-- on that operand.
let reportMultiValued (a : StmtExprMd) (aTy : HighTypeMd) : ResolveM Bool := do

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.

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.

maybe we can discuss the location of this ? If it makes sense to move it, that's perfectly okay

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

yeah let's discuss it monday

Comment on lines +27 to +29
/-- Substring containment. -/
private def containsSubstr (haystack needle : String) : Bool :=
(haystack.splitOn needle).length > 1

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.

[out of scope of this PR] : considering we have a similar helper function used to match annotations in tests, maybe we could factor both of them out ?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

true

julesmt added 3 commits June 26, 2026 15:02
Addresses PR #1422 review (comment on gradualActive vs gradual.isSome).

The reviewer's instinct was right that storing a derived boolean is a
smell, but gradualActive cannot simply be gradual.isSome: it must also
account for GradualConfig.shouldElaborate, which keeps elaboration out of
hand-written prelude bodies and out of top-level (non-procedure) contexts.

Replace the manually save/restored gradualActive : Bool with a meaningful
currentProc : Option String (none = not inside a procedure body, mirroring
answerType/instanceTypeName), and derive activeGradual as
  gradual.filter (currentProc.elim false shouldElaborate).
Centralize the per-procedure save/restore in a withProc combinator, used by
resolveProcedure and resolveInstanceProcedure. Behavior is unchanged.
Addresses PR #1422 review (join, line ~763).

- Doc: join/joinAll are consistency merges, not a least upper bound (they do
  not walk the inheritance tree), so a none result means "cannot merge
  structurally", not "inconsistent".
- Fix: join only erased .Unknown, but the dynamic type is .TCore (e.g. Any),
  which isConsistent treats as top while join did not. At the if-branch merge
  this made the inferred type order-dependent: 'if c then (e:Any) else (e:int)'
  inferred Any, but the swapped branches inferred int. Add .TCore absorbing
  arms so join agrees with isConsistent (dynamic wins), plus a symmetric-branch
  regression test. The operator path is unaffected (elaborateDynamicOp
  intercepts dynamic operands before join; nativeArith gates on isNumeric).
Addresses PR #1422 review (comment on the OpAlt docstring).

Explain that an OpAlt is one candidate in the priority list firstElab walks
(none = try next, some = commit), and add a concrete a + b walkthrough under
synthArith: Any + Int commits via elaborateDynamicOp, String + String falls
to strConcatAlt, and Int + Int falls through to the native Add. Doc-only.
@julesmt
julesmt requested a review from leo-leesco June 26, 2026 23:28
@julesmt julesmt closed this Jun 27, 2026
@julesmt
julesmt deleted the native-types/laurel branch June 27, 2026 02:48
@julesmt

julesmt commented Jun 27, 2026

Copy link
Copy Markdown
Member Author

Reopened as #1424 with the branch renamed to julesmt/feature/native-types/laurel. This PR was auto-closed by GitHub when the head branch was renamed; the same work continues in #1424.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants