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