1use std::sync::Arc;
4
5use bamts_bytecode::{
6 ConstantId, EcmaString, ModuleId, Program, ProgramModule, ProgramVerifyError, Verified,
7};
8
9use crate::{
10 diagnostic::DiagnosticSeverity,
11 lower::{self, LowerError, LowerErrorKind, LowerOptions},
12 parser, scanner,
13 source::{ScriptKind, SourceId, SourceText, Utf16Pos},
14};
15
16const DEFAULT_MODULE_NAME: &str = "evalmachine.<anonymous>";
17
18#[derive(Clone, Debug, Eq, PartialEq)]
20pub enum ScriptCompileError {
21 IllFormedSource { unit_offset: usize },
23 Syntax {
25 message: String,
26 line: u32,
27 column: u32,
28 },
29 Unsupported {
31 message: String,
32 line: u32,
33 column: u32,
34 },
35 Capacity { message: String },
37}
38
39pub fn compile_classic_script(
44 source: &[u16],
45 resource_name: &str,
46) -> Result<Program<Verified>, ScriptCompileError> {
47 let text = EcmaString::from_units(source)
48 .to_utf8_strict()
49 .map_err(|error| ScriptCompileError::IllFormedSource {
50 unit_offset: error.unit_offset,
51 })?;
52 let source = Arc::new(SourceText::new(text));
53 let parsed = parser::parse(scanner::scan(
54 SourceId::new(0),
55 ScriptKind::JavaScript,
56 Arc::clone(&source),
57 ));
58 if let Some(diagnostic) = parsed
59 .diagnostics()
60 .iter()
61 .find(|diagnostic| diagnostic.severity() == DiagnosticSeverity::Error)
62 {
63 let (line, column) = line_column(&source, diagnostic.range().start());
64 return Err(ScriptCompileError::Syntax {
65 message: diagnostic.message().to_owned(),
66 line,
67 column,
68 });
69 }
70
71 let module_name = normalized_module_name(resource_name).unwrap_or(DEFAULT_MODULE_NAME);
72 let options = LowerOptions {
73 javascript_compatibility: true,
74 };
75 let assembled = if module_name == DEFAULT_MODULE_NAME {
76 lower::assemble_classic_script(parsed.product(), options)
77 } else {
78 lower::assemble_classic_script_named(parsed.product(), options, module_name)
79 };
80 let module = assembled
81 .map_err(|error| map_lower_error(&source, error))?
82 .verify()
83 .map_err(|error| ScriptCompileError::Capacity {
84 message: error.to_string(),
85 })?;
86
87 Program::link(
88 vec![ProgramModule {
89 name: ConstantId::new(0),
90 code: module,
91 edges: Vec::new(),
92 bindings: Vec::new(),
93 exports: Vec::new(),
94 }],
95 ModuleId::new(0),
96 )
97 .map_err(map_program_error)
98}
99
100fn map_lower_error(source: &SourceText, error: LowerError) -> ScriptCompileError {
101 let (line, column) = line_column(source, error.range.start());
102 match error.kind {
103 LowerErrorKind::Unsupported(construct) => ScriptCompileError::Unsupported {
104 message: construct.to_string(),
105 line,
106 column,
107 },
108 LowerErrorKind::Capacity(limit) => ScriptCompileError::Capacity {
109 message: limit.to_string(),
110 },
111 kind => ScriptCompileError::Syntax {
112 message: kind.to_string(),
113 line,
114 column,
115 },
116 }
117}
118
119fn map_program_error(error: ProgramVerifyError) -> ScriptCompileError {
120 ScriptCompileError::Capacity {
121 message: error.to_string(),
122 }
123}
124
125fn line_column(source: &SourceText, position: Utf16Pos) -> (u32, u32) {
126 source
127 .line_column(position)
128 .map(|(line, column)| {
129 (
130 u32::try_from(line).unwrap_or(u32::MAX),
131 u32::try_from(column).unwrap_or(u32::MAX),
132 )
133 })
134 .unwrap_or((0, 0))
135}
136
137fn normalized_module_name(resource_name: &str) -> Option<&str> {
138 if resource_name.is_empty()
139 || resource_name.starts_with('/')
140 || resource_name.contains('\\')
141 || resource_name.contains('\0')
142 {
143 return None;
144 }
145 let mut segments = resource_name.split('/');
146 let first = segments.next()?;
147 if first.contains(':') || first.is_empty() || first == "." || first == ".." {
148 return None;
149 }
150 if segments.any(|segment| segment.is_empty() || segment == "." || segment == "..") {
151 return None;
152 }
153 Some(resource_name)
154}
155
156#[cfg(test)]
157mod tests {
158 use bamts_bytecode::Instruction;
159
160 use super::{ScriptCompileError, compile_classic_script};
161
162 #[test]
163 fn classic_script_has_a_single_linkage_free_module() {
164 let program = compile_classic_script(
165 "1 + 1".encode_utf16().collect::<Vec<_>>().as_slice(),
166 "script.js",
167 )
168 .expect("classic script compiles");
169
170 assert_eq!(program.entry().get(), 0);
171 assert_eq!(program.modules().len(), 1);
172 let module = &program.modules()[0];
173 assert!(module.edges().is_empty());
174 assert!(module.bindings().is_empty());
175 assert!(module.exports().is_empty());
176 assert!(matches!(
177 module.code().functions()[module.code().entry().get() as usize]
178 .code()
179 .last(),
180 Some(Instruction::Return { .. })
181 ));
182 assert!(
183 module
184 .code()
185 .functions()
186 .iter()
187 .flat_map(|function| function.code())
188 .all(|instruction| !matches!(
189 instruction,
190 Instruction::Import { .. } | Instruction::Export { .. }
191 ))
192 );
193 }
194
195 #[test]
196 fn classic_script_rejects_module_syntax_before_program_linking() {
197 for source in [
198 "import x from 'y'",
199 "export const a = 1",
200 "export default 1",
201 "import('y')",
202 ] {
203 assert!(matches!(
204 compile_classic_script(&source.encode_utf16().collect::<Vec<_>>(), "script.js"),
205 Err(ScriptCompileError::Unsupported { .. })
206 ));
207 }
208 }
209
210 #[test]
211 fn ill_formed_utf16_source_is_typed() {
212 assert_eq!(
213 compile_classic_script(&[0xD800], "script.js"),
214 Err(ScriptCompileError::IllFormedSource { unit_offset: 0 })
215 );
216 }
217
218 #[test]
219 fn syntax_diagnostics_are_typed() {
220 assert!(matches!(
221 compile_classic_script(&"(".encode_utf16().collect::<Vec<_>>(), "script.js"),
222 Err(ScriptCompileError::Syntax { .. })
223 ));
224 }
225
226 #[test]
227 fn non_normalized_resource_name_is_advisory() {
228 assert!(compile_classic_script(&[], "/tmp/script.js").is_ok());
229 }
230
231 #[test]
232 fn completion_cases_compile_to_verified_returning_scripts() {
233 for source in [
234 "",
235 "var x = 5",
236 "1 + 1",
237 "if (true) { 42 }",
238 "1; if (true) {}",
239 "{ 7 }",
240 "for (let i = 0; i < 3; i++) { i }",
241 "1; while (false) { 2 }",
242 "try { 1 } finally { 2 }",
243 "switch (1) { case 1: 5 }",
244 "function f() {}",
245 ] {
246 let program =
247 compile_classic_script(&source.encode_utf16().collect::<Vec<_>>(), "script.js")
248 .unwrap_or_else(|error| panic!("{source:?} did not compile: {error:?}"));
249 let entry = &program.modules()[0].code().functions()[0];
250 assert!(matches!(
251 entry.code().last(),
252 Some(Instruction::Return { .. })
253 ));
254 }
255 }
256}