mod emit;
use std::collections::{BTreeMap, BTreeSet};
use super::arms::{arm_label, split_arms};
use super::error::StructureError;
use super::model::{EdgeKind, GraphEdge, GraphNode, NodeId, NodePrimitive};
use super::reader::{
end_of_call, find_open_brace, last_identifier_argument, leading_local_call, match_brace,
};
use super::scan::Token;
const OPAQUE_SNIPPET_TOKENS: usize = 12;
type Frontier = Vec<NodeId>;
#[derive(Clone, Default)]
struct Region {
head: Option<NodeId>,
tail: Frontier,
primitives: usize,
}
impl Region {
fn empty(frontier: Frontier) -> Self {
Self {
head: None,
tail: frontier,
primitives: 0,
}
}
}
#[derive(Clone, Copy)]
struct FnBody {
start: usize,
end: usize,
}
enum Subject {
Primitive(Region),
Data,
}
#[derive(Clone, Copy)]
struct Checkpoint {
nodes: usize,
edges: usize,
activity_ordinal: usize,
child_ordinal: usize,
control_ordinal: usize,
opaque_ordinal: usize,
}
pub(super) struct ExtractedGraph {
pub(super) nodes: Vec<GraphNode>,
pub(super) edges: Vec<GraphEdge>,
}
pub(super) struct ControlFlowExtractor<'a> {
entry_module: String,
tokens: &'a [Token],
functions: BTreeMap<String, FnBody>,
declared: &'a BTreeSet<&'a str>,
workflow_alias: String,
nodes: Vec<GraphNode>,
edges: Vec<GraphEdge>,
activity_ordinal: usize,
child_ordinal: usize,
control_ordinal: usize,
opaque_ordinal: usize,
}
impl<'a> ControlFlowExtractor<'a> {
pub(super) fn new(
entry_module: String,
tokens: &'a [Token],
workflow_alias: String,
declared: &'a BTreeSet<&'a str>,
) -> Self {
let functions = map_functions(tokens);
Self {
entry_module,
tokens,
functions,
declared,
workflow_alias,
nodes: Vec::new(),
edges: Vec::new(),
activity_ordinal: 0,
child_ordinal: 0,
control_ordinal: 0,
opaque_ordinal: 0,
}
}
pub(super) fn extract(
mut self,
entry_function: &str,
) -> Result<ExtractedGraph, StructureError> {
let body = self.functions.get(entry_function).copied().ok_or_else(|| {
StructureError::EntryFunctionNotFound {
module: self.entry_module.clone(),
function: entry_function.to_owned(),
}
})?;
let mut stack: Vec<String> = vec![entry_function.to_owned()];
self.walk(body, Vec::new(), &mut stack)?;
Ok(ExtractedGraph {
nodes: self.nodes,
edges: self.edges,
})
}
fn walk(
&mut self,
body: FnBody,
frontier: Frontier,
stack: &mut Vec<String>,
) -> Result<Region, StructureError> {
let mut region = Region::empty(frontier);
let mut index = body.start;
while index < body.end {
match &self.tokens[index] {
Token::Ident(word) if word == "case" => {
let (case_region, after) = self.walk_case(index, ®ion.tail, body, stack)?;
region.head = region.head.or(case_region.head);
if !case_region.tail.is_empty() || case_region.primitives > 0 {
region.tail = case_region.tail;
}
region.primitives += case_region.primitives;
index = after;
}
Token::Qualified { left, right } if *left == self.workflow_alias => {
if let Some(primitive) = recognise(right) {
let node = self.emit_primitive(primitive, index)?;
self.sequence(®ion.tail, node);
region.head = region.head.or(Some(node));
region.tail = vec![node];
region.primitives += 1;
index = end_of_call(self.tokens, index, body.end);
continue;
}
if right == "define" {
let after = self.follow_define(index, body.end, &mut region, stack)?;
index = after;
continue;
}
index += 1;
}
Token::Ident(name) if self.is_local_call(name, index) => {
let name = name.clone();
let call = self.follow_named(&name, ®ion.tail, stack)?;
if call.primitives > 0 {
region.head = region.head.or(call.head);
region.tail = call.tail;
region.primitives += call.primitives;
}
index = end_of_call(self.tokens, index, body.end);
}
_ => index += 1,
}
}
Ok(region)
}
fn follow_define(
&mut self,
index: usize,
end: usize,
region: &mut Region,
stack: &mut Vec<String>,
) -> Result<usize, StructureError> {
let after = end_of_call(self.tokens, index, end);
if let Some(entry) = last_identifier_argument(self.tokens, index + 1, after) {
let call = self.follow_named(&entry, ®ion.tail, stack)?;
if call.primitives > 0 {
region.head = region.head.or(call.head);
region.tail = call.tail;
region.primitives += call.primitives;
}
}
Ok(after)
}
fn walk_pruned(
&mut self,
body: FnBody,
frontier: Frontier,
stack: &mut Vec<String>,
) -> Result<Region, StructureError> {
let checkpoint = self.checkpoint();
let region = self.walk(body, frontier.clone(), stack)?;
if region.primitives == 0 {
self.restore(checkpoint);
return Ok(Region::empty(frontier));
}
Ok(region)
}
fn walk_case(
&mut self,
case_index: usize,
frontier: &Frontier,
body: FnBody,
stack: &mut Vec<String>,
) -> Result<(Region, usize), StructureError> {
let Some(brace) = find_open_brace(self.tokens, case_index + 1, body.end) else {
return Ok((self.opaque_region(case_index, body.end, frontier), body.end));
};
let Some(close) = match_brace(self.tokens, brace, body.end) else {
return Ok((self.opaque_region(case_index, body.end, frontier), body.end));
};
let scrutinee = (case_index + 1, brace);
let arms = split_arms(self.tokens, brace + 1, close);
let region = match self.classify_subject(scrutinee, frontier, stack)? {
Subject::Primitive(subject) => self.durable_branch(&subject, &arms, stack)?,
Subject::Data => self.data_case(&arms, frontier, stack)?,
};
Ok((region, close + 1))
}
fn durable_branch(
&mut self,
subject: &Region,
arms: &[super::arms::Arm],
stack: &mut Vec<String>,
) -> Result<Region, StructureError> {
let branch = self.emit_branch();
self.sequence(&subject.tail, branch);
let head = subject.head.or(Some(branch));
let mut merged: Frontier = Vec::new();
let mut has_terminal_arm = false;
for arm in arms {
let label = arm_label(self.tokens, arm);
let arm_body = FnBody {
start: arm.body_start,
end: arm.body_end,
};
let arm_region = self.walk_pruned(arm_body, Vec::new(), stack)?;
if let Some(arm_head) = arm_region.head {
self.edges.push(GraphEdge {
from: branch,
to: arm_head,
kind: EdgeKind::Branch { arm: label },
});
merged.extend(arm_region.tail);
} else {
has_terminal_arm = true;
}
}
if has_terminal_arm || merged.is_empty() {
merged.push(branch);
}
Ok(Region {
head,
tail: merged,
primitives: subject.primitives.max(1),
})
}
fn data_case(
&mut self,
arms: &[super::arms::Arm],
frontier: &Frontier,
stack: &mut Vec<String>,
) -> Result<Region, StructureError> {
let mut bearing: Vec<(super::model::ArmLabel, Region)> = Vec::new();
for arm in arms {
let label = arm_label(self.tokens, arm);
let arm_body = FnBody {
start: arm.body_start,
end: arm.body_end,
};
let arm_region = self.walk_pruned(arm_body, Vec::new(), stack)?;
if arm_region.head.is_some() {
bearing.push((label, arm_region));
}
}
match bearing.len() {
0 => Ok(Region::empty(frontier.clone())),
1 => {
let (_, arm) = bearing.remove(0);
if let Some(head) = arm.head {
self.sequence(frontier, head);
}
Ok(Region {
head: arm.head,
tail: arm.tail,
primitives: arm.primitives,
})
}
_ => {
let branch = self.emit_branch();
self.sequence(frontier, branch);
let mut merged: Frontier = Vec::new();
let mut primitives = 0;
for (label, arm) in bearing {
if let Some(arm_head) = arm.head {
self.edges.push(GraphEdge {
from: branch,
to: arm_head,
kind: EdgeKind::Branch { arm: label },
});
}
merged.extend(arm.tail);
primitives += arm.primitives;
}
Ok(Region {
head: Some(branch),
tail: merged,
primitives: primitives.max(1),
})
}
}
}
fn classify_subject(
&mut self,
scrutinee: (usize, usize),
frontier: &Frontier,
stack: &mut Vec<String>,
) -> Result<Subject, StructureError> {
let (start, end) = scrutinee;
if let Some(prim_index) = self.scrutinee_primitive(start, end) {
if let Token::Qualified { right, .. } = &self.tokens[prim_index] {
if let Some(primitive) = recognise(right) {
let node = self.emit_primitive(primitive, prim_index)?;
self.sequence(frontier, node);
return Ok(Subject::Primitive(Region {
head: Some(node),
tail: vec![node],
primitives: 1,
}));
}
}
}
if let Some(name) = leading_local_call(self.tokens, start, end) {
if self.functions.contains_key(&name) {
let call = self.follow_named(&name, frontier, stack)?;
if call.primitives > 0 {
return Ok(Subject::Primitive(call));
}
}
}
Ok(Subject::Data)
}
fn follow_named(
&mut self,
name: &str,
frontier: &Frontier,
stack: &mut Vec<String>,
) -> Result<Region, StructureError> {
if stack.iter().any(|frame| frame == name) {
return Ok(Region::empty(frontier.clone()));
}
let Some(callee) = self.functions.get(name).copied() else {
return Ok(Region::empty(frontier.clone()));
};
stack.push(name.to_owned());
let region = self.walk_pruned(callee, frontier.clone(), stack)?;
stack.pop();
Ok(region)
}
fn opaque_region(&mut self, start: usize, end: usize, frontier: &Frontier) -> Region {
let node = self.emit_opaque(start, end);
self.sequence(frontier, node);
Region {
head: Some(node),
tail: vec![node],
primitives: 1,
}
}
fn is_local_call(&self, name: &str, index: usize) -> bool {
self.functions.contains_key(name)
&& matches!(self.tokens.get(index + 1), Some(Token::OpenParen))
}
fn scrutinee_primitive(&self, start: usize, end: usize) -> Option<usize> {
for index in start..end {
if let Token::Qualified { left, right } = &self.tokens[index] {
if *left == self.workflow_alias {
return recognise(right).map(|_| index);
}
return None;
}
}
None
}
}
fn recognise(member: &str) -> Option<NodePrimitive> {
match member {
"run" => Some(NodePrimitive::Run),
"all" => Some(NodePrimitive::All),
"race" => Some(NodePrimitive::Race),
"map" => Some(NodePrimitive::Map),
"spawn" => Some(NodePrimitive::Spawn),
"spawn_and_wait" => Some(NodePrimitive::SpawnAndWait),
"receive" => Some(NodePrimitive::Receive),
"sleep" => Some(NodePrimitive::Sleep),
"start_timer" => Some(NodePrimitive::StartTimer),
"cancel_timer" => Some(NodePrimitive::CancelTimer),
_ => None,
}
}
fn map_functions(tokens: &[Token]) -> BTreeMap<String, FnBody> {
let mut functions = BTreeMap::new();
let mut index = 0;
while index < tokens.len() {
if matches!(&tokens[index], Token::Ident(word) if word == "fn") {
if let Token::Ident(name) = tokens.get(index + 1).unwrap_or(&Token::Other(' ')) {
if let Some(open) = find_open_brace(tokens, index + 2, tokens.len()) {
if let Some(close) = match_brace(tokens, open, tokens.len()) {
functions.insert(
name.clone(),
FnBody {
start: open + 1,
end: close,
},
);
index = close + 1;
continue;
}
}
}
}
index += 1;
}
functions
}
#[cfg(test)]
mod unit {
use super::*;
#[test]
fn recognise_covers_the_vocabulary() {
assert_eq!(recognise("run"), Some(NodePrimitive::Run));
assert_eq!(
recognise("spawn_and_wait"),
Some(NodePrimitive::SpawnAndWait)
);
assert_eq!(recognise("now"), None);
}
#[test]
fn map_functions_finds_bodies() {
let tokens = super::super::scan::tokenise(
"pub fn execute(input) { workflow.run(x) }\nfn helper() { ok }\n",
);
let map = map_functions(&tokens);
assert!(map.contains_key("execute"));
assert!(map.contains_key("helper"));
}
}