Skip to main content

brokk_bifrost_cpp/
raii.rs

1//! Provable temporary-free classification of C++ call expressions.
2//!
3//! The C++ lowering marks a callable as having RAII boundaries when its body
4//! contains any call expression, because a call can materialize a class-typed
5//! temporary whose destructor runs at the end of the full expression. That
6//! over-approximation is honest for unknown callees, but it is refutable for
7//! an exact local free-function call whose signature proves that neither the
8//! returned value nor any parameter conversion can materialize an automatic
9//! object that needs destruction (#1984, under #1951).
10//!
11//! [`CppTemporaryFreeCallIndex`] holds the per-file proof. It stays strictly
12//! conservative: any preprocessor content, any second declaration of the
13//! name, any non-callee use of the name (function pointers, address-taking,
14//! shadowing locals, member declarations), templates, overloads, virtual or
15//! member call syntax, default arguments, variadic parameters, and non-trivial
16//! argument expressions all keep the call unproven, so the RAII gap stays.
17
18use tree_sitter::Node;
19
20use crate::declarations::node_text;
21use brokk_bifrost_core::hash::{HashMap, HashSet};
22
23/// Base type specifiers that name provably trivially destructible types.
24/// `type_identifier` names (class types, aliases) stay unproven.
25const TRIVIAL_TYPE_KINDS: &[&str] = &["primitive_type", "sized_type_specifier"];
26
27/// Per-name evidence collected from one translation unit.
28#[derive(Default)]
29struct NameFacts {
30    /// Count of top-level free-function declarations of this name.
31    declarations: usize,
32    /// Whether the single recorded declaration has a provably temporary-free
33    /// signature. Meaningful only while `declarations == 1`.
34    trivial_signature: bool,
35    /// A use of the name outside a callee or its own declarator name
36    /// position: the name may denote something other than the recorded free
37    /// function at some call site.
38    poisoned: bool,
39}
40
41/// The per-file index answering whether a call expression provably
42/// materializes no automatic object that needs destruction.
43pub struct CppTemporaryFreeCallIndex<'a> {
44    source: &'a str,
45    /// `None` when the file itself is unprovable (any preprocessor content
46    /// can introduce declarations this index cannot see).
47    facts: Option<HashMap<&'a str, NameFacts>>,
48    /// Names this file declares only as arrays of a provably trivially
49    /// destructible element type. Subscripting one is the built-in subscript
50    /// operator -- an array type has no user-declarable `operator[]` -- and it
51    /// yields an lvalue of that element type, so such an argument materializes
52    /// no more than a plain identifier does.
53    trivial_arrays: HashSet<&'a str>,
54    /// Named nodes visited while building, for work accounting.
55    visited_nodes: usize,
56}
57
58impl<'a> CppTemporaryFreeCallIndex<'a> {
59    /// Build the index from a parsed C++ translation unit.
60    pub fn build(source: &'a str, root: Node<'a>) -> Self {
61        let mut facts: HashMap<&'a str, NameFacts> = HashMap::default();
62        let mut trivial_arrays: HashSet<&'a str> = HashSet::default();
63        let mut rejected_arrays: HashSet<&'a str> = HashSet::default();
64        let mut declaration_names: HashSet<usize> = HashSet::default();
65        let mut provable_file = true;
66        let mut visited_nodes = 0usize;
67        let mut stack = vec![root];
68        while let Some(node) = stack.pop() {
69            visited_nodes += 1;
70            let kind = node.kind();
71            if kind.starts_with("preproc_") {
72                // An include or macro can declare overloads and names this
73                // index cannot see; nothing in the file stays provable.
74                provable_file = false;
75            }
76            if matches!(
77                kind,
78                "declaration" | "parameter_declaration" | "field_declaration"
79            ) {
80                record_object_declarations(node, source, &mut trivial_arrays, &mut rejected_arrays);
81            }
82            if let Some(candidate) = free_function_candidate(node) {
83                declaration_names.insert(candidate.name.id());
84                let name = node_text(candidate.name, source);
85                let entry = facts.entry(name).or_default();
86                entry.declarations += 1;
87                entry.trivial_signature = candidate.trivial_signature;
88            }
89            if matches!(
90                kind,
91                "identifier" | "field_identifier" | "type_identifier" | "namespace_identifier"
92            ) && !declaration_names.contains(&node.id())
93                && !(kind == "identifier" && is_callee_position(node))
94            {
95                facts.entry(node_text(node, source)).or_default().poisoned = true;
96            }
97            let mut cursor = node.walk();
98            stack.extend(node.named_children(&mut cursor));
99        }
100        for name in &rejected_arrays {
101            trivial_arrays.remove(name);
102        }
103        Self {
104            source,
105            facts: provable_file.then_some(facts),
106            trivial_arrays,
107            visited_nodes,
108        }
109    }
110
111    /// Named nodes visited while building, for lowering work accounting.
112    pub fn visited_nodes(&self) -> usize {
113        self.visited_nodes
114    }
115
116    /// Whether `call` provably materializes no automatic object that needs
117    /// destruction: the callee is one exact, unshadowed, non-template local
118    /// free function whose return and parameter types are provably trivially
119    /// destructible, and every argument is a trivially shaped expression.
120    /// Nested call arguments are accepted here because the caller's scan
121    /// classifies each nested call expression on its own.
122    pub fn call_is_provably_temporary_free(&self, call: Node<'_>) -> bool {
123        debug_assert_eq!(call.kind(), "call_expression");
124        let Some(facts) = &self.facts else {
125            return false;
126        };
127        let Some(callee) = call.child_by_field_name("function") else {
128            return false;
129        };
130        if callee.kind() != "identifier" {
131            return false;
132        }
133        let Some(name) = facts.get(node_text(callee, self.source)) else {
134            return false;
135        };
136        if name.declarations != 1 || !name.trivial_signature || name.poisoned {
137            return false;
138        }
139        let Some(arguments) = call.child_by_field_name("arguments") else {
140            return false;
141        };
142        let mut cursor = arguments.walk();
143        arguments
144            .named_children(&mut cursor)
145            .all(|argument| self.argument_is_trivially_shaped(argument))
146    }
147
148    /// Argument shapes that cannot materialize a class-typed temporary given a
149    /// provably trivial parameter list: names, literals, member accesses
150    /// through `.`, subscripts of a provably trivial array, and nested calls
151    /// (which the caller's scan classifies independently).
152    fn argument_is_trivially_shaped(&self, argument: Node<'_>) -> bool {
153        let mut node = argument;
154        loop {
155            match node.kind() {
156                // Naming a member of an existing object with `.` yields an
157                // lvalue subobject and runs no user code -- `operator.` cannot
158                // be declared in C++ -- so the argument is exactly as
159                // temporary-free as the plain identifier below. `->` may
160                // resolve to a user-defined `operator->`, which is a call
161                // returning whatever it likes.
162                "field_expression" => {
163                    let Some(base) = node.child_by_field_name("argument") else {
164                        return false;
165                    };
166                    let mut cursor = node.walk();
167                    if node
168                        .children(&mut cursor)
169                        .any(|child| matches!(child.kind(), "->" | "->*"))
170                    {
171                        return false;
172                    }
173                    node = base;
174                }
175                "subscript_expression" => {
176                    let Some(base) = node.child_by_field_name("argument") else {
177                        return false;
178                    };
179                    if base.kind() != "identifier"
180                        || !self.trivial_arrays.contains(node_text(base, self.source))
181                    {
182                        return false;
183                    }
184                    let mut cursor = node.walk();
185                    let indices = node
186                        .named_children(&mut cursor)
187                        .filter(|child| child.id() != base.id())
188                        .collect::<Vec<_>>();
189                    if !indices
190                        .iter()
191                        .all(|index| self.subscript_is_trivially_shaped(*index))
192                    {
193                        return false;
194                    }
195                    node = base;
196                }
197                "parenthesized_expression" => {
198                    let mut cursor = node.walk();
199                    let mut children = node.named_children(&mut cursor);
200                    let (Some(inner), None) = (children.next(), children.next()) else {
201                        return false;
202                    };
203                    node = inner;
204                }
205                "identifier"
206                | "number_literal"
207                | "char_literal"
208                | "string_literal"
209                | "concatenated_string"
210                | "true"
211                | "false"
212                | "null"
213                | "nullptr" => return true,
214                "call_expression" => return true,
215                _ => return false,
216            }
217        }
218    }
219
220    /// A subscript operand, which the C++ grammar wraps in a
221    /// `subscript_argument_list` and the C grammar leaves bare.
222    fn subscript_is_trivially_shaped(&self, node: Node<'_>) -> bool {
223        if node.kind() != "subscript_argument_list" {
224            return self.argument_is_trivially_shaped(node);
225        }
226        let mut cursor = node.walk();
227        let children = node.named_children(&mut cursor).collect::<Vec<_>>();
228        children
229            .iter()
230            .all(|child| self.argument_is_trivially_shaped(*child))
231    }
232}
233
234/// Record whether `declaration` declares names as arrays of a provably
235/// trivially destructible element type.
236///
237/// A name declared any other way anywhere in the file is rejected: this index
238/// is name-based and has no scopes, so a second meaning for one name makes
239/// every occurrence of it unprovable.
240fn record_object_declarations<'a>(
241    declaration: Node<'a>,
242    source: &'a str,
243    trivial_arrays: &mut HashSet<&'a str>,
244    rejected: &mut HashSet<&'a str>,
245) {
246    let element_is_trivial = declaration
247        .child_by_field_name("type")
248        .is_some_and(|node| TRIVIAL_TYPE_KINDS.contains(&node.kind()));
249    let mut cursor = declaration.walk();
250    let declarators = declaration
251        .children_by_field_name("declarator", &mut cursor)
252        .collect::<Vec<_>>();
253    for declarator in declarators {
254        let mut current = declarator;
255        let mut is_array = false;
256        // A declarator this walk does not recognize -- a function, a pointer, a
257        // reference -- names no array, and it must not stop the other
258        // declarators of the same declaration from being recorded.
259        let name = loop {
260            match current.kind() {
261                "array_declarator" | "init_declarator" => {
262                    is_array |= current.kind() == "array_declarator";
263                    match current.child_by_field_name("declarator") {
264                        Some(inner) => current = inner,
265                        None => break None,
266                    }
267                }
268                "identifier" => break Some(current),
269                _ => break None,
270            }
271        };
272        let Some(name) = name else {
273            continue;
274        };
275        let name = node_text(name, source);
276        if is_array && element_is_trivial && !rejected.contains(name) {
277            trivial_arrays.insert(name);
278        } else {
279            rejected.insert(name);
280        }
281    }
282}
283
284/// A top-level free-function declaration and whether its signature proves
285/// temporary-free calls.
286struct FreeFunctionCandidate<'a> {
287    name: Node<'a>,
288    trivial_signature: bool,
289}
290
291fn free_function_candidate(node: Node<'_>) -> Option<FreeFunctionCandidate<'_>> {
292    if !matches!(node.kind(), "function_definition" | "declaration") {
293        return None;
294    }
295    // Only translation-unit scope: a template_declaration, class, or
296    // namespace parent leaves the name to the poisoning walk.
297    if node.parent()?.kind() != "translation_unit" {
298        return None;
299    }
300    let type_node = node.child_by_field_name("type")?;
301    let mut declarator = node.child_by_field_name("declarator")?;
302    let mut indirect_return = false;
303    while matches!(
304        declarator.kind(),
305        "pointer_declarator" | "reference_declarator"
306    ) {
307        indirect_return = true;
308        declarator = declarator.child_by_field_name("declarator")?;
309    }
310    if declarator.kind() != "function_declarator" {
311        return None;
312    }
313    let name = declarator.child_by_field_name("declarator")?;
314    if name.kind() != "identifier" {
315        return None;
316    }
317    // A pointer or reference return is trivially destructible regardless of
318    // the pointee; otherwise the written base type must prove it.
319    let trivial_return = indirect_return || TRIVIAL_TYPE_KINDS.contains(&type_node.kind());
320    let trivial_signature = trivial_return
321        && declarator
322            .child_by_field_name("parameters")
323            .is_some_and(|parameters| {
324                let mut cursor = parameters.walk();
325                parameters
326                    .named_children(&mut cursor)
327                    .all(parameter_is_provably_trivial)
328            });
329    Some(FreeFunctionCandidate {
330        name,
331        trivial_signature,
332    })
333}
334
335/// A parameter that provably cannot bind a class-typed temporary: a written
336/// trivially destructible base type, or a pointer at some indirection level.
337/// Default arguments and variadic parameters stay unproven.
338fn parameter_is_provably_trivial(parameter: Node<'_>) -> bool {
339    if parameter.kind() != "parameter_declaration" {
340        return false;
341    }
342    let Some(type_node) = parameter.child_by_field_name("type") else {
343        return false;
344    };
345    if TRIVIAL_TYPE_KINDS.contains(&type_node.kind()) {
346        return true;
347    }
348    // A pointer parameter only ever binds a pointer value; any conversion
349    // from a class argument yields a pointer prvalue, never a class
350    // temporary. A plain reference to a class type can bind a converted
351    // temporary, so it stays unproven.
352    parameter
353        .child_by_field_name("declarator")
354        .is_some_and(declarator_contains_pointer)
355}
356
357fn declarator_contains_pointer(declarator: Node<'_>) -> bool {
358    let mut stack = vec![declarator];
359    while let Some(node) = stack.pop() {
360        if matches!(
361            node.kind(),
362            "pointer_declarator" | "abstract_pointer_declarator"
363        ) {
364            return true;
365        }
366        let mut cursor = node.walk();
367        stack.extend(node.named_children(&mut cursor));
368    }
369    false
370}
371
372fn is_callee_position(node: Node<'_>) -> bool {
373    node.parent().is_some_and(|parent| {
374        parent.kind() == "call_expression"
375            && parent
376                .child_by_field_name("function")
377                .is_some_and(|function| function.id() == node.id())
378    })
379}
380
381#[cfg(test)]
382mod tests {
383    use super::*;
384    use tree_sitter::{Parser, Tree};
385
386    fn parse_cpp(source: &str) -> Tree {
387        let mut parser = Parser::new();
388        parser
389            .set_language(&tree_sitter_cpp::LANGUAGE.into())
390            .expect("cpp language");
391        parser.parse(source, None).expect("cpp tree")
392    }
393
394    /// Every `call_expression` in `source`, paired with its provability.
395    fn classified_calls(source: &str) -> Vec<(String, bool)> {
396        let tree = parse_cpp(source);
397        let index = CppTemporaryFreeCallIndex::build(source, tree.root_node());
398        let mut calls = Vec::new();
399        let mut stack = vec![tree.root_node()];
400        while let Some(node) = stack.pop() {
401            if node.kind() == "call_expression" {
402                calls.push((
403                    node_text(node, source).to_string(),
404                    index.call_is_provably_temporary_free(node),
405                ));
406            }
407            let mut cursor = node.walk();
408            stack.extend(node.named_children(&mut cursor));
409        }
410        calls.sort();
411        calls
412    }
413
414    fn assert_calls(source: &str, expected: &[(&str, bool)]) {
415        let mut expected = expected
416            .iter()
417            .map(|(text, provable)| (text.to_string(), *provable))
418            .collect::<Vec<_>>();
419        expected.sort();
420        assert_eq!(classified_calls(source), expected);
421    }
422
423    /// The #1951 balanced positive: exact local free-function calls with
424    /// provably trivial signatures are temporary-free.
425    #[test]
426    fn exact_local_free_function_calls_are_provable() {
427        assert_calls(
428            r#"
429const char *dfb_source() {
430    return "tainted";
431}
432
433void dfb_sink(const char *value) {}
434
435void run() {
436    dfb_sink(dfb_source());
437}
438"#,
439            &[("dfb_source()", true), ("dfb_sink(dfb_source())", true)],
440        );
441    }
442
443    /// The #1951 balanced negative keeps both calls provable: a discarded
444    /// trivial result and a literal argument.
445    #[test]
446    fn discarded_result_and_literal_argument_are_provable() {
447        assert_calls(
448            r#"
449const char *dfb_source() {
450    return "tainted";
451}
452
453void dfb_sink(const char *value) {}
454
455void run() {
456    dfb_source();
457    dfb_sink("clean");
458}
459"#,
460            &[("dfb_source()", true), ("dfb_sink(\"clean\")", true)],
461        );
462    }
463
464    /// A prototype without a body still proves the signature.
465    #[test]
466    fn local_prototype_is_provable() {
467        assert_calls(
468            r#"
469const char *dfb_source();
470
471void run() {
472    dfb_source();
473}
474"#,
475            &[("dfb_source()", true)],
476        );
477    }
478
479    /// Near miss: overloaded functions stay unproven even when every
480    /// overload is trivially typed.
481    #[test]
482    fn overloaded_functions_stay_unproven() {
483        assert_calls(
484            r#"
485const char *dfb_source() { return "a"; }
486const char *dfb_source(int selector) { return "b"; }
487
488void run() {
489    dfb_source();
490}
491"#,
492            &[("dfb_source()", false)],
493        );
494    }
495
496    /// Near miss: a call through a function pointer stays unproven; the
497    /// pointer declaration is a non-callee use of the name.
498    #[test]
499    fn function_pointer_calls_stay_unproven() {
500        assert_calls(
501            r#"
502const char *real_source() { return "a"; }
503
504void run() {
505    const char *(*dfb_source)() = real_source;
506    dfb_source();
507}
508"#,
509            &[("dfb_source()", false)],
510        );
511    }
512
513    /// Near miss: member call syntax stays unproven, so virtual dispatch
514    /// keeps its RAII gap.
515    #[test]
516    fn virtual_member_calls_stay_unproven() {
517        assert_calls(
518            r#"
519struct Producer {
520    virtual const char *dfb_source() { return "a"; }
521};
522
523void run(Producer *producer) {
524    producer->dfb_source();
525}
526"#,
527            &[("producer->dfb_source()", false)],
528        );
529    }
530
531    /// Near miss: an unqualified call that a same-named member could
532    /// capture stays unproven; the member declaration poisons the name.
533    #[test]
534    fn member_declaration_poisons_unqualified_calls() {
535        assert_calls(
536            r#"
537const char *dfb_source() { return "a"; }
538
539struct Wrapper {
540    const char *dfb_source() { return "b"; }
541    const char *read() { return dfb_source(); }
542};
543"#,
544            &[("dfb_source()", false)],
545        );
546    }
547
548    /// Near miss: template functions stay unproven, with or without
549    /// explicit template arguments.
550    #[test]
551    fn template_functions_stay_unproven() {
552        assert_calls(
553            r#"
554template <typename T>
555T dfb_source() { return T(); }
556
557void run() {
558    dfb_source<const char *>();
559}
560"#,
561            &[("dfb_source<const char *>()", false), ("T()", false)],
562        );
563    }
564
565    /// Near miss: a class return type may construct a destructible
566    /// temporary; the written base type is not provably trivial.
567    #[test]
568    fn class_return_types_stay_unproven() {
569        assert_calls(
570            r#"
571struct Token {};
572Token dfb_source() { return Token{}; }
573
574void run() {
575    dfb_source();
576}
577"#,
578            &[("dfb_source()", false)],
579        );
580    }
581
582    /// Near miss: a const reference parameter of class type can bind a
583    /// converted temporary.
584    #[test]
585    fn class_reference_parameters_stay_unproven() {
586        assert_calls(
587            r#"
588struct Token {};
589void dfb_sink(const Token &value) {}
590
591void run() {
592    dfb_sink(Token{});
593}
594"#,
595            &[("dfb_sink(Token{})", false)],
596        );
597    }
598
599    /// Near miss: default arguments can evaluate arbitrary expressions.
600    #[test]
601    fn default_arguments_stay_unproven() {
602        assert_calls(
603            r#"
604void dfb_sink(const char *value = "d") {}
605
606void run() {
607    dfb_sink();
608}
609"#,
610            &[("dfb_sink()", false)],
611        );
612    }
613
614    /// Near miss: a non-trivial argument expression can materialize a
615    /// class-typed temporary via overloaded operators, even when the
616    /// callee's parameters are trivially typed.
617    #[test]
618    fn complex_argument_expressions_stay_unproven() {
619        assert_calls(
620            r#"
621void dfb_sink(const char *value) {}
622
623void run(const char *left) {
624    dfb_sink(left + 1);
625}
626"#,
627            &[("dfb_sink(left + 1)", false)],
628        );
629    }
630
631    /// Near miss: any preprocessor content can introduce declarations this
632    /// index cannot see, so nothing in the file is provable.
633    #[test]
634    fn preprocessor_content_makes_the_file_unprovable() {
635        assert_calls(
636            r#"
637#include <string>
638
639const char *dfb_source() { return "a"; }
640
641void run() {
642    dfb_source();
643}
644"#,
645            &[("dfb_source()", false)],
646        );
647    }
648
649    /// Near miss: taking the function's address is a non-callee use, so a
650    /// pointer alias can no longer be told apart from the direct call.
651    #[test]
652    fn address_taken_names_stay_unproven() {
653        assert_calls(
654            r#"
655const char *dfb_source() { return "a"; }
656
657void keep(const char *(*pointer)()) {}
658
659void run() {
660    keep(&dfb_source);
661    dfb_source();
662}
663"#,
664            &[("dfb_source()", false), ("keep(&dfb_source)", false)],
665        );
666    }
667
668    /// Pointer returns and pointer parameters are trivial regardless of the
669    /// pointee type; nested parentheses stay transparent.
670    #[test]
671    fn pointer_indirection_over_class_types_is_provable() {
672        assert_calls(
673            r#"
674struct Token;
675Token *dfb_source() { return nullptr; }
676
677void dfb_sink(Token *value) {}
678
679void run() {
680    dfb_sink((dfb_source()));
681}
682"#,
683            &[("dfb_source()", true), ("dfb_sink((dfb_source()))", true)],
684        );
685    }
686
687    /// #2666: an lvalue subobject named through `.`, and a subscript of a name
688    /// this file declares only as an array of arithmetic element type, are the
689    /// built-in operators. Both yield an lvalue and run no user code, so they
690    /// are exactly as temporary-free as the identifier they are rooted at.
691    #[test]
692    fn member_access_and_trivial_array_subscript_arguments_are_provable() {
693        assert_calls(
694            r#"
695struct Holder {
696    int value;
697};
698
699void consume(int value) {}
700
701void run() {
702    Holder holder;
703    int values[2];
704    consume(holder.value);
705    consume(values[0]);
706}
707"#,
708            &[
709                ("consume(holder.value)", true),
710                ("consume(values[0])", true),
711            ],
712        );
713    }
714
715    /// Near miss: a subscript whose base this file does not declare as an
716    /// array of arithmetic element type may resolve through a user-defined
717    /// `operator[]`, which can materialize a class-typed temporary.
718    #[test]
719    fn subscript_of_an_unproven_base_stays_unproven() {
720        assert_calls(
721            r#"
722struct Table {
723    int &operator[](int index);
724};
725
726void consume(int value) {}
727
728void run(Table table) {
729    consume(table[0]);
730}
731"#,
732            &[("consume(table[0])", false)],
733        );
734    }
735}