use std::path::{Path, PathBuf};
use crate::codegen::{mangle_library_symbol, LibFunction};
use crate::lexer::{Lexer, Token, TokenInfo};
use crate::parser::ast::Type;
#[derive(Debug, Clone, PartialEq)]
pub struct LibFileBlock {
pub lib: String,
pub version: String,
pub location: String,
pub funcs: Vec<LibFunction>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ImportedFunction {
pub lib: String,
pub version: String,
pub name: String,
pub mangled: String,
pub params: Vec<(String, Type)>,
pub return_type: Type,
}
#[derive(Debug, Clone)]
pub struct ResolvedImport {
pub lib: String,
pub version: String,
pub functions: Vec<ImportedFunction>,
pub so_path: PathBuf,
}
struct LibParser {
tokens: Vec<TokenInfo>,
pos: usize,
}
impl LibParser {
fn new(text: &str) -> Self {
let mut lexer = Lexer::new(text);
LibParser {
tokens: lexer.tokenize(),
pos: 0,
}
}
fn current(&self) -> &Token {
match self.tokens.get(self.pos) {
Some(t) => &t.token,
None => &Token::EOF,
}
}
fn advance(&mut self) {
if self.pos < self.tokens.len() {
self.pos += 1;
}
}
fn line(&self) -> usize {
self.tokens.get(self.pos).map(|t| t.line).unwrap_or(0)
}
fn err(&self, msg: String) -> String {
format!("line {}: {}", self.line(), msg)
}
fn skip_blank_lines(&mut self) {
while matches!(self.current(), Token::Newline | Token::ParagraphBreak) {
self.advance();
}
}
fn expect_period(&mut self, what: &str) -> Result<(), String> {
if *self.current() == Token::Period {
self.advance();
Ok(())
} else {
Err(self.err(format!("expected '.' at the end of the {}", what)))
}
}
fn take_identifier(&mut self, what: &str) -> Result<String, String> {
match self.current().clone() {
Token::Identifier(s) => {
self.advance();
Ok(s)
}
Token::StringLiteral(_) => Err(self.err(format!(
"expected {} here, found a string literal — names are bare or \
'single-quoted', not \"double-quoted\" (strings are data)",
what
))),
_ => Err(self.err(format!("expected {} here, found {:?}", what, self.current()))),
}
}
fn take_string(&mut self, what: &str) -> Result<String, String> {
match self.current().clone() {
Token::StringLiteral(s) => {
self.advance();
Ok(s)
}
Token::Identifier(_) => Err(self.err(format!(
"expected {} here, found a name — a version/path is a \
\"double-quoted\" string literal, not an identifier",
what
))),
_ => Err(self.err(format!("expected {} here, found {:?}", what, self.current()))),
}
}
fn at_word(&self, word: &str) -> bool {
matches!(self.current(), Token::Identifier(n) if n.eq_ignore_ascii_case(word))
}
fn expect_word(&mut self, word: &str, context: &str) -> Result<(), String> {
if self.at_word(word) {
self.advance();
Ok(())
} else {
Err(self.err(format!(
"expected '{}' in {}, found {:?}",
word,
context,
self.current()
)))
}
}
fn take_type(&mut self, position: &'static str) -> Result<Type, String> {
if *self.current() == Token::List {
return self.take_list_type();
}
let ty = match self.current() {
Token::Number => Some(Type::Integer),
Token::Float => Some(Type::Float),
Token::Text => Some(Type::String),
Token::Boolean => Some(Type::Boolean),
Token::File => Some(Type::File),
Token::Buffer => Some(Type::Buffer),
Token::Map => Some(Type::Map(Box::new(Type::Unknown))),
Token::Time => Some(Type::Time),
Token::Timer => Some(Type::Timer),
Token::Identifier(n) if n.eq_ignore_ascii_case("value") => Some(Type::Value),
_ => None,
};
match ty {
Some(t) => {
self.advance();
Ok(t)
}
None => {
let found = match self.current() {
Token::Identifier(n) => format!("'{}'", n),
other => format!("{:?}", other),
};
Err(self.err(format!(
"unsupported type {} in a {} position — a .lib states types \
as one of: number, float, text, boolean, list, map, buffer, \
file, time, timer, value",
found, position
)))
}
}
}
fn take_list_type(&mut self) -> Result<Type, String> {
self.advance(); if *self.current() != Token::Of {
return Ok(Type::List(Box::new(Type::Unknown)));
}
self.advance(); let elem = match self.current() {
Token::Number => Type::Integer,
Token::Float => Type::Float,
Token::Text => Type::String,
Token::Boolean => Type::Boolean,
Token::File => Type::File,
Token::Buffer => Type::Buffer,
Token::Time => Type::Time,
Token::Timer => Type::Timer,
Token::Identifier(n) if n.eq_ignore_ascii_case("value") => Type::Value,
other => {
let found = match other {
Token::Identifier(n) => format!("'{}'", n),
o => format!("{:?}", o),
};
return Err(self.err(format!(
"unsupported list element type {} — 'list of' takes one of: \
number, float, text, boolean, buffer, file, time, timer, \
value (no nested list or map)",
found
)));
}
};
self.advance();
Ok(Type::List(Box::new(elem)))
}
fn wrap_err(&self, name: &str) -> String {
self.err(format!(
"the entry for \"{}\" does not fit on one line — a \
table-of-contents entry is exactly one line and never wraps",
name
))
}
fn parse_toc_entry(&mut self) -> Result<LibFunction, String> {
self.advance(); let name = self.take_identifier("a function name after 'To'")?;
let mut params: Vec<(String, Type)> = Vec::new();
let mut return_type = Type::Void;
if *self.current() == Token::With {
self.advance();
loop {
if matches!(self.current(), Token::A | Token::An) {
self.advance();
}
let ptype = self.take_type("parameter")?;
if *self.current() == Token::Called {
self.advance();
} else {
return Err(self.err(format!(
"expected 'called' after the parameter type in the entry for \"{}\"",
name
)));
}
let pname = self.take_identifier("a parameter name after 'called'")?;
params.push((pname, ptype));
if *self.current() == Token::And {
self.advance();
} else {
break;
}
}
}
if *self.current() == Token::Comma {
self.advance();
if matches!(self.current(), Token::Newline | Token::ParagraphBreak) {
return Err(self.wrap_err(&name));
}
self.expect_word("returning", "a table-of-contents entry")?;
if matches!(self.current(), Token::A | Token::An) {
self.advance();
}
return_type = self.take_type("return")?;
} else if self.at_word("returning") {
return Err(self.err(format!(
"the entry for '{}' has 'returning' without the comma that \
introduces it — a .lib entry reads 'To 'name' ..., returning a <type>.'",
name
)));
}
if *self.current() == Token::Period {
self.advance();
} else if matches!(self.current(), Token::Newline | Token::ParagraphBreak) {
return Err(self.wrap_err(&name));
} else {
return Err(self.err(format!(
"expected '.' at the end of the entry for \"{}\", found {:?}",
name,
self.current()
)));
}
if !matches!(
self.current(),
Token::Newline | Token::ParagraphBreak | Token::EOF
) {
return Err(self.err(format!(
"the entry for \"{}\" does not end at its period — a \
table-of-contents entry is exactly one line; nothing may \
follow the '.' on that line",
name
)));
}
Ok(LibFunction {
name,
params,
return_type,
})
}
fn parse_block(&mut self) -> Result<LibFileBlock, String> {
self.advance(); let lib = self.take_identifier("a library name after 'Library'")?;
let is_version = *self.current() == Token::Version
|| matches!(self.current(), Token::Identifier(ref id) if id.to_lowercase() == "version");
if is_version {
self.advance();
} else {
return Err(self.err(format!(
"library '{}' has no version — a .lib block reads \
'Library '<name>' version \"<x.y>\".'",
lib
)));
}
let version = self.take_string("a version string")?;
self.expect_period("Library line")?;
self.skip_blank_lines();
self.expect_word("location", "a Library block")?;
let location = self.take_string("a path after 'Location'")?;
self.expect_period("Location line")?;
self.skip_blank_lines();
self.expect_word("table", "a Library block")?;
if *self.current() == Token::Of {
self.advance();
} else {
return Err("expected 'of' in the 'Table of Contents:' header".to_string());
}
self.expect_word("contents", "the 'Table of Contents:' header")?;
if *self.current() == Token::Colon {
self.advance();
} else {
return Err("the 'Table of Contents' header must end with ':'".to_string());
}
let mut funcs = Vec::new();
loop {
self.skip_blank_lines();
match self.current() {
Token::To => funcs.push(self.parse_toc_entry()?),
Token::Library | Token::EOF => break,
other => {
return Err(self.err(format!(
"unexpected {:?} in the table of contents for library \
'{}' — entries are 'To 'name' ... .' lines only; \
a .lib file cannot carry executable statements",
other, lib
)));
}
}
}
Ok(LibFileBlock {
lib,
version,
location,
funcs,
})
}
fn parse_file(&mut self) -> Result<Vec<LibFileBlock>, String> {
let mut blocks = Vec::new();
loop {
self.skip_blank_lines();
match self.current() {
Token::EOF => break,
Token::Library => blocks.push(self.parse_block()?),
other => {
return Err(self.err(format!(
"unexpected {:?} at the top level of a .lib file — a \
.lib contains only 'Library '<name>' version \
\"<x.y>\".' blocks",
other
)));
}
}
}
Ok(blocks)
}
}
pub fn parse_lib_text(text: &str) -> Result<Vec<LibFileBlock>, String> {
let mut p = LibParser::new(text);
p.parse_file()
}
fn normalise_display(p: &Path) -> PathBuf {
use std::path::Component;
let mut out = PathBuf::new();
for c in p.components() {
if let Component::CurDir = c {
continue;
}
out.push(c.as_os_str());
}
if out.as_os_str().is_empty() {
out.push(".");
}
out
}
fn search_paths(name: &str, first_dir: &Path, lib_paths: &[String]) -> (Option<PathBuf>, Vec<PathBuf>) {
let mut tried = Vec::new();
for c in std::iter::once(first_dir.join(name))
.chain(lib_paths.iter().map(|p| Path::new(p).join(name)))
{
tried.push(c.clone());
if c.exists() {
return (Some(c), tried);
}
}
(None, tried)
}
fn resolve_lib_file(path: &str, source_dir: &Path, lib_paths: &[String]) -> Result<PathBuf, String> {
let p = Path::new(path);
if p.is_absolute() {
if p.exists() {
return Ok(p.to_path_buf());
}
return Err(format!(
"the library interface file '{}' does not exist.\n\
The path is absolute; no search locations apply.",
path
));
}
let (found, tried) = search_paths(path, source_dir, lib_paths);
found.ok_or_else(|| {
let tried_list = tried
.iter()
.map(|t| format!(" {}", normalise_display(t).display()))
.collect::<Vec<_>>()
.join("\n");
format!(
"could not find the library interface file '{}'.\nPaths tried:\n{}\n\
Use --lib-path <dir> to add directories to this search.",
path, tried_list
)
})
}
fn resolve_location(location: &str, lib_dir: &Path, lib_paths: &[String]) -> Result<PathBuf, String> {
let p = Path::new(location);
if p.is_absolute() {
if p.exists() {
return Ok(p.to_path_buf());
}
return Err(format!(
"the .so at Location '{}' does not exist (absolute path).",
location
));
}
let (found, _) = search_paths(location, lib_dir, lib_paths);
found.ok_or_else(|| {
let resolved = lib_dir.join(location);
format!(
"the .so named by Location \"{}\" does not exist at the resolved \
path '{}'.\nThe library binary belongs beside the .lib that \
describes it (or in a directory given by --lib-path).",
location,
normalise_display(&resolved).display()
)
})
}
pub fn resolve_see_import(
lib_name: &str,
lib_version: &str,
path: &str,
source_dir: &Path,
lib_paths: &[String],
) -> Result<ResolvedImport, String> {
let lib_path = resolve_lib_file(path, source_dir, lib_paths)?;
let text = std::fs::read_to_string(&lib_path)
.map_err(|e| format!("could not read '{}': {}", normalise_display(&lib_path).display(), e))?;
let blocks = parse_lib_text(&text)
.map_err(|e| format!("could not parse '{}': {}", normalise_display(&lib_path).display(), e))?;
let matching_name: Vec<&LibFileBlock> = blocks.iter().filter(|b| b.lib == lib_name).collect();
if matching_name.is_empty() {
let have = blocks
.iter()
.map(|b| format!("\"{}\" version \"{}\"", b.lib, b.version))
.collect::<Vec<_>>()
.join(", ");
return Err(format!(
"'{}' has no library named \"{}\".\nIt declares: {}.",
normalise_display(&lib_path).display(),
lib_name,
if have.is_empty() { "nothing — the file has no Library blocks".to_string() } else { have }
));
}
let block = match matching_name.iter().find(|b| b.version == lib_version) {
Some(b) => *b,
None => {
let versions = matching_name
.iter()
.map(|b| format!("\"{}\"", b.version))
.collect::<Vec<_>>()
.join(", ");
return Err(format!(
"'{}' has library \"{}\" but not version \"{}\".\n\
The available versions are: {}.",
normalise_display(&lib_path).display(),
lib_name,
lib_version,
versions
));
}
};
let lib_dir = lib_path.parent().unwrap_or(Path::new("."));
let so_path = resolve_location(&block.location, lib_dir, lib_paths)?;
let dynsym = crate::elf::defined_dynamic_symbols(&normalise_display(&so_path))
.map_err(|e| format!("checking the library binary: {}", e))?;
let mut functions = Vec::new();
for f in &block.funcs {
let mangled = mangle_library_symbol(&block.lib, &block.version, &f.name);
if !dynsym.iter().any(|s| s == &mangled) {
return Err(format!(
"the .lib entry 'To '{}' ...' promises the symbol '{}', but \
'{}' does not export it (not in .dynsym).\n\
The .lib is stale: it does not match the library binary. \
Rebuild the library with `vox --shared` to regenerate the pair.",
f.name,
mangled,
normalise_display(&so_path).display()
));
}
functions.push(ImportedFunction {
lib: block.lib.clone(),
version: block.version.clone(),
name: f.name.clone(),
mangled,
params: f.params.clone(),
return_type: f.return_type.clone(),
});
}
Ok(ResolvedImport {
lib: block.lib.clone(),
version: block.version.clone(),
functions,
so_path,
})
}
pub fn resolve_program_imports(
program: &crate::parser::ast::Program,
source_dir: &Path,
lib_paths: &[String],
) -> Result<Vec<ResolvedImport>, String> {
use crate::parser::ast::Statement;
let mut resolved: Vec<ResolvedImport> = Vec::new();
for stmt in &program.statements {
let (path, lib_name, lib_version) = match stmt {
Statement::See {
path,
lib_name,
lib_version,
} => (path, lib_name, lib_version),
_ => continue,
};
if !path.ends_with(".lib") {
if lib_name.is_some() && lib_version.is_some() {
return Err(format!(
"see '{}' version \"{}\" from \"{}\": a library import names a \
.lib interface file, but \"{}\" is not one. Did you forget the \
.lib extension?\n\
Canonical form: see '<lib>' version \"<x.y>\" from \"<path>.lib\".",
lib_name.as_deref().unwrap_or(""),
lib_version.as_deref().unwrap_or(""),
path,
path
));
}
continue;
}
let (name, version) = match (lib_name, lib_version) {
(Some(n), Some(v)) => (n.as_str(), v.as_str()),
_ => {
return Err(format!(
"see \"{}\": a .lib import must name the library and its \
version, so the right block can be selected.\n\
Canonical form: see '<lib>' version \"<x.y>\" from \"<path>.lib\".",
path
));
}
};
let import = resolve_see_import(name, version, path, source_dir, lib_paths)?;
if !resolved
.iter()
.any(|r: &ResolvedImport| r.lib == import.lib && r.version == import.version)
{
resolved.push(import);
}
}
Ok(resolved)
}
#[cfg(test)]
mod tests {
use super::*;
const TWO_BLOCKS: &str = "Library mathkit version \"1.0\".\n\
Location \"./libmathkit.so\".\n\
\n\
Table of Contents:\n\
To 'add two numbers' with a number called aa and a number called bb, returning a number.\n\
To greet.\n\
\n\
Library flags version \"0.1\".\n\
Location \"./libflags.so\".\n\
\n\
Table of Contents:\n\
To hasflag with a number called n, returning a number.\n";
#[test]
fn parses_two_blocks_with_their_own_locations() {
let blocks = parse_lib_text(TWO_BLOCKS).unwrap();
assert_eq!(blocks.len(), 2);
assert_eq!(blocks[0].lib, "mathkit");
assert_eq!(blocks[0].version, "1.0");
assert_eq!(blocks[0].location, "./libmathkit.so");
assert_eq!(blocks[0].funcs.len(), 2);
assert_eq!(blocks[0].funcs[0].name, "add two numbers");
assert_eq!(
blocks[0].funcs[0].params,
vec![
("aa".to_string(), Type::Integer),
("bb".to_string(), Type::Integer)
]
);
assert_eq!(blocks[0].funcs[0].return_type, Type::Integer);
assert_eq!(blocks[0].funcs[1].name, "greet");
assert!(blocks[0].funcs[1].params.is_empty());
assert_eq!(blocks[0].funcs[1].return_type, Type::Void);
assert_eq!(blocks[1].lib, "flags");
assert_eq!(blocks[1].location, "./libflags.so");
assert_eq!(blocks[1].funcs[0].params.len(), 1);
}
#[test]
fn round_trips_the_a3_emitter_output() {
let text = "Library mathkit version \"1.0\".\n\
Location \"./libmath.so\".\n\
\n\
Table of Contents:\n\
To 'add two numbers' with a number called n, returning a number.\n\
To greet.\n\
To makebuf.\n";
let blocks = parse_lib_text(text).unwrap();
assert_eq!(blocks.len(), 1);
assert_eq!(blocks[0].funcs.len(), 3);
assert_eq!(blocks[0].funcs[2].return_type, Type::Void);
}
#[test]
fn value_and_collection_nouns_parse() {
let text = "Library dyn version \"1.0\".\n\
Location \"./libdyn.so\".\n\
\n\
Table of Contents:\n\
To echo with a value called v, returning a value.\n\
To stash with a buffer called b and a list called l and a map called m.\n";
let blocks = parse_lib_text(text).unwrap();
assert_eq!(blocks[0].funcs[0].return_type, Type::Value);
assert_eq!(blocks[0].funcs[0].params[0].1, Type::Value);
assert_eq!(blocks[0].funcs[1].params[0].1, Type::Buffer);
assert!(matches!(blocks[0].funcs[1].params[1].1, Type::List(_)));
assert!(matches!(blocks[0].funcs[1].params[2].1, Type::Map(_)));
}
#[test]
fn returning_with_no_params_parses() {
let text = "Library n version \"1.0\".\n\
Location \"./n.so\".\n\
\n\
Table of Contents:\n\
To makebuf, returning a number.\n";
let blocks = parse_lib_text(text).unwrap();
assert_eq!(blocks[0].funcs[0].return_type, Type::Integer);
assert!(blocks[0].funcs[0].params.is_empty());
}
#[test]
fn unsupported_return_type_is_named() {
let text = "Library n version \"1.0\".\n\
Location \"./n.so\".\n\
\n\
Table of Contents:\n\
To f, returning a widget.\n";
let err = parse_lib_text(text).unwrap_err();
assert!(err.contains("unsupported type"), "got: {}", err);
assert!(err.contains("'widget'"), "got: {}", err);
}
#[test]
fn void_and_unknown_have_no_surface_spelling_in_return_position() {
for word in ["void", "unknown"] {
let text = format!(
"Library n version \"1.0\".\n\
Location \"./n.so\".\n\
\n\
Table of Contents:\n\
To f, returning a {}.\n",
word
);
let err = parse_lib_text(&text).unwrap_err();
assert!(err.contains("unsupported type"), "got: {}", err);
assert!(err.contains(&format!("'{}'", word)), "got: {}", err);
}
}
#[test]
fn executable_statements_are_rejected_structurally() {
let text = "Library n version \"1.0\".\n\
Location \"./n.so\".\n\
\n\
Table of Contents:\n\
To f.\n\
Print \"surprise\".\n";
let err = parse_lib_text(text).unwrap_err();
assert!(
err.contains("cannot carry executable statements"),
"got: {}",
err
);
}
#[test]
fn a_wrapped_entry_is_an_error() {
let text = "Library n version \"1.0\".\n\
Location \"./n.so\".\n\
\n\
Table of Contents:\n\
To f with a number called aa,\n\
and a number called bb, returning a number.\n";
let err = parse_lib_text(text).unwrap_err();
assert!(err.contains("one line"), "got: {}", err);
}
#[test]
fn mangled_names_follow_the_a1_rule() {
let blocks = parse_lib_text(TWO_BLOCKS).unwrap();
assert_eq!(
mangle_library_symbol(
&blocks[0].lib,
&blocks[0].version,
&blocks[0].funcs[0].name
),
"mathkit_1_0_add_two_numbers"
);
assert_eq!(
mangle_library_symbol(&blocks[1].lib, &blocks[1].version, "hasflag"),
"flags_0_1_hasflag"
);
}
#[test]
fn canonical_see_with_non_lib_path_is_an_error() {
use crate::parser::ast::{Program, Statement};
let prog = Program::new(vec![Statement::See {
path: "./libmathkit.lib.txt".to_string(),
lib_name: Some("mathkit".to_string()),
lib_version: Some("1.0".to_string()),
}]);
let err = resolve_program_imports(&prog, std::path::Path::new("."), &[])
.unwrap_err();
assert!(err.contains("not one"), "got: {}", err);
assert!(err.contains("libmathkit.lib.txt"), "got: {}", err);
assert!(err.contains("Canonical form"), "got: {}", err);
}
#[test]
fn bare_see_with_non_lib_path_is_skipped_not_errored() {
use crate::parser::ast::{Program, Statement};
let prog = Program::new(vec![Statement::See {
path: "./notes.txt".to_string(),
lib_name: None,
lib_version: None,
}]);
let resolved = resolve_program_imports(&prog, std::path::Path::new("."), &[])
.unwrap();
assert!(resolved.is_empty());
}
#[test]
fn elf_error_path_is_normalised_no_double_dot_slash() {
use std::fs;
let dir = std::env::temp_dir().join(format!("vox-elf-path-{}", std::process::id()));
fs::create_dir_all(&dir).unwrap();
let sub = dir.join("sub");
fs::create_dir_all(&sub).unwrap();
let lib_text = "Library x version \"1.0\".\n\
Location \"./notelf.so\".\n\
\n\
Table of Contents:\n\
To f.\n";
fs::write(sub.join("good.lib"), lib_text).unwrap();
fs::write(sub.join("notelf.so"), b"not an elf file").unwrap();
let err = resolve_see_import("x", "1.0", "./good.lib", &sub, &[]).unwrap_err();
assert!(!err.contains("././"), "elf error path leaked a '././': {}", err);
assert!(err.contains("notelf.so"), "got: {}", err);
fs::remove_dir_all(&dir).ok();
}
}