types: alpha-rename imported schemes into the consumer's id space - #27
Merged
Conversation
Every module's inference allocates variable ids from its own counter starting at RESERVED_VARS, so a scheme transplanted from a dependency arrives holding ids the importing module will hand out again. `instantiate` survives that on its own, but `env_free_vars` applies the *local* substitution to every env scheme -- so one id collision silently rewrote the imported scheme and leaked local variables into the "free in the environment" set, which blocked generalization of the importing module's own bindings. Those then exported un-quantified, and consumers got one shared type variable across every use site: a spurious "expected int, found string" at a call two modules away, appearing and disappearing as unrelated definitions shifted the id arithmetic. Refresh every id once, at the boundary, for imported values and constructors alike, in sorted order so the ids do not depend on HashMap iteration. Fixes #26
Merged
simontreanor
added a commit
that referenced
this pull request
Jul 31, 2026
Two user-visible additions since 0.2.0 -- the `input` prelude builtin
(`string ->{io} string`, #19) and caller-supplied `extern` keyword slots
(`kw = ...`, #21) -- plus five fixes, four of them from the first program
written in Pyfun in anger:
* imported schemes reused the exporting module's type-variable ids, silently
blocking generalization in the importing module and cascading to its
consumers (#26 / #27)
* a recursive function of arity >= 2 could not perform any effect -- the shape
of every loop in a language with no `while` (#24 / #28)
* a module that only matched an imported `Option` emitted `None_` without
importing it (#25 / #29)
* user identifiers collided with Python keywords, builtins the emitter calls,
and modules an `extern` imports (#23 / #30)
* the local-binder module-alias shadowing residual (#18)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #26.
Imported schemes were seeded into the consumer''s env carrying the exporting module''s raw variable ids, while every module allocates its own ids from the same counter (
RESERVED_VARS).instantiatesurvives that on its own — it refreshes the quantified vars before substituting — butenv_free_varsapplies the local substitution to every env scheme, so a single id collision silently rewrote the imported scheme and leaked local variables into the "free in the environment" set.generalizethen refused to quantify them.The consequences compound:
let f a b = (a, b)goes monomorphic, so a second use at another type is rejected;Both were observed in a real project: an exported
refillcame out as(List ''a) -> (List ''a) -> (List ''a, List ''a)with nothing quantified, and the imported scheme whose collision caused it had been rewritten fromint -> List a -> List aintoint -> List ((List ''a) -> List ''a) -> List ((List ''a) -> List ''a).Fix
Infer::refresh_schemealpha-renames an imported scheme into the consumer''s variable space — type, unit, num and effect ids, carryingord_varsacross — applied to imported values and constructors as they are seeded, in sorted order so the ids do not depend onHashMapiteration. Free ids are renamed as well as quantified ones: a scheme should arrive closed, but a free id is exactly the case that must not alias a local variable.Tests
Two regression tests in
tests/project.rs, both verified to fail onmain:an_importing_module_keeps_its_own_functions_polymorphic— the direct case;Maindoes not even use the import.an_under_generalized_export_does_not_cascade_to_a_consumer— the contagious case, three modules deep.cargo testgreen (all suites),cargo clippy --all-targetsclean,cargo fmtapplied.INTERNALS.mdupdated: the old soundness note claimed closed schemes were sufficient, which is where the reasoning went wrong.