Fork of daniellemaywood.uk/gleam — Wasm codegen work
7.2 kB
250 lines
1function echo(value, message, file, line) {
2 const grey = "\u001b[90m";
3 const reset_color = "\u001b[39m";
4 const file_line = `${file}:${line}`;
5 const inspector = new Echo$Inspector();
6 const string_value = inspector.inspect(value);
7 const string_message = message === undefined ? "" : " " + message;
8
9 if (globalThis.process?.stderr?.write) {
10 // If we're in Node.js, use `stderr`
11 const string = `${grey}${file_line}${reset_color}${string_message}\n${string_value}\n`;
12 globalThis.process.stderr.write(string);
13 } else if (globalThis.Deno) {
14 // If we're in Deno, use `stderr`
15 const string = `${grey}${file_line}${reset_color}${string_message}\n${string_value}\n`;
16 globalThis.Deno.stderr.writeSync(new TextEncoder().encode(string));
17 } else {
18 // Otherwise, use `console.log`
19 // The browser's console.log doesn't support ansi escape codes
20 const string = `${file_line}${string_message}\n${string_value}`;
21 globalThis.console.log(string);
22 }
23
24 return value;
25}
26
27class Echo$Inspector {
28 #references = new globalThis.Set();
29
30 #isDict(value) {
31 try {
32 // We can only check if an object is a stdlib Dict if it is one of the
33 // project's dependencies.
34 // We import the public gleam/dict module, so to check if something is a
35 // dict we compare the `constructor` field on the object with that of a
36 // new dict.
37 const empty_dict = $stdlib$dict.new$();
38 const dict_class = empty_dict.constructor;
39 return value instanceof dict_class;
40 } catch {
41 // If stdlib is not one of the project's dependencies then `$stdlib$dict`
42 // will not have been imported and the check will throw an exception meaning
43 // we can't check if something is actually a `Dict`.
44 return false;
45 }
46 }
47
48 #float(float) {
49 const string = float.toString().replace("+", "");
50 if (string.indexOf(".") >= 0) {
51 return string;
52 } else {
53 const index = string.indexOf("e");
54 if (index >= 0) {
55 return string.slice(0, index) + ".0" + string.slice(index);
56 } else {
57 return string + ".0";
58 }
59 }
60 }
61
62 inspect(v) {
63 const t = typeof v;
64 if (v === true) return "True";
65 if (v === false) return "False";
66 if (v === null) return "//js(null)";
67 if (v === undefined) return "Nil";
68 if (t === "string") return this.#string(v);
69 if (t === "bigint" || globalThis.Number.isInteger(v)) return v.toString();
70 if (t === "number") return this.#float(v);
71 if (v instanceof $UtfCodepoint) return this.#utfCodepoint(v);
72 if (v instanceof $BitArray) return this.#bit_array(v);
73 if (v instanceof globalThis.RegExp) return `//js(${v})`;
74 if (v instanceof globalThis.Date) return `//js(Date("${v.toISOString()}"))`;
75 if (v instanceof globalThis.Error) return `//js(${v.toString()})`;
76 if (v instanceof globalThis.Function) {
77 const args = [];
78 for (const i of globalThis.Array(v.length).keys())
79 args.push(globalThis.String.fromCharCode(i + 97));
80 return `//fn(${args.join(", ")}) { ... }`;
81 }
82
83 if (this.#references.size === this.#references.add(v).size) {
84 return "//js(circular reference)";
85 }
86
87 let printed;
88 if (globalThis.Array.isArray(v)) {
89 printed = `#(${v.map((v) => this.inspect(v)).join(", ")})`;
90 } else if (v instanceof $List) {
91 printed = this.#list(v);
92 } else if (v instanceof $CustomType) {
93 printed = this.#customType(v);
94 } else if (this.#isDict(v)) {
95 printed = this.#dict(v);
96 } else if (v instanceof Set) {
97 return `//js(Set(${[...v].map((v) => this.inspect(v)).join(", ")}))`;
98 } else {
99 printed = this.#object(v);
100 }
101 this.#references.delete(v);
102 return printed;
103 }
104
105 #object(v) {
106 const name =
107 globalThis.Object.getPrototypeOf(v)?.constructor?.name || "Object";
108 const props = [];
109 for (const k of globalThis.Object.keys(v)) {
110 props.push(`${this.inspect(k)}: ${this.inspect(v[k])}`);
111 }
112 const body = props.length ? " " + props.join(", ") + " " : "";
113 const head = name === "Object" ? "" : name + " ";
114 return `//js(${head}{${body}})`;
115 }
116
117 #dict(map) {
118 let body = "dict.from_list([";
119 let first = true;
120
121 let key_value_pairs = $stdlib$dict.fold(map, [], (pairs, key, value) => {
122 pairs.push([key, value]);
123 return pairs;
124 });
125
126 key_value_pairs.sort();
127 key_value_pairs.forEach(([key, value]) => {
128 if (!first) body = body + ", ";
129 body = body + "#(" + this.inspect(key) + ", " + this.inspect(value) + ")";
130 first = false;
131 });
132 return body + "])";
133 }
134
135 #customType(record) {
136 const props = globalThis.Object.keys(record)
137 .map((label) => {
138 const value = this.inspect(record[label]);
139 return isNaN(parseInt(label)) ? `${label}: ${value}` : value;
140 })
141 .join(", ");
142 return props
143 ? `${record.constructor.name}(${props})`
144 : record.constructor.name;
145 }
146
147 #list(list) {
148 if (list instanceof $Empty) {
149 return "[]";
150 }
151
152 let char_out = 'charlist.from_string("';
153 let list_out = "[";
154
155 let current = list;
156 while (current instanceof $NonEmpty) {
157 let element = current.head;
158 current = current.tail;
159
160 if (list_out !== "[") {
161 list_out += ", ";
162 }
163 list_out += this.inspect(element);
164
165 if (char_out) {
166 if (
167 globalThis.Number.isInteger(element) &&
168 element >= 32 &&
169 element <= 126
170 ) {
171 char_out += globalThis.String.fromCharCode(element);
172 } else {
173 char_out = null;
174 }
175 }
176 }
177
178 if (char_out) {
179 return char_out + '")';
180 } else {
181 return list_out + "]";
182 }
183 }
184
185 #string(str) {
186 let new_str = '"';
187 for (let i = 0; i < str.length; i++) {
188 const char = str[i];
189 switch (char) {
190 case "\n":
191 new_str += "\\n";
192 break;
193 case "\r":
194 new_str += "\\r";
195 break;
196 case "\t":
197 new_str += "\\t";
198 break;
199 case "\f":
200 new_str += "\\f";
201 break;
202 case "\\":
203 new_str += "\\\\";
204 break;
205 case '"':
206 new_str += '\\"';
207 break;
208 default:
209 if (char < " " || (char > "~" && char < "\u{00A0}")) {
210 new_str +=
211 "\\u{" +
212 char.charCodeAt(0).toString(16).toUpperCase().padStart(4, "0") +
213 "}";
214 } else {
215 new_str += char;
216 }
217 }
218 }
219 new_str += '"';
220 return new_str;
221 }
222
223 #utfCodepoint(codepoint) {
224 return `//utfcodepoint(${globalThis.String.fromCodePoint(codepoint.value)})`;
225 }
226
227 #bit_array(bits) {
228 if (bits.bitSize === 0) {
229 return "<<>>";
230 }
231
232 let acc = "<<";
233
234 for (let i = 0; i < bits.byteSize - 1; i++) {
235 acc += bits.byteAt(i).toString();
236 acc += ", ";
237 }
238
239 if (bits.byteSize * 8 === bits.bitSize) {
240 acc += bits.byteAt(bits.byteSize - 1).toString();
241 } else {
242 const trailingBitsCount = bits.bitSize % 8;
243 acc += bits.byteAt(bits.byteSize - 1) >> (8 - trailingBitsCount);
244 acc += `:size(${trailingBitsCount})`;
245 }
246
247 acc += ">>";
248 return acc;
249 }
250}