use alloc::borrow::ToOwned;
use alloc::boxed::Box;
use alloc::collections::BTreeMap;
use alloc::format;
use alloc::string::String;
use alloc::vec::Vec;
use crate::core::Error;
use crate::machine::ast::{
Arg, Expr, MachineDecl, Name, NamePart, Path, Property, SourceUnit, Stmt, TemplateParam,
};
use crate::machine::diag::{Diagnostic, Sources};
use crate::machine::parser::parse;
use crate::machine::span::{SourceFile, Span};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct FileId(pub u32);
#[derive(Debug)]
struct Entry {
name: String,
text: String,
base: u32,
}
#[derive(Debug, Default)]
pub struct SourceMap {
files: Vec<Entry>,
next: u32,
}
impl SourceMap {
pub fn new() -> SourceMap {
SourceMap {
files: Vec::new(),
next: 0,
}
}
pub fn add(
&mut self,
name: impl Into<String>,
text: impl Into<String>,
) -> Result<FileId, Diagnostic> {
let name = name.into();
let text = text.into();
let len = u32::try_from(text.len()).ok();
let next = len
.and_then(|len| self.next.checked_add(len))
.and_then(|end| end.checked_add(1));
let Some(next) = next else {
return Err(Diagnostic::new(
Span::at(self.next.saturating_sub(1)),
format!(
"`{name}` does not fit: machine descriptions are limited to 4 GiB in total"
),
));
};
let base = self.next;
self.next = next;
let id = FileId(u32::try_from(self.files.len()).unwrap_or(u32::MAX));
self.files.push(Entry { name, text, base });
Ok(id)
}
pub fn len(&self) -> usize {
self.files.len()
}
pub fn is_empty(&self) -> bool {
self.files.is_empty()
}
pub fn name(&self, id: FileId) -> Option<&str> {
self.entry(id).map(|e| e.name.as_str())
}
pub fn text(&self, id: FileId) -> Option<&str> {
self.entry(id).map(|e| e.text.as_str())
}
pub fn base(&self, id: FileId) -> Option<u32> {
self.entry(id).map(|e| e.base)
}
pub fn file_span(&self, id: FileId) -> Option<Span> {
let e = self.entry(id)?;
let len = u32::try_from(e.text.len()).unwrap_or(u32::MAX);
Some(Span::new(e.base, e.base.saturating_add(len)))
}
pub fn file_at(&self, span: Span) -> Option<FileId> {
self.index_at(span.start)
.map(|i| FileId(u32::try_from(i).unwrap_or(u32::MAX)))
}
pub fn parse(&self, id: FileId) -> Result<SourceUnit, Diagnostic> {
let Some(entry) = self.entry(id) else {
return Err(Diagnostic::new(Span::at(0), "no such source file"));
};
let src = SourceFile::new(&entry.name, &entry.text);
let mut unit = parse(&src).map_err(|d| shift_diagnostic(d, entry.base))?;
shift_unit(&mut unit, entry.base);
Ok(unit)
}
pub fn render(&self, diag: &Diagnostic) -> String {
diag.render_in(self)
}
pub fn to_error(&self, diag: &Diagnostic) -> Error {
diag.to_error_in(self)
}
fn entry(&self, id: FileId) -> Option<&Entry> {
self.files.get(id.0 as usize)
}
fn index_at(&self, offset: u32) -> Option<usize> {
if self.files.is_empty() {
return None;
}
let mut lo = 0usize;
let mut hi = self.files.len() - 1;
while lo < hi {
let mid = lo + (hi - lo).div_ceil(2);
if self.files[mid].base <= offset {
lo = mid;
} else {
hi = mid - 1;
}
}
Some(lo)
}
}
impl Sources for SourceMap {
fn locate(&self, span: Span) -> (SourceFile<'_>, Span) {
let Some(entry) = self.index_at(span.start).and_then(|i| self.files.get(i)) else {
return (SourceFile::new("<none>", ""), Span::at(0));
};
let src = SourceFile::new(&entry.name, &entry.text);
let local = Span::new(
span.start.saturating_sub(entry.base),
span.end.saturating_sub(entry.base),
);
(src, local)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Included {
pub name: String,
pub text: String,
}
pub trait IncludeLoader {
fn load(&mut self, path: &str, from: &str) -> Result<Included, String>;
}
impl<F> IncludeLoader for F
where
F: FnMut(&str, &str) -> Result<Included, String>,
{
fn load(&mut self, path: &str, from: &str) -> Result<Included, String> {
self(path, from)
}
}
impl IncludeLoader for Box<dyn IncludeLoader + '_> {
fn load(&mut self, path: &str, from: &str) -> Result<Included, String> {
(**self).load(path, from)
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct NoIncludes;
impl IncludeLoader for NoIncludes {
fn load(&mut self, path: &str, _from: &str) -> Result<Included, String> {
Err(format!(
"cannot include `{path}`: this build was given no include loader"
))
}
}
#[derive(Debug, Clone, Default)]
pub struct MemoryLoader {
files: BTreeMap<String, String>,
}
impl MemoryLoader {
pub fn new() -> MemoryLoader {
MemoryLoader {
files: BTreeMap::new(),
}
}
#[must_use]
pub fn with(mut self, name: impl Into<String>, text: impl Into<String>) -> MemoryLoader {
self.files.insert(name.into(), text.into());
self
}
pub fn insert(&mut self, name: impl Into<String>, text: impl Into<String>) {
self.files.insert(name.into(), text.into());
}
}
impl IncludeLoader for MemoryLoader {
fn load(&mut self, path: &str, _from: &str) -> Result<Included, String> {
match self.files.get(path) {
Some(text) => Ok(Included {
name: path.to_owned(),
text: text.clone(),
}),
None => {
let mut names = String::new();
for (i, name) in self.files.keys().enumerate() {
if i != 0 {
names.push_str(", ");
}
names.push_str(&format!("`{name}`"));
}
if names.is_empty() {
names.push_str("nothing");
}
Err(format!(
"no file named `{path}`; the search path holds {names}"
))
}
}
}
}
fn shift(span: Span, base: u32) -> Span {
Span::new(
span.start.saturating_add(base),
span.end.saturating_add(base),
)
}
fn shift_diagnostic(mut diag: Diagnostic, base: u32) -> Diagnostic {
diag.span = shift(diag.span, base);
if let Some(note) = &mut diag.note {
note.span = shift(note.span, base);
}
diag
}
fn shift_unit(unit: &mut SourceUnit, base: u32) {
if base == 0 {
return;
}
unit.span = shift(unit.span, base);
for stmt in &mut unit.stmts {
shift_stmt(stmt, base);
}
}
fn shift_stmt(stmt: &mut Stmt, base: u32) {
match stmt {
Stmt::Machine(MachineDecl { name, body, span }) => {
name.span = shift(name.span, base);
for s in body {
shift_stmt(s, base);
}
*span = shift(*span, base);
}
Stmt::Param(s) => {
shift_name(&mut s.name, base);
if let Some(d) = &mut s.default {
shift_expr(d, base);
}
s.span = shift(s.span, base);
}
Stmt::Osc(s) => {
shift_name(&mut s.name, base);
shift_expr(&mut s.freq, base);
s.unit.span = shift(s.unit.span, base);
s.span = shift(s.span, base);
}
Stmt::Space(s) => {
shift_name(&mut s.name, base);
shift_props(&mut s.props, base);
s.span = shift(s.span, base);
}
Stmt::Object(s) => {
shift_name(&mut s.name, base);
s.class.span = shift(s.class.span, base);
shift_props(&mut s.props, base);
s.span = shift(s.span, base);
}
Stmt::Map(s) => {
shift_name(&mut s.space, base);
shift_expr(&mut s.base, base);
shift_expr(&mut s.size, base);
shift_expr(&mut s.target, base);
shift_props(&mut s.props, base);
s.span = shift(s.span, base);
}
Stmt::Wire(s) => {
shift_path(&mut s.from, base);
shift_path(&mut s.to, base);
s.span = shift(s.span, base);
}
Stmt::Include(s) => {
s.path.span = shift(s.path.span, base);
s.span = shift(s.span, base);
}
Stmt::Template(s) => {
shift_name(&mut s.name, base);
for TemplateParam {
name,
default,
span,
} in &mut s.params
{
shift_name(name, base);
if let Some(d) = default {
shift_expr(d, base);
}
*span = shift(*span, base);
}
for st in &mut s.body {
shift_stmt(st, base);
}
s.span = shift(s.span, base);
}
Stmt::Instance(s) => {
shift_name(&mut s.name, base);
shift_name(&mut s.template, base);
for Arg { name, value, span } in &mut s.args {
if let Some(n) = name {
shift_name(n, base);
}
shift_expr(value, base);
*span = shift(*span, base);
}
s.span = shift(s.span, base);
}
Stmt::For(s) => {
shift_name(&mut s.var, base);
shift_expr(&mut s.start, base);
shift_expr(&mut s.end, base);
for st in &mut s.body {
shift_stmt(st, base);
}
s.span = shift(s.span, base);
}
}
}
fn shift_props(props: &mut [Property], base: u32) {
for Property { name, value, span } in props {
shift_name(name, base);
shift_expr(value, base);
*span = shift(*span, base);
}
}
fn shift_name(name: &mut Name, base: u32) {
for part in &mut name.parts {
if let NamePart::Substitution(e) = part {
shift_expr(e, base);
}
}
name.span = shift(name.span, base);
}
fn shift_path(path: &mut Path, base: u32) {
for seg in &mut path.segments {
shift_name(seg, base);
}
path.span = shift(path.span, base);
}
fn shift_expr(expr: &mut Expr, base: u32) {
match expr {
Expr::Num(n) => n.span = shift(n.span, base),
Expr::Str(s) => s.span = shift(s.span, base),
Expr::Bool(b) => b.span = shift(b.span, base),
Expr::Path(p) => shift_path(p, base),
Expr::Call { callee, args, span } => {
shift_path(callee, base);
for a in args {
shift_expr(a, base);
}
*span = shift(*span, base);
}
Expr::Unary { operand, span, .. } => {
shift_expr(operand, base);
*span = shift(*span, base);
}
Expr::Binary { lhs, rhs, span, .. } => {
shift_expr(lhs, base);
shift_expr(rhs, base);
*span = shift(*span, base);
}
Expr::List { items, span } => {
for i in items {
shift_expr(i, base);
}
*span = shift(*span, base);
}
Expr::Map { entries, span } => {
shift_props(entries, base);
*span = shift(*span, base);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::string::ToString;
#[test]
fn spans_from_two_files_locate_to_their_own_file() {
let mut map = SourceMap::new();
let a = map.add("a.machine", "param x = 1\n").expect("fits");
let b = map.add("b.machine", "param y = 2\n").expect("fits");
assert_eq!(map.base(a), Some(0));
assert_eq!(map.base(b), Some(13));
let unit_b = map.parse(b).expect("parses");
let span = unit_b.stmts[0].span();
assert_eq!(map.file_at(span), Some(b));
let (src, local) = map.locate(span);
assert_eq!(src.name(), "b.machine");
assert_eq!(local.start, 0);
assert_eq!(src.position(local.start), "b.machine:1:1");
}
#[test]
fn a_parse_error_in_the_second_file_names_the_second_file() {
let mut map = SourceMap::new();
map.add("a.machine", "param x = 1\n").expect("fits");
let b = map.add("b.machine", "param y = ?\n").expect("fits");
let diag = map.parse(b).expect_err("should fail");
let rendered = map.render(&diag);
assert!(rendered.contains("b.machine:1:11"), "{rendered}");
}
#[test]
fn an_end_of_file_span_still_belongs_to_its_own_file() {
let mut map = SourceMap::new();
let a = map.add("a.machine", "machine \"a\" {\n").expect("fits");
map.add("b.machine", "param y = 2\n").expect("fits");
let diag = map.parse(a).expect_err("unclosed brace");
assert_eq!(map.file_at(diag.span), Some(a));
assert!(map.render(&diag).contains("a.machine"));
}
#[test]
fn locating_an_empty_map_renders_rather_than_failing() {
let map = SourceMap::new();
let diag = Diagnostic::new(Span::at(7), "nothing here");
assert!(map.render(&diag).starts_with("error: nothing here"));
}
#[test]
fn the_memory_loader_lists_what_it_has() {
let mut loader = MemoryLoader::new().with("pci.machine", "param x = 1\n");
assert_eq!(
loader.load("pci.machine", "root").expect("found").text,
"param x = 1\n"
);
let err = loader.load("missing", "root").expect_err("absent");
assert_eq!(
err,
"no file named `missing`; the search path holds `pci.machine`"
);
}
#[test]
fn no_includes_says_so() {
let err = NoIncludes.load("x.machine", "root").expect_err("refused");
assert!(err.contains("no include loader"), "{err}");
}
#[test]
fn a_closure_is_a_loader() {
let mut loader = |path: &str, _from: &str| {
Ok(Included {
name: path.to_string(),
text: String::new(),
})
};
assert_eq!(loader.load("x", "y").expect("ok").name, "x");
}
}