diff --git a/src/v2/std/lens_verdict.dag b/src/v2/std/lens_verdict.dag index 44ac62c3fc6..4d4432e97eb 100644 --- a/src/v2/std/lens_verdict.dag +++ b/src/v2/std/lens_verdict.dag @@ -19,7 +19,10 @@ import v2.std.diagnostic { textual_whole_file_diagnostic, } import v2.std.integer { integer_int_to_decimal_string } +import extdeps.languages.json.emit { quote_json_string } import v2.std.node { Node, Symbol } +import v2.std.grammar { node_atom_identity_optional } +import v2.std.optional { Absent, Present } import v2.std.qualified_name { qualified_name_to_dotted_string } import v2.std.text { String } @@ -94,13 +97,35 @@ fn lens_verdict_extent_text(e: Extent) -> String { // source graph that minted it; resolving it to a byte range needs that graph's SpanIndex, which // the stages past parse do not carry -- so this renders the identity the diagnostic HAS rather // than fabricating a file it does not. +// +// AN ATOM ANCHOR ALSO NAMES ITS SYMBOL. When the anchor is an atom -- a bare reference, the node a +// resolver refusal is located at -- its identity is in the node the diagnostic already carries, +// and the persisted row keeps it; rendering only the ordinal dropped the one word a reader needs +// ("which symbol was ambiguous?") on the text surface while the JSON row still had it. Composite +// anchors carry no single name and render as before. Whether a node is an atom, and which, is +// `v2.std.grammar` `node_atom_identity_optional`'s question -- asked there, not re-destructured here. +// +// THE SPELLING IS QUOTED BY ONE LOSSLESS AUTHORITY, NOT PASTED (side-chat hold on gunbc#12004). A +// symbol's lexeme is any string: pasted between delimiters, an embedded delimiter breaks the +// quoting and a newline or ESC goes onto the operator's line raw. `extdeps.languages.json.emit` +// `quote_json_string` is the repository's modeled RFC 8259 string quotation -- total, lossless and +// unambiguous (backslash and quote escaped, every control character a visible escape) -- so the +// atom renders as a JSON string literal and no second escaping rule is written here. +fn lens_verdict_node_atom_suffix(node: Node) -> String { + match node_atom_identity_optional(node: node) { + Present { value: identity } => concat(" ", quote_json_string(s: symbol_lexeme(sym: identity))) + Absent => "" + } +} + fn lens_verdict_node_occurrence_text(node: Node) -> String { + let atom = lens_verdict_node_atom_suffix(node: node) match node.occurrence_id { - OccurrenceSynthetic => "" + OccurrenceSynthetic => concat("")) OccurrenceMinted { id: id } => - concat("")) + concat(""))) OccurrenceProjected { id: id, caused_by: _ } => - concat("")) + concat(""))) } } diff --git a/src/v2/test/claim/native_route/native_refusal_detail_test.dag b/src/v2/test/claim/native_route/native_refusal_detail_test.dag index 134a2d31215..955a0cd1df5 100644 --- a/src/v2/test/claim/native_route/native_refusal_detail_test.dag +++ b/src/v2/test/claim/native_route/native_refusal_detail_test.dag @@ -45,12 +45,16 @@ import v2.std.diagnostic { SourcePortLocus, Textual, diagnostics_fatal, - diagnostics_fatal_reason + diagnostics_fatal_reason, + ExternalContractUnknown, + Unavailable, + node_locus } import v2.std.lens_verdict { lens_verdict_diagnostic_locus_text } import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } import v2.std.logic { Bool } -import v2.std.node { Symbol } +import v2.std.node { Symbol, Atom, Conj, TypeNode, node_synthetic } +import v2.std.compilers.lexing { symbol_intern_lexeme } import v2.std.optional { Absent, Optional, Present, optional_absent, optional_present } import v2.std.qualified_name { qualified_name_from_dotted_string } import v2.std.text { String } @@ -361,6 +365,18 @@ test fn operator_line_renders_the_source_occurrence_holds() -> Bool { } } +// AND THE OCCURRENCE NAMES ITS SYMBOL. The unbound refusal is anchored at the atom +// ndp_absent_callee; the persisted row carried that identity while the text line rendered only +// the ordinal, so a reader of the line could not tell which reference failed. A locus renderer +// that dropped the atom reds here. +test fn operator_line_names_the_refused_symbol_holds() -> Bool { + match ndp_unbound_refusal() { + Absent => false + Present { value: d } => + string_contains(s: lens_verdict_diagnostic_locus_text(d: diagnostics_fatal(d: d)), pattern: " \"ndp_absent_callee\">") + } +} + // THE DENOMINATOR: a module that resolves. Without it every claim above is satisfied by a route // that refuses everything for any reason. test fn clean_module_resolves_holds() -> Bool { @@ -374,3 +390,39 @@ test fn clean_module_resolves_holds() -> Bool { } } } + +// ── the atom spelling is QUOTED, losslessly, through the real locus renderer (side-chat hold on +// gunbc#12004). Each arm renders a node-anchored diagnostic whose anchor atom's lexeme carries the +// hostile characters, and asserts the EXACT text: a raw paste reds the escaping arms, and a +// renderer that dropped the name reds the ordinary one. + +fn atom_locus_text(lexeme: String) -> String { + lens_verdict_diagnostic_locus_text(d: Diagnostic { + reason: ^native_refusal_detail_quoting_probe, + at: node_locus(node: node_synthetic(kind: TypeNode { connective: Atom { identity: symbol_intern_lexeme(lexeme: lexeme) } }, children: [])), + correction: Unavailable { reason: ExternalContractUnknown } + }) +} + +test fn an_ordinary_atom_renders_readably_holds() -> Bool { + atom_locus_text(lexeme: "SubstrateInputsOnly") == "" +} + +test fn an_embedded_backtick_and_backslash_stay_unambiguous_holds() -> Bool { + atom_locus_text(lexeme: concat("left`right", concat(from_code_point(cp: 92), "x"))) + == concat(""))) +} + +test fn newline_cr_and_esc_become_visible_escapes_holds() -> Bool { + let bs = from_code_point(cp: 92) + atom_locus_text(lexeme: concat("a", concat(from_code_point(cp: 10), concat("b", concat(from_code_point(cp: 13), concat("c", concat(from_code_point(cp: 27), "d"))))))) + == concat("")))))) +} + +test fn a_composite_anchor_renders_unchanged_holds() -> Bool { + lens_verdict_diagnostic_locus_text(d: Diagnostic { + reason: ^native_refusal_detail_quoting_probe, + at: node_locus(node: node_synthetic(kind: TypeNode { connective: Conj }, children: [])), + correction: Unavailable { reason: ExternalContractUnknown } + }) == "" +}