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