diff --git a/packages/grafema-orchestrator/src/main.rs b/packages/grafema-orchestrator/src/main.rs index da1cb9582..9ccb7bc3f 100644 --- a/packages/grafema-orchestrator/src/main.rs +++ b/packages/grafema-orchestrator/src/main.rs @@ -123,12 +123,20 @@ const STDLIB_RULE_PACKS: &[&str] = &[ // CLASS metadata stamps — consumes analyzer EDB only, precedes the // negators below. "@stdlib/kotlin_inheritance", - // Haskell wave: haskell_local_calls PRODUCES CALLS (same-file flat - // (file,name) value-binding resolution, single-target via the FUNCTION > - // VARIABLE > CONSTANT > CONSTRUCTOR > RECORD_FIELD namespace-priority ladder) - // — strictly before the CALLS negators method_calls / shape_verifier - // (shape_verifier negates edge(C,_,"CALLS") with NO file gate). Consumes - // analyzer EDB only, so no inter-pack EDB seam. + // Haskell same-file wave (haskell-resolve migration): haskell_local_refs_nodes + // MINTS the HASKELL_GLOBAL:: EXTERNAL_FUNCTION prelude endpoints that + // haskell_local_refs joins as committed EDB (strict nodes→edges order — the + // js_runtime_globals two-pack split). haskell_local_refs PRODUCES READS_FROM + // (same-file REFERENCE→binder, nearest-binder scope-walk + single-target flat + // fallback). Native haskell-local-refs resolve step RETIRED in favour of these. + "@stdlib/haskell_local_refs_nodes", + "@stdlib/haskell_local_refs", + // haskell_local_calls PRODUCES CALLS (same-file flat (file,name) value-binding + // resolution, single-target via the FUNCTION > VARIABLE > CONSTANT > + // CONSTRUCTOR > RECORD_FIELD namespace-priority ladder) — strictly before the + // CALLS negators method_calls / shape_verifier (shape_verifier negates + // edge(C,_,"CALLS") with NO file gate). Consumes analyzer EDB only, so no + // inter-pack EDB seam. "@stdlib/haskell_local_calls", // Go wave: go_imports / go_imports_nomod PRODUCE IMPORTS_FROM — strictly // before depends (verdict C4). The nomod entry is the orchestrator-selected @@ -604,6 +612,8 @@ fn pack_owned_slice(pack: &str) -> &'static str { "@stdlib/java_calls" => "java INSTANTIATES + CALLS (ctor/same-class/static/super-this)", "@stdlib/java_annotations" => "java ANNOTATION_RESOLVES_TO", "@stdlib/kotlin_inheritance" => "kotlin EXTENDS + IMPLEMENTS", + "@stdlib/haskell_local_refs_nodes" => "haskell prelude EXTERNAL_FUNCTION nodes (HASKELL_GLOBAL::)", + "@stdlib/haskell_local_refs" => "haskell same-file READS_FROM (REFERENCE→binder)", "@stdlib/go_imports" => "go same-module IMPORT→MODULE IMPORTS_FROM", "@stdlib/go_imports_nomod" => "go no-go.mod suffix-fallback IMPORTS_FROM", "@stdlib/go_calls" => "go CALLS (package-qualified / same-package / method)", @@ -1615,7 +1625,14 @@ async fn main() -> Result<()> { let results = plugin::stream_and_resolve_single_worker( &mut rfdb, &[config::Language::Haskell], - &[("haskell-imports", &[]), ("haskell-local-refs", &[]), ("haskell-cross-module-calls", &[]), ("haskell-globals", &[])], + // haskell-local-refs / haskell-local-calls RETIRED: + // same-file READS_FROM/CALLS resolution now runs + // in-engine via the @stdlib/haskell_local_refs* + + // haskell_local_calls .dl packs (rfdb-server + // derive/stdlib). Dropping them here avoids + // double-emit. The three KEPT arms are cross-file / + // stdlib and the packs do NOT replace them. + &[("haskell-imports", &[]), ("haskell-cross-module-calls", &[]), ("haskell-globals", &[])], &hs_pool, ).await?; for (cmd, mut output) in results { @@ -2584,7 +2601,14 @@ async fn main() -> Result<()> { let results = plugin::stream_and_resolve_single_worker( &mut rfdb, &[config::Language::Haskell], - &[("haskell-imports", &[]), ("haskell-local-refs", &[]), ("haskell-cross-module-calls", &[]), ("haskell-globals", &[])], + // haskell-local-refs / haskell-local-calls RETIRED: + // same-file READS_FROM/CALLS resolution now runs + // in-engine via the @stdlib/haskell_local_refs* + + // haskell_local_calls .dl packs (rfdb-server + // derive/stdlib). Dropping them here avoids + // double-emit. The three KEPT arms are cross-file / + // stdlib and the packs do NOT replace them. + &[("haskell-imports", &[]), ("haskell-cross-module-calls", &[]), ("haskell-globals", &[])], &pool, ).await?; for (cmd, mut output) in results { diff --git a/packages/haskell-resolve/haskell-resolve.cabal b/packages/haskell-resolve/haskell-resolve.cabal index 390299d3c..5f297f074 100644 --- a/packages/haskell-resolve/haskell-resolve.cabal +++ b/packages/haskell-resolve/haskell-resolve.cabal @@ -9,9 +9,11 @@ executable haskell-resolve hs-source-dirs: src ghc-options: -Wall -Wno-name-shadowing + -- HaskellLocalRefs / HaskellLocalCalls RETIRED — same-file READS_FROM/CALLS + -- resolution now runs in-engine via the @stdlib/haskell_local_refs* + + -- haskell_local_calls .dl packs (rfdb-server derive/stdlib). other-modules: HaskellImportResolution - HaskellLocalRefs HaskellCrossModuleCalls build-depends: diff --git a/packages/haskell-resolve/src/HaskellLocalRefs.hs b/packages/haskell-resolve/src/HaskellLocalRefs.hs deleted file mode 100644 index e341b1e0d..000000000 --- a/packages/haskell-resolve/src/HaskellLocalRefs.hs +++ /dev/null @@ -1,177 +0,0 @@ -{-# LANGUAGE OverloadedStrings #-} --- | Haskell local reference resolution plugin. --- --- Creates READS_FROM edges for REFERENCE nodes that refer to same-file --- declarations (FUNCTION, VARIABLE, CONSTANT, DATA_TYPE, TYPE_SYNONYM, --- CONSTRUCTOR, RECORD_FIELD, PARAMETER). --- --- Also resolves standard Haskell names (pure, Just, Nothing, return, show, etc.) --- by creating virtual HASKELL_GLOBAL nodes. --- --- Skip logic: --- - Imported names (IMPORT_BINDING) — handled by HaskellImportResolution -module HaskellLocalRefs (run, resolveAll) where - -import Grafema.Types (GraphNode(..), GraphEdge(..), MetaValue(..)) -import Grafema.Protocol (PluginCommand(..), readNodesFromStdin, writeCommandsToStdout) - -import Data.Text (Text) -import qualified Data.Text as T -import qualified Data.Map.Strict as Map -import Data.Map.Strict (Map) -import qualified Data.Set as Set -import Data.Set (Set) - --- --------------------------------------------------------------------------- --- Types --- --------------------------------------------------------------------------- - --- | Declaration index: (file, name) -> node ID -type DeclIndex = Map (Text, Text) Text - --- | Import binding index: (file, localName) -type ImportIndex = Set (Text, Text) - --- --------------------------------------------------------------------------- --- Index construction --- --------------------------------------------------------------------------- - --- | Haskell node types that can be referenced as declarations. -declTypes :: [Text] -declTypes = - [ "FUNCTION", "VARIABLE", "CONSTANT", "DATA_TYPE" - , "TYPE_SYNONYM", "CONSTRUCTOR", "RECORD_FIELD" - , "PARAMETER" - ] - --- | Build declaration index. -buildDeclIndex :: [GraphNode] -> DeclIndex -buildDeclIndex nodes = - Map.fromList - [ ((gnFile n, gnName n), gnId n) - | n <- nodes - , gnType n `elem` declTypes - , not (T.null (gnName n)) - ] - --- | Build import binding index to skip imported names. -buildImportIndex :: [GraphNode] -> ImportIndex -buildImportIndex nodes = - Set.fromList - [ (gnFile n, gnName n) - | n <- nodes - , gnType n == "IMPORT_BINDING" - ] - --- | Standard Haskell names (Prelude, base library). --- These are always in scope without explicit import. -haskellPreludeNames :: Set Text -haskellPreludeNames = Set.fromList - [ -- Data constructors - "True", "False", "Just", "Nothing", "Left", "Right" - , "LT", "EQ", "GT", "IO" - -- Common functions - , "pure", "return", "show", "read", "print", "putStrLn", "putStr" - , "getLine", "getContents", "interact" - , "map", "filter", "foldl", "foldr", "foldl'", "concatMap", "zip" - , "head", "tail", "init", "last", "length", "null", "reverse" - , "take", "drop", "takeWhile", "dropWhile", "span", "break" - , "elem", "notElem", "lookup", "concat", "replicate" - , "any", "all", "and", "or", "sum", "product", "maximum", "minimum" - , "id", "const", "flip", "not", "otherwise", "undefined", "error" - , "fst", "snd", "curry", "uncurry" - , "maybe", "either", "fromMaybe", "isJust", "isNothing" - , "mapM", "mapM_", "forM", "forM_", "sequence", "sequence_" - , "when", "unless", "guard", "void" - -- Type classes - , "Eq", "Ord", "Show", "Read", "Enum", "Bounded", "Num", "Integral" - , "Fractional", "Floating", "Real", "RealFrac", "RealFloat" - , "Functor", "Applicative", "Monad", "MonadIO", "Monoid", "Semigroup" - , "Foldable", "Traversable" - -- Types - , "Int", "Integer", "Float", "Double", "Bool", "Char", "String" - , "Maybe", "Either", "Ordering" - -- Operators as functions - , "succ", "pred", "toEnum", "fromEnum" - , "compare", "max", "min" - , "negate", "abs", "signum", "fromInteger" - , "div", "mod", "quot", "rem" - ] - --- --------------------------------------------------------------------------- --- Resolution --- --------------------------------------------------------------------------- - --- | Core resolution logic. -resolveAll :: [GraphNode] -> [PluginCommand] -resolveAll nodes = - let declIndex = buildDeclIndex nodes - importIndex = buildImportIndex nodes - refNodes = filter (\n -> gnType n == "REFERENCE") nodes - (cmds, _seen) = foldl (resolveRef declIndex importIndex) - ([], Set.empty :: Set Text) refNodes - in cmds - --- | Resolve a single REFERENCE node. --- Returns (accumulated commands, seen global names). -resolveRef :: DeclIndex -> ImportIndex -> ([PluginCommand], Set Text) -> GraphNode -> ([PluginCommand], Set Text) -resolveRef declIndex importIndex (acc, seen) refNode = - let file = gnFile refNode - name = gnName refNode - in - -- Skip imported names (handled by HaskellImportResolution) - if Set.member (file, name) importIndex - then (acc, seen) - -- Try same-file declaration lookup - else case Map.lookup (file, name) declIndex of - Just targetId -> - ( EmitEdge GraphEdge - { geSource = gnId refNode - , geTarget = targetId - , geType = "READS_FROM" - , geMetadata = Map.singleton "resolvedVia" (MetaText "haskell-local-refs") - } : acc - , seen) - Nothing -> - -- Check if it's a known Prelude/base name - if Set.member name haskellPreludeNames - then - let globalId = "HASKELL_GLOBAL::" <> name - edge = EmitEdge GraphEdge - { geSource = gnId refNode - , geTarget = globalId - , geType = "READS_FROM" - , geMetadata = Map.fromList - [ ("resolvedVia", MetaText "haskell-local-refs") - , ("globalCategory", MetaText "haskell-prelude") - ] - } - in if Set.member name seen - then (edge : acc, seen) - else - let virtualNode = EmitNode GraphNode - { gnId = globalId - , gnType = "EXTERNAL_FUNCTION" - , gnName = name - , gnFile = "" - , gnExported = False - , gnLine = 0 - , gnColumn = 0 - , gnEndLine = 0 - , gnEndColumn = 0 - , gnMetadata = Map.fromList - [ ("category", MetaText "haskell-prelude") - , ("source", MetaText "haskell-local-refs") - ] - } - in (edge : virtualNode : acc, Set.insert name seen) - else (acc, seen) - --- --------------------------------------------------------------------------- --- CLI entry point --- --------------------------------------------------------------------------- - -run :: IO () -run = do - nodes <- readNodesFromStdin - writeCommandsToStdout (resolveAll nodes) diff --git a/packages/rfdb-server/src/derive/stdlib.rs b/packages/rfdb-server/src/derive/stdlib.rs index 3c4c83812..b3dd8db33 100644 --- a/packages/rfdb-server/src/derive/stdlib.rs +++ b/packages/rfdb-server/src/derive/stdlib.rs @@ -380,6 +380,33 @@ pub const KOTLIN_INHERITANCE_DL: &str = include_str!("stdlib/kotlin_inheritance. /// numbered in the `.dl` header. pub const HASKELL_LOCAL_CALLS_DL: &str = include_str!("stdlib/haskell_local_calls.dl"); +/// Haskell same-file refs (haskell-resolve migration). NODE half: mints the +/// virtual `HASKELL_GLOBAL::` EXTERNAL_FUNCTION prelude endpoints +/// (category="haskell-prelude") that `haskell_local_refs` joins as committed +/// EDB for its prelude fall-through (the js_runtime_globals two-pack split — +/// a @materialize edge endpoint must be a committed node-ID column). Byte- +/// identical sids to HaskellLocalRefs.hs:139,152-157 (dedup against the legacy +/// step + hybrid graphs). `mode = "exclusive"` is PROVENANCE-SCOPED. Negation + +/// node minting ⇒ scratch floor under maintain. +pub const HASKELL_LOCAL_REFS_NODES_DL: &str = include_str!("stdlib/haskell_local_refs_nodes.dl"); + +/// Haskell same-file refs (haskell-resolve migration). EDGE half: REFERENCE → +/// READS_FROM, `resolvedVia = "haskell-local-refs"` meta — replaces +/// HaskellLocalRefs.resolveAll. Fixes the legacy flat last-write-wins +/// (file,name) Map (Hs:48-55) cross-scope collapse: a same-scope PARAMETER/ +/// VARIABLE local hit (the analyzer CONTAINS-parents these at the ref's own +/// scope id — the dominant bug) suppresses the single-target flat (file,name) +/// fallback over the value decl types (FUNCTION > VARIABLE > CONSTANT > +/// CONSTRUCTOR > RECORD_FIELD) then the TYPE namespace last (DATA_TYPE > +/// TYPE_SYNONYM); PARAMETER is INTENTIONALLY ABSENT from the flat arm (a flat +/// PARAMETER target is a foreign scope's param — the misroute bug as fan-out). +/// The flat arm in turn suppresses the prelude global fall-through. Local +/// FUNCTIONs are MODULE-flattened by the analyzer (Declarations.hs:48-49 etc.) +/// so the flat arm is the only recoverable resolution for them — no-worse than +/// legacy. CONSUMER of the nodes pack's minted endpoints (must run strictly +/// after). `READS_FROM` additive. Negation ⇒ scratch floor. +pub const HASKELL_LOCAL_REFS_DL: &str = include_str!("stdlib/haskell_local_refs.dl"); + /// The JS module-path kernel pack (Wave 3b, path/string-kit-unblocked): the /// in-engine replacement for the module-level arms of `ImportResolution.hs` — /// IMPORT → MODULE `IMPORTS_FROM` (`resolveModuleImports`) and star re-export @@ -653,7 +680,14 @@ pub const STDLIB_PACKS: &[(&str, &str)] = &[ // EXTENDS-closed member lookup — strictly before the negators; consumes // analyzer EDB only (CLASS metadata stamps), so no run-after seam. ("kotlin_inheritance", KOTLIN_INHERITANCE_DL), - // Haskell wave: haskell_local_calls PRODUCES CALLS (same-file flat (file,name) + // Haskell same-file refs wave (haskell-resolve migration): haskell_local_refs_nodes + // MINTS the HASKELL_GLOBAL:: EXTERNAL_FUNCTION prelude endpoints that + // haskell_local_refs joins as committed EDB (strict nodes→edges order — the + // js_runtime_globals two-pack split). haskell_local_refs PRODUCES READS_FROM + // (REFERENCE→binder, nearest-binder scope-walk + single-target flat fallback). + ("haskell_local_refs_nodes", HASKELL_LOCAL_REFS_NODES_DL), + ("haskell_local_refs", HASKELL_LOCAL_REFS_DL), + // haskell_local_calls PRODUCES CALLS (same-file flat (file,name) // value-binding resolution, single-target via namespace priority) — strictly // before the CALLS negators method_calls / shape_verifier (shape_verifier // negates edge(C,_,"CALLS") with NO file gate). Consumes analyzer EDB only @@ -1108,6 +1142,312 @@ mod tests { ); } + // ── Haskell same-file wave (haskell-resolve migration) ───────────────── + + /// haskell_local_refs.dl — the scope-walk READS_FROM resolver, on the + /// CONTAINS-only Haskell shape (REFERENCE / PARAMETER / VARIABLE all + /// CONTAINS'd by the same enclosing-FUNCTION scope id; FUNCTION binders + /// CONTAINS'd by MODULE). Pins all four required behaviours: + /// (a) NEAREST-binder shadowing: a ref in fnA whose name is a PARAMETER of + /// fnA resolves to THAT param, not to a same-named module-level decl; + /// (b) THE CROSS-SCOPE-COLLAPSE BUG, FIXED: two functions each with a local + /// VARIABLE `tmp` — the ref in each resolves to its OWN local, NOT the + /// last-emitted one (the legacy flat last-write-wins Map collapsed + /// these); + /// (c) top-level / forward / mutually-recursive ref resolves via the flat + /// fallback over the decl types (FUNCTION), no regression; + /// (d) an imported name (IMPORT_BINDING in the same file) is NOT + /// mis-resolved to a same-named local — local emission suppressed. + /// Plus: the local-hit suppresses the flat fallback (mutual exclusion); a + /// FUNCTION-only name with no local binder still resolves flat; the .hs gate. + #[test] + fn haskell_local_refs_scope_walk_fixes_cross_scope_collapse() { + let mut v = FixtureStorageView::new(1); + + // ── Two enclosing FUNCTION scopes in app.hs, both MODULE-contained ── + named_node(&mut v, "m_app", "App", "MODULE", "app.hs"); + named_node(&mut v, "fnA", "fnA", "FUNCTION", "app.hs"); + named_node(&mut v, "fnB", "fnB", "FUNCTION", "app.hs"); + edge(&mut v, "m_app", "fnA", "CONTAINS"); // FUNCTION binders are MODULE-contained + edge(&mut v, "m_app", "fnB", "CONTAINS"); + + // (b) THE BUG: each fn has its OWN local VARIABLE `tmp`; a ref to `tmp` + // in each fn must resolve to its own — NOT a single flat winner. + named_node(&mut v, "tmpA", "tmp", "VARIABLE", "app.hs"); + edge(&mut v, "fnA", "tmpA", "CONTAINS"); // binder at fnA's scope id + named_node(&mut v, "r_tmpA", "tmp", "REFERENCE", "app.hs"); + edge(&mut v, "fnA", "r_tmpA", "CONTAINS"); // ref at the SAME scope id + named_node(&mut v, "tmpB", "tmp", "VARIABLE", "app.hs"); + edge(&mut v, "fnB", "tmpB", "CONTAINS"); + named_node(&mut v, "r_tmpB", "tmp", "REFERENCE", "app.hs"); + edge(&mut v, "fnB", "r_tmpB", "CONTAINS"); + + // (a) NEAREST: `cap` is a PARAMETER of fnA AND a module-level CONSTANT; + // a ref in fnA resolves to the PARAMETER (the same-scope local hit + // suppresses the flat module-level fallback). + named_node(&mut v, "p_cap", "cap", "PARAMETER", "app.hs"); + edge(&mut v, "fnA", "p_cap", "CONTAINS"); + named_node(&mut v, "c_cap_mod", "cap", "CONSTANT", "app.hs"); // module-level decl + named_node(&mut v, "r_cap", "cap", "REFERENCE", "app.hs"); + edge(&mut v, "fnA", "r_cap", "CONTAINS"); + + // (c) FLAT FALLBACK: mutual recursion / forward ref — `helper` is a + // top-level FUNCTION (MODULE-flattened), no same-scope binder, so a + // ref inside fnB resolves to it via the flat decl fallback. + named_node(&mut v, "helper", "helper", "FUNCTION", "app.hs"); + edge(&mut v, "m_app", "helper", "CONTAINS"); + named_node(&mut v, "r_helper", "helper", "REFERENCE", "app.hs"); + edge(&mut v, "fnB", "r_helper", "CONTAINS"); + + // (d) IMPORT SKIP: `fromList` has an IMPORT_BINDING in this file AND a + // same-named local VARIABLE; the ref must NOT resolve locally. + named_node(&mut v, "b_fromList", "fromList", "IMPORT_BINDING", "app.hs"); + named_node(&mut v, "v_fromList", "fromList", "VARIABLE", "app.hs"); + edge(&mut v, "fnA", "v_fromList", "CONTAINS"); + named_node(&mut v, "r_fromList", "fromList", "REFERENCE", "app.hs"); + edge(&mut v, "fnA", "r_fromList", "CONTAINS"); + + // Negative: a non-.hs ref (gate) with a same-scope binder. + named_node(&mut v, "m_rs", "Rs", "MODULE", "main.rs"); + named_node(&mut v, "v_rs", "rv", "VARIABLE", "main.rs"); + edge(&mut v, "m_rs", "v_rs", "CONTAINS"); + named_node(&mut v, "r_rs", "rv", "REFERENCE", "main.rs"); + edge(&mut v, "m_rs", "r_rs", "CONTAINS"); + + let (eval, specs, _node_specs) = evaluate_with_materialize( + &v, + HASKELL_LOCAL_REFS_DL, + Stats::default(), + EvalLimits::none(), + EventLog::discard(), + ) + .expect("haskell_local_refs.dl evaluates"); + + let edge = |r: &str, d: &str| (id_of(r), id_of(d), "haskell-local-refs".to_string()); + let mut expected: BTreeSet<(u128, u128, String)> = BTreeSet::new(); + // (b) each `tmp` ref → its OWN local (the cross-scope-collapse fix). + expected.insert(edge("r_tmpA", "tmpA")); + expected.insert(edge("r_tmpB", "tmpB")); + // (a) `cap` ref in fnA → the PARAMETER (nearest), NOT the module CONSTANT. + expected.insert(edge("r_cap", "p_cap")); + // (c) `helper` ref → the top-level FUNCTION via flat fallback. + expected.insert(edge("r_helper", "helper")); + + assert_eq!( + triples(&eval, "hs_reads_from"), + expected, + "scope-walk: each local `tmp` to its own binder (collapse FIXED); \ + same-scope PARAMETER `cap` shadows the module CONSTANT; top-level \ + `helper` via flat fallback; imported `fromList` + .rs derive nothing" + ); + + // (a) explicit: the module-level CONSTANT `cap` is NOT derived. + assert!( + !triples(&eval, "hs_reads_from").contains(&edge("r_cap", "c_cap_mod")), + "same-scope PARAMETER must shadow the module-level CONSTANT" + ); + // (b) explicit: NO cross-scope edge (the bug). + assert!( + !triples(&eval, "hs_reads_from").contains(&edge("r_tmpA", "tmpB")) + && !triples(&eval, "hs_reads_from").contains(&edge("r_tmpB", "tmpA")), + "cross-scope collapse must be gone: fnA's tmp ref never reaches fnB's tmp" + ); + // (d) explicit: the imported name resolves to NEITHER local. + assert!( + !triples(&eval, "hs_reads_from").contains(&edge("r_fromList", "v_fromList")), + "an IMPORT_BINDING name must not resolve to a same-named local" + ); + + // READS_FROM shared vocabulary — additive, resolvedVia projected. + let rf: Vec<_> = specs.iter().filter(|s| s.edge_type == "READS_FROM").collect(); + assert!(!rf.is_empty(), "at least one READS_FROM head"); + assert!(rf.iter().all(|s| s.additive), "READS_FROM is shared — additive"); + } + + /// haskell_local_refs SINGLE-TARGET DISCIPLINE — the v1 ×4.8 fan-out guard. + /// The retired flat native resolver was a Map.fromList LAST-WRITE-WINS: ONE + /// target per source. The flat fallback must match that cardinality + /// (RETARGET, not ADD). This pins the two precision corrections: + /// (1) NO PARAMETER in the flat fallback — a same-name PARAMETER in a + /// FOREIGN scope must NOT be a flat target (that is the misroute bug as + /// fan-out); the only sound PARAMETER hit is the same-scope local_hit; + /// (2) a name with binders in MANY namespaces (FUNCTION + CONSTANT + + /// CONSTRUCTOR + DATA_TYPE) emits exactly ONE edge, to the highest + /// priority (FUNCTION) — the namespace-priority ladder; + /// (3) TYPE namespace kept at LOWEST priority (lost_real≈0): a value ref to + /// a name whose ONLY binder is a DATA_TYPE still resolves to it. + #[test] + fn haskell_local_refs_flat_fallback_is_single_target() { + let mut v = FixtureStorageView::new(1); + named_node(&mut v, "m_app", "App", "MODULE", "app.hs"); + named_node(&mut v, "fnA", "fnA", "FUNCTION", "app.hs"); + edge(&mut v, "m_app", "fnA", "CONTAINS"); + + // `node` has FOUR same-file binders across namespaces + a FOREIGN-scope + // PARAMETER. A top-level ref (MODULE scope, no same-scope binder) must + // emit EXACTLY ONE flat edge — to the FUNCTION (highest priority) — and + // NEVER to the CONSTANT / CONSTRUCTOR / DATA_TYPE / foreign PARAMETER. + named_node(&mut v, "f_node", "node", "FUNCTION", "app.hs"); + edge(&mut v, "m_app", "f_node", "CONTAINS"); + named_node(&mut v, "c_node", "node", "CONSTANT", "app.hs"); + named_node(&mut v, "ctr_node", "node", "CONSTRUCTOR", "app.hs"); + named_node(&mut v, "dt_node", "node", "DATA_TYPE", "app.hs"); + // A foreign-scope PARAMETER `node` (bound inside fnA) — the misroute trap. + named_node(&mut v, "p_node", "node", "PARAMETER", "app.hs"); + edge(&mut v, "fnA", "p_node", "CONTAINS"); + // The ref is at MODULE scope (top-level), NOT fnA — so NO same-scope hit. + named_node(&mut v, "r_node", "node", "REFERENCE", "app.hs"); + edge(&mut v, "m_app", "r_node", "CONTAINS"); + + // `onlyType` has a single binder, a DATA_TYPE — the TYPE-namespace + // lowest-priority arm must still resolve it (lost_real≈0). + named_node(&mut v, "dt_only", "onlyType", "DATA_TYPE", "app.hs"); + named_node(&mut v, "r_only", "onlyType", "REFERENCE", "app.hs"); + edge(&mut v, "m_app", "r_only", "CONTAINS"); + + let (eval, _specs, _ns) = evaluate_with_materialize( + &v, + HASKELL_LOCAL_REFS_DL, + Stats::default(), + EvalLimits::none(), + EventLog::discard(), + ) + .expect("haskell_local_refs.dl evaluates (single-target)"); + + let got = triples(&eval, "hs_reads_from"); + let edge = |r: &str, d: &str| (id_of(r), id_of(d), "haskell-local-refs".to_string()); + + // Exactly one edge for `node`, to the FUNCTION — and the ONLY `node` + // edge in the whole result (no fan-out to the other 4 same-name binders). + let node_edges: Vec<_> = got.iter().filter(|(s, _, _)| *s == id_of("r_node")).collect(); + assert_eq!( + node_edges.len(), + 1, + "single-target: a ref to a 5-binder name emits ONE flat edge, not a fan-out" + ); + assert!(got.contains(&edge("r_node", "f_node")), "the one edge targets the FUNCTION"); + assert!(!got.contains(&edge("r_node", "c_node")), "no edge to the CONSTANT"); + assert!(!got.contains(&edge("r_node", "ctr_node")), "no edge to the CONSTRUCTOR"); + assert!(!got.contains(&edge("r_node", "dt_node")), "no edge to the DATA_TYPE"); + assert!( + !got.contains(&edge("r_node", "p_node")), + "NO flat edge to a foreign-scope PARAMETER (the misroute bug)" + ); + // The pure-type ref still resolves (TYPE namespace, lowest priority). + assert!( + got.contains(&edge("r_only", "dt_only")), + "a value ref whose only binder is a DATA_TYPE still resolves (lost_real≈0)" + ); + } + + /// haskell_local_refs prelude fall-through: a REFERENCE with NO same-file + /// binder and NO import binding, whose name is a Prelude name, resolves to + /// the virtual HASKELL_GLOBAL endpoint (minted by haskell_local_refs_nodes, + /// committed as EDB here) with meta globalCategory="haskell-prelude". A + /// Prelude-named ref that HAS a local binder takes the local edge instead + /// (mutual exclusion). Also pins the nodes-pack minting + sids. + #[test] + fn haskell_local_refs_prelude_fall_through() { + // ── EDGES PACK: the prelude endpoint committed as EDB (file="") ── + let mut v = FixtureStorageView::new(1); + named_node(&mut v, "m_app", "App", "MODULE", "app.hs"); + // `mapM_` is a Prelude name with no same-file binder → prelude global. + named_node(&mut v, "HASKELL_GLOBAL::mapM_", "mapM_", "EXTERNAL_FUNCTION", ""); + named_node(&mut v, "r_mapM", "mapM_", "REFERENCE", "app.hs"); + edge(&mut v, "m_app", "r_mapM", "CONTAINS"); + // `show` is a Prelude name BUT has a local FUNCTION binder → local wins. + named_node(&mut v, "HASKELL_GLOBAL::show", "show", "EXTERNAL_FUNCTION", ""); + named_node(&mut v, "f_show", "show", "FUNCTION", "app.hs"); + edge(&mut v, "m_app", "f_show", "CONTAINS"); + named_node(&mut v, "r_show", "show", "REFERENCE", "app.hs"); + edge(&mut v, "m_app", "r_show", "CONTAINS"); + + let (eval, specs, _ns) = evaluate_with_materialize( + &v, + HASKELL_LOCAL_REFS_DL, + Stats::default(), + EvalLimits::none(), + EventLog::discard(), + ) + .expect("haskell_local_refs.dl evaluates (prelude)"); + + let glob: BTreeSet<(u128, u128, String, String)> = eval + .facts("hs_reads_global") + .into_iter() + .map(|r| (r[0].as_id().unwrap(), r[1].as_id().unwrap(), r[2].as_str(), r[3].as_str())) + .collect(); + assert_eq!( + glob, + BTreeSet::from([( + id_of("r_mapM"), + id_of("HASKELL_GLOBAL::mapM_"), + "haskell-local-refs".to_string(), + "haskell-prelude".to_string(), + )]), + "only the binder-less Prelude ref `mapM_` falls through; `show` \ + has a local FUNCTION binder so it resolves locally, not as global" + ); + // `show` resolved locally (flat fallback), NOT as a prelude global. + assert!( + triples(&eval, "hs_reads_from") + .contains(&(id_of("r_show"), id_of("f_show"), "haskell-local-refs".to_string())), + "a Prelude-named ref with a local FUNCTION binder resolves locally" + ); + // The prelude head carries resolvedVia + globalCategory. + let g: Vec<_> = specs + .iter() + .filter(|s| s.edge_type == "READS_FROM" && s.meta.len() == 2) + .collect(); + assert_eq!(g.len(), 1, "one prelude READS_FROM head with two meta cols"); + assert_eq!( + g[0].meta, + vec!["resolvedVia".to_string(), "globalCategory".to_string()] + ); + + // ── NODES PACK: mint the HASKELL_GLOBAL endpoint for the binder-less + // Prelude ref, byte-identical sid; the local-bound + imported names + // mint nothing. ── + let mut vn = FixtureStorageView::new(1); + named_node(&mut vn, "m_app", "App", "MODULE", "app.hs"); + named_node(&mut vn, "r_mapM", "mapM_", "REFERENCE", "app.hs"); + edge(&mut vn, "m_app", "r_mapM", "CONTAINS"); + // local-bound Prelude name → no mint. + named_node(&mut vn, "f_show", "show", "FUNCTION", "app.hs"); + named_node(&mut vn, "r_show", "show", "REFERENCE", "app.hs"); + edge(&mut vn, "m_app", "r_show", "CONTAINS"); + // imported Prelude name → no mint. + named_node(&mut vn, "b_when", "when", "IMPORT_BINDING", "app.hs"); + named_node(&mut vn, "r_when", "when", "REFERENCE", "app.hs"); + edge(&mut vn, "m_app", "r_when", "CONTAINS"); + // non-Prelude unresolved name → no mint. + named_node(&mut vn, "r_widget", "renderWidget", "REFERENCE", "app.hs"); + edge(&mut vn, "m_app", "r_widget", "CONTAINS"); + + let (evn, _s, node_specs) = evaluate_with_materialize( + &vn, + HASKELL_LOCAL_REFS_NODES_DL, + Stats::default(), + EvalLimits::none(), + EventLog::discard(), + ) + .expect("haskell_local_refs_nodes.dl evaluates"); + + let minted: BTreeSet<(String, String)> = evn + .facts("hs_global_node") + .into_iter() + .map(|r| (r[0].as_str(), r[1].as_str())) + .collect(); + assert_eq!( + minted, + BTreeSet::from([("HASKELL_GLOBAL::mapM_".to_string(), "mapM_".to_string())]), + "only the binder-less, non-imported Prelude name is minted; \ + byte-identical sid HASKELL_GLOBAL::" + ); + assert_eq!(node_specs.len(), 1, "one @materialize_node head"); + assert_eq!(node_specs[0].node_type, "EXTERNAL_FUNCTION"); + assert!(!node_specs[0].additive, "prelude mint is exclusive (provenance-scoped)"); + } + /// An edge plus a metadata blob attached to it (for `edge_attr` probes — /// PASSES_ARGUMENT `index` etc.). fn edge_meta(v: &mut FixtureStorageView, src: &str, dst: &str, ty: &str, meta: &str) { @@ -1651,6 +1991,8 @@ mod tests { "java_calls", "java_annotations", "kotlin_inheritance", + "haskell_local_refs_nodes", + "haskell_local_refs", "haskell_local_calls", "go_imports", "go_imports_nomod", diff --git a/packages/rfdb-server/src/derive/stdlib/haskell_local_refs.dl b/packages/rfdb-server/src/derive/stdlib/haskell_local_refs.dl new file mode 100644 index 000000000..e1ba8fc42 --- /dev/null +++ b/packages/rfdb-server/src/derive/stdlib/haskell_local_refs.dl @@ -0,0 +1,181 @@ +% haskell_local_refs.dl — EDGE half of the in-engine replacement for the legacy +% `haskell-local-refs` resolve step (HaskellLocalRefs.hs). Emits READS_FROM from +% a REFERENCE to its same-file binder, stamped meta resolvedVia="haskell-local- +% refs". MUST run AFTER haskell_local_refs_nodes.dl, which mints the +% "HASKELL_GLOBAL::" EXTERNAL_FUNCTION endpoints this pack's prelude arm +% joins as committed EDB (the js_runtime_globals two-pack split). +% +% ── THE BUG THIS FIXES (the 91% param/let/where-VAR cross-scope collapse) ───── +% The legacy resolver built a FLAT (file, name) -> id Map with LAST-WRITE-WINS +% (HaskellLocalRefs.hs:48-55). For a name with multiple same-file binders (the +% common case: a parameter `x` in fn A and a parameter `x` in fn B), every +% reference to that name resolved to whichever binder the Map happened to keep +% last — a cross-scope false edge for the majority of references. +% +% ── THE CONTAINS-ONLY SCOPE SHAPE (design ground truth — NOT a faithful lexical +% chain; build the walk around THIS) ───────────────────────────────────── +% The Haskell analyzer does NOT thread scopeParent (Walker.hs:99, Expressions.hs: +% 605 set scopeParent=Nothing); enclosure is carried ONLY by CONTAINS edges, +% emitted ASYMMETRICALLY: +% (a) a REFERENCE is CONTAINS'd from askScopeId = the nearest enclosing +% FUNCTION id, or the MODULE id at top level (Expressions.hs:128-132, +% Context.hs:53-54). +% (b) a PARAMETER / VARIABLE binder is ALSO CONTAINS'd from askScopeId +% (Patterns.hs:51/69,80/98,118/136) — so it sits at the SAME scope id as +% the references in that scope. THIS is what makes the precise local hit +% possible and is exactly the binder class the flat Map collapsed. +% (c) EVERY FUNCTION binder — top-level AND local where/let — is CONTAINS'd +% from askModuleId, NOT the enclosing scope (Declarations.hs:48-49,66, +% 81-82,101). So local helper FUNCTIONs are FLATTENED under MODULE, +% indistinguishable from true top-level FUNCTIONs. A multi-level lexical +% scope (where inside where) is therefore NOT recoverable from CONTAINS — +% that residual ambiguity is a PRE-EXISTING analyzer limitation, no-worse +% than the legacy flat Map (which had zero scope at all). +% +% ── THE SOUND DESIGN (exactly this, no multi-hop ancestor climb) ───────────── +% A REFERENCE with enclosing scope S (the node that CONTAINS it) resolves to: +% LOCAL HIT — a PARAMETER or VARIABLE binder CONTAINS'd by that SAME scope S +% with the matching name (fixes (b), the dominant bug); else +% FALLBACK — the flat (file, name) match over the decl/callable types +% (FUNCTION/VARIABLE/CONSTANT/DATA_TYPE/TYPE_SYNONYM/CONSTRUCTOR/ +% RECORD_FIELD/PARAMETER). This preserves top-level resolution +% (mutual recursion, forward refs) AND handles local helper +% FUNCTIONs (MODULE-flattened, so flat is the only option and is +% no-worse than legacy); else +% PRELUDE — the virtual "HASKELL_GLOBAL::" node for a known prelude +% name (HaskellLocalRefs.hs:135-167), joined as committed EDB. +% The three arms are MUTUALLY EXCLUSIVE (local hit suppresses fallback; fallback +% suppresses prelude) to avoid double-resolution — the negation ladder below. +% +% READS_FROM is SHARED vocabulary ⇒ mode = "additive" is mandatory. +% resolvedVia = "haskell-local-refs" projected as a meta column (byte-identical +% to the legacy edge meta, Hs:132,145) so downstream is unchanged. +% +% MAINTAIN ENVELOPE: negation ⇒ scratch floor (accepted). + +% ── The Haskell REFERENCE gate ─────────────────────────────────────────────── +hs_ref(R, F, N) :- node(R, "REFERENCE"), attr(R, "file", F), ends_with(F, ".hs"), attr(R, "name", N), neq(N, ""). + +% Import skip-set (Hs:58-64,123-124): a name with an IMPORT_BINDING in the same +% file is never resolved locally — handled by HaskellImportResolution. +hs_imported(F, N) :- node(B, "IMPORT_BINDING"), attr(B, "file", F), attr(B, "name", N). + +% A reference eligible for local resolution: Haskell, not import-shadowed. +% Leads with hs_ref (the small, bound set) so the scope join does not enumerate +% every CONTAINS edge (planner-filter-before-generator). +local_ref(R, F, N) :- hs_ref(R, F, N), \+ hs_imported(F, N). + +% ── The reference's OWN enclosing scope (the single node that CONTAINS it: an +% enclosing FUNCTION, or the MODULE at top level). No climb — the analyzer +% flattens local FUNCTIONs to MODULE, so a multi-hop ancestor walk would be +% UNSOUND (it cannot tell a sibling where-helper from a parent). ────────── +ref_scope(R, S) :- local_ref(R, F, N), edge(S, R, "CONTAINS"). + +% ── LOCAL HIT — a PARAMETER / VARIABLE binder CONTAINS'd by the ref's OWN scope +% S with the matching name. attr(D,"name",N) is a CHECK (N pre-bound from +% local_ref); ref_scope binds S first. These are the binder types that carry +% the correct scope id (design (b)) — the ones the flat Map collapsed. ──── +local_hit(R, D) :- local_ref(R, F, N), ref_scope(R, S), + edge(S, D, "CONTAINS"), node(D, "PARAMETER"), attr(D, "name", N). +local_hit(R, D) :- local_ref(R, F, N), ref_scope(R, S), + edge(S, D, "CONTAINS"), node(D, "VARIABLE"), attr(D, "name", N). + +% A reference that found ANY local same-scope binder — suppresses the fallback. +has_local_hit(R) :- local_hit(R, _). + +% ── FALLBACK — flat (file, name), only when NO same-scope param/var binder was +% found. This is the top-level / local-helper-FUNCTION arm (those are +% MODULE-flattened, so flat is the only recoverable resolution — no-worse +% than the legacy Map). +% +% ⚠ PRECISION (the v1 ×4.8 SPURIOUS-edge blow-up — do NOT re-introduce): +% the legacy resolver was a Map.fromList LAST-WRITE-WINS = EXACTLY ONE target +% per source. The fallback MUST match that cardinality (RETARGET, not ADD). +% Two corrections vs a naive multi-type union: +% (1) NO PARAMETER in the flat fallback. Falling through to a same-name +% PARAMETER routes a ref to a FOREIGN scope's parameter — the exact +% misroute bug re-introduced as fan-out. Flat targets are TOP-LEVEL value +% bindings only; the legitimate PARAMETER hit is the SAME-SCOPE local_hit +% arm above, never the flat arm. +% (2) SINGLE TARGET via a namespace-PRIORITY ladder (the gold +% js_same_file_calls idiom: each lower arm fires only when no +% higher-priority candidate exists for the source). Priority, VALUE first: +% FUNCTION > VARIABLE > CONSTANT > CONSTRUCTOR > RECORD_FIELD, then the +% TYPE namespace LAST: DATA_TYPE > TYPE_SYNONYM. A (file,name) with +% candidates in several namespaces emits ONE edge to the highest-priority +% namespace — collapsing the dominant cross-namespace fan-out to legacy +% cardinality (RETARGET, not ADD). +% (3) The TYPE namespace is kept at LOWEST priority (not dropped): a value +% REFERENCE to a type name (e.g. `Int` in a signature position is emitted +% as a REFERENCE) still resolves to its DATA_TYPE/TYPE_SYNONYM when no +% VALUE binder of the name exists — so the legacy declType set (which +% included DATA_TYPE/TYPE_SYNONYM, Hs:40-45) loses no real edge +% (lost_real≈0), while a value binder always wins when present so no +% spurious type edge is added (false_added≈0). +flat_fn(F, N, D) :- node(D, "FUNCTION"), attr(D, "file", F), attr(D, "name", N), neq(N, ""). +flat_var(F, N, D) :- node(D, "VARIABLE"), attr(D, "file", F), attr(D, "name", N), neq(N, ""). +flat_con(F, N, D) :- node(D, "CONSTANT"), attr(D, "file", F), attr(D, "name", N), neq(N, ""). +flat_ctr(F, N, D) :- node(D, "CONSTRUCTOR"), attr(D, "file", F), attr(D, "name", N), neq(N, ""). +flat_rec(F, N, D) :- node(D, "RECORD_FIELD"), attr(D, "file", F), attr(D, "name", N), neq(N, ""). +flat_dt(F, N, D) :- node(D, "DATA_TYPE"), attr(D, "file", F), attr(D, "name", N), neq(N, ""). +flat_ts(F, N, D) :- node(D, "TYPE_SYNONYM"), attr(D, "file", F), attr(D, "name", N), neq(N, ""). + +% Per-namespace presence gates (stratified above the negating fallback arms). +has_flat_fn(F, N) :- flat_fn(F, N, _). +has_flat_var(F, N) :- flat_var(F, N, _). +has_flat_con(F, N) :- flat_con(F, N, _). +has_flat_ctr(F, N) :- flat_ctr(F, N, _). +has_flat_rec(F, N) :- flat_rec(F, N, _). +has_flat_dt(F, N) :- flat_dt(F, N, _). + +% Priority ladder. Each arm fires only when no higher-priority namespace has a +% same-name binder — so each source emits exactly ONE flat edge. +% NOTE: PARAMETER is INTENTIONALLY ABSENT — the only sound PARAMETER hit is the +% same-scope local_hit arm; a flat PARAMETER target would be a foreign scope's +% param (the misroute bug as fan-out). +flat_hit(R, D) :- local_ref(R, F, N), \+ has_local_hit(R), flat_fn(F, N, D). +flat_hit(R, D) :- local_ref(R, F, N), \+ has_local_hit(R), flat_var(F, N, D), + \+ has_flat_fn(F, N). +flat_hit(R, D) :- local_ref(R, F, N), \+ has_local_hit(R), flat_con(F, N, D), + \+ has_flat_fn(F, N), \+ has_flat_var(F, N). +flat_hit(R, D) :- local_ref(R, F, N), \+ has_local_hit(R), flat_ctr(F, N, D), + \+ has_flat_fn(F, N), \+ has_flat_var(F, N), \+ has_flat_con(F, N). +flat_hit(R, D) :- local_ref(R, F, N), \+ has_local_hit(R), flat_rec(F, N, D), + \+ has_flat_fn(F, N), \+ has_flat_var(F, N), \+ has_flat_con(F, N), + \+ has_flat_ctr(F, N). +flat_hit(R, D) :- local_ref(R, F, N), \+ has_local_hit(R), flat_dt(F, N, D), + \+ has_flat_fn(F, N), \+ has_flat_var(F, N), \+ has_flat_con(F, N), + \+ has_flat_ctr(F, N), \+ has_flat_rec(F, N). +flat_hit(R, D) :- local_ref(R, F, N), \+ has_local_hit(R), flat_ts(F, N, D), + \+ has_flat_fn(F, N), \+ has_flat_var(F, N), \+ has_flat_con(F, N), + \+ has_flat_ctr(F, N), \+ has_flat_rec(F, N), \+ has_flat_dt(F, N). + +% A reference that resolved to ANY local-or-flat binder — suppresses the prelude +% fall-through (the legacy Map.lookup-hit precedes the prelude check, Hs:126). +has_decl(R) :- local_hit(R, _). +has_decl(R) :- flat_hit(R, _). + +% ── PRELUDE fall-through — the virtual HASKELL_GLOBAL node minted by the nodes +% pack, joined as committed EDB, only when NO same-file binder exists. The +% edge carries the EXACT legacy meta: resolvedVia + globalCategory (Hs:144- +% 147). ────────────────────────────────────────────────────────────────── +% The target is pinned by (name=N, file="") — the minted HASKELL_GLOBAL node's +% gnName is the bare prelude name and gnFile="" (Hs:154-157). A single virtual +% node is shared by every prelude reference of that name across files (set +% semantics, the legacy seen-Set), so name+file is the correct join key (the +% js_runtime_globals_edges idiom). The local-hit/flat-fallback negation guard +% (\+ has_decl) keeps this arm to references with NO same-file binder. +prelude_hit(R, G) :- + local_ref(R, F, N), \+ has_decl(R), + node(G, "EXTERNAL_FUNCTION"), attr(G, "name", N), attr(G, "file", ""). + +% ── EMIT — local-hit + flat-fallback share the single resolvedVia-only head; +% the prelude arm additionally stamps globalCategory. @materialize binds by +% HEAD PREDICATE, so the resolvedVia head rides arms 1-2 (one annotation) and +% the prelude head is a separate materialized predicate. ────────────────── +@materialize(edge_type = "READS_FROM", mode = "additive", meta(resolvedVia)) +hs_reads_from(R, D, "haskell-local-refs") :- local_hit(R, D). +hs_reads_from(R, D, "haskell-local-refs") :- flat_hit(R, D). + +@materialize(edge_type = "READS_FROM", mode = "additive", meta(resolvedVia, globalCategory)) +hs_reads_global(R, G, "haskell-local-refs", "haskell-prelude") :- prelude_hit(R, G). diff --git a/packages/rfdb-server/src/derive/stdlib/haskell_local_refs_nodes.dl b/packages/rfdb-server/src/derive/stdlib/haskell_local_refs_nodes.dl new file mode 100644 index 000000000..f5a592d2c --- /dev/null +++ b/packages/rfdb-server/src/derive/stdlib/haskell_local_refs_nodes.dl @@ -0,0 +1,200 @@ +% haskell_local_refs_nodes.dl — NODE half of the in-engine replacement for the +% legacy `haskell-local-refs` resolve step (HaskellLocalRefs.hs). It mints the +% virtual EXTERNAL_FUNCTION prelude-global endpoints ("HASKELL_GLOBAL::", +% category="haskell-prelude") that haskell_local_refs.dl joins as committed EDB +% when emitting the prelude fall-through READS_FROM edge (the js_runtime_globals +% two-pack split: a @materialize edge endpoint must be a committed node-ID +% column, so this pack runs strictly BEFORE the edges pack). +% +% LEGACY semantics reproduced (HaskellLocalRefs.hs:135-167): a REFERENCE whose +% (file, name) is NEITHER an IMPORT_BINDING in the same file NOR resolvable to a +% same-file declaration, but whose bare name is one of the compiled-in +% haskellPreludeNames (Hs:68-99), gets a virtual node +% gnId="HASKELL_GLOBAL::", gnType="EXTERNAL_FUNCTION", gnName=, +% gnMetadata={category="haskell-prelude", source="haskell-local-refs"} +% minted ONCE (the seen-Set dedup, Hs:149-167 — set semantics here), plus a +% READS_FROM edge (emitted by the edges pack). +% +% mode = "exclusive" is PROVENANCE-SCOPED: only nodes stamped with THIS rule's +% _source are owned/retracted; the legacy resolver's identically-sid'd nodes +% (same "HASKELL_GLOBAL::" convention) are foreign and never touched — +% byte-identical sids are REQUIRED for dedup against the still-emitting legacy +% step and against hybrid graphs. +% +% ENCODING NOTE (planner §3): the prelude name set is a ground fact relation +% hs_prelude/1; the head binds Sid via concat off the matched name, so the +% minting rule is filter-before-generator (hs_prelude_ref leads, bound name). +% +% The prelude-name set is the EXACT haskellPreludeNames list (Hs:68-99). + +% ── The 8-suffix Haskell file gate is unnecessary here: REFERENCE/file/name come +% from the analyzer and only Haskell analyzers emit "HASKELL_GLOBAL" preludes; +% we still gate on ".hs" to stay language-scoped (cheap, leads the join). ──── +hs_ref(R, F, N) :- node(R, "REFERENCE"), attr(R, "file", F), ends_with(F, ".hs"), attr(R, "name", N), neq(N, ""). + +% Same-file declaration index over the decl types (HaskellLocalRefs.hs:40-45): +% FUNCTION, VARIABLE, CONSTANT, DATA_TYPE, TYPE_SYNONYM, CONSTRUCTOR, +% RECORD_FIELD, PARAMETER. A reference resolves LOCALLY (and so is NOT a prelude +% global) when some same-file decl of one of these types shares its name. NOTE: +% this is the flat (file, name) presence test — it is INTENTIONALLY file-flat +% (not scope-walked) because the prelude fall-through fires only when NO binder +% of the name exists ANYWHERE in the file; the scope-precision of which binder +% wins lives in the edges pack and never affects the "does any binder exist" +% question. Mirrors the legacy Map.lookup-miss precondition (Hs:126,135). +hs_decl(F, N) :- node(D, "FUNCTION"), attr(D, "file", F), attr(D, "name", N), neq(N, ""). +hs_decl(F, N) :- node(D, "VARIABLE"), attr(D, "file", F), attr(D, "name", N), neq(N, ""). +hs_decl(F, N) :- node(D, "CONSTANT"), attr(D, "file", F), attr(D, "name", N), neq(N, ""). +hs_decl(F, N) :- node(D, "DATA_TYPE"), attr(D, "file", F), attr(D, "name", N), neq(N, ""). +hs_decl(F, N) :- node(D, "TYPE_SYNONYM"), attr(D, "file", F), attr(D, "name", N), neq(N, ""). +hs_decl(F, N) :- node(D, "CONSTRUCTOR"), attr(D, "file", F), attr(D, "name", N), neq(N, ""). +hs_decl(F, N) :- node(D, "RECORD_FIELD"), attr(D, "file", F), attr(D, "name", N), neq(N, ""). +hs_decl(F, N) :- node(D, "PARAMETER"), attr(D, "file", F), attr(D, "name", N), neq(N, ""). + +% Import skip-set (HaskellLocalRefs.hs:58-64,123-124): a name with an +% IMPORT_BINDING in the same file is NEVER resolved locally (and so is never a +% prelude global — it is handled by HaskellImportResolution / CrossModule). +hs_imported(F, N) :- node(B, "IMPORT_BINDING"), attr(B, "file", F), attr(B, "name", N). + +% A reference whose name is a known prelude global AND has no same-file binder +% AND is not imported — the legacy fall-through precondition (Hs:135-137). +hs_prelude_ref(N) :- + hs_ref(R, F, N), hs_prelude(N), \+ hs_decl(F, N), \+ hs_imported(F, N). + +% ── Mint one EXTERNAL_FUNCTION per matched prelude name (set semantics = the +% legacy seen-Set dedup). Sid = "HASKELL_GLOBAL::"; gnFile="" (Hs:157); +% metadata category="haskell-prelude", source="haskell-local-refs". ──────── +@materialize_node(node_type = "EXTERNAL_FUNCTION", mode = "exclusive", meta(category, source)) +hs_global_node(Sid, N, "", "haskell-prelude", "haskell-local-refs") :- + hs_prelude_ref(N), concat("HASKELL_GLOBAL::", N, Sid). + +% ── haskellPreludeNames — the EXACT compiled-in set (HaskellLocalRefs.hs:68-99) ─ +% Data constructors +hs_prelude("True"). +hs_prelude("False"). +hs_prelude("Just"). +hs_prelude("Nothing"). +hs_prelude("Left"). +hs_prelude("Right"). +hs_prelude("LT"). +hs_prelude("EQ"). +hs_prelude("GT"). +hs_prelude("IO"). +% Common functions +hs_prelude("pure"). +hs_prelude("return"). +hs_prelude("show"). +hs_prelude("read"). +hs_prelude("print"). +hs_prelude("putStrLn"). +hs_prelude("putStr"). +hs_prelude("getLine"). +hs_prelude("getContents"). +hs_prelude("interact"). +hs_prelude("map"). +hs_prelude("filter"). +hs_prelude("foldl"). +hs_prelude("foldr"). +hs_prelude("foldl'"). +hs_prelude("concatMap"). +hs_prelude("zip"). +hs_prelude("head"). +hs_prelude("tail"). +hs_prelude("init"). +hs_prelude("last"). +hs_prelude("length"). +hs_prelude("null"). +hs_prelude("reverse"). +hs_prelude("take"). +hs_prelude("drop"). +hs_prelude("takeWhile"). +hs_prelude("dropWhile"). +hs_prelude("span"). +hs_prelude("break"). +hs_prelude("elem"). +hs_prelude("notElem"). +hs_prelude("lookup"). +hs_prelude("concat"). +hs_prelude("replicate"). +hs_prelude("any"). +hs_prelude("all"). +hs_prelude("and"). +hs_prelude("or"). +hs_prelude("sum"). +hs_prelude("product"). +hs_prelude("maximum"). +hs_prelude("minimum"). +hs_prelude("id"). +hs_prelude("const"). +hs_prelude("flip"). +hs_prelude("not"). +hs_prelude("otherwise"). +hs_prelude("undefined"). +hs_prelude("error"). +hs_prelude("fst"). +hs_prelude("snd"). +hs_prelude("curry"). +hs_prelude("uncurry"). +hs_prelude("maybe"). +hs_prelude("either"). +hs_prelude("fromMaybe"). +hs_prelude("isJust"). +hs_prelude("isNothing"). +hs_prelude("mapM"). +hs_prelude("mapM_"). +hs_prelude("forM"). +hs_prelude("forM_"). +hs_prelude("sequence"). +hs_prelude("sequence_"). +hs_prelude("when"). +hs_prelude("unless"). +hs_prelude("guard"). +hs_prelude("void"). +% Type classes +hs_prelude("Eq"). +hs_prelude("Ord"). +hs_prelude("Show"). +hs_prelude("Read"). +hs_prelude("Enum"). +hs_prelude("Bounded"). +hs_prelude("Num"). +hs_prelude("Integral"). +hs_prelude("Fractional"). +hs_prelude("Floating"). +hs_prelude("Real"). +hs_prelude("RealFrac"). +hs_prelude("RealFloat"). +hs_prelude("Functor"). +hs_prelude("Applicative"). +hs_prelude("Monad"). +hs_prelude("MonadIO"). +hs_prelude("Monoid"). +hs_prelude("Semigroup"). +hs_prelude("Foldable"). +hs_prelude("Traversable"). +% Types +hs_prelude("Int"). +hs_prelude("Integer"). +hs_prelude("Float"). +hs_prelude("Double"). +hs_prelude("Bool"). +hs_prelude("Char"). +hs_prelude("String"). +hs_prelude("Maybe"). +hs_prelude("Either"). +hs_prelude("Ordering"). +% Operators as functions +hs_prelude("succ"). +hs_prelude("pred"). +hs_prelude("toEnum"). +hs_prelude("fromEnum"). +hs_prelude("compare"). +hs_prelude("max"). +hs_prelude("min"). +hs_prelude("negate"). +hs_prelude("abs"). +hs_prelude("signum"). +hs_prelude("fromInteger"). +hs_prelude("div"). +hs_prelude("mod"). +hs_prelude("quot"). +hs_prelude("rem").