feat: sound module-qualified type & effect paths, dot separator (Refs #228)#231
feat: sound module-qualified type & effect paths, dot separator (Refs #228)#231hyperpolymath wants to merge 2 commits into
Conversation
…tor (Refs #228) The type/effect grammar had no module-qualified path production, so `Externs.Res` / `prelude.Option` / any `Pkg.Type` in type or effect position failed `parse error` at the `.` — contradicting ADR-011 (real modules with qualified paths). This adds full, SOUND module-qualified paths in type and effect position, with `.` as the canonical separator (`::` stays value/import per ADR-011); decision recorded as ADR-014. Resolution is by sound module-scoped member lookup, never flat scope: load module `[A;B]` through the loader, resolve+typecheck it as an import would, then look the member up *inside that module's own resolved symbols*, requiring Public/PubCrate and the right kind (SKType / SKEffect). Unknown/wrong module, missing member, private member, or wrong-kind member is a resolution error at the use-site span — never a parse error and never silently accepted. - lib/ast.ml: `ident` gains `modpath : string list` ([]=unqualified); add `mk_ident` helper so synthetic idents set the field in one place. - lib/parser.mly: `mk_qualified_ident`; `path_seg`/`module_prefix`/ `qualified_type_path` helpers; qualified bare/[args]/<args> forms for types and bare/[args] for effects. Module-prefix segments accept lower OR upper case (stdlib modules are `prelude`/`option` as well as `Network`/`Http`); member segment is upper_ident. - lib/typecheck.ml: `lower_type_expr`/`lower_effect_expr` validate the qualified case via a loader-backed `qualified_member_check` threaded into the context; `Qualified_path_error` -> `QualifiedPathError` at the check_program boundary (same pattern as Effect_validation_error). A validated qualified effect's name is admitted as a declared effect (issue #59) so a sibling module's public effect is usable like a local one. Resolved representation is the canonical nominal type/effect — identical to the bare/imported form; the validation, not the representation, is the soundness gate. Typecheck/codegen otherwise unchanged. - lib/resolve.ml: `make_qualified_member_check` built where the loader lives, inside the `resolve_and_typecheck_module` recursive group so transitively-qualified stdlib paths resolve too. - bin/main.ml: thread the validator into the 6 patterned check_program calls (via the type_ctx returned by resolve_program_with_loader). - Fixed ~10 internal direct `ident` record literals (trait/codegen_gc/ borrow/verilog) to route through `Ast.mk_ident`. - docs/specs/SETTLED-DECISIONS.adoc + .machine_readable/6a2/META.a2ml: ADR-014 (full stanza, same shape as ADR-011/012/013). - tests/conformance/qualified-paths/{valid,invalid}: 5 conformance fixtures wired into the e2e gate (valid qualified type+effect; unknown module; private member; absent member). Parser-conflict delta: ZERO. menhir --explain summary is byte-identical to the pre-change parser — 21 shift/reduce states, 1 reduce/reduce state, 68 s/r + 7 r/r arbitrarily resolved. The `upper_ident` vs `upper_ident DOT ...` decision is the expected benign DOT shift (Menhir shifts, disclosed per ADR-012); no new unexplained conflict. Test gate: 258 -> 263 (5 new #228 cases), zero regressions. Adding `modpath` to `ident` reflows every golden .expected AST dump; regenerated — the span-normalised golden harness confirms no structural change. STAGE-C peer of #225 (typed-wasm Http) and #160 (C-spine). Language decision is human-gated (ISSUE-CLOSURE): Refs #228, not Closes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ture limitation - .machine_readable/6a2/STATE.a2ml: bump last-updated; add session-note-2026-05-18 recording #228/ADR-014/PR#231 (machine state/progress convention was stale). - docs/specs/SETTLED-DECISIONS.adoc (ADR-014): add 'Known limitation' paragraph — qualified-effect positive fixture is harness-only (no public stdlib effect for a self-contained positive); bare-oracle audits must exclude it. - tests/conformance/qualified-paths/valid/qualified_effect.affine: header now states the harness-search-path dependency explicitly. Turns a silent landmine into a stated limitation. No code change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#228 estate-payoff proof (controlled before/after)Method: same Result — #228 works, scope is narrow (honest correction)Seed: 13 repos, ~98 files. Only accessibility-everywhere — controlled before/after (per-package cwd)
Sharpest proof:
What this means for review
|
Independent gate re-confirmation (not relayed from the implementing agent)Both #231 gates re-verified from scratch: Test suite — Menhir conflict delta — standalone Byte-identical on both grammar versions — Net: implementation gates verified independently. #231 residual is now |
🔍 Hypatia Security ScanFindings: 44 issues detected
View findings[
{
"reason": "Stray AI.a2ml in root -- use 0-AI-MANIFEST.a2ml only",
"type": "banned",
"file": "AI.a2ml",
"action": "delete",
"rule_module": "root_hygiene",
"severity": "high"
},
{
"reason": "Superseded by 0-AI-MANIFEST.a2ml",
"type": "banned",
"file": "AI.djot",
"action": "delete",
"rule_module": "root_hygiene",
"severity": "high"
},
{
"reason": "Issue in quality.yml",
"type": "missing_workflow",
"file": "quality.yml",
"action": "create",
"rule_module": "workflow_audit",
"severity": "high"
},
{
"reason": "Issue in security-policy.yml",
"type": "missing_workflow",
"file": "security-policy.yml",
"action": "create",
"rule_module": "workflow_audit",
"severity": "medium"
},
{
"reason": "Action hyperpolymath/standards/.github/workflows/governance-reusable.yml@main needs attention",
"type": "unpinned_action",
"file": "governance.yml",
"action": "pin_sha",
"rule_module": "workflow_audit",
"severity": "high"
},
{
"reason": "TypeScript file detected -- banned language",
"type": "banned_language_file",
"file": "/home/runner/work/affinescript/affinescript/affinescript-deno-test/example/smoke_driver.ts",
"action": "flag",
"rule_module": "cicd_rules",
"severity": "critical"
},
{
"reason": "TypeScript file detected -- banned language",
"type": "banned_language_file",
"file": "/home/runner/work/affinescript/affinescript/affinescript-deno-test/cli.ts",
"action": "flag",
"rule_module": "cicd_rules",
"severity": "critical"
},
{
"reason": "TypeScript file detected -- banned language",
"type": "banned_language_file",
"file": "/home/runner/work/affinescript/affinescript/affinescript-deno-test/mod.ts",
"action": "flag",
"rule_module": "cicd_rules",
"severity": "critical"
},
{
"reason": "TypeScript file detected -- banned language",
"type": "banned_language_file",
"file": "/home/runner/work/affinescript/affinescript/affinescript-deno-test/lib/compile.ts",
"action": "flag",
"rule_module": "cicd_rules",
"severity": "critical"
},
{
"reason": "TypeScript file detected -- banned language",
"type": "banned_language_file",
"file": "/home/runner/work/affinescript/affinescript/affinescript-deno-test/lib/runner.ts",
"action": "flag",
"rule_module": "cicd_rules",
"severity": "critical"
}
]Powered by Hypatia Neurosymbolic CI/CD Intelligence |
Refs #228
STAGE-C peer of #225 (typed-wasm Http) and #160 (C-spine). Language
decision is human-gated per the repo ISSUE-CLOSURE rule, so this is
Refs #228, not Closes — do not auto-merge, do not close the issue.
What
The type/effect grammar had no module-qualified path production:
Externs.Res/prelude.Option/ anyPkg.Typein type or effectposition failed
parse errorat the., contradicting already-settledADR-011 (real modules with qualified paths). This implements full,
sound module-qualified type & effect paths with
.as the canonicalseparator (
::stays value/import per ADR-011). Recorded as ADR-014.Resolution is by sound module-scoped member lookup, never flat
scope: load module
[A;B]through the loader, resolve+typecheck itexactly as an import would, then look the member up inside that
module's own resolved symbols, requiring
Public/PubCrateand theright kind (
SKTypefor types,SKEffectfor effects). An unknown orwrong module, a missing member, a private member, or a wrong-kind
member is a resolution error at the use-site span — never a parse
error, never silently accepted. Importing a member flatly does not make
a bogus
A.B.Cresolve.New grammar productions (verbatim)
type_expr_primarygains qualified bare /[args]/<args>forms(
TyCon/TyApp (mk_qualified_ident ...));effect_termgainsqualified bare /
[args]forms (EffVar/EffCon). Module-prefixsegments accept lower OR upper case because real stdlib modules are
prelude/option/string/resultas well asNetwork/Http.Files changed
lib/ast.mlidentgainsmodpath : string list; addmk_identhelper so synthetic idents set it in one placelib/parser.mlymk_qualified_ident;path_seg/module_prefix/qualified_type_path; qualified type & effect productionslib/typecheck.mllower_type_expr/lower_effect_exprvalidate qualified paths via loader-backedqualified_member_check;Qualified_path_error→QualifiedPathErrorat the check_program boundary; validated qualified effect admitted as declared effect (#59)lib/resolve.mlmake_qualified_member_check(sound module-scoped lookup) inside theresolve_and_typecheck_modulerecursive groupbin/main.mlcheck_programcallslib/{trait,codegen_gc,borrow,verilog_codegen}.mlidentliterals throughAst.mk_identdocs/specs/SETTLED-DECISIONS.adoc,.machine_readable/6a2/META.a2mltest/test_e2e.ml,test/dune,tests/conformance/qualified-paths/**test/golden/*.expectedmodpathreflows every AST dump; span-normalised golden harness confirms no structural changeParser-conflict delta: ZERO
menhir --explainsummary is byte-identical to the pre-changeparser:
The
upper_identvsupper_ident DOT …decision is the expectedbenign DOT shift (Menhir shifts; disclosed per ADR-012). No new
unexplained conflict;
just buildmasks the same count as before.Test gate
dune runtestZero regressions. The 12 golden tests pass with regenerated
.expected(the change is purely the new
modpathfield +ppx_deriving.showreflow; the span-normalising golden harness verifies no structural
change).
Oracle re-audit (build is the new oracle)
28 repos / 1176
.affinefiles. Pre-fix baseline vs post-fix:7 files moved
DRIFT-SYNTAX → TYPE-ONLY(accessibility-everywhere ×5,standards ×2): their only parse blocker was a qualified type/effect
path; they now parse and resolve. Spot-check (before → after):
accessibility-everywhere/tools/cli/src/Index.affine:6:17 parse error→ resolves (nowUndefinedModule, sound)standards/a2ml/.../Json.affine:15:29 parse erroratA2ml.Inline→ resolvesstandards/k9-svc/.../K9_Renderer.affine:17:37 parse erroratK9_Types.Pedigree→ resolvesThe PASS count is unchanged (not the "large jump" the spec hoped for)
because the remaining DRIFT-SYNTAX is unrelated, out-of-#228-scope
syntax —
extern fn … = "console" "warn"FFI string-bindings,pub extern fn, record-construction expression syntax — confirmed bysampling the residual parse errors. And because the audit harness uses
a bare loader without each repo's local module search paths, a
qualified path to a repo's own
Externs/Typesmodule now correctlyreports
UndefinedModule(sound — not silently accepted) rather thanmasquerading as a parse error. This is the correct sound outcome per
the spec ("a wrong/unknown module … is an error").
A/B sanity probes (all hold)
prelude.Option[Int]/prelude.Option<Int>→ Type checking passedOption[Int]→ still Type checking passedFoo.Bar[Int]→ resolution errorUnknown module 'Foo' … (ADR-014)(not parse)effects.IO→Member 'IO' of module 'effects' is private … (ADR-014)(sound visibility)effmod.Logging(apub effect) → Type checking passedprelude.Optionused as effect →'prelude.Option' resolves to a type, not a effect (ADR-014)prelude.DefinitelyNotHere→Module 'prelude' has no member … (ADR-014)Int -{IO}-> Int→ still parses (Type checking passed)Deviations from spec (reality)
directed edits to
Resolve.resolve_type_ident/resolve_effect_ident,but those functions are dead code — actual type/effect name
binding is in
Typecheck.lower_type_expr/lower_effect_expr.typecheck.ml cannot depend on resolve.ml (would be circular), so the
sound module-scoped validator is built in resolve.ml (where the
loader +
resolve_and_typecheck_modulelive) and injected intothe typecheck context via an optional
qualified_member_checkclosure threaded through
check_program. Same soundness, nocircular dependency.
path of
upper_identsegments", but real stdlib modules arelowercase (
prelude,option). The grammar accepts lower-or-upperprefix segments (mirroring
module_path/use), member isupper_ident. Conflict count still zero-delta.
that passes the module-scoped gate is registered in
declared_effectsso issue-[Repo] Effect-row v1 name list: define IO/Async/Partial/Throws[E]/Mut early #59's effect-name validator accepts it(a sibling module's public effect is as legitimate as a local one).
Soundness preserved — only after the gate passes.
qualified ident is treated by its resolved symbol exactly like an
unqualified one.
🤖 Generated with Claude Code
Known limitation (disclosed, not a blocker)
The qualified-type positive conformance fixture is self-contained
(
prelude.Option— a stdlib module). The qualified-effect positivefixture (
tests/conformance/qualified-paths/valid/qualified_effect.affine)references a sibling support module
effmodbecause no stdlib modulecurrently declares a public effect. Consequence:
search-path) but fails a bare
affinescript checkwithUnknown module 'effmod'..affine-auditharness — must treat that one fixture as harness-onlyand exclude it from drift counts. This is now stated in the fixture
header and in ADR-014.
by
invalid/private_member.affine(loads stdlibeffects, rejects theprivate member). The limitation is fixture self-containment, not the
feature.
Also disclosed: the estate-payoff numbers from the re-audit are
understated by the bare-loader audit harness (repo-local modules
report
UndefinedModule). A re-audit with each repo's own module graphresolvable is pending and is the load-bearing review evidence.