Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ pub mod domain;
pub mod formatting;
pub mod highlighting;
pub mod language;
pub mod linking;
pub mod parsing;
pub mod program;
pub(crate) mod regex;
Expand Down
102 changes: 102 additions & 0 deletions src/linking/checks/linker.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// Hand-written check suite for the linking phase. Source strings are parsed
// and translated through the real pipeline, then linked against a stub
// `Library`, matching what the runner sees in production.

use std::path::Path;

use crate::linking::{link, LinkingError};
use crate::parsing;
use crate::program::{Executable, ExecutableRef, Operation};
use crate::runner::Library;
use crate::translation::translate;

fn first_execute<'a, 'i>(op: &'a Operation<'i>) -> Option<&'a Executable<'i>> {
match op {
Operation::Execute(executable) => Some(executable),
Operation::Sequence(ops) => ops
.iter()
.find_map(first_execute),
Operation::Section { body, .. } => first_execute(body),
Operation::Step { body, .. } => first_execute(body),
Operation::Loop { over, body, .. } => over
.as_deref()
.and_then(first_execute)
.or_else(|| first_execute(body)),
Operation::Bind { value, .. } => first_execute(value),
Operation::Tablet(entries) => entries
.iter()
.find_map(|entry| first_execute(&entry.value)),
Operation::List(items) => items
.iter()
.find_map(first_execute),
_ => None,
}
}

#[test]
fn known_function_resolves_to_reference() {
let source = r#"
% technique v1

powerdown :
1. Inhibit the node { cmd("Inhibit") }
"#
.trim_ascii();
let path = Path::new("Test.tq");
let document = parsing::parse(path, source).expect("parse");
let mut program = translate(&document).expect("translate");

link(&mut program, &Library::stub()).expect("link");

let executable = first_execute(&program.subroutines[0].body).expect("an Execute in the body");
let ExecutableRef::Resolved(_) = executable.target else {
panic!("expected Resolved, got {:?}", executable.target);
};
}

#[test]
fn unknown_function_left_unresolved() {
let source = r#"
% technique v1

probe :
1. Run a mystery { mystery("x") }
"#
.trim_ascii();
let path = Path::new("Test.tq");
let document = parsing::parse(path, source).expect("parse");
let mut program = translate(&document).expect("translate");

link(&mut program, &Library::stub()).expect("link");

let executable = first_execute(&program.subroutines[0].body).expect("an Execute in the body");
let ExecutableRef::Unresolved(target) = &executable.target else {
panic!("expected Unresolved, got {:?}", executable.target);
};
assert_eq!(target.value, "mystery");
}

#[test]
fn wrong_arity_is_an_error() {
let source = r#"
% technique v1

powerdown :
1. Inhibit too much { cmd("Inhibit", "Extra") }
"#
.trim_ascii();
let path = Path::new("Test.tq");
let document = parsing::parse(path, source).expect("parse");
let mut program = translate(&document).expect("translate");

let errors = link(&mut program, &Library::stub()).expect_err("arity error");
assert_eq!(errors.len(), 1);
let LinkingError::ArityMismatch {
function,
expected,
actual,
} = &errors[0];
assert_eq!(function.value, "cmd");
assert_eq!(*expected, 1);
assert_eq!(*actual, 2);
}
113 changes: 113 additions & 0 deletions src/linking/linker.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
use crate::language::{self, Span};
use crate::program::{ExecutableRef, Fragment, Operation, Program};
use crate::runner::Library;

#[derive(Debug)]
pub enum LinkingError<'i> {
/// A function called with a number of arguments that doesn't match its
/// declared arity in the function table.
ArityMismatch {
function: language::Identifier<'i>,
expected: usize,
actual: usize,
},
}

impl<'i> LinkingError<'i> {
pub fn span(&self) -> Span {
match self {
LinkingError::ArityMismatch { function, .. } => function.span,
}
}
}

/// Resolve every `Execute` target in the program against `library`. A target
/// naming a table entry becomes `Resolved` once its argument count matches the
/// entry's arity; a target naming nothing stays `Unresolved`. Returns the
/// collected arity errors, if any.
pub fn link<'i>(program: &mut Program<'i>, library: &Library) -> Result<(), Vec<LinkingError<'i>>> {
let mut problems = Vec::new();
for subroutine in &mut program.subroutines {
link_operation(&mut subroutine.body, library, &mut problems);
}
if problems.is_empty() {
Ok(())
} else {
Err(problems)
}
}

// Walks the same Operation arms as the translator's `resolve_operation`;
// executable content is hoisted into `body` during translation, so only
// bodies need walking.
fn link_operation<'i>(
op: &mut Operation<'i>,
library: &Library,
problems: &mut Vec<LinkingError<'i>>,
) {
match op {
Operation::Execute(executable) => {
if let ExecutableRef::Unresolved(id) = &executable.target {
if let Some(exec_id) = library.resolve(id.value) {
let expected = library.arity(exec_id);
let actual = executable
.arguments
.len();
if actual == expected {
executable.target = ExecutableRef::Resolved(exec_id);
} else {
problems.push(LinkingError::ArityMismatch {
function: *id,
expected,
actual,
});
}
}
}
for arg in &mut executable.arguments {
link_operation(arg, library, problems);
}
}
Operation::Invoke(invocable) => {
for arg in &mut invocable.arguments {
link_operation(arg, library, problems);
}
}
Operation::Sequence(ops) => {
for op in ops {
link_operation(op, library, problems);
}
}
Operation::Section { body, .. } => link_operation(body, library, problems),
Operation::Step { body, .. } => link_operation(body, library, problems),
Operation::Loop { over, body, .. } => {
if let Some(over) = over {
link_operation(over, library, problems);
}
link_operation(body, library, problems);
}
Operation::Bind { value, .. } => link_operation(value, library, problems),
Operation::String(fragments) => {
for fragment in fragments {
if let Fragment::Interpolation(op) = fragment {
link_operation(op, library, problems);
}
}
}
Operation::Tablet(entries) => {
for entry in entries {
link_operation(&mut entry.value, library, problems);
}
}
Operation::List(items) => {
for item in items {
link_operation(item, library, problems);
}
}
Operation::Variable(_) | Operation::Number(_) | Operation::Multiline(_, _) => {}
}
}

#[cfg(test)]
#[path = "checks/linker.rs"]
mod check;
6 changes: 6 additions & 0 deletions src/linking/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
//! Linking phase: resolve the function references against the function table
//! the program will run against.

mod linker;

pub use linker::{link, LinkingError};
Loading