dodo dodo / dodoc
dodo docs github
dodo / dna
open format
dodo dna
a lineage format for data workflows. every variable and result, traced to raw data.

Dodo DNA records data provenance in research code. It traces each variable and each statistical result back through its ancestors to raw source data. This page is the complete, normative definition of the formats. Version 0.6.0.

This specification is CC0 — anyone may implement it. The format is called Dodo DNA. It is used in Dodo products like Dodo Review and, soon, Dodo Studio.

architecture

Dodo DNA is built in steps. Source code passes through five representations. Each arrow in the diagram is one translation step. Each representation has its own section below.

source text
   │  parse (one tree-sitter grammar per language)
   ▼
CST         native concrete syntax tree, per-language node names
   │  harmonize (per-language mapping table)
   ▼
hCST        one fixed vocabulary of kinds over the native tree
   │  lower — canonical names are assigned here, once
   ▼
Expr IR     Sym | Lit | Call, language-independent expression trees
   │  extract
   ▼
DNA         one flat document per repository: files, symbols, ops
   │  slice
   ▼
RNA         per-target backward slices of the DNA document

Two rules hold at every step of this pipeline. First: function names are translated to their canonical names in one place, the lowering step. No later step renames anything. Second: when the tools cannot determine a value from the code alone, they do not fail and they do not guess. They record the fact with a confidence marker and continue.

The five supported source languages are Stata, Python, R, Julia, and MATLAB. The Stata grammar is our own; it is open source (MIT) at codedthinking/tree-sitter-stata.

the hCST

Every language parses into its own tree, and every language names its tree nodes differently. A function call is a call node in Python, a call_expression node in Julia, and a funcall node in Stata. Code that reads five kinds of trees must know five sets of names.

The hCST (harmonized concrete syntax tree) removes this problem. It is a read-only view over the native tree. The view gives each node one kind from one fixed list. The two tables below are that list: eleven statement kinds and thirteen expression kinds.

Three properties of the view:

Language tags: py Python, r R, jl Julia, m MATLAB, st Stata.

statement kinds

kindfieldsdefinition
programchildrenfile root
statementchildone logical statement; transparent in printed output
commandname, args[], options[], cond?, range?, weight?, using?, prefix?word-command statement: [prefix:] name args [=exp] [if cond] [in range] [weight] [using] [, options]. An =exp appears as an assign in args[0]
optionname, payloadcommand option. payload is one opaque node
varlist_itemname, ops[]variable reference in a command varlist. ops holds factor and time-series markers (i., L2.)
assignlhs, rhsbinding of rhs to the target lhs
loopvar?, domain?, bodyiteration statement
blockchildrenbraced or indented statement group
commenttext
macro_refnameunexpanded Stata macro reference (`x', $x)
errornative parse-error node

expression kinds

kindfieldsdefinition
callcallee, receiver?, argsfunction application. Method receivers stay in receiver
argschildrenargument container of a call
argumentvaluepositional argument; transparent in printed output
kwargname, valuenamed argument
binopop, lhs, rhsinfix operation. op is the literal operator text
unopop, operandprefix or postfix operation
indexbase, subscripts[]element or column access of base
rangefrom?, to?, step?interval. A missing to is an open bound
accessorbase, fieldmember access (x.y, df$y)
arrayrows[][]vector or matrix literal
formulalhs?, rhsmodel formula (y ~ x); one-sided when lhs is absent
identifiertextname
literaltextconstant, printed with a class: num, str, bool, null

mapping rules

the Expr IR

The hCST still contains language-specific detail. The Expr IR (intermediate representation) removes it. Every expression lowers into a tree built from only three node types:

Expr = Sym | Lit | Call

Rules:

canonical vocabulary

Canonical names are single words with no hyphens. When one language has the clearest word for a concept, that word wins. Julia separates minimum(x) (smallest element of a collection) from min(a, b) (smaller of two values), so the vocabulary does too.

groupcanonical names
mathlog log10 exp sqrt abs floor ceil round
reductionssum mean std minimum maximum count median
pairwisemin max
runningcumsum (Stata's expression-context sum() maps here)
logicismissing ifelse
time serieslag (Call(lag, x, n); surface forms include Stata L2.x, shift, lag)
stringsstrlen lowercase uppercase trim substr split strpos contains regexmatch regexreplace concat interpolate tostring tonumber
structureindex slice vcat hcat formula end
operatorsadd sub mul div pow gt lt ge le eq ne and or not

Example. The same computation in five languages lowers to one Expr:

gen d = cond(missing(x), 0, x)        // Stata
np.where(df["x"].isna(), 0, df["x"])  #  Python
ifelse(is.na(x), 0, x)                #  R
ifelse.(ismissing.(df.x), 0, df.x)    #  Julia

  s-expression:  (call ifelse (call ismissing (sym x)) (lit 0) (sym x))
  rendered:      ifelse(ismissing(x), 0, x)

the DNA document

The DNA document is one flat file per repository, written in KDL — a small document language, similar to JSON but with named nodes. The document has three sections. The order of nodes is meaningful: it is the identity and the tiebreak rule for everything downstream.

dna "repo" spec="0.6" {
  files { ... }
  symbols { ... }
  ops { ... }
}

Every node has the same shape. The node name is a verb or a kind. The positional arguments are the operands. The properties (key=value) are metadata; their values are scalars. The children hold repeated or detailed content. Names stay clean: uncertainty is recorded in properties, never as marks on names.

files

Files, dependency edges, and execution-order results.

files {
  script "analysis.do"
  data "raw/firms.dta"
  link from="config.do" to="analysis.do" basis="..." confidence="evidenced" {
    evidence "analysis.do" 1
  }
  topo-order "config.do" "analysis.do"
  execution-chain "config.do" "analysis.do"
  entry-points "config.do"
  pipeline {
    scripts "config.do" "analysis.do"
    output key="tables/table3.tex" rule="research-output-extension"
  }
}

Node names carry the file kind: script, data, figure, config, doc, other. The path is the first positional argument. link edges are the ground truth for dependencies. Diagnostics (cycle, orphan-scripts, chain-ambiguity, unsupported-languages) are list or property nodes.

symbols

One node per symbol. The kind is the node name; the display name is the first positional argument. Kinds: variable, dataset, local, global, scalar, frame, tempfile.

symbols {
  variable "main.lnwage" inferred-type="numeric" description="natural log of wage" {
    def "analysis.do" 2
    use "analysis.do" 7
  }
  dataset "firms.dta" origin="external" {
    use "analysis.do" 1
  }
  global "controls" origin="runtime" confidence="unknown" {
    use "analysis.do" 7
  }
}

Three properties describe a symbol's status. origin says where the symbol comes from: external (a source file), runtime (exists only when the code runs), derived (computed from other symbols), or merged. confidence follows the table below. note is optional free text; it may add detail but never replaces the structured properties.

ops

One op per source statement, grouped per script in execution order, in source order within a script. Eleven verbs:

verbmeaning
loada data file becomes the working rowset
mergerowsets combine (type= cardinality, keys= join keys; merge-type="append" for row stacking)
assigna column is created or modified; the first argument is the target, the rest are source columns
leta named value is defined (local.x, global.g, scalar.s); a foldable right-hand side carries its resolved value=
filterrow selection
sortrow reordering
collapseaggregation that replaces the rowset
reshapewide/long restructuring
savethe rowset is written to a file
emita statistical result is produced (estimator=, depvar=, regressors=; to= names an output file)
ops {
  script "analysis.do" {
    load "firms.dta" line=1
    assign "lnwage" "wage" expr="gen lnwage = log(wage)" line=2 {
      ir "(call log (sym wage))"
    }
    assign "lag_emp" "emp" by="firmid" expr="bysort firmid: gen lag_emp = L.emp" line=7 {
      ir "(call lag (sym emp) (lit 1))"   // renders: lag(emp, 1)
    }
    emit "lnwage" "lag_emp" estimator="ols" depvar="lnwage" line=10
  }
}

Six properties modify a verb. They are not verbs themselves:

Source names in an assign are bare column names. Detail such as lag depth lives in the ir child. The verbatim source line stays in expr= as the citation.

confidence

Four values, on ops, symbols, links, and Expr nodes alike:

valuemeaning
(absent)resolved statically
abbrevresolved through name abbreviation
runtimethe value exists only at run time; the defining expression is known
unknowncould not be resolved statically

The rule is uniform: mark, never guess, never fail.

RNA slices

The DNA document answers: what does this repository do? A reviewer usually asks a narrower question: where does this one number come from? An RNA slice answers that question. It contains every operation upstream of one target — and nothing else — nested, newest first. Slices are derived and disposable. The DNA document is the ground truth they are cut from.

Two slice shapes. A symbol slice roots at the latest definition of one variable. A result slice roots at one emit.

symbol "main.lag_emp" {
  description "lag_emp = previous-period emp; defined analysis.do:7; on rows of firms.dta"
  assign "lag_emp" by="firmid" expr="bysort firmid: gen lag_emp = L.emp" file="analysis.do" line=7 {
    from "emp" lag=1 {
      column "emp" of="firms.dta"
    }
    rows {
      load "firms.dta" file="analysis.do" line=1
    }
  }
  consumers {
    emit "lnwage" "lag_emp" estimator="ols" file="analysis.do" line=10
  }
}

version history

versionchange
0.6.0vocabulary simplified to plain single words (dna/files/symbols/ops, confidence, sort, def/use, consumers, ref); semantics unchanged
0.5.0Expr IR (ir child on ops); bare source names
0.4.0scopes for named values
0.3.0value ops (let) and the confidence lattice
0.2.0two-layer model: flat document + slices