diff --git a/bin/frama-c-script b/bin/frama-c-script
index 0179f39113f1bc5b953a841c2fc23a41f0d5fc72..1843c1da6696c9873e12e26bdfb7b0153ab9eaf4 100755
--- a/bin/frama-c-script
+++ b/bin/frama-c-script
@@ -70,6 +70,22 @@ usage() {
    echo "      and non-POSIX external libraries."
    echo "      (run 'frama-c -machdep help' to get the list of machdeps)."
    echo ""
+   echo "  - heuristic-print-callgraph [--dot outfile] file..."
+   echo "      Prints a heuristic, syntactic-based, callgraph for the"
+   echo "      specified files. Use --dot outfile to print it in DOT"
+   echo "      (Graphviz) format, to [outfile]. If [outfile] is '-',"
+   echo "      prints to stdout."
+   echo ""
+   echo "  - heuristic-detect-recursion file..."
+   echo "      Uses a heuristic, syntactic-based, callgraph to detect recursive"
+   echo "      calls. Results are guaranteed neither correct nor complete."
+   echo ""
+   echo "  - heuristic-list-functions want_defs want_decls file..."
+   echo "      Uses heuristics to find possible function definitions and declarations."
+   echo "      If [want_defs] is true, lists definitions."
+   echo "      If [want_decls] is true, lists declarations."
+   echo "      Results are guaranteed neither correct nor complete."
+   echo ""
    echo "  - make-wrapper target arg..."
    echo "      Runs 'make target arg...', parsing the output to suggest"
    echo "      useful commands in case of failure."
@@ -215,6 +231,18 @@ case "$command" in
         shift;
         configure_for_frama_c "$@";
         ;;
+    "heuristic-print-callgraph")
+        shift;
+        ${FRAMAC_SHARE}/analysis-scripts/print_callgraph.py "$@";
+        ;;
+    "heuristic-detect-recursion")
+        shift;
+        ${FRAMAC_SHARE}/analysis-scripts/detect_recursion.py "$@";
+        ;;
+    "heuristic-list-functions")
+        shift;
+        ${FRAMAC_SHARE}/analysis-scripts/heuristic_list_functions.py "$@";
+        ;;
     "make-wrapper")
         shift;
         "${FRAMAC_SHARE}"/analysis-scripts/make_wrapper.py "$0" "$@";
diff --git a/share/analysis-scripts/build_callgraph.py b/share/analysis-scripts/build_callgraph.py
index 7d0e7e28c53c30721ff883ef99aaef1f727fc063..47a18b40477be12aaada19b262f5b2cd152e11bf 100755
--- a/share/analysis-scripts/build_callgraph.py
+++ b/share/analysis-scripts/build_callgraph.py
@@ -28,93 +28,126 @@
 import sys
 import os
 import re
-import glob
 import function_finder
 
-MIN_PYTHON = (3, 5) # for glob(recursive)
+MIN_PYTHON = (3, 5)
 if sys.version_info < MIN_PYTHON:
     sys.exit("Python %s.%s or later is required.\n" % MIN_PYTHON)
 
-debug = os.getenv("DEBUG")
-
 arg = ""
 if len(sys.argv) < 2:
-   print(f"usage: {sys.argv[0]} [file1 file2 ...]")
-   print("       builds a heuristic callgraph for the specified files.")
-   sys.exit(1)
+    print(f"usage: {sys.argv[0]} file...")
+    print("        builds a heuristic callgraph for the specified files.")
+    sys.exit(1)
 else:
-   files = sys.argv[1:]
-
-
-'''
-re_fun = function_finder.prepare_definition_regex()
-for f in files:
-    (found, match) = function_finder.find_first_match(re_fun, f)
-    if match:
-       fname = match.group(1)
-    else:
-        print(f"No function declaration or definition found in {f} !")
-    if found:
-        if found == 1:
-            print(f"Found declarator for {fname.upper()}, ignoring !")
+    files = sys.argv[1:]
+
+class Callgraph:
+    """
+    Heuristics-based callgraphs.
+    Nodes are function names. Edges (caller, callee, locations) contain the source
+    and target nodes, plus a list of locations (file, line) where calls from
+    [caller] to [callee] occur.
+    """
+
+    # maps each caller to the list of its callees
+    succs = {}
+
+    # maps (caller, callee) to the list of call locations
+    edges = {}
+
+    def add_edge(self, caller, callee, loc):
+        if (caller, callee) in self.edges:
+            # edge already exists
+            self.edges[(caller, callee)].append(loc)
         else:
-            print(f"Found definition for {fname.upper()} !")
-'''
-
-re_function_def = function_finder.prepare_definition_regex()
-re_function_call = r"[a-zA-Z_][a-zA-Z0-9_]*\s*\("
-# here, get for each loop iteration a tuple (Match Object, int) of the name of the Match Object in file and it's line
-# find_definitions is a generator function so appending all iterations results is to be expected in order to get a data_structure to further process
-def function_definition_mapper(regex, File):
-    return [x for x in function_finder.find_definitions(regex, File, 0)]
-
-def function_calls_mapper(regex, File):
-    return [x for x in function_finder.find_calls(regex, File, 0)]
-
-# here starts the inspection of each of the files passed in command line
-for f in files:
-    if debug:
-        print(f"Entering file {os.path.relpath(f)}:")
-    function_defs = function_definition_mapper(re_function_def, f)
-    if not function_defs:
-        if debug:
-            print(f"No call or potential call found in file {f} !")
-        continue
-    # function_ranges is a list of [name_function_def, (int, int)]
-    # its size == len(function_defs)
-    # name_function_def is a string
-    # (int, int) is a tuple of that describe the range of name_function_def
-    function_ranges = []
-    [[function_ranges.append((element[0].group(1), (element[1], function_defs[i + 1][1] - 1)))\
-    for i, element in enumerate(function_defs) if i < len(function_defs) - 1]]
-    function_calls = function_calls_mapper(re_function_call,f)
-    if not function_calls:
-        print(f"No definition found in file {f} !")
-        continue
-    if debug:
-        for i in range(len(function_ranges)):
-            print(f"{function_ranges[i]}")
-        print("\n")
-        for i in range(len(function_calls)):
-            print(f"{function_calls[i][0].group(0)} appears at line {function_calls[i][1]}")
-        print("\n")
-    func_def_calls = []
-    # for each function call:
-    #   Go through function_def list
-    #       keep range of current function_def 
-    #       check if current function call is in that range
-    #       append a tuple (string, string, int) as (function_being_defined, function_being_called, line of function call) to the list
-    for i in range(len(function_calls)):
-        for index, e in enumerate(function_ranges):
-            min_range, max_range = e[1]
-            if min_range < function_calls[i][1] <= max_range:
-                func_def_calls.append((e[0], function_calls[i][0].group(0)[:-1], function_calls[i][1]))
-    for i in range(len(func_def_calls)):
-        print(f"{os.path.relpath(f)}:{func_def_calls[i][2]}: {func_def_calls[i][0]} -> {func_def_calls[i][1]}")
-
-
-
-
-
-
-
+            # new edge: check if caller exists
+            if not caller in self.succs:
+                self.succs[caller] = []
+            # add callee as successor of caller
+            self.succs[caller].append(callee)
+            # add call location to edge information
+            self.edges[(caller, callee)] = [loc]
+
+    def nodes(self):
+        return self.succs.keys()
+
+    def __repr__(self):
+        return f"Callgraph({self.succs}, {self.edges})"
+
+def compute(files):
+    #print(f"Computing callgraph for {len(files)} file(s)...")
+    cg = Callgraph()
+    for f in files:
+        #print(f"Processing {os.path.relpath(f)}...")
+        newlines = function_finder.compute_newline_offsets(f)
+        defs = function_finder.find_definitions_and_declarations(True, False, f, newlines)
+        calls = function_finder.find_calls(f, newlines)
+        for call in calls:
+            caller = function_finder.find_caller(defs, call)
+            if caller:
+                called = call[0]
+                line = call[1]
+                loc = (f, line)
+                cg.add_edge(caller, called, loc)
+    #print(f"Callgraph computed ({len(cg.succs)} node(s), {len(cg.edges)} edge(s))")
+    return cg
+
+def print_edge(cg, caller, called, padding="", end="\n"):
+    locs = cg.edges[(caller, called)]
+    for (filename, line) in locs:
+        print(f"{padding}{os.path.relpath(filename)}:{line}: {caller} -> {called}", end=end)
+
+def print_cg(cg):
+    for (caller, called) in cg.edges:
+        print_edge(cg, caller, called)
+
+# succs: dict, input, not modified
+# visited: set, input-output, modified
+# just_visited: set, input-output, modified
+# n: input, not modified
+#
+# The difference between visited and just_visited is that the latter refers
+# to the current dfs; nodes visited in previous dfs already had their cycles
+# reported, so we do not report them multiple times.
+def cycle_dfs(cg, visited, just_visited, n):
+    just_visited.add(n)
+    if n not in cg.succs:
+        return []
+    for succ in cg.succs[n]:
+        if succ in just_visited:
+            return [(n, succ)]
+        elif succ in visited:
+            # already reported in a previous iteration
+            return []
+        else:
+            res = cycle_dfs(cg, visited, just_visited, succ)
+            if res:
+                caller = res[0][0]
+                return [(n, caller)] + res
+            else:
+                return []
+    return []
+
+def detect_recursion(cg):
+    #print(f"Detecting recursive calls...")
+    to_visit = set(cg.nodes())
+    #if len(to_visit) > 100:
+    #    print(f"Checking recursion ({len(to_visit)} nodes)...")
+    if not to_visit: # empty graph -> no recursion
+        return False
+    visited = set()
+    has_cycle = False
+    while to_visit:
+        just_visited = set()
+        n = sorted(list(to_visit))[0]
+        cycle = cycle_dfs(cg, visited, just_visited, n)
+        visited = visited.union(just_visited)
+        if cycle:
+            has_cycle = True
+            print(f"recursive cycle detected: ")
+            for (caller, called) in cycle:
+                print_edge(cg, caller, called, padding="  ")
+        to_visit -= visited
+    if not has_cycle:
+        print(f"no recursive calls detected")
diff --git a/share/analysis-scripts/detect_recursion.py b/share/analysis-scripts/detect_recursion.py
new file mode 100755
index 0000000000000000000000000000000000000000..ab3302352e8c03918212ded663b59add4c4c44e4
--- /dev/null
+++ b/share/analysis-scripts/detect_recursion.py
@@ -0,0 +1,44 @@
+#!/usr/bin/env python3
+#-*- coding: utf-8 -*-
+##########################################################################
+#                                                                        #
+#  This file is part of Frama-C.                                         #
+#                                                                        #
+#  Copyright (C) 2007-2020                                               #
+#    CEA (Commissariat à l'énergie atomique et aux énergies              #
+#         alternatives)                                                  #
+#                                                                        #
+#  you can redistribute it and/or modify it under the terms of the GNU   #
+#  Lesser General Public License as published by the Free Software       #
+#  Foundation, version 2.1.                                              #
+#                                                                        #
+#  It is distributed in the hope that it will be useful,                 #
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of        #
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         #
+#  GNU Lesser General Public License for more details.                   #
+#                                                                        #
+#  See the GNU Lesser General Public License version 2.1                 #
+#  for more details (enclosed in the file licenses/LGPLv2.1).            #
+#                                                                        #
+##########################################################################
+
+# This script finds files containing likely declarations and definitions
+# for a given function name, via heuristic syntactic matching.
+
+import sys
+import build_callgraph
+
+MIN_PYTHON = (3, 5) # for glob(recursive)
+if sys.version_info < MIN_PYTHON:
+    sys.exit("Python %s.%s or later is required.\n" % MIN_PYTHON)
+
+arg = ""
+if len(sys.argv) < 2:
+    print(f"usage: {sys.argv[0]} [file1 file2 ...]")
+    print("        prints a heuristic callgraph for the specified files.")
+    sys.exit(1)
+else:
+    files = sys.argv[1:]
+
+cg = build_callgraph.compute(files)
+build_callgraph.detect_recursion(cg)
diff --git a/share/analysis-scripts/find_fun.py b/share/analysis-scripts/find_fun.py
index 18588b293bd0452317f4ae72f90e8b396775c3a1..e50d3d3f287e7caa95998cc614ee3a82e8e9ff5b 100755
--- a/share/analysis-scripts/find_fun.py
+++ b/share/analysis-scripts/find_fun.py
@@ -73,9 +73,9 @@ print("Looking for '%s' inside %d file(s)..." % (fname, len(files)))
 
 possible_declarators = []
 possible_definers = []
-re_fun = function_finder.prepare(fname)
+re_fun = function_finder.prepare_re_specific_name(fname)
 for f in files:
-    found = function_finder.find(re_fun, f)
+    found = function_finder.find_specific_name(re_fun, f)
     if found:
         if found == 1:
             possible_declarators.append(f)
@@ -95,7 +95,7 @@ else:
     relative_path = relative_path_to(reldir)
     if possible_declarators != []:
         print(f"Possible declarations for function '{fname}' in the following file(s){reldir_msg}:")
-        print("  " + "\n  ".join([os.path.relpath(path, start=reldir) for path in possible_declarators]))
+        print("  " + "\n  ".join(sorted([os.path.relpath(path, start=reldir) for path in possible_declarators])))
     if possible_definers != []:
         print(f"Possible definitions for function '{fname}' in the following file(s){reldir_msg}:")
-        print("  " + "\n  ".join([os.path.relpath(path, start=reldir) for path in possible_definers]))
+        print("  " + "\n  ".join(sorted([os.path.relpath(path, start=reldir) for path in possible_definers])))
diff --git a/share/analysis-scripts/function_finder.py b/share/analysis-scripts/function_finder.py
index 43a58ce665b413b8fc0e38467e63bbc747d60ad9..cfa761757e3e1bd34493e318a530eddcba4d620e 100755
--- a/share/analysis-scripts/function_finder.py
+++ b/share/analysis-scripts/function_finder.py
@@ -24,6 +24,8 @@
 
 # Exports find_function_in_file, to be used by other scripts
 
+import bisect
+import os
 import re
 
 # To minimize the amount of false positives, we try to match the following:
@@ -37,18 +39,27 @@ import re
 # - identifiers are allowed after the parentheses, to allow for some macros/
 #   modifiers
 
+# auxiliary regexes
+c_identifier = "[a-zA-Z_][a-zA-Z0-9_]*"
+c_id_maybe_pointer = c_identifier + "\**"
+optional_c_id = "(?:" + c_identifier + ")?"
+non_empty_whitespace = "[ \t\r\n]+" # includes newline/CR
+whitespace = "[ \t\r\n]*" # includes newline/CR
+type_prefix = c_id_maybe_pointer + "(?:\s+\**" + c_id_maybe_pointer + ")*" + non_empty_whitespace + "\**"
+optional_type_prefix = "(?:" + type_prefix + whitespace + ")?"
+argument_list = "\([^)]*\)"
+
+debug = bool(os.getenv("DEBUG", False))
+
 # Precomputes the regex for 'fname'
-def prepare(fname):
-    c_identifier = "[a-zA-Z_][a-zA-Z0-9_]*"
-    c_id_maybe_pointer = c_identifier + "\**"
-    type_prefix = c_id_maybe_pointer + "(?:\s+\**" + c_id_maybe_pointer + ")*\s+\**"
-    parentheses_suffix = "\s*\([^)]*\)"
-    re_fun = re.compile("^(?:" + type_prefix + "\s*)?" + fname + parentheses_suffix
-                        + "\s*(?:" + c_identifier + ")?\s*(;|{)", flags=re.MULTILINE)
+def prepare_re_specific_name(fname):
+    re_fun = re.compile("^" + optional_type_prefix + fname + whitespace +
+                        argument_list + whitespace +
+                        optional_c_id + whitespace + "(;|{)", flags=re.DOTALL | re.MULTILINE)
     return re_fun
 
 # Returns 0 if not found, 1 if declaration, 2 if definition
-def find(prepared_re, f):
+def find_specific_name(prepared_re, f):
    with open(f, encoding="ascii", errors='ignore') as content_file:
       content = content_file.read()
       has_decl_or_def = prepared_re.search(content)
@@ -58,84 +69,138 @@ def find(prepared_re, f):
          is_decl = has_decl_or_def.group(1) == ";"
          return 1 if is_decl else 2
 
-# Precomputes the regex for a function
-def prepare_definition_regex():
-    c_identifier = "[a-zA-Z_][a-zA-Z0-9_]*"
-    c_id_maybe_pointer = c_identifier + "\**"
-    type_prefix = c_id_maybe_pointer + "(?:\s+\**" + c_id_maybe_pointer + ")*\s+\**"
-    parentheses_suffix = "\s*\([^)]*\)"
-    re_fun = re.compile("^(?:" + type_prefix + "\s*)?" + "([a-zA-Z_][a-zA-Z0-9_]*)" + parentheses_suffix
-                        + "\s*(?:" + c_identifier + ")?\s*(;|{)", flags=re.MULTILINE)
-    return re_fun
 
-# Returns tuple (0, None) if not found, (1, match) if declaration, (2, match) if definition
-# Returns only at the first match of {prepared_re} regex in file {f}, not iterable unlike find3 function
-def find_first_match(prepared_re, f):
-   with open(f, encoding="ascii", errors='ignore') as content_file:
-      content = content_file.read()
-      match = prepared_re.search(content)
-      if match is None:
-          return (0, None)
-      else:
-         is_decl = match.group(2) == ";"
-         return (1, match) if is_decl else (2, match)
-
-
-def find_definitions(pattern, f, flags):
-    with open(f, encoding="ascii", errors='ignore') as content_file:
-        content = content_file.read()
-        # create a list of Match objects that fit "pattern" regex
-        matches = list(re.finditer(pattern, content, flags))
-        if not matches:
-            return (None, 0)
-        # keep the offset of the last occurence of the matching pattern in the content string
-        # to avoiding checking past the last occurence in the newline_table
-        end = matches[-1].start()
-        newline_table = {-1: 0}
-        for i, m in enumerate(re.finditer(r'\n', content, 1)):
-            offset = m.start()
-            if offset > end:
-                break
-            # + 1 because convention states lines in a file start at 1
-            # + 1 because we are counting newlines
-            newline_table[offset] = i + 1 + 1
-    for m in matches:
-        newline_offset = content.rfind('\n', 0, m.start())
-        # delete declarator and corresponding newline_table element if a ; is found instead of {
-        if m.group(2) == ';':
-            del(newline_table[newline_offset])
-            del(m)
+# matches function definitions
+re_fundef_or_decl = re.compile("^" + optional_type_prefix +
+                               "(" + c_identifier + ")" + whitespace +
+                               argument_list + whitespace +
+                               optional_c_id + whitespace + "(;|{)",
+                               flags=re.DOTALL | re.MULTILINE)
+
+# matches function calls
+re_funcall = re.compile("(" + c_identifier + ")" + whitespace + "\(")
+
+# Computes the offset (in bytes) of each '\n' in the file,
+# returning them as a list
+def compute_newline_offsets(filename):
+    offsets = []
+    current = 0
+    with open(filename, encoding="ascii", errors='ignore') as data:
+        for line in data:
+            current += len(line)
+            offsets.append(current)
+    return offsets
+
+# Returns the line number (starting at 1) containing the character
+# of offset [offset].
+# [offsets] is the sorted list of offsets for newline characters in the file.
+def line_of_offset(offsets, offset):
+    i = bisect.bisect_right(offsets, offset)
+    return i+1
+
+# Returns the line number (starting at 1) of each line starting with '}'
+# as its first character.
+#
+# This is a heuristic to attempt to detect function closing braces:
+# it assumes that the first '}' (without preceding whitespace) after a
+# function definition denotes its closing brace.
+def compute_closing_braces(filename):
+    braces = []
+    with open(filename, encoding="ascii", errors='ignore') as data:
+        for i, line in enumerate(data, start=1):
+            if line.startswith("}"):
+               braces.append(i)
+    return braces
+
+# Returns the first element of [line_numbers] greater than [n], or [None]
+# if all numbers are smaller than [n] (this may happen e.g. when no
+# closing braces were found).
+#
+# [line_numbers] must be sorted in ascending order.
+def get_first_line_after(line_numbers, n):
+    for line in line_numbers:
+        if line > n:
+            return line
+    return None
+
+# Returns a list of tuples (fname, is_def, line_start, line_end, terminator_offset)
+# for each function definition or declaration.
+# If [want_defs] is True, definitions are included.
+# If [want_decls] is True, declarations are included.
+# [terminator_offset] is the byte offset of the `{` or `;`.
+# The list is sorted w.r.t. line numbers (in ascending order).
+#
+# [terminator_offset] is used by the caller to filter the function prototype
+# itself and avoid considering it as a call. For function definitions,
+# this is the opening brace; for function declarations, this is the semicolon.
+def find_definitions_and_declarations(want_defs, want_decls, filename, newlines):
+    braces = compute_closing_braces(filename)
+    with open(filename, encoding="ascii", errors='ignore') as data:
+        content = data.read()
+    res = []
+    for match in re.finditer(re_fundef_or_decl, content):
+        funcname = match.group(1)
+        is_def = match.group(2) == "{"
+        is_decl = match.group(2) == ";"
+        assert is_def or is_decl
+        start = line_of_offset(newlines, match.start(1))
+        if is_decl:
+            if not want_decls:
+                continue
+            end = line_of_offset(newlines, match.start(2))
         else:
-            line_number = newline_table[newline_offset]
-            yield (m, line_number)
-    return (None, 0)
-
-def find_calls(pattern, f, flags):
-    with open(f, encoding="ascii", errors='ignore') as content_file:
-        content = content_file.read()
-        # create a list of Match objects that fit "pattern" regex
-        matches = list(re.finditer(pattern, content, flags))
-        if not matches:
-            return (None, 0)
-        # Here, we filter all occurences of C keywords captured as possible func calls
-        # replace `filtre` assignement by a more exhaustive list for better results
-        filtre = ["if", "return", "while", "for"]
-        matches = [n for n in matches if not any(m in n[0] for m in filtre)]
-
-        # keep the offset of the last occurence of the matching pattern in the content string
-        # to avoiding checking past the last occurence in the newline_table
-        end = matches[-1].start()
-        newline_table = {-1: 0}
-        for i, m in enumerate(re.finditer(r'\n', content, 1)):
-            offset = m.start()
-            if offset > end:
-                break
-            # + 1 because convention states lines in a file start at 1
-            # + 1 because we are counting newlines
-            newline_table[offset] = i + 1 + 1
- 
-    for m in matches:
-        newline_offset = content.rfind('\n', 0, m.start())
-        line_number = newline_table[newline_offset]
-        yield (m, line_number)
-    return (None, 0)
+            if not want_defs:
+                continue
+            end = get_first_line_after(braces, start)
+            if not end:
+                # no closing braces found; use "single-line function heuristic":
+                # assume the function is defined as 'type f(...) { code; }',
+                # in a single line
+                def_start_newline_offset = newlines[start-1]
+                line_of_opening_brace = line_of_offset(newlines, match.start(2))
+                definition = content[match.start(1):newlines[start-1]]
+                if start == line_of_opening_brace and definition.rstrip().endswith("}"):
+                    # assume the '}' is closing the '{' from the same line
+                    end = line_of_opening_brace
+                else:
+                    # no opening brace; assume a false positive and skip definition
+                    print(f"{os.path.relpath(filename)}:{start}:closing brace not found, " +
+                          f"skipping potential definition of '{funcname}'")
+                    continue
+        terminator_offset = match.start(2)
+        res.append((funcname, is_def, start, end, terminator_offset))
+    return res
+
+# list of identifiers which are never function calls
+calls_blacklist = ["if", "while", "for", "return"]
+
+# Returns a list of tuples (fname, line, offset) for each function call.
+#
+# Note: this may include the function prototype itself;
+# it must be filtered by the caller.
+def find_calls(filename, newlines):
+    with open(filename, encoding="ascii", errors='ignore') as data:
+        content = data.read()
+    # create a list of Match objects that fit "pattern" regex
+    res = []
+    for match in re.finditer(re_funcall, content):
+        funcname = match.group(1)
+        offset = match.start(1)
+        line = line_of_offset(newlines, offset)
+        if funcname not in calls_blacklist:
+            res.append((funcname, line, offset))
+    return res
+
+# Returns the caller of [call], that is, the function whose definition
+# contains the line where [call] happens.
+# Returns [None] if there is no function at such line (i.e. a false positive).
+#
+# [defs] must be sorted in ascending order.
+def find_caller(defs, call):
+    (called, line, offset) = call
+    for (fname, _is_def, start, end, brace_offset) in defs:
+        if line >= start and line <= end and offset > brace_offset:
+            return fname
+        elif start > line:
+            return None
+    return None
diff --git a/share/analysis-scripts/heuristic_list_functions.py b/share/analysis-scripts/heuristic_list_functions.py
new file mode 100755
index 0000000000000000000000000000000000000000..86df5e9d7fcc53324371749b4dcaf65d4e10c4c6
--- /dev/null
+++ b/share/analysis-scripts/heuristic_list_functions.py
@@ -0,0 +1,57 @@
+#!/usr/bin/env python3
+#-*- coding: utf-8 -*-
+##########################################################################
+#                                                                        #
+#  This file is part of Frama-C.                                         #
+#                                                                        #
+#  Copyright (C) 2007-2020                                               #
+#    CEA (Commissariat à l'énergie atomique et aux énergies              #
+#         alternatives)                                                  #
+#                                                                        #
+#  you can redistribute it and/or modify it under the terms of the GNU   #
+#  Lesser General Public License as published by the Free Software       #
+#  Foundation, version 2.1.                                              #
+#                                                                        #
+#  It is distributed in the hope that it will be useful,                 #
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of        #
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         #
+#  GNU Lesser General Public License for more details.                   #
+#                                                                        #
+#  See the GNU Lesser General Public License version 2.1                 #
+#  for more details (enclosed in the file licenses/LGPLv2.1).            #
+#                                                                        #
+##########################################################################
+
+# This script uses heuristics to list all function definitions and
+# declarations in a set of files.
+
+import sys
+import os
+import re
+import function_finder
+
+MIN_PYTHON = (3, 5) # for glob(recursive)
+if sys.version_info < MIN_PYTHON:
+    sys.exit("Python %s.%s or later is required.\n" % MIN_PYTHON)
+
+debug = bool(os.getenv("DEBUG", False))
+
+arg = ""
+if len(sys.argv) < 4:
+   print("usage: %s want_defs want_decls file..." % sys.argv[0])
+   print("       looks for likely function definitions and/or declarations")
+   print("       in the specified files.")
+   sys.exit(1)
+
+want_defs = sys.argv[1]
+want_decls = sys.argv[2]
+files = sys.argv[3:]
+
+for f in files:
+    newlines = function_finder.compute_newline_offsets(f)
+    defs_and_decls = function_finder.find_definitions_and_declarations(want_defs, want_decls, f, newlines)
+    for (funcname, is_def, start, end, _offset) in defs_and_decls:
+        if is_def:
+            print(f"{os.path.relpath(f)}:{start}:{end}: {funcname} (definition)")
+        else:
+            print(f"{os.path.relpath(f)}:{start}:{end}: {funcname} (declaration)")
diff --git a/share/analysis-scripts/make_template.py b/share/analysis-scripts/make_template.py
index c986e4551c446b47caff148e869d65ef1713c58d..6e47b60c60993be74bbc2d40a32e9cd45150f880 100755
--- a/share/analysis-scripts/make_template.py
+++ b/share/analysis-scripts/make_template.py
@@ -98,11 +98,11 @@ main = input("Main target name: ")
 if not re.match("^[a-zA-Z_0-9-]+$", main):
     sys.exit("error: invalid main target name (can only contain letters, digits, dash or underscore)")
 
-main_fun_finder_re = function_finder.prepare("main")
+main_fun_finder_re = function_finder.prepare_re_specific_name("main")
 
 # returns 0 if none, 1 if declaration, 2 if definition
 def defines_or_declares_main(f):
-    return function_finder.find(main_fun_finder_re, f)
+    return function_finder.find_specific_name(main_fun_finder_re, f)
 
 def expand_and_normalize_sources(expression, relprefix):
     subexps = shlex.split(expression)
diff --git a/share/analysis-scripts/print_callgraph.py b/share/analysis-scripts/print_callgraph.py
new file mode 100755
index 0000000000000000000000000000000000000000..5071c4ba4ccfe85c3066eb77048169326aaec3ab
--- /dev/null
+++ b/share/analysis-scripts/print_callgraph.py
@@ -0,0 +1,44 @@
+#!/usr/bin/env python3
+#-*- coding: utf-8 -*-
+##########################################################################
+#                                                                        #
+#  This file is part of Frama-C.                                         #
+#                                                                        #
+#  Copyright (C) 2007-2020                                               #
+#    CEA (Commissariat à l'énergie atomique et aux énergies              #
+#         alternatives)                                                  #
+#                                                                        #
+#  you can redistribute it and/or modify it under the terms of the GNU   #
+#  Lesser General Public License as published by the Free Software       #
+#  Foundation, version 2.1.                                              #
+#                                                                        #
+#  It is distributed in the hope that it will be useful,                 #
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of        #
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         #
+#  GNU Lesser General Public License for more details.                   #
+#                                                                        #
+#  See the GNU Lesser General Public License version 2.1                 #
+#  for more details (enclosed in the file licenses/LGPLv2.1).            #
+#                                                                        #
+##########################################################################
+
+# This script finds files containing likely declarations and definitions
+# for a given function name, via heuristic syntactic matching.
+
+import sys
+import build_callgraph
+
+MIN_PYTHON = (3, 5) # for glob(recursive)
+if sys.version_info < MIN_PYTHON:
+    sys.exit("Python %s.%s or later is required.\n" % MIN_PYTHON)
+
+arg = ""
+if len(sys.argv) < 2:
+    print(f"usage: {sys.argv[0]} [file1 file2 ...]")
+    print("        prints a heuristic callgraph for the specified files.")
+    sys.exit(1)
+else:
+    files = sys.argv[1:]
+
+cg = build_callgraph.compute(files)
+build_callgraph.print_cg(cg)
diff --git a/tests/fc_script/build-callgraph.i b/tests/fc_script/build-callgraph.i
new file mode 100644
index 0000000000000000000000000000000000000000..b8105502931616ce2861af2459303855112b449a
--- /dev/null
+++ b/tests/fc_script/build-callgraph.i
@@ -0,0 +1,69 @@
+/* run.config
+   NOFRAMAC: testing frama-c-script, not frama-c itself
+   EXECNOW: LOG build-callgraph.res LOG build-callgraph.err bin/frama-c-script heuristic-print-callgraph @PTEST_DIR@/@PTEST_NAME@.i > @PTEST_DIR@/result/build-callgraph.res 2> @PTEST_DIR@/result/build-callgraph.err
+ */
+
+#include <stdio.h>
+void main() {
+  strlen("");
+}
+struct s {
+  int a; int b;
+} s;
+
+volatile int v;
+
+int fn2(int, int);
+
+int fn1(int x, int y)
+{
+  Frama_C_show_each_1(x);
+  Frama_C_show_each_2(y);
+  return x + y;
+}
+
+int X, Y;
+int main1 () {
+  R1 = fn1(G, G|0);
+  R2 = fn2(G, G|0);
+  Frama_C_show_each_d(G);
+  pv = (int *) &X;
+  return Y;
+}
+
+int * main2() {
+  return 0;
+}
+
+#define not_a_function_call(v)                  \
+  yes_a_function_call(v)
+
+#define yet_another_not_a_call(v) do {          \
+    yes_again();                                \
+  } while (0)
+
+/* call_inside_comment(evaluation); */
+void main3 () {
+  //@ not_a_function_call(v);
+  yet_another_not_a_call(v);
+}
+
+/* Tests the initialization of local variables. */
+void main4 () {
+  f(g(h(i, j), k (l, m  (  n(
+                             o, p(
+                                  q)
+                             )
+                           )
+                  )
+      )
+    );
+  f();
+  g();
+}
+void main() {
+  main1();
+  main2();
+  main3();
+  main4();
+}
diff --git a/tests/fc_script/list_functions.i b/tests/fc_script/list_functions.i
new file mode 100644
index 0000000000000000000000000000000000000000..f30547abb73355730c35f523382b6b37a21f7aea
--- /dev/null
+++ b/tests/fc_script/list_functions.i
@@ -0,0 +1,4 @@
+/* run.config
+   NOFRAMAC: testing frama-c-script, not frama-c itself
+   EXECNOW: LOG list_functions.res LOG list_functions.err bin/frama-c-script heuristic-list-functions @PTEST_DIR@/*.c @PTEST_DIR@/*.i > @PTEST_DIR@/result/list_functions.res 2> @PTEST_DIR@/result/list_functions.err
+ */
diff --git a/tests/fc_script/oracle/build-callgraph.err b/tests/fc_script/oracle/build-callgraph.err
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/tests/fc_script/oracle/build-callgraph.res b/tests/fc_script/oracle/build-callgraph.res
new file mode 100644
index 0000000000000000000000000000000000000000..145f1bccaf869fe75bcfed8dbd2834e13396ee33
--- /dev/null
+++ b/tests/fc_script/oracle/build-callgraph.res
@@ -0,0 +1,21 @@
+tests/fc_script/build-callgraph.i:8: main -> strlen
+tests/fc_script/build-callgraph.i:20: fn1 -> Frama_C_show_each_1
+tests/fc_script/build-callgraph.i:21: fn1 -> Frama_C_show_each_2
+tests/fc_script/build-callgraph.i:27: main1 -> fn1
+tests/fc_script/build-callgraph.i:28: main1 -> fn2
+tests/fc_script/build-callgraph.i:29: main1 -> Frama_C_show_each_d
+tests/fc_script/build-callgraph.i:47: main3 -> not_a_function_call
+tests/fc_script/build-callgraph.i:48: main3 -> yet_another_not_a_call
+tests/fc_script/build-callgraph.i:53: main4 -> f
+tests/fc_script/build-callgraph.i:61: main4 -> f
+tests/fc_script/build-callgraph.i:53: main4 -> g
+tests/fc_script/build-callgraph.i:62: main4 -> g
+tests/fc_script/build-callgraph.i:53: main4 -> h
+tests/fc_script/build-callgraph.i:53: main4 -> k
+tests/fc_script/build-callgraph.i:53: main4 -> m
+tests/fc_script/build-callgraph.i:53: main4 -> n
+tests/fc_script/build-callgraph.i:54: main4 -> p
+tests/fc_script/build-callgraph.i:65: main -> main1
+tests/fc_script/build-callgraph.i:66: main -> main2
+tests/fc_script/build-callgraph.i:67: main -> main3
+tests/fc_script/build-callgraph.i:68: main -> main4
diff --git a/tests/fc_script/oracle/find_fun1.res b/tests/fc_script/oracle/find_fun1.res
index 0ceb3c8c68b411868c7fd156287a3c668e09466c..7d423014f36cda2c6e9e7f405bb272dd0f42d8e3 100644
--- a/tests/fc_script/oracle/find_fun1.res
+++ b/tests/fc_script/oracle/find_fun1.res
@@ -1,5 +1,6 @@
-Looking for 'main2' inside 11 file(s)...
+Looking for 'main2' inside 14 file(s)...
 Possible declarations for function 'main2' in the following file(s):
   tests/fc_script/for-find-fun.c
 Possible definitions for function 'main2' in the following file(s):
+  tests/fc_script/build-callgraph.i
   tests/fc_script/main2.c
diff --git a/tests/fc_script/oracle/find_fun2.res b/tests/fc_script/oracle/find_fun2.res
index e59924a72f49f8278f2ff8fcec02062b9c68b2e3..ded016c9272e4dccb55c80cbb45e4fe3115c214b 100644
--- a/tests/fc_script/oracle/find_fun2.res
+++ b/tests/fc_script/oracle/find_fun2.res
@@ -1,5 +1,6 @@
-Looking for 'main3' inside 11 file(s)...
+Looking for 'main3' inside 14 file(s)...
 Possible declarations for function 'main3' in the following file(s):
   tests/fc_script/for-find-fun2.c
 Possible definitions for function 'main3' in the following file(s):
+  tests/fc_script/build-callgraph.i
   tests/fc_script/for-find-fun.c
diff --git a/tests/fc_script/oracle/find_fun3.res b/tests/fc_script/oracle/find_fun3.res
index 6d151d463efe8d8df1a1ac874653880564ee4eb1..4a42d9117089960eb086c47a8e4b07b44795e3c3 100644
--- a/tests/fc_script/oracle/find_fun3.res
+++ b/tests/fc_script/oracle/find_fun3.res
@@ -1,2 +1,2 @@
-Looking for 'false_positive' inside 11 file(s)...
+Looking for 'false_positive' inside 14 file(s)...
 No declaration/definition found for function 'false_positive'
diff --git a/tests/fc_script/oracle/recursions.err b/tests/fc_script/oracle/recursions.err
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/tests/fc_script/oracle/recursions.res b/tests/fc_script/oracle/recursions.res
new file mode 100644
index 0000000000000000000000000000000000000000..784a72aeb5ffd1c48837607bd3707eff14280562
--- /dev/null
+++ b/tests/fc_script/oracle/recursions.res
@@ -0,0 +1,8 @@
+recursive cycle detected: 
+  tests/fc_script/recursions.i:13: f -> f
+recursive cycle detected: 
+  tests/fc_script/recursions.i:18: h -> h
+recursive cycle detected: 
+  tests/fc_script/recursions.i:34: k -> l
+  tests/fc_script/recursions.i:38: l -> m
+  tests/fc_script/recursions.i:42: m -> k
diff --git a/tests/fc_script/recursions.i b/tests/fc_script/recursions.i
new file mode 100644
index 0000000000000000000000000000000000000000..d471e8c461858cf371f040e13d2abb1e1763b0fa
--- /dev/null
+++ b/tests/fc_script/recursions.i
@@ -0,0 +1,46 @@
+/* run.config
+   NOFRAMAC: testing frama-c-script, not frama-c itself
+   EXECNOW: LOG recursions.res LOG recursions.err bin/frama-c-script heuristic-detect-recursion @PTEST_FILE@ > @PTEST_DIR@/result/recursions.res 2> @PTEST_DIR@/result/recursions.err
+*/
+
+volatile int v;
+
+void g() {
+  int g = 42;
+}
+
+void f() {
+  if (v) f();
+  else g();
+}
+
+void h() {
+  if (v) h();
+  else g();
+}
+
+void i() {
+  g();
+}
+
+void j() {
+  f();
+}
+
+void l(void);
+void m(void);
+
+void k() {
+  if (v) l();
+}
+
+void l() {
+  if (v) m();
+}
+
+void m() {
+  if (v) k();
+}
+
+void norec() {
+}