command_stream/fy/
formalizer.rs1use std::collections::HashSet;
10
11use meta_language::language_profile::LanguageProfile;
12use meta_language::link_network::{LinkId, LinkNetwork, LinkType};
13
14use super::parser::{parse_shell_script, Node};
15use super::word_expander::expand_word;
16
17pub const SHELL_LANGUAGE: &str = "Shell";
19
20const TEXT_BEARING: [&str; 18] = [
22 "assignment",
23 "export",
24 "local",
25 "comment",
26 "name",
27 "literal",
28 "variable",
29 "env-variable",
30 "positional",
31 "script-name",
32 "all-arguments",
33 "argument-count",
34 "exit-status",
35 "process-id",
36 "unsupported-expansion",
37 "default-expansion",
38 "pattern",
39 "redirect",
40];
41
42#[derive(Debug, Default, Clone)]
45pub struct DeclaredNames {
46 pub bound: Vec<String>,
48 pub locals: HashSet<String>,
50}
51
52impl DeclaredNames {
53 fn bind(&mut self, name: &str) {
54 if !self.bound.iter().any(|bound| bound == name) {
55 self.bound.push(name.to_string());
56 }
57 }
58
59 pub fn hoisted(&self) -> Vec<&str> {
61 self.bound
62 .iter()
63 .filter(|name| !self.locals.contains(*name))
64 .map(String::as_str)
65 .collect()
66 }
67}
68
69fn collect_declared_names(tree: &Node, names: &mut DeclaredNames) {
74 if let Some(text) = tree.text.as_deref() {
75 if tree.term == "assignment" || tree.term == "local" {
76 names.bind(text);
77 if tree.term == "local" {
78 names.locals.insert(text.to_string());
79 }
80 }
81 }
82 if tree.term == "for" {
83 if let Some(text) = tree
84 .children
85 .first()
86 .and_then(|child| child.text.as_deref())
87 {
88 names.bind(text);
89 names.locals.insert(text.to_string());
90 }
91 }
92 for child in &tree.children {
93 collect_declared_names(child, names);
94 }
95}
96
97fn expand_words(tree: Node, declared: &HashSet<String>) -> Node {
99 if tree.term == "word" {
100 let parse_substitution = |source: &str| {
101 let parsed = parse_shell_script(source);
102 expand_words(parsed, declared)
103 };
104 let parts = expand_word(
105 tree.text.as_deref().unwrap_or_default(),
106 declared,
107 &parse_substitution,
108 );
109 return Node::new("word", parts);
110 }
111 Node {
112 term: tree.term,
113 text: tree.text,
114 children: tree
115 .children
116 .into_iter()
117 .map(|child| expand_words(child, declared))
118 .collect(),
119 }
120}
121
122pub fn shell_profile(concepts: &[String]) -> LanguageProfile {
127 let mut profile = LanguageProfile::new("command-stream shell", SHELL_LANGUAGE)
128 .with_link_type(LinkType::Syntax)
129 .with_link_type(LinkType::Token)
130 .with_link_type(LinkType::Semantic);
131 for concept in concepts {
132 profile = profile.with_concept(concept.clone());
133 }
134 profile
135}
136
137fn insert_syntax_node(network: &mut LinkNetwork, term: &str, children: Vec<LinkId>) -> LinkId {
139 network.insert_dynamic_syntax_node(SHELL_LANGUAGE, term, &children)
140}
141
142pub struct Formalization {
144 pub network: LinkNetwork,
145 pub root: LinkId,
147 pub terms: HashSet<String>,
149 pub names: DeclaredNames,
150}
151
152fn insert_tree(network: &mut LinkNetwork, node: &Node, terms: &mut HashSet<String>) -> LinkId {
153 terms.insert(node.term.clone());
154 let mut children = Vec::new();
155 if TEXT_BEARING.contains(&node.term.as_str()) || node.text.is_some() {
156 children
157 .push(network.insert_source_token(SHELL_LANGUAGE, node.text.as_deref().unwrap_or("")));
158 }
159 for child in &node.children {
160 children.push(insert_tree(network, child, terms));
161 }
162 insert_syntax_node(network, &node.term, children)
163}
164
165pub fn formalize_shell(source: &str) -> Formalization {
167 let parsed = parse_shell_script(source);
168 let mut names = DeclaredNames::default();
169 collect_declared_names(&parsed, &mut names);
170
171 let declared: HashSet<String> = names.bound.iter().cloned().collect();
172 let tree = expand_words(parsed, &declared);
173
174 let mut network = LinkNetwork::new();
175 let mut terms = HashSet::new();
176 let root = insert_tree(&mut network, &tree, &mut terms);
177
178 Formalization {
179 network,
180 root,
181 terms,
182 names,
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189
190 #[test]
191 fn builds_a_typed_network() {
192 let formalization = formalize_shell("NAME=world\necho \"hi $NAME\"\n");
193 assert!(formalization.terms.contains("assignment"));
194 assert!(formalization.terms.contains("variable"));
195 assert_eq!(formalization.names.bound, vec!["NAME".to_string()]);
196
197 let root = formalization
198 .network
199 .link(formalization.root)
200 .expect("root link exists");
201 assert_eq!(root.metadata().term(), Some("script"));
202 assert_eq!(root.metadata().link_type(), Some(LinkType::Syntax));
203 }
204
205 #[test]
206 fn inserts_nodes_of_dynamic_arity() {
207 let commands: String = (0..20)
208 .map(|index| format!("echo {index}\n"))
209 .collect::<Vec<_>>()
210 .concat();
211 let formalization = formalize_shell(&commands);
212 let root = formalization
213 .network
214 .link(formalization.root)
215 .expect("root link exists");
216 assert_eq!(root.references().len(), 20);
218 assert!(formalization.terms.contains("command"));
219 }
220
221 #[test]
222 fn the_profile_accepts_the_formalized_network() {
223 let formalization = formalize_shell("ls | wc -l\n");
224 let concepts: Vec<String> = formalization.terms.iter().cloned().collect();
225 assert!(shell_profile(&concepts)
226 .validate_network(&formalization.network)
227 .is_ok());
228 }
229}