Fork of daniellemaywood.uk/gleam — Wasm codegen work
14 kB
367 lines
1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2021 The Gleam contributors
3
4use crate::{
5 analyse::TargetSupport,
6 build::{Origin, Target},
7 config::PackageConfig,
8 inline,
9 javascript::*,
10 uid::UniqueIdGenerator,
11 warning::{TypeWarningEmitter, WarningEmitter},
12};
13use camino::{Utf8Path, Utf8PathBuf};
14use lsp_types::Position;
15
16mod assert;
17mod assignments;
18mod bit_arrays;
19mod blocks;
20mod bools;
21mod case;
22mod case_clause_guards;
23mod consts;
24mod custom_types;
25mod echo;
26mod externals;
27mod functions;
28mod generics;
29mod inlining;
30mod lists;
31mod modules;
32mod numbers;
33mod panic;
34mod prelude;
35mod records;
36mod recursion;
37mod results;
38mod sourcemaps;
39mod strings;
40mod todo;
41mod tuples;
42mod type_alias;
43mod use_;
44
45pub static CURRENT_PACKAGE: &str = "thepackage";
46
47#[macro_export]
48macro_rules! assert_js {
49 ($(($name:literal, $module_src:literal)),+, $src:literal $(,)?) => {
50 let compiled =
51 $crate::javascript::tests::compile_js($src, vec![$(($crate::javascript::tests::CURRENT_PACKAGE, $name, $module_src)),*]);
52 let mut output = String::from("----- SOURCE CODE\n");
53 for (name, src) in [$(($name, $module_src)),*] {
54 output.push_str(&format!("-- {name}.gleam\n{src}\n\n"));
55 }
56 output.push_str(&format!("-- main.gleam\n{}\n\n----- COMPILED JAVASCRIPT\n{compiled}", $src));
57 insta::assert_snapshot!(insta::internals::AutoName, output, $src);
58 };
59
60 ($(($dep_package:expr, $dep_name:expr, $dep_src:expr)),+, $src:literal $(,)?) => {{
61 let compiled =
62 $crate::javascript::tests::compile_js($src, vec![$(($dep_package, $dep_name, $dep_src)),*]);
63 let output = format!(
64 "----- SOURCE CODE\n{}\n\n----- COMPILED JAVASCRIPT\n{}",
65 $src, compiled
66 );
67 insta::assert_snapshot!(insta::internals::AutoName, output, $src);
68 }};
69
70 (($dep_package:expr, $dep_name:expr, $dep_src:expr), $src:expr, $js:expr $(,)?) => {{
71 let output =
72 $crate::javascript::tests::compile_js($src, Some(($dep_package, $dep_name, $dep_src)));
73 assert_eq!(($src, output), ($src, $js.to_string()));
74 }};
75
76 ($src:expr $(,)?) => {{
77 let compiled =
78 $crate::javascript::tests::compile_js($src, vec![]);
79 let output = format!(
80 "----- SOURCE CODE\n{}\n\n----- COMPILED JAVASCRIPT\n{}",
81 $src, compiled
82 );
83 insta::assert_snapshot!(insta::internals::AutoName, output, $src);
84 }};
85
86 ($src:expr, $js:expr $(,)?) => {{
87 let output =
88 $crate::javascript::tests::compile_js($src, vec![]);
89 assert_eq!(($src, output), ($src, $js.to_string()));
90 }};
91}
92
93#[macro_export]
94macro_rules! assert_source_map {
95 ($src:expr $(,)?) => {{
96 let (compiled, source_map) =
97 $crate::javascript::tests::compile_js_with_source_map($src, vec![]);
98
99 let output = format!(
100 "----- SOURCE CODE\n{}\n\n----- COMPILED JAVASCRIPT\n{}\n\n----- SOURCE MAP\n{}",
101 $crate::javascript::tests::append_line_numbers($src),
102 $crate::javascript::tests::append_line_numbers(&compiled),
103 $crate::javascript::tests::source_map_to_string($src, &compiled, source_map)
104 );
105 insta::assert_snapshot!(insta::internals::AutoName, output, $src);
106 }};
107}
108
109#[macro_export]
110macro_rules! assert_ts_def {
111 (($dep_1_package:expr, $dep_1_name:expr, $dep_1_src:expr), ($dep_2_package:expr, $dep_2_name:expr, $dep_2_src:expr), $src:expr $(,)?) => {{
112 let compiled = $crate::javascript::tests::compile_ts(
113 $src,
114 vec![
115 ($dep_1_package, $dep_1_name, $dep_1_src),
116 ($dep_2_package, $dep_2_name, $dep_2_src),
117 ],
118 );
119 let output = format!(
120 "----- SOURCE CODE\n{}\n\n----- TYPESCRIPT DEFINITIONS\n{}",
121 $src, compiled
122 );
123 insta::assert_snapshot!(insta::internals::AutoName, output, $src);
124 }};
125
126 (($dep_package:expr, $dep_name:expr, $dep_src:expr), $src:expr $(,)?) => {{
127 let compiled =
128 $crate::javascript::tests::compile_ts($src, vec![($dep_package, $dep_name, $dep_src)]);
129 let output = format!(
130 "----- SOURCE CODE\n{}\n\n----- TYPESCRIPT DEFINITIONS\n{}",
131 $src, compiled
132 );
133 insta::assert_snapshot!(insta::internals::AutoName, output, $src);
134 }};
135
136 ($src:expr $(,)?) => {{
137 let compiled = $crate::javascript::tests::compile_ts($src, vec![]);
138 let output = format!(
139 "----- SOURCE CODE\n{}\n\n----- TYPESCRIPT DEFINITIONS\n{}",
140 $src, compiled
141 );
142 insta::assert_snapshot!(insta::internals::AutoName, output, $src);
143 }};
144}
145
146pub fn compile(src: &str, deps: Vec<(&str, &str, &str)>) -> TypedModule {
147 let mut modules = im::HashMap::new();
148 let ids = UniqueIdGenerator::new();
149 // DUPE: preludeinsertion
150 // TODO: Currently we do this here and also in the tests. It would be better
151 // to have one place where we create all this required state for use in each
152 // place.
153 let _ = modules.insert(
154 PRELUDE_MODULE_NAME.into(),
155 crate::type_::build_prelude(&ids),
156 );
157 let mut direct_dependencies = HashMap::from_iter(vec![]);
158
159 deps.iter().for_each(|(dep_package, dep_name, dep_src)| {
160 let mut dep_config = PackageConfig::default();
161 dep_config.name = (*dep_package).into();
162 let parsed = crate::parse::parse_module(
163 Utf8PathBuf::from("test/path"),
164 dep_src,
165 &WarningEmitter::null(),
166 )
167 .expect("dep syntax error");
168 let mut ast = parsed.module;
169 ast.name = (*dep_name).into();
170 let line_numbers = LineNumbers::new(dep_src);
171
172 let dep = crate::analyse::ModuleAnalyzerConstructor::<()> {
173 target: Target::JavaScript,
174 ids: &ids,
175 origin: Origin::Src,
176 importable_modules: &modules,
177 warnings: &TypeWarningEmitter::null(),
178 direct_dependencies: &HashMap::new(),
179 dev_dependencies: &HashSet::new(),
180 target_support: TargetSupport::Enforced,
181 package_config: &dep_config,
182 }
183 .infer_module(ast, line_numbers, "".into())
184 .expect("should successfully infer");
185 let _ = modules.insert((*dep_name).into(), dep.type_info);
186 let _ = direct_dependencies.insert((*dep_package).into(), ());
187 });
188
189 let parsed =
190 crate::parse::parse_module(Utf8PathBuf::from("test/path"), src, &WarningEmitter::null())
191 .expect("syntax error");
192 let mut ast = parsed.module;
193 ast.name = "my/mod".into();
194 let line_numbers = LineNumbers::new(src);
195 let mut config = PackageConfig::default();
196 config.name = "thepackage".into();
197
198 let module = crate::analyse::ModuleAnalyzerConstructor::<()> {
199 target: Target::JavaScript,
200 ids: &ids,
201 origin: Origin::Src,
202 importable_modules: &modules,
203 warnings: &TypeWarningEmitter::null(),
204 direct_dependencies: &direct_dependencies,
205 dev_dependencies: &HashSet::new(),
206 target_support: TargetSupport::NotEnforced,
207 package_config: &config,
208 }
209 .infer_module(ast, line_numbers, "src/module.gleam".into())
210 .expect("should successfully infer");
211
212 inline::module(module, &modules)
213}
214
215pub fn compile_js(src: &str, deps: Vec<(&str, &str, &str)>) -> String {
216 let ast = compile(src, deps);
217 let line_numbers = LineNumbers::new(src);
218 let stdlib_package = StdlibPackage::Present;
219 let (output, _) = module(ModuleConfig {
220 module: &ast,
221 line_numbers: &line_numbers,
222 src: &"".into(),
223 typescript: TypeScriptDeclarations::None,
224 source_map: false,
225 stdlib_package,
226 path: Utf8Path::new("src/module.gleam"),
227 project_root: "project/root".into(),
228 });
229
230 output.replace(
231 std::include_str!("../../templates/echo.mjs"),
232 "// ...omitted code from `templates/echo.mjs`...",
233 )
234}
235
236pub fn compile_js_with_source_map(src: &str, deps: Vec<(&str, &str, &str)>) -> (String, SourceMap) {
237 let ast = compile(src, deps);
238 let line_numbers = LineNumbers::new(src);
239 let stdlib_package = StdlibPackage::Present;
240 let (output, source_map) = module(ModuleConfig {
241 module: &ast,
242 line_numbers: &line_numbers,
243 src: &"".into(),
244 typescript: TypeScriptDeclarations::None,
245 source_map: true,
246 stdlib_package,
247 path: Utf8Path::new("src/module.gleam"),
248 project_root: "project/root".into(),
249 });
250 let source_map = source_map.expect("source map should always be present");
251
252 let output = output.replace(
253 std::include_str!("../../templates/echo.mjs"),
254 "// ...omitted code from `templates/echo.mjs`...",
255 );
256 (output, source_map)
257}
258
259pub fn compile_ts(src: &str, deps: Vec<(&str, &str, &str)>) -> String {
260 let ast = compile(src, deps);
261 ts_declaration(&ast)
262}
263
264// Append zero indexed line numbers to the code for easier reading of source maps.
265pub fn append_line_numbers(src: &str) -> String {
266 src.lines()
267 .enumerate()
268 .map(|(line, content)| format!("{} |{}", line, content))
269 .collect::<Vec<String>>()
270 .join("\n")
271}
272
273// Pretty-print a sourcemap to a string that might be readable by humans.
274pub fn source_map_to_string(src: &str, compiled: &str, source_map: SourceMap) -> String {
275 let mut output = String::new();
276 output.push_str("File: ");
277 output.push_str(source_map.get_file().expect("source map file"));
278 output.push_str("\nSources:");
279 for source in source_map.sources() {
280 output.push(' ');
281 output.push_str(source);
282 }
283 output.push_str("\nMappings:\n");
284 output.push_str("----- \n");
285 let src_line_numbers = LineNumbers::new(&src);
286 let compiled_line_numbers = LineNumbers::new(&compiled);
287
288 // Since source maps can have multiple tokens for the same source index, we skip over the
289 // intermediate tokens and only keep tokens with a new source index.
290 //
291 // Construct a vector of tuples of (src_line, src_col, dst_line, dst_col)
292 //
293 let mut merged_tokens: Vec<((u32, u32), (u32, u32))> = Vec::new();
294 // Push a sentinel token at 0, 0
295 merged_tokens.push(((0, 0), (0, 0)));
296 let mut prev_token_src = (0, 0);
297 for token in source_map.tokens() {
298 if prev_token_src == token.get_src() {
299 // Same source index, so extend the current token
300 // This is for making the source map more readable
301 continue;
302 } else {
303 // Different source index, so add the current token to the merged tokens
304 merged_tokens.push((token.get_src(), token.get_dst()));
305 prev_token_src = token.get_src();
306 };
307 }
308 let sorted_src_tokens = merged_tokens
309 .iter()
310 .sorted_by_key(|(src, _)| src)
311 .map(|(src, _)| *src)
312 .collect::<Vec<_>>();
313 let by_dst = merged_tokens
314 .iter()
315 .sorted_by_key(|(_, dst)| dst)
316 .collect::<Vec<_>>();
317
318 let mut prev_compiled_index = 0;
319 // iterate over all but the last mapping
320 for i in 0..by_dst.len() - 1 {
321 let ((src_line, src_col), (dst_line, dst_col)) = by_dst[i];
322 let ((_, _), (next_dst_line, next_dst_col)) = by_dst[i + 1];
323 let (next_src_line, next_src_col) = sorted_src_tokens
324 .iter()
325 .find(|src| **src > (*src_line, *src_col))
326 .expect("next src token must exist");
327
328 let src_index = src_line_numbers.byte_index(Position::new(*src_line, *src_col)) as usize;
329 let compiled_index =
330 compiled_line_numbers.byte_index(Position::new(*dst_line, *dst_col)) as usize;
331 let next_src_index =
332 src_line_numbers.byte_index(Position::new(*next_src_line, *next_src_col)) as usize;
333 let next_compiled_index =
334 compiled_line_numbers.byte_index(Position::new(*next_dst_line, *next_dst_col)) as usize;
335 output.push_str(&format!("{}\n", &src[src_index..next_src_index]));
336 output.push_str("⏷\n");
337 output.push_str(&format!(
338 "{}\n",
339 &compiled[compiled_index..next_compiled_index]
340 ));
341 output.push_str("----- \n");
342 prev_compiled_index = next_compiled_index;
343 }
344 // Print the last mapping. this needs to be done separately because the last mapping
345 // will likely extend to the rest of the source code.
346 let (src_token, _) = by_dst.last().expect("last mapping must exist");
347 let src_index: usize =
348 src_line_numbers.byte_index(Position::new(src_token.0, src_token.1)) as usize;
349 // find the next src token after the last mapping if one exists
350 let next_src_token = sorted_src_tokens.iter().find(|src| **src > *src_token);
351 match next_src_token {
352 Some(next_src_token) => {
353 let next_src_index = src_line_numbers
354 .byte_index(Position::new(next_src_token.0, next_src_token.1))
355 as usize;
356 output.push_str(&format!("{}\n", &src[src_index..next_src_index]));
357 }
358 None => {
359 // If there is no next src token, print the rest of the source and compiled code
360 output.push_str(&format!("{}\n", &src[src_index..]));
361 }
362 }
363 output.push_str("⏷\n");
364 output.push_str(&format!("{}\n", &compiled[prev_compiled_index..]));
365 output.push_str("----- \n");
366 output
367}