GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — feat(typecheck): module-qualified type/effect path resolution (ADR-014, Refs #228) by hyperpolymath · Pull Request #447 · hyperpolymath/affinescript

Conversation

@hyperpolymath

@hyperpolymath hyperpolymath commented May 30, 2026

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 use → UnknownModule
  • qualified effect, no use → UnknownModule (not UnknownEffect)
  • bare TyCon unchanged
  • qualified reserved effect (Net) + use → strips & resolves into the reserved-effect path

Full suite: dune build @test/runtest → 352/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:

  • lowercase-module qualified refs (json.Value) still parse-error — qualified_type_name head requires upper_ident while stdlib has lowercase module json; / option; / prelude;
  • use A.B; registers only B as the qualifier (last segment)
  • use A::{Item} (multi-segment + brace-list) parse-errors
  • bare unknown TyCons silently pass typecheck — pre-existing leniency, much broader than LANG: type/effect grammar has no module-qualified path — Pkg.Type/Pkg.Effect unrepresentable (estate-wide port blocker; ADR-014) #228; would change behaviour for many existing files

Refs

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

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@hyperpolymath
feat(typecheck): resolve module-qualified type/effect paths (ADR-014)
68a54f3
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 metadatastician/standards#124
@hyperpolymath
hyperpolymath enabled auto-merge (squash) May 30, 2026 12:47
@hyperpolymath hyperpolymath mentioned this pull request May 30, 2026
LANG: qualified-path adjacent gaps (lowercase modules, use multi-seg, brace-list) — Refs #228 #448
Closed
@github-actions

github-actions Bot commented May 30, 2026

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

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@hyperpolymath
hyperpolymath merged commit 9b9b321 into main May 30, 2026
26 of 27 checks passed

Uh oh!

There was an error while loading. Please reload this page.

@hyperpolymath
hyperpolymath deleted the feat/qualified-type-effect-resolution-228 branch May 30, 2026 12:51
This was referenced May 30, 2026
affinescript: estate-wide ReScript→AffineScript migration tracker [meta] #406
Open
feat(parser): accept lower_ident head in qualified_type_name (closes #448 item 1) #523
Merged
hyperpolymath added a commit that referenced this pull request Jun 1, 2026
@hyperpolymath
feat(parser): accept lower_ident head in qualified_type_name (closes #…
5ebdba2
…448 item 1) (#523)

## Summary

Item 1 of the qualified-path adjacent gaps surfaced by #448: the
`qualified_type_name` production in `lib/parser.mly:154-161` required
`upper_ident` as the head segment, so qualified type paths whose first
segment was a lowercase stdlib module name (`json`, `option`, `prelude`,
`dict`, `alib`, `collections`, `io`, `string`, `result`, `testing`,
`effects`, `math`) parse-errored at the separator (`.` or `::`).

Reported symptom: `parse error at .` on `x: json.Value` even with `use
json;` in scope.

## Fix

Add a parallel production with `lower_ident` head:

```ocaml
qualified_type_name:
  | head = upper_ident qsep rest = qualified_type_name_rest
    { head ^ "::" ^ rest }
  | head = lower_ident qsep rest = qualified_type_name_rest
    { head ^ "::" ^ rest }
```

`qualified_type_name_rest` stays `upper_ident`-only — the tail segment
is the type/effect name proper (a TyCon), so it must remain uppercase.
Only the *qualifier* segment was wrongly constrained.

Option (b) from #448 (relax the parser) rather than (a) (rename stdlib
modules), per the original triage.

## LR(1) safety

The disambiguation strategy is symmetric to the existing `upper_ident`
path: a bare `lower_ident` in type position reduces to TyVar (existing
rule at line 504); the parser only shifts past the `lower_ident` if the
next token is a `qsep` (`.` or `::`).

Menhir reports the **same conflict counts** as before the change (21 / 1
/ 68 / 7), confirming no new conflicts introduced.

## Tests

Three new cases in `test/test_qualified_paths.ml`:

- `#448(1) lowercase qualifier head parses` — `x: json.Value` no longer
emits a parse error
- `#448(1) lowercase qualifier head + ::` parses — same with the `::`
separator
- `#448(1) fully-lowercase qualified path still rejected` — `x:
json.value` MUST still parse-error because the tail segment is now
reaching `qualified_type_name_rest` which still requires `upper_ident`
(regression guard)

Test results: `dune runtest` → **366 tests passed** (was 363); no
regressions on existing `test_qualified_paths` cases.

## Scope

Closes **#448 item 1** only. Does NOT address:
- Item 2 (`use A.B;` leaf-only qualifier registration)
- Item 3 (`use A::{Item}` multi-segment brace-list parse)
- Item 4 (bare unknown TyCon permissive lookup)

Those remain open under the parent issue.

## Downstream

Unblocks `Pkg.Type` syntax with lowercase stdlib heads — directly helps
hyperpolymath/echidna#117 (Client.res AffineScript-TEA port) which had
`json.Value` / `dict.Dict` references that were stranded behind this
parse error.

Refs #228, refs #447, refs #241.
This was referenced Jun 11, 2026
feat(parser): accept slash-effect-row in type-expression position (closes #547) #550
Merged
ESC-04: ReScript block-module — multi-module-per-file has no clean target (Refs 229) #262
Open
hyperpolymath added a commit that referenced this pull request Jun 12, 2026
@hyperpolymath @claude
feat(parser): accept slash-effect-row in type-expression position (cl…
3e53f1d
…oses #547) (#550)

## Summary

Closes #547. The slash-effect-row form `fn(...) -> T / { E1, E2 }` (and
its single-effect short form `fn(...) -> T / E`) parses today only in
fn-decl signatures (`return_type`, lines 271-282); it parse-errors in
record-field and type-alias positions, forcing every effectful
function-typed record field to fall back to the older `-{E}->` arrow
form.

This is a parser asymmetry, not a language-design question. Surfaced
during the post-#228 idaptik port batch in
[`hyperpolymath/idaptik#153`](https://github.com/hyperpolymath/idaptik/pull/153).

## Surface-grammar diff

```
type_expr_arrow:
  | T -> R                         (already)
  | T -{E}-> R                     (already)
+ | T -> R / { E1, E2, ... }       (this PR)
+ | T -> R / E                     (this PR)

type_expr_primary  (FN form, fn-as-type):
  | fn(A, ...) -> R                (already)
  | fn(A, ...) -{E}-> R            (already)
+ | fn(A, ...) -> R / { E1, ... }  (this PR)
+ | fn(A, ...) -> R / E            (this PR)
```

Both spellings remain accepted. `-{E}->` semantics are unchanged.

## Verification

- `dune build bin/main.exe` — clean
- `dune runtest` — **376/376 PASS** (368 baseline + 8 new)
- **Conflict count unchanged** (21 s/r, 1 r/r — same as pre-PR)
- Headline probes (all `Type checking passed`):

  ```
  pub type AsyncCall = fn(Int) -> Int / { Async };
  pub type H = { call: fn(Int) -> Int / { Async } }
pub type H = { call: fn(Int) -> Int / Async } // single short
  pub type Multi = fn(Int, String) -> Bool / { IO, Async };
pub type R = Int -> Int / { Mut }; // arrow-as-type
  pub type R = fn() -> () / { IO };                        // zero-arg
  ```

## Semantic note (which arrow the effect attaches to)

- In `type_expr_arrow` (no `fn(...)` wrapper), the slash effect attaches
to the **outermost** arrow — the position of the slash itself. This is
the natural reading of `A -> B / { E }`.
- In the FN-form (`fn(A, B) -> R / { E }`), the effect attaches to the
**innermost** arrow (the call site), matching the established prefix-row
`-{E}->` FN-form precedent (lines 555-582). This is what consumers
expect when porting `fn(A, B) -{E}-> R`.

For users who want inner-arrow effect attachment in arrow-as-type
positions, the existing `-{E}->` form continues to express that.

## Refs

- Closes #547
- Refs #228 (sibling parser-asymmetry gap, closed in #241 / #447 / #523)
- Refs #229 (estate-wide ReScript-surface elimination)
- Refs #548 (sibling gap from the same port batch — module-level mutable
bindings, PR #549)
- Refs hyperpolymath/idaptik#153 (the port batch that surfaced this)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: hyperpolymath <[email protected]>
Signed-off-by: Jonathan D.A. Jewell <[email protected]>
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

No reviews

Uh oh!

There was an error while loading. Please reload this page.

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

Uh oh!

There was an error while loading. Please reload this page.

1 participant

@hyperpolymath
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.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.