@teasel/parser 0.0.2 → 0.0.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/api.js CHANGED
@@ -51,8 +51,9 @@ function stops(list) {
51
51
  * @property {() => ArrayLike<number>} shapes
52
52
  */
53
53
 
54
- // words the engine has to judge: keywords, the strict-mode reserved words and the contextual ones
55
- const KEYWORD = new Set('await break case catch class const continue debugger default delete do else enum export extends false finally for function if implements import in instanceof interface let new null package private protected public return static super switch this throw true try typeof var void while with yield'.split(' '));
54
+ // words the engine has to judge: keywords, the strict-mode reserved words, the contextual ones,
55
+ // and the two names strict mode refuses to bind
56
+ const KEYWORD = new Set('arguments await break case catch class const continue debugger default delete do else enum eval export extends false finally for function if implements import in instanceof interface let new null package private protected public return static super switch this throw true try typeof var void while with yield'.split(' '));
56
57
 
57
58
  /**
58
59
  * The offset after an identifier the host's syntax follows directly, so the answer needs no
@@ -63,10 +64,12 @@ const KEYWORD = new Set('await break case catch class const continue debugger de
63
64
  * @returns {[number, number] | null} the identifier's end and where the parse ends
64
65
  */
65
66
  function bare(source, at, end, stopAt, typescript) {
67
+ if (at >= end) return null;
66
68
  let i = at;
67
69
  const first = source.codePointAt(i);
68
70
  if (first === undefined || !isIdentifierStart(first) || first === 0x5c) return null;
69
71
  i += first > 0xffff ? 2 : 1;
72
+ if (i > end) return null;
70
73
  while (i < end) {
71
74
  const code = /** @type {number} */ (source.codePointAt(i));
72
75
  if (code === 0x5c) return null;
@@ -85,7 +88,7 @@ function bare(source, at, end, stopAt, typescript) {
85
88
  const after = source.codePointAt(i + stop.length);
86
89
  if (isIdentifierStart(/** @type {number} */ (stop.codePointAt(0))) && after !== undefined && isIdentifierChar(after)) continue;
87
90
  // a stop TypeScript reads as its own is the engine's to judge
88
- if (typescript && (stop === 'as' || stop === 'satisfies')) return null;
91
+ if (typescript && (stop === 'as' || stop === 'satisfies' || stop === ':')) return null;
89
92
  return [name_end, name_end];
90
93
  }
91
94
  }
@@ -112,7 +115,8 @@ export function bind(engine) {
112
115
  constructor(source, options) {
113
116
  this.#held = engine.create(source, names(options), options?.host ?? '');
114
117
  this.#source = source;
115
- this.#options = options ?? {};
118
+ // what the engine was prepared with, however the caller's object changes after
119
+ this.#options = { ...options };
116
120
  registry?.register(this, this.#held, this);
117
121
  }
118
122
 
@@ -123,14 +127,14 @@ export function bind(engine) {
123
127
  */
124
128
  parse(entry = 'program', offset = 0, { end, stopAt } = {}) {
125
129
  if (this.#held === undefined) throw new TypeError('the source is freed');
126
- const index = ENTRY[entry];
130
+ const index = Object.hasOwn(ENTRY, entry) ? ENTRY[entry] : undefined;
127
131
  if (index === undefined) throw new TypeError(`${JSON.stringify(entry)} is not an entry`);
128
132
  const stop = stops(stopAt);
129
133
  if (this.#options.host !== undefined && index === ENTRY.program) return result(engine.parse(this.#held, index, 0, undefined, ''), this.#source);
130
134
  if ((index === ENTRY.expression || index === ENTRY.pattern) && Number.isInteger(offset) && offset >= 0) {
131
135
  const cut = end === undefined ? this.#source.length : end;
132
136
  const found = Number.isInteger(cut) && cut <= this.#source.length && offset <= cut ? bare(this.#source, offset, cut, index === ENTRY.pattern ? [',', '(', ':', '='] : stopAt, !!this.#options.typescript) : null;
133
- if (found !== null && (index === ENTRY.expression || !this.#source.startsWith(':', found[0]))) return this.#identifier(offset, found[0], index === ENTRY.pattern);
137
+ if (found !== null) return this.#identifier(offset, found[0], index === ENTRY.pattern);
134
138
  }
135
139
  return result(engine.parse(this.#held, index, offset, end, stop), this.#source);
136
140
  }
@@ -164,7 +168,7 @@ export function bind(engine) {
164
168
  #position(offset) {
165
169
  if (this.#lines === undefined) {
166
170
  this.#lines = [0];
167
- for (let i = this.#source.indexOf('\n'); i !== -1; i = this.#source.indexOf('\n', i + 1)) this.#lines.push(i + 1);
171
+ for (const m of this.#source.matchAll(/\r\n?|[\n\u2028\u2029]/g)) this.#lines.push(m.index + m[0].length);
168
172
  }
169
173
  let lo = 0, hi = this.#lines.length - 1;
170
174
  while (lo < hi) {
package/index.js CHANGED
@@ -17,10 +17,12 @@ function bytes(text) {
17
17
  return room.subarray(0, written);
18
18
  }
19
19
 
20
- export const Source = bind({
20
+ export const engine = {
21
21
  create: (source, names, host) => native.create(bytes(source), names, host),
22
22
  parse: native.parse,
23
23
  free: native.free,
24
24
  constants: native.constants,
25
25
  shapes: native.shapes,
26
- });
26
+ };
27
+
28
+ export const Source = bind(engine);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teasel/parser",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "A JavaScript and TypeScript parser in Rust. It answers in ESTree.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -53,10 +53,10 @@
53
53
  "@changesets/cli": "^3.0.1"
54
54
  },
55
55
  "optionalDependencies": {
56
- "@teasel/parser-linux-x64-gnu": "0.0.2",
57
- "@teasel/parser-linux-arm64-gnu": "0.0.2",
58
- "@teasel/parser-darwin-x64": "0.0.2",
59
- "@teasel/parser-darwin-arm64": "0.0.2",
60
- "@teasel/parser-win32-x64-msvc": "0.0.2"
56
+ "@teasel/parser-linux-x64-gnu": "0.0.3",
57
+ "@teasel/parser-linux-arm64-gnu": "0.0.3",
58
+ "@teasel/parser-darwin-x64": "0.0.3",
59
+ "@teasel/parser-darwin-arm64": "0.0.3",
60
+ "@teasel/parser-win32-x64-msvc": "0.0.3"
61
61
  }
62
62
  }
package/teasel.wasm CHANGED
Binary file
package/wasm.js CHANGED
@@ -8,17 +8,34 @@ const utf8 = new TextDecoder();
8
8
 
9
9
  // `teasel.wasm` next to this file, read where there is a file system and fetched elsewhere
10
10
  const url = new URL('./teasel.wasm', import.meta.url);
11
- const { instance } =
11
+ const { module, instance } =
12
12
  url.protocol === 'file:'
13
13
  ? await WebAssembly.instantiate(await (await import('node:fs/promises')).readFile(url), {})
14
14
  : await WebAssembly.instantiateStreaming(fetch(url), {});
15
15
  /** @type {WebAssembly.Exports & Record<string, Function> & { memory: WebAssembly.Memory }} */
16
- const wasm = /** @type {any} */ (instance.exports);
16
+ let wasm = /** @type {any} */ (instance.exports);
17
17
  /** @type {string[]} */
18
18
  let constants = [];
19
19
  /** @type {number[]} */
20
20
  let shapes = [];
21
21
  let shapes_known = 0;
22
+ // a panic traps the instance for good: a fresh one takes over, and the sources held by the old one are gone
23
+ let generation = 0;
24
+
25
+ /** @template T @param {() => T} f @returns {T} */
26
+ function guarded(f) {
27
+ try {
28
+ return f();
29
+ } catch (error) {
30
+ if (!(error instanceof WebAssembly.RuntimeError)) throw error;
31
+ wasm = /** @type {any} */ (new WebAssembly.Instance(module, {}).exports);
32
+ constants = [];
33
+ shapes = [];
34
+ shapes_known = 0;
35
+ generation++;
36
+ throw new Error('the engine panicked and started over; the sources it held are gone', { cause: error });
37
+ }
38
+ }
22
39
 
23
40
  // the module takes the bytes over
24
41
  function bytes(text) {
@@ -29,9 +46,15 @@ function bytes(text) {
29
46
  }
30
47
 
31
48
  function create(source, names, host) {
32
- const handle = wasm.source_new(...bytes(source), ...bytes(names), ...bytes(host));
49
+ const handle = guarded(() => wasm.source_new(...bytes(source), ...bytes(names), ...bytes(host)));
33
50
  if (handle === 0) throw new Error(JSON.parse(text()).error.message);
34
- return handle;
51
+ return { handle, generation };
52
+ }
53
+
54
+ /** @param {{ handle: number, generation: number }} held */
55
+ function handle(held) {
56
+ if (held.generation !== generation) throw new Error('the source was held by an engine that panicked and started over');
57
+ return held.handle;
35
58
  }
36
59
 
37
60
  const text = () => utf8.decode(new Uint8Array(wasm.memory.buffer, wasm.text_ptr(), wasm.text_len()));
@@ -52,11 +75,15 @@ function answer(status) {
52
75
  return words();
53
76
  }
54
77
 
55
- export const Source = bind({
78
+ export const engine = {
56
79
  create,
57
80
  // the words outlive the source: they sit in the answer buffer until the next parse
58
- parse: (held, entry, offset, end, stop) => answer(wasm.source_parse(held, entry, offset, end ?? 0, end === undefined ? 0 : 1, ...bytes(stop))),
59
- free: (held) => wasm.source_free(held),
81
+ parse: (held, entry, offset, end, stop) => answer(guarded(() => wasm.source_parse(handle(held), entry, offset, end ?? 0, end === undefined ? 0 : 1, ...bytes(stop)))),
82
+ free: (held) => {
83
+ if (held.generation === generation) wasm.source_free(held.handle);
84
+ },
60
85
  constants: () => constants,
61
86
  shapes: () => shapes,
62
- });
87
+ };
88
+
89
+ export const Source = bind(engine);