@josd/eyecode 0.0.0-stage → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/writer.js ADDED
@@ -0,0 +1,261 @@
1
+ // Canonical term output, as JavaScript source.
2
+ //
3
+ // Every term has one spelling, and that spelling is a JavaScript expression
4
+ // that the reader in reader.js turns back into the same term: an atom is a
5
+ // string literal, a number a numeric literal, a variable a name, a compound
6
+ // term a call such as type('socrates', 'mortal'), and a list an array with
7
+ // ...T for an open tail. A conjunction or disjunction built left to right,
8
+ // and(and(A, B), C), is written and(A, B, C). A compound whose name is not a
9
+ // plain identifier is written struct('name', Arg, ...).
10
+ import { Struct, Var, deref } from './terms.js';
11
+
12
+ // Words a JavaScript expression cannot use as a plain name.
13
+ export const RESERVED_WORDS = new Set([
14
+ 'await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default', 'delete',
15
+ 'do', 'else', 'enum', 'export', 'extends', 'false', 'finally', 'for', 'function', 'if',
16
+ 'implements', 'import', 'in', 'instanceof', 'interface', 'let', 'new', 'null', 'package',
17
+ 'private', 'protected', 'public', 'return', 'static', 'super', 'switch', 'this', 'throw', 'true',
18
+ 'try', 'typeof', 'var', 'void', 'while', 'with', 'yield',
19
+ ]);
20
+ // Calls to these names have a fixed meaning to the reader.
21
+ export const RESERVED_CALLS = new Set(['struct']);
22
+ // Left-nested chains of these binary compounds are written as one call.
23
+ export const CHAINED = new Set(['and', 'or']);
24
+
25
+ const PLAIN = /^[A-Za-z_][A-Za-z0-9_]*$/;
26
+
27
+ export function plainName(name) {
28
+ return PLAIN.test(name) && !RESERVED_WORDS.has(name);
29
+ }
30
+
31
+ // Whether a compound with this name and arity is written name(...).
32
+ export function callableName(name, arity) {
33
+ if (!plainName(name) || RESERVED_CALLS.has(name)) return false;
34
+ // and(A, B, C) reads as a chain, so a compound and/3 is struct('and', ...).
35
+ return !(CHAINED.has(name) && arity > 2);
36
+ }
37
+
38
+ export function validVariableName(name) {
39
+ return plainName(name) && name !== '_' && !name.startsWith('VAR_');
40
+ }
41
+
42
+ // An injective identifier for a variable name that is not a valid one: X#12,
43
+ // the name a renamed-apart clause variable gets, becomes VAR_X_23_12. ASCII
44
+ // letters and digits stay, an underscore doubles and any other character
45
+ // becomes _hex_. Names already starting with the prefix are encoded too.
46
+ export function encodeName(name) {
47
+ const out = ['VAR_'];
48
+ for (const ch of name) {
49
+ if (/^[A-Za-z0-9]$/.test(ch)) out.push(ch);
50
+ else if (ch === '_') out.push('__');
51
+ else out.push(`_${ch.codePointAt(0).toString(16)}_`);
52
+ }
53
+ return out.join('');
54
+ }
55
+
56
+ const ENCODED = /([A-Za-z0-9])|__|_([0-9a-f]+)_/y;
57
+
58
+ // The variable name an identifier written by encodeName stands for, or null
59
+ // when the identifier is not such an encoding.
60
+ export function decodeName(text) {
61
+ const out = [];
62
+ let position = 4;
63
+ while (position < text.length) {
64
+ ENCODED.lastIndex = position;
65
+ const match = ENCODED.exec(text);
66
+ if (match === null) return null;
67
+ if (match[1]) {
68
+ out.push(match[1]);
69
+ } else if (match[2]) {
70
+ const code = parseInt(match[2], 16);
71
+ if (code > 0x10ffff) return null;
72
+ out.push(String.fromCodePoint(code));
73
+ } else {
74
+ out.push('_');
75
+ }
76
+ position = ENCODED.lastIndex;
77
+ }
78
+ const name = out.join('');
79
+ return encodeName(name) === text ? name : null;
80
+ }
81
+
82
+ export function variableText(name) {
83
+ return validVariableName(name) ? name : encodeName(name);
84
+ }
85
+
86
+ const ESCAPES = { '\\': '\\\\', "'": "\\'", '\n': '\\n', '\r': '\\r', '\t': '\\t', '\b': '\\b', '\f': '\\f', '\v': '\\v' };
87
+
88
+ // An atom as a single-quoted string literal. Control characters, line
89
+ // separators and lone surrogates are escaped; any other character is written
90
+ // as it is.
91
+ export function quote(text) {
92
+ let out = "'";
93
+ for (let i = 0; i < text.length; i++) {
94
+ const ch = text[i];
95
+ const code = text.charCodeAt(i);
96
+ if (ESCAPES[ch] !== undefined) {
97
+ out += ESCAPES[ch];
98
+ } else if (code >= 0xd800 && code <= 0xdbff && i + 1 < text.length &&
99
+ text.charCodeAt(i + 1) >= 0xdc00 && text.charCodeAt(i + 1) <= 0xdfff) {
100
+ out += ch + text[i + 1];
101
+ i++;
102
+ } else if (code < 0x20 || (code >= 0x7f && code <= 0x9f) || code === 0x2028 || code === 0x2029 ||
103
+ (code >= 0xd800 && code <= 0xdfff)) {
104
+ out += `\\u${code.toString(16).padStart(4, '0')}`;
105
+ } else {
106
+ out += ch;
107
+ }
108
+ }
109
+ return `${out}'`;
110
+ }
111
+
112
+ // A float as Python's repr spells it: the shortest digits that read back as
113
+ // the same value, in fixed notation from 1e-4 up to 1e16 and in scientific
114
+ // notation otherwise, always with a fraction or an exponent.
115
+ export function floatText(value) {
116
+ if (value === 0) return Object.is(value, -0) ? '-0.0' : '0.0';
117
+ const sign = value < 0 ? '-' : '';
118
+ const [mantissa, exponentText] = Math.abs(value).toExponential().split('e');
119
+ const digits = mantissa.replace('.', '');
120
+ const exponent = Number(exponentText);
121
+ if (exponent >= -4 && exponent < 16) {
122
+ if (exponent < 0) return `${sign}0.${'0'.repeat(-exponent - 1)}${digits}`;
123
+ const whole = digits.slice(0, exponent + 1).padEnd(exponent + 1, '0');
124
+ const fraction = digits.slice(exponent + 1);
125
+ return `${sign}${whole}.${fraction || '0'}`;
126
+ }
127
+ const lead = digits.length > 1 ? `${digits[0]}.${digits.slice(1)}` : digits;
128
+ const magnitude = String(Math.abs(exponent)).padStart(2, '0');
129
+ return `${sign}${lead}e${exponent < 0 ? '-' : '+'}${magnitude}`;
130
+ }
131
+
132
+ export function letterName(n) {
133
+ return String.fromCharCode(65 + (n % 26)) + (n >= 26 ? String(Math.floor(n / 26)) : '');
134
+ }
135
+
136
+ // Names for write(): each variable met is named A, B, ..., Z, A1, ... in the
137
+ // order the writing meets it, across every write sharing the lettering.
138
+ export class Lettering {
139
+ constructor() {
140
+ this.names = new Map();
141
+ }
142
+
143
+ get(name) {
144
+ let letter = this.names.get(name);
145
+ if (letter === undefined) {
146
+ letter = letterName(this.names.size);
147
+ this.names.set(name, letter);
148
+ }
149
+ return letter;
150
+ }
151
+ }
152
+
153
+ // Names for write() that name nothing but notice a variable.
154
+ class Spy {
155
+ constructor() {
156
+ this.seen = false;
157
+ }
158
+
159
+ get() {
160
+ this.seen = true;
161
+ return undefined;
162
+ }
163
+ }
164
+
165
+ const NO_NAMES = { get: () => undefined };
166
+
167
+ // The canonical text of a term, and whether it has a variable.
168
+ export function writeNotingVariables(term) {
169
+ const spy = new Spy();
170
+ const text = format(term, null, spy);
171
+ return [text, spy.seen];
172
+ }
173
+
174
+ // The canonical spelling of a term. names maps variable names to the names
175
+ // to write, as a Lettering does.
176
+ export function write(term, env = null, names = NO_NAMES) {
177
+ if (env !== null && !env.bindings.size) env = null;
178
+ return format(term, env, names);
179
+ }
180
+
181
+ // Written with an explicit stack, so deeply nested terms and long lists need
182
+ // no host recursion. Each entry is a term to write or a string to emit.
183
+ function format(root, env, names) {
184
+ const out = [];
185
+ const stack = [root];
186
+ while (stack.length) {
187
+ let term = stack.pop();
188
+ if (term instanceof Text) {
189
+ out.push(term.text);
190
+ continue;
191
+ }
192
+ if (env !== null) term = deref(term, env);
193
+ const type = typeof term;
194
+ if (type === 'string') {
195
+ out.push(term === '[]' ? '[]' : quote(term));
196
+ } else if (type === 'bigint') {
197
+ out.push(term.toString());
198
+ } else if (type === 'number') {
199
+ out.push(floatText(term));
200
+ } else if (term instanceof Var) {
201
+ const name = names.get(term.name);
202
+ out.push(name !== undefined ? name : variableText(term.name));
203
+ } else if (term.name === '.' && term.args.length === 2) {
204
+ const items = [];
205
+ let cursor = term;
206
+ while (cursor instanceof Struct && cursor.name === '.' && cursor.args.length === 2) {
207
+ items.push(cursor.args[0]);
208
+ cursor = env === null ? cursor.args[1] : deref(cursor.args[1], env);
209
+ }
210
+ const parts = [OPEN_LIST];
211
+ items.forEach((item, index) => {
212
+ if (index) parts.push(COMMA);
213
+ parts.push(item);
214
+ });
215
+ if (cursor !== '[]') parts.push(SPREAD, cursor);
216
+ parts.push(CLOSE_LIST);
217
+ for (let i = parts.length - 1; i >= 0; i--) stack.push(parts[i]);
218
+ } else {
219
+ let args = term.args;
220
+ if (CHAINED.has(term.name) && args.length === 2) {
221
+ // A left-nested chain is written as one call: and(A, B, C).
222
+ const chain = [args[1]];
223
+ let left = args[0];
224
+ while (true) {
225
+ const resolved = env === null ? left : deref(left, env);
226
+ if (!(resolved instanceof Struct) || resolved.name !== term.name || resolved.args.length !== 2) break;
227
+ chain.push(resolved.args[1]);
228
+ left = resolved.args[0];
229
+ }
230
+ chain.push(left);
231
+ args = chain.reverse();
232
+ }
233
+ const parts = [];
234
+ if (callableName(term.name, term.args.length)) {
235
+ parts.push(new Text(`${term.name}(`));
236
+ } else {
237
+ parts.push(new Text(`struct(${quote(term.name)}`));
238
+ if (args.length) parts.push(COMMA);
239
+ }
240
+ args.forEach((arg, index) => {
241
+ if (index) parts.push(COMMA);
242
+ parts.push(arg);
243
+ });
244
+ parts.push(CLOSE_CALL);
245
+ for (let i = parts.length - 1; i >= 0; i--) stack.push(parts[i]);
246
+ }
247
+ }
248
+ return out.join('');
249
+ }
250
+
251
+ class Text {
252
+ constructor(text) {
253
+ this.text = text;
254
+ }
255
+ }
256
+
257
+ const OPEN_LIST = new Text('[');
258
+ const CLOSE_LIST = new Text(']');
259
+ const CLOSE_CALL = new Text(')');
260
+ const COMMA = new Text(', ');
261
+ const SPREAD = new Text(', ...');