Fork of daniellemaywood.uk/gleam — Wasm codegen work
8.1 kB
293 lines
1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2021 The Gleam contributors
3
4use std::collections::{HashMap, HashSet};
5
6use ecow::EcoString;
7use itertools::Itertools;
8
9use crate::javascript::{INDENT, JavaScriptCodegenTarget};
10use pretty_arena::*;
11
12/// A collection of JavaScript import statements from Gleam imports and from
13/// external functions, to be rendered into a JavaScript module.
14///
15#[derive(Debug, Default)]
16pub(crate) struct Imports<'a, 'doc> {
17 imports: HashMap<EcoString, Import<'a, 'doc>>,
18 exports: HashSet<EcoString>,
19}
20
21impl<'a, 'doc> Imports<'a, 'doc> {
22 pub fn new() -> Self {
23 Self::default()
24 }
25
26 pub fn register_export(&mut self, export: EcoString) {
27 let _ = self.exports.insert(export);
28 }
29
30 pub fn register_module(
31 &mut self,
32 path: EcoString,
33 aliases: impl IntoIterator<Item = EcoString>,
34 unqualified_imports: impl IntoIterator<Item = Member<'a, 'doc>>,
35 ) {
36 let import = self
37 .imports
38 .entry(path.clone())
39 .or_insert_with(|| Import::new(path.clone()));
40 import.aliases.extend(aliases);
41 import.unqualified.extend(unqualified_imports);
42 }
43
44 pub fn into_doc(
45 self,
46 arena: &'doc DocumentArena<'a, 'doc>,
47 codegen_target: JavaScriptCodegenTarget,
48 ) -> Document<'a, 'doc> {
49 let imports = arena.concat(
50 self.imports
51 .into_values()
52 .sorted_by(|a, b| a.path.cmp(&b.path))
53 .map(|import| import.into_doc(arena, codegen_target)),
54 );
55
56 if self.exports.is_empty() {
57 imports
58 } else {
59 let names = arena.join(
60 self.exports
61 .into_iter()
62 .sorted()
63 .map(|string| string.to_doc(arena)),
64 COMMA_BREAK_DOCUMENT,
65 );
66 let names = docvec![
67 arena,
68 docvec![arena, BREAKABLE_SPACE_DOCUMENT, names].nest(arena, INDENT),
69 TRAILING_COMMA_OR_SPACE_BREAK_DOCUMENT
70 ]
71 .group(arena);
72
73 let export_keyword = match codegen_target {
74 JavaScriptCodegenTarget::JavaScript => EXPORT_SPACE_OPEN_CURLY_DOCUMENT,
75 JavaScriptCodegenTarget::TypeScriptDeclarations => {
76 EXPORT_TYPE_SPACE_OPEN_CURLY_DOCUMENT
77 }
78 };
79
80 docvec![
81 arena,
82 imports,
83 LINE_DOCUMENT,
84 export_keyword,
85 names,
86 CLOSE_CURLY_SEMICOLON_DOCUMENT,
87 LINE_DOCUMENT
88 ]
89 }
90 }
91
92 pub fn is_empty(&self) -> bool {
93 self.imports.is_empty() && self.exports.is_empty()
94 }
95
96 /// Remove variants which are imported in Gleam code, but not needed to be
97 /// imported because singleton constants are used instead.
98 ///
99 pub fn filter_unused_variants<I>(&mut self, unused: I)
100 where
101 I: Iterator<Item = (EcoString, EcoString)>,
102 {
103 for (path, name) in unused {
104 if let Some(import_) = self.imports.get_mut(&path)
105 && let Some(index) = import_
106 .unqualified
107 .iter()
108 .position(|member| member.name == name)
109 {
110 _ = import_.unqualified.remove(index);
111 }
112 }
113 }
114}
115
116#[derive(Debug)]
117struct Import<'a, 'doc> {
118 path: EcoString,
119 aliases: HashSet<EcoString>,
120 unqualified: Vec<Member<'a, 'doc>>,
121}
122
123impl<'a, 'doc> Import<'a, 'doc> {
124 fn new(path: EcoString) -> Self {
125 Self {
126 path,
127 aliases: HashSet::new(),
128 unqualified: vec![],
129 }
130 }
131
132 pub fn into_doc(
133 self,
134 arena: &'doc DocumentArena<'a, 'doc>,
135 codegen_target: JavaScriptCodegenTarget,
136 ) -> Document<'a, 'doc> {
137 let path = self.path.to_doc(arena);
138 let import_modifier = if codegen_target == JavaScriptCodegenTarget::TypeScriptDeclarations {
139 TYPE_SPACE_DOCUMENT
140 } else {
141 EMPTY_DOCUMENT
142 };
143 let alias_imports = arena.concat(self.aliases.into_iter().sorted().map(|alias| {
144 docvec![
145 arena,
146 IMPORT_SPACE_DOCUMENT,
147 import_modifier,
148 TIMES_SPACE_AS_SPACE_DOCUMENT,
149 alias,
150 SPACE_FROM_SPACE_DOUBLE_QUOTE_DOCUMENT,
151 path,
152 DOUBLE_QUOTE_SEMICOLON_DOCUMENT,
153 LINE_DOCUMENT
154 ]
155 }));
156 if self.unqualified.is_empty() {
157 alias_imports
158 } else {
159 let members = self
160 .unqualified
161 .into_iter()
162 .map(|member| member.into_doc(arena));
163 let members = arena.join(members, COMMA_BREAK_DOCUMENT);
164 let members = docvec![
165 arena,
166 docvec![arena, BREAKABLE_SPACE_DOCUMENT, members].nest(arena, INDENT),
167 TRAILING_COMMA_OR_SPACE_BREAK_DOCUMENT
168 ]
169 .group(arena);
170 docvec![
171 arena,
172 alias_imports,
173 IMPORT_SPACE_DOCUMENT,
174 import_modifier,
175 OPEN_CURLY_DOCUMENT,
176 members,
177 CLOSE_CURLY_SPACE_FROM_SPACE_DOUBLE_QUOTE_DOCUMENT,
178 path,
179 DOUBLE_QUOTE_SEMICOLON_DOCUMENT,
180 LINE_DOCUMENT
181 ]
182 }
183 }
184}
185
186#[derive(Debug)]
187pub struct Member<'a, 'doc> {
188 pub name: EcoString,
189 pub alias: Option<Document<'a, 'doc>>,
190}
191
192impl<'a, 'doc> Member<'a, 'doc> {
193 fn into_doc(self, arena: &'doc DocumentArena<'a, 'doc>) -> Document<'a, 'doc> {
194 match self.alias {
195 None => self.name.to_doc(arena),
196 Some(alias) => docvec![arena, self.name, SPACE_AS_SPACE_DOCUMENT, alias],
197 }
198 }
199}
200
201#[test]
202fn into_doc() {
203 let arena = DocumentArena::new();
204 let mut imports = Imports::new();
205 imports.register_module("./gleam/empty".into(), [], []);
206 imports.register_module(
207 "./multiple/times".into(),
208 ["wibble".into(), "wobble".into()],
209 [],
210 );
211 imports.register_module("./multiple/times".into(), ["wubble".into()], []);
212 imports.register_module(
213 "./multiple/times".into(),
214 [],
215 [Member {
216 name: "one".into(),
217 alias: None,
218 }],
219 );
220
221 imports.register_module(
222 "./other".into(),
223 [],
224 [
225 Member {
226 name: "one".into(),
227 alias: None,
228 },
229 Member {
230 name: "one".into(),
231 alias: Some("onee".to_doc(&arena)),
232 },
233 Member {
234 name: "two".into(),
235 alias: Some("twoo".to_doc(&arena)),
236 },
237 ],
238 );
239
240 imports.register_module(
241 "./other".into(),
242 [],
243 [
244 Member {
245 name: "three".into(),
246 alias: None,
247 },
248 Member {
249 name: "four".into(),
250 alias: None,
251 },
252 ],
253 );
254
255 imports.register_module(
256 "./zzz".into(),
257 [],
258 [
259 Member {
260 name: "one".into(),
261 alias: None,
262 },
263 Member {
264 name: "two".into(),
265 alias: None,
266 },
267 ],
268 );
269
270 assert_eq!(
271 LINE_DOCUMENT
272 .append(
273 &arena,
274 imports.into_doc(&arena, JavaScriptCodegenTarget::JavaScript)
275 )
276 .to_pretty_string(40),
277 r#"
278import * as wibble from "./multiple/times";
279import * as wobble from "./multiple/times";
280import * as wubble from "./multiple/times";
281import { one } from "./multiple/times";
282import {
283 one,
284 one as onee,
285 two as twoo,
286 three,
287 four,
288} from "./other";
289import { one, two } from "./zzz";
290"#
291 .to_string()
292 );
293}