The Systemtap Translator - a tour on the inside

Outline:
- general principles
- main data structures
- pass 1: parsing
- pass 2: semantic analysis (parts 1, 2, 3)
- pass 3: translation (parts 1, 2)
- pass 4: compilation
- pass 5: run

------------------------------------------------------------------------
Translator general principles

- written in standard C++
- mildly O-O, sparing use of C++ features
- uses "visitor" concept for type-dependent (virtual) traversal

------------------------------------------------------------------------
Main data structures

- abstract syntax tree <staptree.h>
  - family of types and subtypes for language parts: expressions,
    literals, statements
  - includes outermost constructs: probes, aliases, functions
  - an instance of "stapfile" represents an entire script file
  - each annotated with a token (script source coordinates)
  - data persists throughout run

- session <session.h>
  - contains run-time parameters from command line
  - contains all globals
  - passed by reference to many functions

------------------------------------------------------------------------
Pass 1 - parsing

- hand-written recursive-descent <parse.cxx>
- language specified in man page <stap.1>
- reads user-specified script file
- also searches path for all <*.stp> files, parses them too
- => syntax errors are caught immediately, throughout tapset
- now includes baby preprocessor
     probe kernel.
     %( kernel_v == "2.6.9" %? inline("foo") %: function("bar") %)
     { }
- enforces guru mode for embedded code %{ C %}

------------------------------------------------------------------------
Pass 2 - semantic analysis - step 1: resolve symbols

- code in <elaborate.cxx>
- want to know all global and per-probe/function local variables
- one "vardecl" instance interned per variable
- fills in "referent" field in AST for nodes that refer to it
- collect "needed" probe/global/function list in session variable
- loop over file queue, starting with user script "stapfile"
  - add to "needed" list this file's globals, functions, probes
  - resolve any symbols used in this file (function calls, variables)
    against "needed" list
  - if not resolved, search through all tapset "stapfile" instances;
    add to file queue if matched
  - if still not resolved, create as local scalar, or signal an error

------------------------------------------------------------------------
Pass 2 - semantic analysis - step 2: resolve types

- fills in "type" field in AST
- iterate along all probes and functions, until convergence
- infer types of variables from usage context / operators:
    a = 5   #  a is a pe_long
    b["foo",a]++  # b is a pe_long array with indexes pe_string and pe_long
- loop until no further variable types can be inferred
- signal error if any still unresolved

------------------------------------------------------------------------
Pass 2 - semantic analysis - step 3: resolve probes

- probe points turned to "derived_probe" instances by code in <tapsets.cxx>
- derived_probes know how to talk to kernel API for registration/callbacks
- aliases get expanded at this point
- some probe points ("begin", "end", "timer*") are very simple
- dwarf ("kernel*", "module*") implementation very complicated
  - target-variables "$foo" expanded to getter/setter functions
    with synthesized embedded-C

------------------------------------------------------------------------
Pass 3 - translation - step 1: data

- <translate.cxx>
- we now know all types, all variables
- strings are everywhere copied by value (MAXSTRINGLEN bytes)
- emit data storage mega-struct "context" for all probes/functions
- array instantiated per-CPU, per-nesting-level
- can be pretty big static data

------------------------------------------------------------------------
Pass 3 - translation - step 2: code

- map script functions to C functions taking a context pointer
- map probes to two C functions:
  - one to interface with the probe point infrastructure (kprobes,
    kernel timer): reserves per-cpu context
  - one to implement probe body, just like a script function
- emit global startup/shutdown routine to manage orderly 
  registration/deregistration of probes
- expressions/statements emitted in "natural" evaluation sequence
- emit code to enforce activity-count limits, simple safety tests
- global variables protected by locks
    global k
    function foo () { k ++ }  # write lock around increment
    probe bar { if (k>5) ... } # read lock around read
- same thing for arrays, except foreach/sort take longer-duration locks

------------------------------------------------------------------------
Pass 4 - compilation

- <buildrun.cxx>
- write out C code in a temporary directory
- call into kbuild makefile to build module

------------------------------------------------------------------------
Pass 5 - running

- run "staprun"
- clean up temporary directory

- nothing to it!

------------------------------------------------------------------------
Peculiarities

- We tend to use visitor idioms for polymorphic traversals of parse
  trees, in preference to dynamic_cast<> et al.  The former is a
  little more future-proof and harder to break accidentally.
  {reinterpret,static}_cast<> should definitely be avoided.

- We use our interned_string type (a derivative of boost::string_ref)
  to use shareable references to strings that may be used in duplicate
  many times.  It can slide in for std::string most of the time.  It
  can save RAM and maybe even CPU, if used judiciously: such as for
  frequently duplicated strings, duplicated strings, duplicated strings,
  duplicated.
  
  OTOH, it costs CPU (for management of the interned string set, or if
  copied between std::string and interned_string unnecessarily), and
  RAM (2 pointers when empty, vs. 1 for std::string), and its
  instances are not modifiable, so tradeoffs must be confirmed with
  tools like memusage, massif, perf-stat, etc.
