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