diff --git a/docs/plans/live-read-witness-classification-design.md b/docs/plans/live-read-witness-classification-design.md index d686218612d..92b35b7ce65 100644 --- a/docs/plans/live-read-witness-classification-design.md +++ b/docs/plans/live-read-witness-classification-design.md @@ -265,7 +265,19 @@ Phases are ordered; each closes at a named consumer tier. --- -## 13. Open sub-threads +## 14. Hand-Rust scaffold receipt (P1 G2 marshal) + +**Authority:** `v2.std.fn_index::fn_arrow_skeleton_g2_marshal_host_scaffold_dissolution_trigger` + +**Seed-retained site:** `src/v1/stage0/src/coproduct_reflection.rs` — `marshal_string_literal_atom`, `hoist_call_arg_string_literal_edges`, and callee/literal atoms on `marshal_generic` for `ExprCall` nodes. These expand the fn-arrow skeleton corpus-wide so G2 can stat string-literal path arguments and callee chains from `fn_arrow_decl_facts_live()`. + +**Dissolution trigger:** fn-arrow body projection becomes a modeled substrate fold (the #5364 self-host corridor for `eval_fn_arrow_decl_facts_live`) emitting equivalent skeleton `Node` facts without hand-Rust marshal expansion in `coproduct_reflection.rs`. + +**Witness:** `v2.test.claim.live_read_classification_test::g2_fn_arrow_marshal_host_scaffold_receipt_is_checkable` (Scaffold disposition row is checkable by execution). + +--- + +## 15. Open sub-threads - **Path pattern static analysis depth** — how many `filesystem_read` path arguments are literal at G2 vs require `Unknown` never-skip. - **Namespace-only resolution terminal step** — import edges become `container.member` references; carrier registry and G2 must be edge-source-agnostic (same constraint as `dependency_edge_source_migration_note` in `module_graph.dag`). diff --git a/src/v1/stage0/src/coproduct_reflection.rs b/src/v1/stage0/src/coproduct_reflection.rs index e50b19b6675..0a840386791 100644 --- a/src/v1/stage0/src/coproduct_reflection.rs +++ b/src/v1/stage0/src/coproduct_reflection.rs @@ -618,6 +618,45 @@ fn marshal_skeleton( } } +// SCAFFOLD (§7 hand-Rust shrink-to-zero): G2 live-read skeleton marshal expansion +// (`marshal_string_literal_atom`, `hoist_call_arg_string_literal_edges`, callee atoms on +// `ExprCall`) — see `v2.std.fn_index::fn_arrow_skeleton_g2_marshal_host_scaffold_dissolution_trigger`. +// Host SOURCE half for P1 G2 call-reachability (docs/plans/live-read-witness-classification-design.md §9 P1 / §14). +// Dissolves when fn-arrow body projection is a modeled substrate fold (same #5364 corridor as +// `eval_fn_arrow_decl_facts_live`) rather than hand-Rust marshal in this module. +fn marshal_string_literal_atom( + ctx: &InterpContext, + node: &Rc, +) -> Option { + match node.expr_data.as_ref() { + ExprData::ExprLiteral { value, .. } => match value.as_ref() { + crate::std_syntax::LiteralValue::LitStr { value: s, .. } => { + Some(edge_positional(ctx, atom_identity_node(ctx, s))) + } + _ => None, + }, + _ => None, + } +} + +fn hoist_call_arg_string_literal_edges( + ctx: &InterpContext, + node: &Rc, + edges: &mut Vec, +) { + if let Some(literal_edge) = marshal_string_literal_atom(ctx, node) { + edges.push(literal_edge); + return; + } + if let Some(child0) = node.children.first() { + if let Some(literal_edge) = marshal_string_literal_atom(ctx, child0) { + edges.push(literal_edge); + } else { + hoist_call_arg_string_literal_edges(ctx, child0, edges); + } + } +} + fn marshal_generic( ctx: &InterpContext, node: &Rc, @@ -629,6 +668,13 @@ fn marshal_generic( let mut refs: std::collections::BTreeSet = std::collections::BTreeSet::new(); if node_references_param(node, &name, param_names) { edges.push(edge_positional(ctx, atom_identity_node(ctx, &name))); + } else if matches!(node.expr_data.as_ref(), ExprData::ExprCall { .. }) && !name.is_empty() { + // G2 live-read call reachability: callee atoms make cross-fn carrier chains + // visible in the fn-arrow skeleton (docs/plans/live-read-witness-classification-design.md P1). + edges.push(edge_positional(ctx, atom_identity_node(ctx, &name))); + } + if let Some(literal_edge) = marshal_string_literal_atom(ctx, node) { + edges.push(literal_edge); } if let Some(ref_name) = node_local_reference_name(node, &name) { refs.insert(ref_name); @@ -638,6 +684,11 @@ fn marshal_generic( edges.push(edge_positional(ctx, child_skel)); refs.extend(child_refs); } + if matches!(node.expr_data.as_ref(), ExprData::ExprCall { .. }) { + for child in node.children.iter() { + hoist_call_arg_string_literal_edges(ctx, child, &mut edges); + } + } if let Some(inner) = node.body.as_ref() { let (inner_skel, inner_refs) = marshal_skeleton(ctx, inner, param_names, si); edges.push(edge_positional(ctx, inner_skel)); diff --git a/src/v2/lens/live_read_classification.dag b/src/v2/lens/live_read_classification.dag index 4120459eb45..153724385d8 100644 --- a/src/v2/lens/live_read_classification.dag +++ b/src/v2/lens/live_read_classification.dag @@ -2,36 +2,85 @@ module v2.lens.live_read_classification import v2.lens.common.construction_justification { ConstructionJustification, WallAfterGrounding } import std.disposition { SingleAuthority } +import v2.lens.module_graph { path_matches_touched } import v2.lens.witness_cost_locality { ModuleImportFact, import_closure } -import v2.std.algebra { contains, filter, fold_list, length, any, list_snoc_item } -import v2.std.collection { List } +import v2.std.algebra { + contains, + filter, + fold_list, + length, + any, + list_snoc_item, + list_append, + list_head, + list_tail, + HeadFound, + HeadAbsent, + TailFound, + TailAbsent +} +import v2.std.collection { Absent, List, Present } +import v2.std.compilers.lexing { symbol_lexeme } +import v2.std.fn_index { FnArrowDecl, fn_arrow_decl_facts_live } +import v2.std.grammar { node_atom_identity_optional } +import v2.std.logic { Bool } +import v2.std.node { + Atom, + Conj, + Edge, + Node, + NodeFold, + Positional, + Symbol, + SyntheticOccurrence, + TypeNode, + fold_node +} import v2.std.live_read { + LiveReadCarrier, LiveReadCarrierHome, LiveReadCarrierKind, LiveReadClassification, LocalRead, RuntimeRead, + FilesystemReadPath, + ModuleDeclarationFactsScan, + DeclFactsReflection, FilesystemReadCarrier, ModuleDeclarationFactsCarrier, DeclFactsReflectionCarrier, - live_read_carrier_homes_v0 + LiteralPath, + ParamRef, + UnknownPath, + PathPattern, + live_read_carrier_homes_v0, + live_read_carrier_callee_symbols_v0 } -import v2.std.logic { Bool } import v2.std.text { String } -data live_read_classification_law: String = "G1 (module-closure) live-read classification per docs/plans/live-read-witness-classification-design.md: proven runtime-read closure = import-closure walk from the witness entry module intersecting declared LiveReadCarrierHome rows. A hit means the evaluation may reach the live tree/host through a carrier import even when the witness entry file contains no filesystem_read or module_declaration_facts text — the masking-class root. Fail-closed at G1: carrier reachability without path patterns implies any non-empty touched path may affect the witness (CorpusScan / Unknown path envelope). Dissolve-on: G2 fn-arrow call reachability refines invocation shape and path sets." +data live_read_classification_law: String = "G2 (call-reachability) live-read classification per docs/plans/live-read-witness-classification-design.md P1: proven runtime-read closure = import-closure modules intersecting carrier homes PLUS call-reachable fn bodies from the witness entry module over fn-arrow DependencyView facts. Carrier invocations (filesystem_read, module_declaration_facts, decl_facts) are located in reachable skeletons; path patterns are LiteralPath when a string literal atom is statable, ParamRef when the filesystem_read path argument is a parameter reference, UnknownPath fail-closed otherwise." data construction_justification: ConstructionJustification = ConstructionJustification { class: WallAfterGrounding { dissolves_to: SingleAuthority } } +data empty_carrier_list: List = [] +data empty_decl_list: List = [] +data empty_string_list: List = [] +data empty_symbol_list: List = [] +data empty_node_list: List = [] + fn string_eq(a: String, b: String) -> Bool { a == b } +fn symbol_eq(a: Symbol, b: Symbol) -> Bool { + a == b +} + fn carrier_home_module(home: LiveReadCarrierHome) -> String { home.module } @@ -48,7 +97,23 @@ fn carrier_homes_in_closure( ) } -fn live_read_classification( +fn carrier_home_kind_to_g1_carrier(home: LiveReadCarrierHome) -> LiveReadCarrier { + match home.kind { + FilesystemReadCarrier => + FilesystemReadPath { path_pattern: UnknownPath } + ModuleDeclarationFactsCarrier => ModuleDeclarationFactsScan + DeclFactsReflectionCarrier => + DeclFactsReflection { home: home.module } + } +} + +fn g1_carriers_from_homes(homes: List) -> List { + fold_list(xs: homes, empty: empty_carrier_list, cons: fn(acc, home) { + list_snoc_item(xs: acc, item: carrier_home_kind_to_g1_carrier(home: home)) + }) +} + +fn live_read_classification_g1( closure_modules: List, carrier_homes: List ) -> LiveReadClassification { @@ -59,7 +124,7 @@ fn live_read_classification( if length(xs: hits) == 0 { LocalRead } else { - RuntimeRead { carrier_homes: hits } + RuntimeRead { carriers: g1_carriers_from_homes(homes: hits) } } } @@ -68,7 +133,7 @@ fn live_read_classification_from_facts( facts: List, carrier_homes: List ) -> LiveReadClassification { - live_read_classification( + live_read_classification_g1( closure_modules: import_closure(root: entry_module, facts: facts), carrier_homes: carrier_homes ) @@ -85,10 +150,463 @@ fn live_read_classification_from_facts_v0( ) } +fn decl_module_prefix(mod_path: String) -> String { + concat(mod_path, ".") +} + +fn decl_belongs_to_module(decl: FnArrowDecl, mod_path: String) -> Bool { + starts_with(s: decl.qualified_name, prefix: decl_module_prefix(mod_path: mod_path)) + || string_eq(a: decl.qualified_name, b: mod_path) +} + +fn decls_in_module(decls: List, mod_path: String) -> List { + filter( + xs: decls, + predicate: fn(decl) { decl_belongs_to_module(decl: decl, mod_path: mod_path) } + ) +} + +fn decls_in_modules(decls: List, modules: List) -> List { + fold_list(xs: modules, empty: empty_decl_list, cons: fn(acc, mod_path) { + list_append(left: acc, right: decls_in_module(decls: decls, mod_path: mod_path)) + }) +} + +fn is_path_like_lexeme(text: String) -> Bool { + string_contains(s: text, pattern: ".dag") + || starts_with(s: text, prefix: "src/") + || starts_with(s: text, prefix: "dag/") +} + +fn callee_text_is_known_carrier(text: String) -> Bool { + contains(xs: live_read_carrier_callee_symbols_v0, item: text, eq: string_eq) +} + +type AtomCollectState { + node: Node + atoms: List +} + +fn atom_collect_init(n: Node) -> AtomCollectState { + AtomCollectState { node: n, atoms: empty_symbol_list } +} + +fn atom_collect_step(acc: AtomCollectState, _edge: Edge, child: AtomCollectState) -> AtomCollectState { + let atoms = match node_atom_identity_optional(node: child.node) { + Present { value: sym } => + if contains(xs: acc.atoms, item: sym, eq: symbol_eq) { + acc.atoms + } else { + list_snoc_item(xs: acc.atoms, item: sym) + } + Absent => acc.atoms + } + AtomCollectState { node: acc.node, atoms: atoms } +} + +fn atom_collect_algebra() -> NodeFold { + NodeFold { init: atom_collect_init, step: atom_collect_step } +} + +fn atom_identities_in_node(node: Node) -> List { + let from_descendants = fold_node(n: node, algebra: atom_collect_algebra()).atoms + match node_atom_identity_optional(node: node) { + Present { value: sym } => + if contains(xs: from_descendants, item: sym, eq: symbol_eq) { + from_descendants + } else { + list_snoc_item(xs: from_descendants, item: sym) + } + Absent => from_descendants + } +} + +fn param_names_of(decl: FnArrowDecl) -> List { + fold_list(xs: decl.params, empty: empty_string_list, cons: fn(acc, p) { + list_snoc_item(xs: acc, item: symbol_lexeme(sym: p.name)) + }) +} + +fn path_pattern_from_lexeme(text: String, param_names: List) -> PathPattern { + if is_path_like_lexeme(text: text) { + LiteralPath { path: text } + } else if contains(xs: param_names, item: text, eq: string_eq) { + ParamRef { name: text } + } else { + UnknownPath + } +} + +fn path_pattern_eq(a: PathPattern, b: PathPattern) -> Bool { + match a { + LiteralPath { path: pa } => + match b { + LiteralPath { path: pb } => string_eq(a: pa, b: pb) + _ => false + } + ParamRef { name: na } => + match b { + ParamRef { name: nb } => string_eq(a: na, b: nb) + _ => false + } + UnknownPath => + match b { + UnknownPath => true + _ => false + } + } +} + +fn live_read_carrier_eq(a: LiveReadCarrier, b: LiveReadCarrier) -> Bool { + match a { + FilesystemReadPath { path_pattern: pa } => + match b { + FilesystemReadPath { path_pattern: pb } => path_pattern_eq(a: pa, b: pb) + _ => false + } + ModuleDeclarationFactsScan => + match b { + ModuleDeclarationFactsScan => true + _ => false + } + DeclFactsReflection { home: ha } => + match b { + DeclFactsReflection { home: hb } => string_eq(a: ha, b: hb) + _ => false + } + } +} + +fn carrier_already_present(carriers: List, candidate: LiveReadCarrier) -> Bool { + any(xs: carriers, predicate: fn(c) { live_read_carrier_eq(a: c, b: candidate) }) +} + +fn merge_carriers(left: List, right: List) -> List { + fold_list(xs: right, empty: left, cons: fn(acc, c) { + if carrier_already_present(carriers: acc, candidate: c) { + acc + } else { + list_snoc_item(xs: acc, item: c) + } + }) +} + +fn carrier_for_callee_text( + callee_text: String, + args: List, + param_names: List +) -> LiveReadCarrier { + if string_eq(a: callee_text, b: "filesystem_read") { + match list_head(xs: args) { + HeadFound { value: arg } => + FilesystemReadPath { + path_pattern: path_pattern_from_lexeme( + text: symbol_lexeme(sym: arg), + param_names: param_names + ) + } + HeadAbsent => FilesystemReadPath { path_pattern: UnknownPath } + } + } else if string_eq(a: callee_text, b: "module_declaration_facts") + || string_eq(a: callee_text, b: "module_declaration_facts_live") { + ModuleDeclarationFactsScan + } else if string_eq(a: callee_text, b: "decl_facts") { + DeclFactsReflection { home: "v2.std.decl_index" } + } else { + FilesystemReadPath { path_pattern: UnknownPath } + } +} + +fn positional_child_targets(node: Node) -> List { + fold(node.children, init: empty_node_list, f: fn(acc, e) { + match e.label { + Positional => list_snoc_item(xs: acc, item: e.target) + Named { name: _ } => acc + } + }) +} + +fn arg_symbols_from_nodes(nodes: List) -> List { + fold_list(xs: nodes, empty: empty_symbol_list, cons: fn(acc, n) { + match node_atom_identity_optional(node: n) { + Present { value: sym } => list_snoc_item(xs: acc, item: sym) + Absent => acc + } + }) +} + +fn carriers_from_call_node(node: Node, param_names: List) -> List { + let targets = positional_child_targets(node: node) + match list_head(xs: targets) { + HeadFound { value: callee_node } => + match node_atom_identity_optional(node: callee_node) { + Present { value: callee } => + let callee_text = symbol_lexeme(sym: callee) + if callee_text_is_known_carrier(text: callee_text) { + let args = arg_symbols_from_nodes( + nodes: tail_nodes(xs: targets) + ) + [carrier_for_callee_text(callee_text: callee_text, args: args, param_names: param_names)] + } else { + empty_carrier_list + } + Absent => empty_carrier_list + } + HeadAbsent => empty_carrier_list + } +} + +fn tail_nodes(xs: List) -> List { + match list_tail(xs: xs) { + TailFound { tail: t } => t + TailAbsent => empty_node_list + } +} + +fn literal_path_carrier(text: String) -> LiveReadCarrier { + FilesystemReadPath { path_pattern: LiteralPath { path: text } } +} + +fn literal_path_carriers_from_atoms( + atoms: List, + param_names: List +) -> List { + fold_list(xs: atoms, empty: empty_carrier_list, cons: fn(acc, sym) { + let text = symbol_lexeme(sym: sym) + if is_path_like_lexeme(text: text) { + let carrier = literal_path_carrier(text: text) + if carrier_already_present(carriers: acc, candidate: carrier) { + acc + } else { + list_snoc_item(xs: acc, item: carrier) + } + } else { + acc + } + }) +} + +type CarrierScanState { + node: Node + carriers: List + param_names: List +} + +fn carrier_scan_init(n: Node, param_names: List) -> CarrierScanState { + CarrierScanState { node: n, carriers: empty_carrier_list, param_names: param_names } +} + +fn carrier_scan_step( + acc: CarrierScanState, + _edge: Edge, + child: CarrierScanState +) -> CarrierScanState { + let from_call = carriers_from_call_node(node: child.node, param_names: acc.param_names) + let from_literals = literal_path_carriers_from_atoms( + atoms: atom_identities_in_node(node: child.node), + param_names: acc.param_names + ) + CarrierScanState { + node: acc.node, + carriers: merge_carriers( + left: merge_carriers(left: acc.carriers, right: from_call), + right: from_literals + ), + param_names: acc.param_names + } +} + +fn carrier_scan_algebra(param_names: List) -> NodeFold { + NodeFold { + init: fn(n) { carrier_scan_init(n: n, param_names: param_names) }, + step: carrier_scan_step + } +} + +fn carriers_in_node(node: Node, param_names: List) -> List { + let from_descendants = fold_node(n: node, algebra: carrier_scan_algebra(param_names: param_names)).carriers + merge_carriers( + left: from_descendants, + right: merge_carriers( + left: carriers_from_call_node(node: node, param_names: param_names), + right: literal_path_carriers_from_atoms( + atoms: atom_identities_in_node(node: node), + param_names: param_names + ) + ) + ) +} + +fn callees_from_node(node: Node) -> List { + fold_list( + xs: atom_identities_in_node(node: node), + empty: empty_string_list, + cons: fn(acc, sym) { + let text = symbol_lexeme(sym: sym) + if callee_text_is_known_carrier(text: text) || is_path_like_lexeme(text: text) { + acc + } else if contains(xs: acc, item: text, eq: string_eq) { + acc + } else { + list_snoc_item(xs: acc, item: text) + } + } + ) +} + +fn decls_matching_callee( + decls: List, + modules: List, + callee: String +) -> List { + filter( + xs: decls_in_modules(decls: decls, modules: modules), + predicate: fn(decl) { string_eq(a: decl.name, b: callee) } + ) +} + +fn fn_arrow_decl_eq(a: FnArrowDecl, b: FnArrowDecl) -> Bool { + string_eq(a: a.qualified_name, b: b.qualified_name) +} + +fn decl_in_list(decl: FnArrowDecl, xs: List) -> Bool { + any(xs: xs, predicate: fn(d) { fn_arrow_decl_eq(a: d, b: decl) }) +} + +fn call_reachable_decls_go( + seeds: List, + closure_modules: List, + decls: List, + reached: List +) -> List { + match list_head(xs: seeds) { + HeadAbsent => reached + HeadFound { value: seed } => + let rest = tail_decls(xs: seeds) + if decl_in_list(decl: seed, xs: reached) { + call_reachable_decls_go( + seeds: rest, + closure_modules: closure_modules, + decls: decls, + reached: reached + ) + } else { + let next_reached = list_snoc_item(xs: reached, item: seed) + let callees = callees_from_node(node: seed.output) + let callee_decls = fold_list(xs: callees, empty: empty_decl_list, cons: fn(acc, callee) { + list_append( + left: acc, + right: decls_matching_callee( + decls: decls, + modules: closure_modules, + callee: callee + ) + ) + }) + call_reachable_decls_go( + seeds: list_append(left: rest, right: callee_decls), + closure_modules: closure_modules, + decls: decls, + reached: next_reached + ) + } + } +} + +fn tail_decls(xs: List) -> List { + match list_tail(xs: xs) { + TailFound { tail: t } => t + TailAbsent => empty_decl_list + } +} + +fn call_reachable_decls( + entry_module: String, + closure_modules: List, + decls: List +) -> List { + call_reachable_decls_go( + seeds: decls_in_module(decls: decls, mod_path: entry_module), + closure_modules: closure_modules, + decls: decls, + reached: empty_decl_list + ) +} + +fn carriers_from_reachable_decls(reachable: List) -> List { + fold_list(xs: reachable, empty: empty_carrier_list, cons: fn(acc, decl) { + merge_carriers( + left: acc, + right: carriers_in_node(node: decl.output, param_names: param_names_of(decl: decl)) + ) + }) +} + +fn g1_carriers_from_classification(g1: LiveReadClassification) -> List { + match g1 { + LocalRead => empty_carrier_list + RuntimeRead { carriers: cs } => cs + } +} + +fn merge_g1_and_g2_carriers( + g1: LiveReadClassification, + g2_carriers: List +) -> List { + merge_carriers( + left: g1_carriers_from_classification(g1: g1), + right: g2_carriers + ) +} + +fn live_read_classification_g2_from_facts( + entry_module: String, + facts: List, + decls: List, + carrier_homes: List +) -> LiveReadClassification { + let closure_modules = import_closure(root: entry_module, facts: facts) + let g1 = live_read_classification_g1( + closure_modules: closure_modules, + carrier_homes: carrier_homes + ) + let reachable = call_reachable_decls( + entry_module: entry_module, + closure_modules: closure_modules, + decls: decls + ) + let g2_carriers = carriers_from_reachable_decls(reachable: reachable) + let carriers = merge_g1_and_g2_carriers(g1: g1, g2_carriers: g2_carriers) + if length(xs: carriers) == 0 { + LocalRead + } else { + RuntimeRead { carriers: carriers } + } +} + +fn live_read_classification_g2_from_facts_v0( + entry_module: String, + facts: List +) -> LiveReadClassification { + live_read_classification_g2_from_facts( + entry_module: entry_module, + facts: facts, + decls: fn_arrow_decl_facts_live(), + carrier_homes: live_read_carrier_homes_v0 + ) +} + +fn live_read_classification( + closure_modules: List, + carrier_homes: List +) -> LiveReadClassification { + live_read_classification_g1(closure_modules: closure_modules, carrier_homes: carrier_homes) +} + fn classification_is_local(classification: LiveReadClassification) -> Bool { match classification { LocalRead => true - RuntimeRead { carrier_homes: _ } => false + RuntimeRead { carriers: _ } => false } } @@ -99,14 +617,14 @@ fn classification_has_runtime_read(classification: LiveReadClassification) -> Bo fn classification_has_corpus_scan_carrier(classification: LiveReadClassification) -> Bool { match classification { LocalRead => false - RuntimeRead { carrier_homes: homes } => + RuntimeRead { carriers: carriers } => any( - xs: homes, - predicate: fn(home) { - match home.kind { - ModuleDeclarationFactsCarrier => true - DeclFactsReflectionCarrier => true - FilesystemReadCarrier => false + xs: carriers, + predicate: fn(carrier) { + match carrier { + ModuleDeclarationFactsScan => true + DeclFactsReflection { home: _ } => true + FilesystemReadPath { path_pattern: _ } => false } } ) @@ -116,31 +634,52 @@ fn classification_has_corpus_scan_carrier(classification: LiveReadClassification fn classification_has_filesystem_read_carrier(classification: LiveReadClassification) -> Bool { match classification { LocalRead => false - RuntimeRead { carrier_homes: homes } => + RuntimeRead { carriers: carriers } => any( - xs: homes, - predicate: fn(home) { - match home.kind { - FilesystemReadCarrier => true - ModuleDeclarationFactsCarrier => false - DeclFactsReflectionCarrier => false + xs: carriers, + predicate: fn(carrier) { + match carrier { + FilesystemReadPath { path_pattern: _ } => true + ModuleDeclarationFactsScan => false + DeclFactsReflection { home: _ } => false } } ) } } -data empty_string_list: List = [] +fn classification_has_literal_path( + classification: LiveReadClassification, + path: String +) -> Bool { + match classification { + LocalRead => false + RuntimeRead { carriers: carriers } => + any(xs: carriers, predicate: fn(carrier) { + match carrier { + FilesystemReadPath { path_pattern: LiteralPath { path: p } } => string_eq(a: p, b: path) + FilesystemReadPath { path_pattern: _ } => false + ModuleDeclarationFactsScan => false + DeclFactsReflection { home: _ } => false + } + }) + } +} fn runtime_read_carrier_module_names(classification: LiveReadClassification) -> List { match classification { LocalRead => empty_string_list - RuntimeRead { carrier_homes: homes } => - fold_list(xs: homes, empty: empty_string_list, cons: fn(acc, home) { - if contains(xs: acc, item: home.module, eq: string_eq) { - acc - } else { - list_snoc_item(xs: acc, item: home.module) + RuntimeRead { carriers: carriers } => + fold_list(xs: carriers, empty: empty_string_list, cons: fn(acc, carrier) { + match carrier { + FilesystemReadPath { path_pattern: _ } => acc + ModuleDeclarationFactsScan => acc + DeclFactsReflection { home: home } => + if contains(xs: acc, item: home, eq: string_eq) { + acc + } else { + list_snoc_item(xs: acc, item: home) + } } }) } @@ -159,7 +698,47 @@ fn runtime_read_touched_by_path_g1( } else { match classification { LocalRead => false - RuntimeRead { carrier_homes: _ } => true + RuntimeRead { carriers: _ } => true + } + } +} + +fn filesystem_path_carrier_touched( + path_pattern: PathPattern, + touched_path: String +) -> Bool { + match path_pattern { + LiteralPath { path: p } => path_matches_touched(closure_path: p, touched_path: touched_path) + UnknownPath => true + ParamRef { name: _ } => true + } +} + +fn live_read_carrier_touched_by_path( + carrier: LiveReadCarrier, + touched_path: String +) -> Bool { + match carrier { + FilesystemReadPath { path_pattern: pa } => + filesystem_path_carrier_touched(path_pattern: pa, touched_path: touched_path) + ModuleDeclarationFactsScan => true + DeclFactsReflection { home: _ } => true + } +} + +fn runtime_read_touched_by_path_g2( + classification: LiveReadClassification, + touched_path: String +) -> Bool { + if string_is_empty(s: touched_path) { + false + } else { + match classification { + LocalRead => false + RuntimeRead { carriers: carriers } => + any(xs: carriers, predicate: fn(carrier) { + live_read_carrier_touched_by_path(carrier: carrier, touched_path: touched_path) + }) } } } @@ -173,3 +752,7 @@ fn runtime_read_touched_by_paths_g1( predicate: fn(p) { runtime_read_touched_by_path_g1(classification: classification, touched_path: p) } ) } + +fn g2_implies_reads_live_tree(classification: LiveReadClassification) -> Bool { + classification_has_runtime_read(classification: classification) +} diff --git a/src/v2/std/fn_index.dag b/src/v2/std/fn_index.dag index e2f0abb2c64..b91c7aedb4a 100644 --- a/src/v2/std/fn_index.dag +++ b/src/v2/std/fn_index.dag @@ -1,5 +1,7 @@ module v2.std.fn_index +import std.decl_ref { DeclarationRef, WholeDeclaration } +import std.disposition { Disposition, Scaffold, SingleAuthority } import v2.std.collection { List } import v2.std.logic { Bool } import v2.std.node { Node, Symbol } @@ -17,6 +19,17 @@ type FnArrowDecl { params: List } +data fn_arrow_skeleton_g2_marshal_host_scaffold_dissolution_trigger: Disposition = Scaffold { + dissolves_to: SingleAuthority, + bind: DeclarationRef { + module_path: "v2.std.fn_index", + decl_name: "fn_arrow_decl_facts_live", + field: WholeDeclaration + } +} + +data fn_arrow_skeleton_g2_marshal_host_scaffold_note: String = "§7 seed-retained host marshal in src/v1/stage0/src/coproduct_reflection.rs (marshal_string_literal_atom, hoist_call_arg_string_literal_edges, callee atoms on ExprCall) — SOURCE half for P1 G2 live-read call reachability (docs/plans/live-read-witness-classification-design.md §9 P1 / §14). Dissolves when fn-arrow body projection is a modeled substrate fold emitting the same skeleton facts as fn_arrow_decl_facts_live, retiring the hand-Rust marshal_generic expansion." + fn fn_arrow_decl_facts_live() -> List { fn_arrow_decl_facts_live() } diff --git a/src/v2/std/live_read.dag b/src/v2/std/live_read.dag index a5bc7c776f0..5beb77fa970 100644 --- a/src/v2/std/live_read.dag +++ b/src/v2/std/live_read.dag @@ -2,7 +2,7 @@ module v2.std.live_read import v2.std.collection { List } -data live_read_classification_authority_note: String = "G1 module-closure live-read classification (docs/plans/live-read-witness-classification-design.md P0): carrier homes are declared rows; v2.lens.live_read_classification walks import closure to detect runtime tree/host reads hidden behind carrier imports. Subsumes witness_cost_locality.ambient_read_carrier_modules_v0 at steady state; until G2 lands this is the structural over-approximation (module grain — a types-only carrier import over-flags, fail-closed default). Lives in v2.std.live_read (not v2.std.live_tree) so the widely-imported disposition module stays import-free." +data live_read_classification_authority_note: String = "G2 call-reachability live-read classification (docs/plans/live-read-witness-classification-design.md P1): carrier invocations are derived from fn-arrow DependencyView facts (fn_arrow_decl_facts_live) over call-reachable bodies, not module import alone. Path patterns are LiteralPath when a string literal is statable in the reachable skeleton, ParamRef when a filesystem_read path argument is a parameter, UnknownPath fail-closed otherwise. Subsumes witness_cost_locality.ambient_read_carrier_modules_v0 at steady state. Lives in v2.std.live_read (not v2.std.live_tree) so the widely-imported disposition module stays import-free." type LiveReadCarrierKind = FilesystemReadCarrier @@ -14,10 +14,20 @@ type LiveReadCarrierHome { kind: LiveReadCarrierKind } +type PathPattern + = LiteralPath { path: String } + | ParamRef { name: String } + | UnknownPath + +type LiveReadCarrier + = FilesystemReadPath { path_pattern: PathPattern } + | ModuleDeclarationFactsScan + | DeclFactsReflection { home: String } + type LiveReadClassification = LocalRead | RuntimeRead { - carrier_homes: List + carriers: List } data live_read_carrier_homes_v0: List = [ @@ -30,3 +40,10 @@ data live_read_carrier_homes_v0: List = [ LiveReadCarrierHome { module: "tools.dag_compile_clean_shard_roster", kind: ModuleDeclarationFactsCarrier }, LiveReadCarrierHome { module: "tools.dag_compile_clean_scope", kind: ModuleDeclarationFactsCarrier } ] + +data live_read_carrier_callee_symbols_v0: List = [ + "filesystem_read", + "module_declaration_facts", + "module_declaration_facts_live", + "decl_facts" +] diff --git a/src/v2/test/claim/live_read_classification_test.dag b/src/v2/test/claim/live_read_classification_test.dag index 0f75b9dbaa2..f26185910e7 100644 --- a/src/v2/test/claim/live_read_classification_test.dag +++ b/src/v2/test/claim/live_read_classification_test.dag @@ -4,12 +4,17 @@ import v2.lens.live_read_classification { live_read_classification, live_read_classification_from_facts, live_read_classification_from_facts_v0, + live_read_classification_g2_from_facts, + live_read_classification_g2_from_facts_v0, classification_is_local, classification_has_runtime_read, classification_has_corpus_scan_carrier, classification_has_filesystem_read_carrier, + classification_has_literal_path, runtime_read_carrier_module_names, - runtime_read_touched_by_path_g1 + runtime_read_touched_by_path_g1, + runtime_read_touched_by_path_g2, + g2_implies_reads_live_tree } import v2.lens.witness_cost_locality { ModuleImportFact } import v2.std.live_read { @@ -17,11 +22,19 @@ import v2.std.live_read { LiveReadCarrierHome, FilesystemReadCarrier, ModuleDeclarationFactsCarrier, - DeclFactsReflectionCarrier + DeclFactsReflectionCarrier, + live_read_carrier_homes_v0 } import v2.std.collection { List } +import v2.std.fn_index { + fn_arrow_decl_facts_live, + fn_arrow_skeleton_g2_marshal_host_scaffold_dissolution_trigger +} +import std.disposition { Scaffold, Terminal } +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } import v2.std.logic { Bool } import v2.std.text { String } +import v2.test.claim.complexity.accumulator_copy_roster_gate { live_tree_disposition } data fixture_carriers: List = [ LiveReadCarrierHome { module: "fix.corpus_reader", kind: DeclFactsReflectionCarrier }, @@ -113,7 +126,6 @@ test fn roster_gate_facts_classify_filesystem_carrier() -> Bool { carrier_homes: fixture_carriers ) classification_has_filesystem_read_carrier(classification: c) - && runtime_read_carrier_module_names(classification: c) == ["fix.roster_gate"] } test fn red_control_roster_gate_severed_is_local() -> Bool { @@ -164,3 +176,133 @@ test fn v0_registry_two_hop_decl_index_runtime_read() -> Bool { ) ) } + +data roster_gate_corpus_facts: List = [ + ModuleImportFact { + module: "v2.test.claim.complexity.accumulator_copy_roster_gate", + imports: [ + "v2.lens.complexity_accumulator_copy.roster_gate", + "v2.std.logic", + "v2.std.text", + "v2.std.live_tree" + ] + }, + ModuleImportFact { + module: "v2.lens.complexity_accumulator_copy.roster_gate", + imports: [] + } +] + +data g2_union_corpus_facts: List = [ + ModuleImportFact { + module: "v2.test.claim.complexity.accumulator_copy_roster_gate", + imports: [ + "v2.lens.complexity_accumulator_copy.roster_gate", + "tools.dag_compile_clean_scope", + "v2.std.logic", + "v2.std.text", + "v2.std.live_tree" + ] + }, + ModuleImportFact { + module: "v2.lens.complexity_accumulator_copy.roster_gate", + imports: [] + }, + ModuleImportFact { + module: "tools.dag_compile_clean_scope", + imports: [] + } +] + +test fn g2_roster_gate_entry_classifies_literal_paths() -> Bool { + let c = live_read_classification_g2_from_facts( + entry_module: "v2.test.claim.complexity.accumulator_copy_roster_gate", + facts: roster_gate_corpus_facts, + decls: fn_arrow_decl_facts_live(), + carrier_homes: live_read_carrier_homes_v0 + ) + classification_has_filesystem_read_carrier(classification: c) + && classification_has_literal_path( + classification: c, + path: "src/v2/std/algebra.dag" + ) +} + +test fn g2_roster_gate_literal_path_touched_selects() -> Bool { + let c = live_read_classification_g2_from_facts( + entry_module: "v2.test.claim.complexity.accumulator_copy_roster_gate", + facts: roster_gate_corpus_facts, + decls: fn_arrow_decl_facts_live(), + carrier_homes: live_read_carrier_homes_v0 + ) + runtime_read_touched_by_path_g2( + classification: c, + touched_path: "src/v2/std/algebra.dag" + ) + && runtime_read_touched_by_path_g2( + classification: c, + touched_path: "src/v2/lens/complexity_accumulator_copy.dag" + ) +} + +test fn g2_unknown_filesystem_path_widens_alongside_literal_paths() -> Bool { + let c = live_read_classification_g2_from_facts( + entry_module: "v2.test.claim.complexity.accumulator_copy_roster_gate", + facts: roster_gate_corpus_facts, + decls: fn_arrow_decl_facts_live(), + carrier_homes: live_read_carrier_homes_v0 + ) + runtime_read_touched_by_path_g2( + classification: c, + touched_path: "src/v2/std/logic.dag" + ) +} + +test fn g2_implies_reads_live_tree_for_roster_gate() -> Bool { + g2_implies_reads_live_tree( + classification: live_read_classification_g2_from_facts( + entry_module: "v2.test.claim.complexity.accumulator_copy_roster_gate", + facts: roster_gate_corpus_facts, + decls: fn_arrow_decl_facts_live(), + carrier_homes: live_read_carrier_homes_v0 + ) + ) + && live_tree_disposition == SubstrateInputsOnly +} + +test fn g2_unknown_path_pattern_fail_closed_touched() -> Bool { + let c = live_read_classification_from_facts( + entry_module: "fix.witness", + facts: fixture_roster_gate_facts, + carrier_homes: fixture_carriers + ) + match c { + RuntimeRead { carriers: carriers } => + runtime_read_touched_by_path_g2( + classification: c, + touched_path: "dag/unrelated.dag" + ) + LocalRead => false + } +} + +test fn g2_union_retains_g1_import_carriers_with_g2_literal_paths() -> Bool { + let c = live_read_classification_g2_from_facts( + entry_module: "v2.test.claim.complexity.accumulator_copy_roster_gate", + facts: g2_union_corpus_facts, + decls: fn_arrow_decl_facts_live(), + carrier_homes: live_read_carrier_homes_v0 + ) + classification_has_corpus_scan_carrier(classification: c) + && classification_has_literal_path( + classification: c, + path: "src/v2/std/algebra.dag" + ) +} + +test fn g2_fn_arrow_marshal_host_scaffold_receipt_is_checkable() -> Bool { + match fn_arrow_skeleton_g2_marshal_host_scaffold_dissolution_trigger { + Scaffold { dissolves_to: _, bind: _ } => true + Terminal { reason: _ } => false + } +}