use std::collections::HashSet;
use meta_language::language_profile::LanguageProfile;
use meta_language::link_network::{LinkId, LinkNetwork, LinkType};
use super::parser::{parse_shell_script, Node};
use super::word_expander::expand_word;
pub const SHELL_LANGUAGE: &str = "Shell";
const TEXT_BEARING: [&str; 18] = [
"assignment",
"export",
"local",
"comment",
"name",
"literal",
"variable",
"env-variable",
"positional",
"script-name",
"all-arguments",
"argument-count",
"exit-status",
"process-id",
"unsupported-expansion",
"default-expansion",
"pattern",
"redirect",
];
#[derive(Debug, Default, Clone)]
pub struct DeclaredNames {
pub bound: Vec<String>,
pub locals: HashSet<String>,
}
impl DeclaredNames {
fn bind(&mut self, name: &str) {
if !self.bound.iter().any(|bound| bound == name) {
self.bound.push(name.to_string());
}
}
pub fn hoisted(&self) -> Vec<&str> {
self.bound
.iter()
.filter(|name| !self.locals.contains(*name))
.map(String::as_str)
.collect()
}
}
fn collect_declared_names(tree: &Node, names: &mut DeclaredNames) {
if let Some(text) = tree.text.as_deref() {
if tree.term == "assignment" || tree.term == "local" {
names.bind(text);
if tree.term == "local" {
names.locals.insert(text.to_string());
}
}
}
if tree.term == "for" {
if let Some(text) = tree
.children
.first()
.and_then(|child| child.text.as_deref())
{
names.bind(text);
names.locals.insert(text.to_string());
}
}
for child in &tree.children {
collect_declared_names(child, names);
}
}
fn expand_words(tree: Node, declared: &HashSet<String>) -> Node {
if tree.term == "word" {
let parse_substitution = |source: &str| {
let parsed = parse_shell_script(source);
expand_words(parsed, declared)
};
let parts = expand_word(
tree.text.as_deref().unwrap_or_default(),
declared,
&parse_substitution,
);
return Node::new("word", parts);
}
Node {
term: tree.term,
text: tree.text,
children: tree
.children
.into_iter()
.map(|child| expand_words(child, declared))
.collect(),
}
}
pub fn shell_profile(concepts: &[String]) -> LanguageProfile {
let mut profile = LanguageProfile::new("command-stream shell", SHELL_LANGUAGE)
.with_link_type(LinkType::Syntax)
.with_link_type(LinkType::Token)
.with_link_type(LinkType::Semantic);
for concept in concepts {
profile = profile.with_concept(concept.clone());
}
profile
}
fn insert_syntax_node(network: &mut LinkNetwork, term: &str, children: Vec<LinkId>) -> LinkId {
network.insert_dynamic_syntax_node(SHELL_LANGUAGE, term, &children)
}
pub struct Formalization {
pub network: LinkNetwork,
pub root: LinkId,
pub terms: HashSet<String>,
pub names: DeclaredNames,
}
fn insert_tree(network: &mut LinkNetwork, node: &Node, terms: &mut HashSet<String>) -> LinkId {
terms.insert(node.term.clone());
let mut children = Vec::new();
if TEXT_BEARING.contains(&node.term.as_str()) || node.text.is_some() {
children
.push(network.insert_source_token(SHELL_LANGUAGE, node.text.as_deref().unwrap_or("")));
}
for child in &node.children {
children.push(insert_tree(network, child, terms));
}
insert_syntax_node(network, &node.term, children)
}
pub fn formalize_shell(source: &str) -> Formalization {
let parsed = parse_shell_script(source);
let mut names = DeclaredNames::default();
collect_declared_names(&parsed, &mut names);
let declared: HashSet<String> = names.bound.iter().cloned().collect();
let tree = expand_words(parsed, &declared);
let mut network = LinkNetwork::new();
let mut terms = HashSet::new();
let root = insert_tree(&mut network, &tree, &mut terms);
Formalization {
network,
root,
terms,
names,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builds_a_typed_network() {
let formalization = formalize_shell("NAME=world\necho \"hi $NAME\"\n");
assert!(formalization.terms.contains("assignment"));
assert!(formalization.terms.contains("variable"));
assert_eq!(formalization.names.bound, vec!["NAME".to_string()]);
let root = formalization
.network
.link(formalization.root)
.expect("root link exists");
assert_eq!(root.metadata().term(), Some("script"));
assert_eq!(root.metadata().link_type(), Some(LinkType::Syntax));
}
#[test]
fn inserts_nodes_of_dynamic_arity() {
let commands: String = (0..20)
.map(|index| format!("echo {index}\n"))
.collect::<Vec<_>>()
.concat();
let formalization = formalize_shell(&commands);
let root = formalization
.network
.link(formalization.root)
.expect("root link exists");
assert_eq!(root.references().len(), 20);
assert!(formalization.terms.contains("command"));
}
#[test]
fn the_profile_accepts_the_formalized_network() {
let formalization = formalize_shell("ls | wc -l\n");
let concepts: Vec<String> = formalization.terms.iter().cloned().collect();
assert!(shell_profile(&concepts)
.validate_network(&formalization.network)
.is_ok());
}
}