Skip to content

feat(typecheck): module-qualified type/effect path resolution (ADR-014, Refs #228)#447

Merged
hyperpolymath merged 1 commit into
mainfrom
feat/qualified-type-effect-resolution-228
May 30, 2026
Merged

feat(typecheck): module-qualified type/effect path resolution (ADR-014, Refs #228)#447
hyperpolymath merged 1 commit into
mainfrom
feat/qualified-type-effect-resolution-228

Conversation

@hyperpolymath
Copy link
Copy Markdown
Owner

Summary

Symmetric to #178 (Resolve.lower_qualified_value_paths) at the value-path layer: when a folded Mod::T reaches lower_type_expr / lower_effect_expr, the leading Mod:: qualifier is stripped iff Mod was introduced by use Mod;. Otherwise a clear UnknownModule error is raised — replacing the pre-existing silent typecheck-pass (type position) and misleading declare \effect ...`` hint (effect position).

Closes the resolution half of the #228 work whose parser half landed in #241 (ADR-014).

What changes

  • type_error: UnknownModule of string constructor + formatter (cites ADR-014/LANG: type/effect grammar has no module-qualified path — Pkg.Type/Pkg.Effect unrepresentable (estate-wide port blocker; ADR-014) #228, suggests use M; / use M::{Item};)
  • Module_resolution_error exception caught at check_program boundary (mirrors Effect_validation_error)
  • context.module_quals: populated at check_program entry from prog.prog_imports (ImportSimple only — matches value-path semantics in Resolve.import_qualifiers)
  • strip_module_qualifier helper applied in TyCon / TyApp arms of lower_type_expr and the resolve helper of lower_effect_expr. No AST walk — the lowering chokepoints already visit every type/effect site.

Before / after

Source Before After
pub fn f(x: NoSuchMod.Thing) -> () silently passes typecheck Unknown module 'NoSuchMod' in qualified type/effect reference (ADR-014, #228). Add \use NoSuchMod;` ...`
pub fn f() -{NoSuchMod.IO}-> () Unknown effect 'NoSuchMod::IO'. ... declare it with \effect NoSuchMod::IO;`` same UnknownModule text as above
use Ajv; pub fn f(x: Ajv.Schema) -> () passes (TyCon lenient) passes (strip → bare lenient lookup, unchanged outcome)
pub fn f(x: Int) -> () passes passes (regression guard)

Tests

test/test_qualified_paths.ml — 6 alcotest cases, all green:

  • qualified type + use (both . and ::) → passes
  • qualified type, no useUnknownModule
  • qualified effect, no useUnknownModule (not UnknownEffect)
  • bare TyCon unchanged
  • qualified reserved effect (Net) + use → strips & resolves into the reserved-effect path

Full suite: dune build @test/runtest352/352 OK.

Out of scope (surfaced in #228 design comment)

These are real but separate; I'll file follow-ups if the seam-analyst note isn't already tracked elsewhere:

Refs

Refs #228 (this is the resolution slice; parser slice was #241)
Refs hyperpolymath/standards#124 (estate proof-debt / language-readiness umbrella)

Symmetric to PR #178 (`Resolve.lower_qualified_value_paths`) at the
value-path layer: when a folded `Mod::T` reaches `lower_type_expr` /
`lower_effect_expr`, the leading `Mod::` qualifier is stripped iff
`Mod` was introduced by a `use Mod;` decl in the program, raising a
clear `UnknownModule` error otherwise.

Before: `pub fn f(x: NoSuchMod.Thing) -> ()` passed typecheck silently
(folded `TyCon "NoSuchMod::Thing"` fell through the TyCon lookup as an
abstract `TCon`). After: it fails with `Unknown module 'NoSuchMod' in
qualified type/effect reference (ADR-014, #228). Add `use NoSuchMod;`
to bring the module into scope, …`. The same path now governs effect
position — replacing the pre-existing misleading
`declare `effect NoSuchMod::IO;`` hint with the correct
UnknownModule message.

Surface:
  • `type_error`: new `UnknownModule of string` constructor + formatter
    (cites ADR-014/#228, suggests `use M;` or `use M::{Item};`).
  • new `Module_resolution_error` exception (paralleling
    `Effect_validation_error`); caught at `check_program` boundary.
  • `context`: new `module_quals : (string, unit) Hashtbl.t` populated
    from `prog.prog_imports` at `check_program` entry — `ImportSimple`
    only, matching the value-path semantics in `Resolve.import_qualifiers`.
  • new `strip_module_qualifier` helper called from `TyCon`/`TyApp`
    arms of `lower_type_expr` and the `resolve` helper in
    `lower_effect_expr`. No AST walk; the lowering chokepoints already
    visit every type/effect site.

Tests (`test/test_qualified_paths.ml`, 6 cases, all green):
  • qualified type + `use` (both `.` and `::` separators) → passes
  • qualified type, no `use` → UnknownModule with correct attribution
  • qualified effect, no `use` → UnknownModule (not UnknownEffect)
  • bare TyCon unchanged (regression guard for lenient-unknown
    pre-existing behaviour)
  • qualified reserved effect (`Net`) + `use` → strips & resolves into
    the reserved-effect path (proves strip happens before the
    canonical-effect lookup)

Out of scope for this PR (surfaced in the #228 design comment):
  • lowercase-module qualified refs (`json.Value`) still parse-error
    because `qualified_type_name` head requires `upper_ident`
  • `use A.B;` registers only `B` as the qualifier
  • `use A::{Item}` (multi-segment + brace-list) parse-errors
  • bare unknown TyCons silently pass typecheck (broader, pre-existing)

Build: `dune build @test/runtest` → 352/352 OK.

Refs #228
Refs hyperpolymath/standards#124
@github-actions
Copy link
Copy Markdown

🔍 Hypatia Security Scan

Findings: 82 issues detected

Severity Count
🔴 Critical 4
🟠 High 10
🟡 Medium 68

⚠️ Action Required: Critical security issues found!

View findings
[
  {
    "reason": "Action perpolymath/standards/.github/workflows/governance-reusable.yml@main\n needs attention",
    "type": "unpinned_action",
    "file": "governance.yml",
    "action": "pin_sha",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Action ons/checkout@v6\n    needs attention",
    "type": "unpinned_action",
    "file": "publish-jsr.yml",
    "action": "pin_sha",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Action land/setup-deno@v2\n    needs attention",
    "type": "unpinned_action",
    "file": "publish-jsr.yml",
    "action": "pin_sha",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in affine-vscode-publish.yml",
    "type": "unknown",
    "file": "affine-vscode-publish.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in casket-pages.yml",
    "type": "unknown",
    "file": "casket-pages.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in casket-pages.yml",
    "type": "unknown",
    "file": "casket-pages.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in ci.yml",
    "type": "unknown",
    "file": "ci.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in ci.yml",
    "type": "unknown",
    "file": "ci.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in ci.yml",
    "type": "unknown",
    "file": "ci.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in ci.yml",
    "type": "unknown",
    "file": "ci.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  }
]

Powered by Hypatia Neurosymbolic CI/CD Intelligence

@hyperpolymath hyperpolymath merged commit 9b9b321 into main May 30, 2026
26 of 27 checks passed
@hyperpolymath hyperpolymath deleted the feat/qualified-type-effect-resolution-228 branch May 30, 2026 12:51
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.

1 participant