mod language;
mod libraries;
mod load;
mod names;
mod origin;
mod source;
mod uses;
pub use language::Language;
use libraries::Libraries;
pub(crate) use names::{Binding, Names, Renderer};
pub(crate) use origin::Origin;
use origin::RESERVED_SCOPE;
pub(crate) use source::range;
pub(crate) use uses::{code_blocks, invocations};
use crate::diagnostic::Diagnostic;
use crate::host::{Files, NeededPack, Packs, Release};
use crate::lines::{LineStarts, Lines};
use crate::parse::{Parsed, Spliced};
use crate::policy::{Policy, Trust};
use crate::reserved::Keyword;
use crate::settings::Settings;
use crate::tree::{Move, Node, Param, Point, Position, Root, Shift, Signature};
use std::collections::{BTreeMap, HashMap, HashSet};
use std::rc::Rc;
pub(crate) const CORE_TEXT: &str = include_str!("core.comp.md");
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Ambient {
pub packs: Vec<String>,
pub report: bool,
}
#[derive(Clone, Copy)]
pub(crate) struct Store<'h> {
pub(crate) files: &'h dyn Files,
pub(crate) packs: &'h dyn Packs,
}
impl Store<'_> {
pub(crate) fn read(&self, origin: &Origin) -> Option<String> {
match origin {
Origin::File(path) => self.files.file(path),
_ if origin.is_core() => Some(CORE_TEXT.to_string()),
Origin::Pack { name, .. } if name.starts_with(RESERVED_SCOPE) => None,
Origin::Pack {
name,
version,
path,
} => self.packs.pack_file(name, version, path),
}
}
pub(crate) fn releases(&self, pack: &str) -> Vec<Release> {
if pack.starts_with(RESERVED_SCOPE) {
Vec::new()
} else {
self.packs.releases(pack)
}
}
pub(crate) fn pending(&self, pack: &str) -> bool {
!pack.starts_with(RESERVED_SCOPE) && self.packs.pending(pack)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub(crate) struct DocumentId(usize);
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub(crate) struct ComponentId(usize);
#[derive(Clone, Debug)]
pub(crate) struct Document {
pub(crate) origin: Origin,
pub(crate) text: String,
pub(crate) line_starts: LineStarts,
pub(crate) root: Rc<Root>,
pub(crate) names: Names,
pub(crate) exports: BTreeMap<String, Binding>,
pub(crate) renderers: BTreeMap<String, Renderer>,
pub(crate) provided: bool,
pub(crate) settings: Settings,
}
impl Document {
pub(crate) fn lines(&self) -> Lines<'_> {
Lines::new(&self.text, &self.line_starts)
}
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) enum Body {
Compmd(Vec<Node>),
Host {
language: String,
runtime: Option<String>,
source: String,
},
File {
origin: Origin,
language: Language,
},
Processor,
}
impl Body {
pub(crate) fn supported(&self, policy: &Policy) -> bool {
let runs = |language: Language, runtime: Option<&str>| {
language.runs(runtime)
&& language
.runtime(runtime)
.is_none_or(|runtime| policy.runs(runtime))
};
match self {
Self::Host {
language, runtime, ..
} => Language::from_name(language)
.is_some_and(|language| runs(language, runtime.as_deref())),
Self::File { language, .. } => runs(*language, None),
Self::Compmd(_) | Self::Processor => true,
}
}
pub(crate) fn is_host(&self) -> bool {
matches!(self, Self::Host { .. } | Self::File { .. })
}
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct Component {
pub(crate) name: String,
pub(crate) document: DocumentId,
pub(crate) signature: Option<Signature>,
pub(crate) body: Body,
pub(crate) fence: Vec<Param>,
pub(crate) position: Option<Position>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct HostComponent {
pub name: String,
pub language: Language,
pub runtime: Option<String>,
pub code: String,
pub module: Option<Module>,
pub trust: Trust,
pub libraries: BTreeMap<String, LibraryModule>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LibraryModule {
pub pack: String,
pub version: semver::Version,
pub path: String,
pub libraries: BTreeMap<String, LibraryModule>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Module {
pub pack: String,
pub version: semver::Version,
pub path: String,
pub integrity: Option<String>,
}
impl Component {
fn host_code(
&self,
name: &str,
store: Store<'_>,
policy: &Policy,
trust: Trust,
libraries: BTreeMap<String, LibraryModule>,
) -> Option<HostComponent> {
self.body.supported(policy).then_some(())?;
let (language, runtime, code, module) = match &self.body {
Body::Host {
language,
runtime,
source,
} => (
Language::from_name(language)?,
runtime.clone(),
source.clone(),
None,
),
Body::File {
origin:
Origin::Pack {
name: pack,
version,
path,
},
language: Language::Js,
} => {
let integrity = store
.releases(pack)
.into_iter()
.find(|release| release.version == *version)
.and_then(|release| release.integrity);
let module = Module {
pack: pack.clone(),
version: version.clone(),
path: path.clone(),
integrity,
};
(Language::Js, None, String::new(), Some(module))
}
Body::File { origin, language } => (*language, None, store.read(origin)?, None),
Body::Compmd(_) | Body::Processor => return None,
};
Some(HostComponent {
name: name.to_string(),
language,
runtime,
code,
module,
trust,
libraries,
})
}
pub(crate) fn keyword(&self) -> Option<Keyword> {
Keyword::from_name(&self.name)
}
pub(crate) fn takes_breakpoints(&self) -> bool {
match self.body {
Body::Compmd(_) => true,
Body::Processor => self.keyword() == Some(Keyword::Box),
Body::Host { .. } | Body::File { .. } => false,
}
}
pub(crate) fn is_contents(&self) -> bool {
matches!(self.body, Body::Compmd(_))
&& self
.fence
.iter()
.any(|param| param.is_display_contents() && param.breakpoint.is_none())
}
}
#[derive(Clone, Debug)]
pub(crate) struct Resolved {
pub(crate) documents: Vec<Document>,
pub(crate) components: Vec<Component>,
pub(crate) main: DocumentId,
pub(crate) cycles: HashSet<(DocumentId, Point)>,
pub(crate) policy: Policy,
pub(crate) missing: Vec<NeededPack>,
pub(crate) pending: Vec<String>,
pub(crate) libraries: Libraries,
diagnostics: Found,
}
#[derive(Clone, Debug, Default)]
struct Found {
before: Vec<Diagnostic>,
blocks: Vec<Vec<Diagnostic>>,
after: Vec<Diagnostic>,
}
impl Resolved {
pub(crate) fn document(&self, id: DocumentId) -> &Document {
&self.documents[id.0]
}
pub(crate) fn component(&self, id: ComponentId) -> &Component {
&self.components[id.0]
}
pub(crate) fn ids(&self) -> impl Iterator<Item = DocumentId> + use<> {
(0..self.documents.len()).map(DocumentId)
}
pub(crate) fn file(&self, id: DocumentId) -> Option<String> {
(id != self.main).then(|| self.document(id).origin.to_string())
}
pub(crate) fn closes_cycle(&self, id: DocumentId, position: Option<Position>) -> bool {
position.is_some_and(|position| self.cycles.contains(&(id, position.start)))
}
pub(crate) fn take_text(&mut self) -> (String, LineStarts) {
let main = &mut self.documents[self.main.0];
main.root = Rc::default();
let lines = std::mem::replace(&mut main.line_starts, LineStarts::new(""));
(std::mem::take(&mut main.text), lines)
}
pub(crate) fn host_components(&self, store: Store<'_>) -> Vec<HostComponent> {
let mut found: BTreeMap<&str, HostComponent> = BTreeMap::new();
let main_first = std::iter::once(self.main).chain(self.ids().filter(|id| *id != self.main));
for id in main_first {
for (name, component) in self.document(id).names.components() {
if found.contains_key(name) {
continue;
}
let component = self.component(component);
let (trust, libraries) = match &self.document(component.document).origin {
Origin::Pack {
name: pack,
version,
..
} => (self.policy.trust(pack), self.libraries.of(pack, version)),
Origin::File(_) => (Trust::Strict, BTreeMap::new()),
};
if let Some(host) = component.host_code(name, store, &self.policy, trust, libraries)
{
found.insert(name, host);
}
}
}
found.into_values().collect()
}
pub(crate) fn diagnostics(&self) -> impl Iterator<Item = &Diagnostic> {
let found = &self.diagnostics;
found
.before
.iter()
.chain(found.blocks.iter().flatten())
.chain(&found.after)
}
pub(crate) fn splice(
&mut self,
(text, lines): (String, LineStarts),
root: Rc<Root>,
spliced: &Spliced,
report_ambient: bool,
) {
let main = self.main;
let moved = spliced.moved;
let document = &mut self.documents[main.0];
document.text = text;
document.line_starts = lines;
document.root = root;
for component in &mut self.components {
if component.document == main
&& component
.position
.is_some_and(|position| moved.moves(position.start))
{
component.shift(moved.by);
}
}
self.cycles = self
.cycles
.drain()
.map(|(id, mut start)| {
if id == main {
moved.point(&mut start);
}
(id, start)
})
.collect();
let start = spliced.blocks.start;
let blocks = &self.document(main).root.children[start..start + spliced.inserted];
let found: Vec<Vec<Diagnostic>> = blocks
.iter()
.map(|block| uses::block(self, block, report_ambient))
.collect();
let diagnostics = &mut self.diagnostics;
let kept = diagnostics.before.iter_mut().chain(&mut diagnostics.after);
for diagnostic in kept.chain(diagnostics.blocks.iter_mut().flatten()) {
moved.diagnostic(diagnostic);
}
diagnostics.blocks.splice(spliced.blocks.clone(), found);
}
}
impl Shift for Component {
fn shift(&mut self, by: Move) {
self.signature.shift(by);
if let Body::Compmd(body) = &mut self.body {
body.shift(by);
}
self.fence.shift(by);
self.position.shift(by);
}
}
#[derive(Clone, Debug, Default)]
pub(crate) struct Parses(HashMap<Origin, (String, Parsed)>);
pub(crate) fn resolve(
path: &str,
text: String,
parsed: Parsed,
store: Store,
host: Host<'_>,
known: &mut Parses,
) -> Resolved {
load::Loader::new(store, Origin::File(path.to_string()), known, host.policy).resolve(
text,
parsed,
host.ambient,
)
}
#[derive(Clone, Copy)]
pub(crate) struct Host<'a> {
pub(crate) ambient: &'a Ambient,
pub(crate) policy: &'a Policy,
}
#[cfg(test)]
mod tests;