Skip to content

feat(resolve): INT-01 — wire qualified-value path use Mod; Mod.fn(x) - #253

Merged
hyperpolymath merged 1 commit into
mainfrom
int01-qualified-value
May 19, 2026
Merged

feat(resolve): INT-01 — wire qualified-value path use Mod; Mod.fn(x)#253
hyperpolymath merged 1 commit into
mainfrom
int01-qualified-value

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

INT-01 — qualified-value path use Mod; Mod.fn(x) wired

Refs #178 (not Closes — a distinct ::-in-expression parser gap
remains; owner-gated). The follow-up recorded after #244 (#244 proved the
emission substrate; this closes the resolver gap).

Root cause

use Mod; (ImportSimple) flat-imports Mod's public symbols
(import_resolved_symbols) but binds no module namespace. The parser
yields ExprField(ExprVar Mod, fn) (ExprSpan-wrapped); resolve.ml's
ExprField case drops the field and resolves the base
ExprVar ModUndefinedVariable. So Mod.fn could never resolve even
though fn was in scope flat.

Fix

A pure, idempotent, total parse-boundary lowering
Resolve.lower_qualified_value_paths : program -> program:
ExprField(ExprVar m, fld)ExprVar fld when m is an ImportSimple
qualifier (alias preferred; ImportList/ImportGlob bind no qualifier).
Span-peels the base; genuine record access r.f untouched. The
value-expression analogue of #241/ADR-014. Applied once in
parse_with_face → resolve/typecheck/borrow/quantity/codegen all see the
lowered form; the formatter (Formatter.format_file, separate path) is
unaffected so fmt preserves source Mod.fn.

Verification

Case Result
use CrossCallee; CrossCallee.consume(42) ✅ Type checking passed
use CrossCallee as CC; CC.consume(7) ✅ Type checking passed
use CrossCallee::{consume}; consume(42) ✅ still passes (no regression)
genuine p.x record access ✅ preserved (not rewritten)
Full gate 275/275 (was 271; +4 hermetic tests + 2 fixtures)

New suite "E2E Qualified Value #178" (4 hermetic tests) locks it.

Honest scope

The .-qualified value path (the recorded gap) is closed.
Mod::fn(x) in expression position is still a parse error — :: is
reserved for Type::Variant there; that is a distinct parser gap, not
the resolver, deliberately not folded in. Tracked as the remaining
INT-01 follow-up.

🤖 Generated with Claude Code

…(Refs #178)

The INT-01 follow-up recorded after #244: `use Mod;` + qualified *value*
call `Mod.fn(x)` failed with Resolve.UndefinedVariable on the qualifier.

Root cause: `use Mod;` (ImportSimple) flat-imports Mod's public symbols
(import_resolved_symbols) but binds no module namespace; the parser yields
ExprField(ExprVar Mod, fn) (ExprSpan-wrapped), and resolve.ml's ExprField
case drops the field and resolves the base ExprVar Mod -> undefined. So
`Mod.fn` could never resolve even though `fn` was in scope flat.

Fix: a pure, idempotent, total parse-boundary lowering
`Resolve.lower_qualified_value_paths : program -> program` that rewrites
`ExprField(ExprVar m, fld)` -> `ExprVar fld` when `m` is an ImportSimple
qualifier (alias preferred; ImportList/ImportGlob bind no qualifier).
Span-peels the base; genuine record access `r.f` is untouched (`r` is not
an import qualifier). This is the value-expression analogue of #241/ADR-014
(qualified type/effect paths in the grammar). Applied once in
`parse_with_face` so resolve/typecheck/borrow/quantity/codegen all see the
lowered form uniformly; the formatter uses a separate path
(Formatter.format_file) so `fmt` preserves source `Mod.fn`. Embedders that
bypass parse_with_face call the exposed function (as the tests do) —
boundary documented in INT-01.

Verified (oracle): `use CrossCallee; CrossCallee.consume(42)` and
`use CrossCallee as CC; CC.consume(7)` -> Type checking passed;
`use CrossCallee::{consume}; consume(42)` still passes (no regression);
genuine `p.x` preserved. Full gate 275/275 (was 271; +4 hermetic
"E2E Qualified Value #178" regression tests + 2 fixtures).

Scope (honest): the `.`-qualified value path (the recorded gap) is closed.
`Mod::fn(x)` in *expression* position remains a parse error — a DISTINCT
parser gap (`::` is reserved for Type::Variant in expr position), not the
resolver; tracked as a separate follow-up. Refs #178 (not Closes).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@hyperpolymath
hyperpolymath force-pushed the int01-qualified-value branch from ee181e8 to 1d2332f Compare May 19, 2026 17:03
@github-actions

Copy link
Copy Markdown

🔍 Hypatia Security Scan

Findings: 44 issues detected

Severity Count
🔴 Critical 12
🟠 High 21
🟡 Medium 11

⚠️ Action Required: Critical security issues found!

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

@hyperpolymath

Copy link
Copy Markdown
Owner Author

Rebased onto current main: only conflict was the ECOSYSTEM.adoc roadmap table — kept this PR's INT-01 update (qualified-value WIRED+locked) and main's newer INT-02/INT-03 rows (loader #250 landed; ADR-015 #252 accepted), since this branch's INT-02/03 lines predated those merges. Code commits applied untouched. Lone red is the pre-existing ReScript #229 baseline; build+lint+e2e pass; no .idr (not a proof PR). Admin-merging.

@hyperpolymath
hyperpolymath merged commit d187eec into main May 19, 2026
15 of 16 checks passed
@hyperpolymath
hyperpolymath deleted the int01-qualified-value branch May 19, 2026 17:07
hyperpolymath added a commit that referenced this pull request May 19, 2026
`lib/tea_bridge.ml` defines the TEA runtime ABI (exports
affinescript_init / affinescript_update(msg:Linear) / getters /
memory; affinescript.tea_layout + affinescript.ownership custom
sections) but the `affinescript-tea` satellite never existed (the
"first slice scaffolded" claim was aspirational, like dom.as).

New `affinescript-tea/` satellite:
- `parseTeaLayout()` — decodes the `affinescript.tea_layout` section
  (binary fmt matched to Tea_bridge.build_tea_layout_section), so the
  runtime is GENERIC over the model, not hard-coded to the demo
  TitleModel.
- `TeaApp` — load/init/dispatch/model/setScreen/run. Reuses the INT-02
  host-agnostic loader for Deno/Node/browser parity + the ownership
  accessor. `model()` is layout-driven.
- `run({messages, view})` — generic run loop over any (async) iterable
  of i32 msgs; `view(model)` after init and each dispatch.
- Linear-msg invariant enforced host-side: `affinescript_update` called
  exactly once per dispatch, non-re-entrant (a synchronous host-callback
  re-entry throws).

Tests: `affinescript-tea/mod_test.js` — 9 Deno tests driving the
canonical `affinescript tea-bridge` module (layout, ownership=Linear
msg, init/update/setScreen, run loop) + a hand-built importing fixture
that genuinely exercises the re-entrancy guard. All green.

Gate: `dune test --force` 278/278 (no compiler change; zero regression).
INT-01 dep cleared (#253 merged). Router/nav runtime is the separate
INT-09 (`lib/tea_router.ml`), out of scope.

Refs #182. Not Closes — owner closes per ISSUE-CLOSURE.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant