@libmorpheus/wasm 0.0.0-stage → 0.1.0
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/LICENSE +661 -0
- package/LICENSES/EMSCRIPTEN.txt +101 -0
- package/LICENSES/MPL-2.0.txt +373 -0
- package/NOTICE +15 -0
- package/README.md +299 -2
- package/dist/morpheus.mjs +4 -0
- package/dist/morpheus.wasm +0 -0
- package/index.d.ts +244 -0
- package/index.mjs +781 -0
- package/internal/version.mjs +16 -0
- package/package.json +49 -4
package/index.mjs
ADDED
|
@@ -0,0 +1,781 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// @ts-self-types="./index.d.ts"
|
|
3
|
+
//
|
|
4
|
+
// JavaScript facade for the libmorpheus WebAssembly module.
|
|
5
|
+
//
|
|
6
|
+
// The module (dist/morpheus.mjs plus dist/morpheus.wasm) is the complete
|
|
7
|
+
// public C API compiled by Emscripten. This file owns the memory discipline
|
|
8
|
+
// at the boundary: it marshals Beta Code in, copies fixed-layout result
|
|
9
|
+
// structures out, and frees every native object it creates. It runs wherever
|
|
10
|
+
// the module runs: browsers, web workers, Node.js, Deno, and Bun.
|
|
11
|
+
//
|
|
12
|
+
// No linguistic data is bundled. Callers place a compiled stemlib in the
|
|
13
|
+
// module's virtual file system, either by mounting a host directory (Node.js)
|
|
14
|
+
// or by writing fetched bytes into memory (browsers).
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
MORPHEUS_EMSCRIPTEN_VERSION,
|
|
18
|
+
MORPHEUS_NATIVE_ABI_VERSION,
|
|
19
|
+
MORPHEUS_NATIVE_VERSION,
|
|
20
|
+
MORPHEUS_WASM_VERSION,
|
|
21
|
+
} from "./internal/version.mjs";
|
|
22
|
+
|
|
23
|
+
export {
|
|
24
|
+
MORPHEUS_EMSCRIPTEN_VERSION,
|
|
25
|
+
MORPHEUS_NATIVE_ABI_VERSION,
|
|
26
|
+
MORPHEUS_NATIVE_VERSION,
|
|
27
|
+
MORPHEUS_WASM_VERSION,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** ABI version this facade understands. */
|
|
31
|
+
export const ABI_VERSION = MORPHEUS_NATIVE_ABI_VERSION;
|
|
32
|
+
|
|
33
|
+
/** Numeric language selectors accepted by `open()`. */
|
|
34
|
+
export const MorpheusLanguage = Object.freeze({
|
|
35
|
+
Greek: 0,
|
|
36
|
+
Latin: 1,
|
|
37
|
+
Italian: 2,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
/** Status codes returned by the stable ABI. */
|
|
41
|
+
export const MorpheusStatus = Object.freeze({
|
|
42
|
+
Ok: 0,
|
|
43
|
+
InvalidArgument: 1,
|
|
44
|
+
AbiMismatch: 2,
|
|
45
|
+
NoMemory: 3,
|
|
46
|
+
InputTooLong: 4,
|
|
47
|
+
OutOfRange: 5,
|
|
48
|
+
InternalError: 6,
|
|
49
|
+
BufferTooSmall: 7,
|
|
50
|
+
StemlibError: 8,
|
|
51
|
+
ResultLimitExceeded: 9,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
/** Bit flags customizing `analyze()`. Combine with `|` (BigInt). */
|
|
55
|
+
export const MorpheusOption = Object.freeze({
|
|
56
|
+
StrictCase: 1n << 0n,
|
|
57
|
+
IgnoreAccents: 1n << 1n,
|
|
58
|
+
VerbsOnly: 1n << 2n,
|
|
59
|
+
NoCrasis: 1n << 3n,
|
|
60
|
+
Quick: 1n << 4n,
|
|
61
|
+
HqDictionary: 1n << 5n,
|
|
62
|
+
DialectAeolic: 1n << 16n,
|
|
63
|
+
DialectAttic: 2n << 16n,
|
|
64
|
+
DialectDoric: 4n << 16n,
|
|
65
|
+
DialectHomeric: 8n << 16n,
|
|
66
|
+
DialectIonic: 16n << 16n,
|
|
67
|
+
DialectLesbian: 32n << 16n,
|
|
68
|
+
DialectNonHomericEpic: 64n << 16n,
|
|
69
|
+
DialectParadigm: 128n << 16n,
|
|
70
|
+
DialectEpic: 72n << 16n,
|
|
71
|
+
DialectProse: 256n << 16n,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
/** Flags for the historical text output of `analyzeCompat()`. */
|
|
75
|
+
export const MorpheusCompatFlag = Object.freeze({
|
|
76
|
+
BufferAnalyses: 0o10,
|
|
77
|
+
CheckPreverb: 0o20,
|
|
78
|
+
KeepBeta: 0o40,
|
|
79
|
+
ShowFullInfo: 0o100,
|
|
80
|
+
DatabaseFormat: 0o200,
|
|
81
|
+
DatabaseShort: 0o600,
|
|
82
|
+
StrictCase: 0o1000,
|
|
83
|
+
ParseFormat: 0o2000,
|
|
84
|
+
PerseusFormat: 0o4000,
|
|
85
|
+
EndingIndex: 0o10000,
|
|
86
|
+
IgnoreAccents: 0o20000,
|
|
87
|
+
LexiconOutput: 0o40000,
|
|
88
|
+
LemmaCount: 0o200000,
|
|
89
|
+
VerbsOnly: 0o400000,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Files that `open()` requires under `<root>/<Language>/`. The analyzer also
|
|
94
|
+
* reads lazily, by stem class and analysis path: `endtables/out/*.out`,
|
|
95
|
+
* `derivs/indices/derivind`, `stemsrc/vbs.cmp.ml` (compound verbs) and
|
|
96
|
+
* `stemsrc/lemlist` (dictionary entries); Greek generation reads
|
|
97
|
+
* `<root>/gener.index`.
|
|
98
|
+
*/
|
|
99
|
+
export const REQUIRED_STEMLIB_FILES = Object.freeze({
|
|
100
|
+
common: Object.freeze([
|
|
101
|
+
"rule_files/vowcontr.table",
|
|
102
|
+
"rule_files/conseuph.table",
|
|
103
|
+
"rule_files/stemtypes.table",
|
|
104
|
+
"rule_files/derivtypes.table",
|
|
105
|
+
"rule_files/domainlist.table",
|
|
106
|
+
"rule_files/raw_preverbs.table",
|
|
107
|
+
"steminds/nomind",
|
|
108
|
+
"steminds/nomind.lindex",
|
|
109
|
+
"steminds/vbind",
|
|
110
|
+
"steminds/vbind.lindex",
|
|
111
|
+
"endtables/indices/nendind",
|
|
112
|
+
"endtables/indices/vbendind",
|
|
113
|
+
"derivs/indices/derivind",
|
|
114
|
+
]),
|
|
115
|
+
greek: Object.freeze(["rule_files/ppasslist.table"]),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
/** Directory name of each language inside a stemlib root. */
|
|
119
|
+
export const LANGUAGE_DIRECTORIES = Object.freeze({
|
|
120
|
+
[MorpheusLanguage.Greek]: "Greek",
|
|
121
|
+
[MorpheusLanguage.Latin]: "Latin",
|
|
122
|
+
[MorpheusLanguage.Italian]: "Italian",
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const TEXT_CAPACITY = 64;
|
|
126
|
+
const DOMAIN_CAPACITY = 24;
|
|
127
|
+
const MORPH_FLAG_CAPACITY = 11;
|
|
128
|
+
/* Minimum sizes of the version-2 structures; the module reports the actual
|
|
129
|
+
* sizes, which may grow in later ABI-compatible releases. */
|
|
130
|
+
const MIN_ANALYSIS_SIZE = 852;
|
|
131
|
+
const MIN_GENERATION_SIZE = 188;
|
|
132
|
+
const GENERATION_OPTIONS_SIZE = 64;
|
|
133
|
+
const GENERATION_OPTIONS_VERSION = 1;
|
|
134
|
+
const POINTER_SIZE = 4;
|
|
135
|
+
|
|
136
|
+
const decoder = new TextDecoder();
|
|
137
|
+
const encoder = new TextEncoder();
|
|
138
|
+
|
|
139
|
+
/** Error carrying the native status code. */
|
|
140
|
+
export class MorpheusError extends Error {
|
|
141
|
+
constructor(status, message) {
|
|
142
|
+
super(message);
|
|
143
|
+
this.name = "MorpheusError";
|
|
144
|
+
this.status = status;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function statusName(status) {
|
|
149
|
+
for (const [name, value] of Object.entries(MorpheusStatus)) {
|
|
150
|
+
if (value === status) return name;
|
|
151
|
+
}
|
|
152
|
+
return `Status${status}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function cString(bytes, offset, capacity) {
|
|
156
|
+
const field = bytes.subarray(offset, offset + capacity);
|
|
157
|
+
const terminator = field.indexOf(0);
|
|
158
|
+
return decoder.decode(terminator < 0 ? field : field.subarray(0, terminator));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function decodeAnalysis(bytes, truncatedFields) {
|
|
162
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
163
|
+
const numbers = [];
|
|
164
|
+
for (let offset = 0; offset < 48; offset += 4) {
|
|
165
|
+
numbers.push(view.getUint32(offset, true));
|
|
166
|
+
}
|
|
167
|
+
let offset = 48;
|
|
168
|
+
const text = () => {
|
|
169
|
+
const value = cString(bytes, offset, TEXT_CAPACITY);
|
|
170
|
+
offset += TEXT_CAPACITY;
|
|
171
|
+
return value;
|
|
172
|
+
};
|
|
173
|
+
const raw = text();
|
|
174
|
+
const workword = text();
|
|
175
|
+
const lemma = text();
|
|
176
|
+
const preverb = text();
|
|
177
|
+
const augment = text();
|
|
178
|
+
const stem = text();
|
|
179
|
+
const suffix = text();
|
|
180
|
+
const ending = text();
|
|
181
|
+
const crasis = text();
|
|
182
|
+
const dictionaryForm = text();
|
|
183
|
+
const englishForm = text();
|
|
184
|
+
const rawPreverb = text();
|
|
185
|
+
const domains = cString(bytes, offset, DOMAIN_CAPACITY);
|
|
186
|
+
offset += DOMAIN_CAPACITY;
|
|
187
|
+
const morphFlags = bytes.slice(offset, offset + MORPH_FLAG_CAPACITY);
|
|
188
|
+
return {
|
|
189
|
+
structSize: numbers[0],
|
|
190
|
+
partOfSpeech: numbers[1],
|
|
191
|
+
dialect: numbers[2],
|
|
192
|
+
geographicRegion: numbers[3],
|
|
193
|
+
person: numbers[4],
|
|
194
|
+
number: numbers[5],
|
|
195
|
+
gender: numbers[6],
|
|
196
|
+
grammaticalCase: numbers[7],
|
|
197
|
+
tense: numbers[8],
|
|
198
|
+
mood: numbers[9],
|
|
199
|
+
voice: numbers[10],
|
|
200
|
+
degree: numbers[11],
|
|
201
|
+
raw,
|
|
202
|
+
workword,
|
|
203
|
+
lemma,
|
|
204
|
+
preverb,
|
|
205
|
+
augment,
|
|
206
|
+
stem,
|
|
207
|
+
suffix,
|
|
208
|
+
ending,
|
|
209
|
+
crasis,
|
|
210
|
+
dictionaryForm,
|
|
211
|
+
englishForm,
|
|
212
|
+
rawPreverb,
|
|
213
|
+
domains,
|
|
214
|
+
morphFlags,
|
|
215
|
+
truncatedFields,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function decodeGeneration(bytes, truncatedFields) {
|
|
220
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
221
|
+
const numbers = [];
|
|
222
|
+
for (let offset = 0; offset < 48; offset += 4) {
|
|
223
|
+
numbers.push(view.getUint32(offset, true));
|
|
224
|
+
}
|
|
225
|
+
const surface = cString(bytes, 48, TEXT_CAPACITY);
|
|
226
|
+
const lemma = cString(bytes, 48 + TEXT_CAPACITY, TEXT_CAPACITY);
|
|
227
|
+
const morphFlagOffset = 48 + 2 * TEXT_CAPACITY;
|
|
228
|
+
return {
|
|
229
|
+
structSize: numbers[0],
|
|
230
|
+
partOfSpeech: numbers[1],
|
|
231
|
+
dialect: numbers[2],
|
|
232
|
+
geographicRegion: numbers[3],
|
|
233
|
+
person: numbers[4],
|
|
234
|
+
number: numbers[5],
|
|
235
|
+
gender: numbers[6],
|
|
236
|
+
grammaticalCase: numbers[7],
|
|
237
|
+
tense: numbers[8],
|
|
238
|
+
mood: numbers[9],
|
|
239
|
+
voice: numbers[10],
|
|
240
|
+
degree: numbers[11],
|
|
241
|
+
surface,
|
|
242
|
+
lemma,
|
|
243
|
+
morphFlags: bytes.slice(morphFlagOffset, morphFlagOffset + MORPH_FLAG_CAPACITY),
|
|
244
|
+
truncatedFields,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function encodeGenerationOptions(options) {
|
|
249
|
+
const resultLimit = options.resultLimit ?? 0;
|
|
250
|
+
if (
|
|
251
|
+
!Number.isSafeInteger(resultLimit) || resultLimit < 0 ||
|
|
252
|
+
resultLimit > 65_536
|
|
253
|
+
) {
|
|
254
|
+
throw new RangeError("resultLimit must be an integer from 0 to 65536");
|
|
255
|
+
}
|
|
256
|
+
const bytes = new Uint8Array(GENERATION_OPTIONS_SIZE);
|
|
257
|
+
const view = new DataView(bytes.buffer);
|
|
258
|
+
view.setUint32(0, GENERATION_OPTIONS_VERSION, true);
|
|
259
|
+
view.setUint32(4, GENERATION_OPTIONS_SIZE, true);
|
|
260
|
+
view.setBigUint64(8, BigInt(resultLimit), true);
|
|
261
|
+
view.setUint32(16, options.excludeDuals ? 1 : 0, true);
|
|
262
|
+
view.setUint32(20, options.partOfSpeech ?? 0, true);
|
|
263
|
+
view.setUint32(24, options.dialect ?? 0, true);
|
|
264
|
+
view.setUint32(28, options.geographicRegion ?? 0, true);
|
|
265
|
+
view.setUint32(32, options.person ?? 0, true);
|
|
266
|
+
view.setUint32(36, options.number ?? 0, true);
|
|
267
|
+
view.setUint32(40, options.gender ?? 0, true);
|
|
268
|
+
view.setUint32(44, options.grammaticalCase ?? 0, true);
|
|
269
|
+
view.setUint32(48, options.tense ?? 0, true);
|
|
270
|
+
view.setUint32(52, options.mood ?? 0, true);
|
|
271
|
+
view.setUint32(56, options.voice ?? 0, true);
|
|
272
|
+
view.setUint32(60, options.degree ?? 0, true);
|
|
273
|
+
return bytes;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function encodeInput(text, label) {
|
|
277
|
+
if (typeof text !== "string") throw new TypeError(`${label} must be a string`);
|
|
278
|
+
const bytes = encoder.encode(text);
|
|
279
|
+
if (bytes.includes(0)) {
|
|
280
|
+
throw new MorpheusError(
|
|
281
|
+
MorpheusStatus.InvalidArgument,
|
|
282
|
+
`${label} must not contain NUL`,
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
return bytes;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function normalizeAnalyzeOptions(options) {
|
|
289
|
+
if (options === undefined) return MorpheusOption.StrictCase;
|
|
290
|
+
if (typeof options === "bigint") return options;
|
|
291
|
+
if (typeof options === "number") return BigInt(options);
|
|
292
|
+
if (typeof options !== "object" || options === null) {
|
|
293
|
+
throw new TypeError("analyze options must be a bigint, number, or object");
|
|
294
|
+
}
|
|
295
|
+
let flags = 0n;
|
|
296
|
+
if (options.strictCase ?? true) flags |= MorpheusOption.StrictCase;
|
|
297
|
+
if (options.ignoreAccents) flags |= MorpheusOption.IgnoreAccents;
|
|
298
|
+
if (options.verbsOnly) flags |= MorpheusOption.VerbsOnly;
|
|
299
|
+
if (options.noCrasis) flags |= MorpheusOption.NoCrasis;
|
|
300
|
+
if (options.quick) flags |= MorpheusOption.Quick;
|
|
301
|
+
if (options.hqDictionary) flags |= MorpheusOption.HqDictionary;
|
|
302
|
+
for (const dialect of options.dialects ?? []) {
|
|
303
|
+
const flag = MorpheusOption[`Dialect${dialect}`];
|
|
304
|
+
if (flag === undefined) throw new RangeError(`unknown dialect: ${dialect}`);
|
|
305
|
+
flags |= flag;
|
|
306
|
+
}
|
|
307
|
+
return flags;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** Scoped native allocations released together. */
|
|
311
|
+
class Arena {
|
|
312
|
+
#module;
|
|
313
|
+
#pointers = [];
|
|
314
|
+
constructor(module) {
|
|
315
|
+
this.#module = module;
|
|
316
|
+
}
|
|
317
|
+
alloc(size) {
|
|
318
|
+
const pointer = this.#module._malloc(Math.max(size, 1));
|
|
319
|
+
if (!pointer) throw new MorpheusError(MorpheusStatus.NoMemory, "out of memory");
|
|
320
|
+
this.#pointers.push(pointer);
|
|
321
|
+
return pointer;
|
|
322
|
+
}
|
|
323
|
+
bytes(source) {
|
|
324
|
+
const pointer = this.alloc(source.length);
|
|
325
|
+
this.#module.HEAPU8.set(source, pointer);
|
|
326
|
+
return pointer;
|
|
327
|
+
}
|
|
328
|
+
release() {
|
|
329
|
+
for (const pointer of this.#pointers) this.#module._free(pointer);
|
|
330
|
+
this.#pointers.length = 0;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function withArena(module, body) {
|
|
335
|
+
const arena = new Arena(module);
|
|
336
|
+
try {
|
|
337
|
+
return body(arena);
|
|
338
|
+
} finally {
|
|
339
|
+
arena.release();
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** One opened stemlib and language, owning a native context. */
|
|
344
|
+
export class MorpheusContext {
|
|
345
|
+
#library;
|
|
346
|
+
#module;
|
|
347
|
+
#pointer;
|
|
348
|
+
#language;
|
|
349
|
+
|
|
350
|
+
/** @internal */
|
|
351
|
+
constructor(library, module, pointer, language) {
|
|
352
|
+
this.#library = library;
|
|
353
|
+
this.#module = module;
|
|
354
|
+
this.#pointer = pointer;
|
|
355
|
+
this.#language = language;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** Numeric language of this context. */
|
|
359
|
+
get language() {
|
|
360
|
+
return this.#language;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** Whether `close()` has run. */
|
|
364
|
+
get closed() {
|
|
365
|
+
return this.#pointer === 0;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
#require() {
|
|
369
|
+
if (!this.#pointer) throw new MorpheusError(MorpheusStatus.InvalidArgument, "context is closed");
|
|
370
|
+
return this.#pointer;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Analyze one Beta Code form. `options` is a BigInt mask of
|
|
375
|
+
* `MorpheusOption`, or an object with camel-cased option names and a
|
|
376
|
+
* `dialects` array of `MorpheusOption` dialect suffixes.
|
|
377
|
+
*/
|
|
378
|
+
analyze(betaCode, options) {
|
|
379
|
+
const context = this.#require();
|
|
380
|
+
const module = this.#module;
|
|
381
|
+
const input = encodeInput(betaCode, "betaCode");
|
|
382
|
+
const flags = normalizeAnalyzeOptions(options);
|
|
383
|
+
return withArena(module, (arena) => {
|
|
384
|
+
const inputPointer = arena.bytes(input);
|
|
385
|
+
const resultSlot = arena.alloc(POINTER_SIZE);
|
|
386
|
+
module.setValue(resultSlot, 0, "i32");
|
|
387
|
+
const status = module._morpheus_analyze(
|
|
388
|
+
context,
|
|
389
|
+
inputPointer,
|
|
390
|
+
input.length,
|
|
391
|
+
flags,
|
|
392
|
+
resultSlot,
|
|
393
|
+
);
|
|
394
|
+
this.#library._check(status, "analysis failed");
|
|
395
|
+
const result = module.getValue(resultSlot, "i32");
|
|
396
|
+
try {
|
|
397
|
+
const count = module._morpheus_result_count(result);
|
|
398
|
+
const size = this.#library.analysisSize;
|
|
399
|
+
const buffer = arena.alloc(size);
|
|
400
|
+
const fieldsSlot = arena.alloc(4);
|
|
401
|
+
const analyses = [];
|
|
402
|
+
for (let index = 0; index < count; index++) {
|
|
403
|
+
this.#library._check(
|
|
404
|
+
module._morpheus_result_copy(result, index, buffer, size),
|
|
405
|
+
"result copy failed",
|
|
406
|
+
);
|
|
407
|
+
this.#library._check(
|
|
408
|
+
module._morpheus_result_truncated_fields(result, index, fieldsSlot),
|
|
409
|
+
"truncation query failed",
|
|
410
|
+
);
|
|
411
|
+
const bytes = module.HEAPU8.slice(buffer, buffer + size);
|
|
412
|
+
analyses.push(decodeAnalysis(bytes, module.getValue(fieldsSlot, "i32") >>> 0));
|
|
413
|
+
}
|
|
414
|
+
return analyses;
|
|
415
|
+
} finally {
|
|
416
|
+
module._morpheus_result_free(result);
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Run the historical text analyzer (`cruncher` output). Returns the text
|
|
423
|
+
* together with the engine's analysis and lemma counts.
|
|
424
|
+
*/
|
|
425
|
+
analyzeCompat(betaCode, flags = MorpheusCompatFlag.PerseusFormat | MorpheusCompatFlag.StrictCase) {
|
|
426
|
+
const context = this.#require();
|
|
427
|
+
const module = this.#module;
|
|
428
|
+
const input = encodeInput(betaCode, "betaCode");
|
|
429
|
+
return withArena(module, (arena) => {
|
|
430
|
+
const inputPointer = arena.bytes(input);
|
|
431
|
+
const outputSlot = arena.alloc(POINTER_SIZE);
|
|
432
|
+
module.setValue(outputSlot, 0, "i32");
|
|
433
|
+
const status = module._morpheus_compat_analyze(
|
|
434
|
+
context,
|
|
435
|
+
inputPointer,
|
|
436
|
+
input.length,
|
|
437
|
+
flags >>> 0,
|
|
438
|
+
outputSlot,
|
|
439
|
+
);
|
|
440
|
+
this.#library._check(status, "compat analysis failed");
|
|
441
|
+
const output = module.getValue(outputSlot, "i32");
|
|
442
|
+
try {
|
|
443
|
+
const data = module._morpheus_compat_output_data(output);
|
|
444
|
+
const length = module._morpheus_compat_output_length(output);
|
|
445
|
+
return {
|
|
446
|
+
text: decoder.decode(module.HEAPU8.subarray(data, data + length)),
|
|
447
|
+
analysisCount: module._morpheus_compat_output_analysis_count(output),
|
|
448
|
+
lemmaCount: module._morpheus_compat_output_lemma_count(output),
|
|
449
|
+
};
|
|
450
|
+
} finally {
|
|
451
|
+
module._morpheus_compat_output_free(output);
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Generate Greek Beta Code forms for one lemma (experimental). Requires
|
|
458
|
+
* `<root>/gener.index`.
|
|
459
|
+
*/
|
|
460
|
+
generate(lemma, options = {}) {
|
|
461
|
+
const context = this.#require();
|
|
462
|
+
const module = this.#module;
|
|
463
|
+
const input = encodeInput(lemma, "lemma");
|
|
464
|
+
const encodedOptions = encodeGenerationOptions(options);
|
|
465
|
+
return withArena(module, (arena) => {
|
|
466
|
+
const inputPointer = arena.bytes(input);
|
|
467
|
+
const optionsPointer = arena.bytes(encodedOptions);
|
|
468
|
+
const resultSlot = arena.alloc(POINTER_SIZE);
|
|
469
|
+
module.setValue(resultSlot, 0, "i32");
|
|
470
|
+
const status = module._morpheus_generate(
|
|
471
|
+
context,
|
|
472
|
+
inputPointer,
|
|
473
|
+
input.length,
|
|
474
|
+
optionsPointer,
|
|
475
|
+
resultSlot,
|
|
476
|
+
);
|
|
477
|
+
this.#library._check(status, "generation failed");
|
|
478
|
+
const result = module.getValue(resultSlot, "i32");
|
|
479
|
+
try {
|
|
480
|
+
const count = module._morpheus_generation_result_count(result);
|
|
481
|
+
const size = this.#library.generationSize;
|
|
482
|
+
const buffer = arena.alloc(size);
|
|
483
|
+
const fieldsSlot = arena.alloc(4);
|
|
484
|
+
const generations = [];
|
|
485
|
+
for (let index = 0; index < count; index++) {
|
|
486
|
+
this.#library._check(
|
|
487
|
+
module._morpheus_generation_result_copy(result, index, buffer, size),
|
|
488
|
+
"generation copy failed",
|
|
489
|
+
);
|
|
490
|
+
this.#library._check(
|
|
491
|
+
module._morpheus_generation_result_truncated_fields(result, index, fieldsSlot),
|
|
492
|
+
"truncation query failed",
|
|
493
|
+
);
|
|
494
|
+
const bytes = module.HEAPU8.slice(buffer, buffer + size);
|
|
495
|
+
generations.push(decodeGeneration(bytes, module.getValue(fieldsSlot, "i32") >>> 0));
|
|
496
|
+
}
|
|
497
|
+
return generations;
|
|
498
|
+
} finally {
|
|
499
|
+
module._morpheus_generation_result_free(result);
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/** Release the native context. Safe to call more than once. */
|
|
505
|
+
close() {
|
|
506
|
+
if (!this.#pointer) return;
|
|
507
|
+
this.#module._morpheus_close(this.#pointer);
|
|
508
|
+
this.#pointer = 0;
|
|
509
|
+
this.#library._forget(this);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (typeof Symbol.dispose === "symbol") {
|
|
515
|
+
MorpheusContext.prototype[Symbol.dispose] = function dispose() {
|
|
516
|
+
this.close();
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/** One instantiated WebAssembly module and its virtual file system. */
|
|
521
|
+
export class Morpheus {
|
|
522
|
+
#module;
|
|
523
|
+
#contexts = new Set();
|
|
524
|
+
|
|
525
|
+
/** @internal */
|
|
526
|
+
constructor(module) {
|
|
527
|
+
this.#module = module;
|
|
528
|
+
const abi = module._morpheus_abi_version();
|
|
529
|
+
if (abi !== ABI_VERSION) {
|
|
530
|
+
throw new MorpheusError(
|
|
531
|
+
MorpheusStatus.AbiMismatch,
|
|
532
|
+
`module implements ABI ${abi}, this facade expects ${ABI_VERSION}`,
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
this.analysisSize = module._morpheus_analysis_size();
|
|
536
|
+
this.generationSize = module._morpheus_generation_size();
|
|
537
|
+
if (this.analysisSize < MIN_ANALYSIS_SIZE) {
|
|
538
|
+
throw new MorpheusError(MorpheusStatus.AbiMismatch, "unexpected morpheus_analysis layout");
|
|
539
|
+
}
|
|
540
|
+
if (this.generationSize < MIN_GENERATION_SIZE) {
|
|
541
|
+
throw new MorpheusError(MorpheusStatus.AbiMismatch, "unexpected morpheus_generation layout");
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/** Native size of morpheus_analysis in bytes. */
|
|
546
|
+
analysisSize;
|
|
547
|
+
/** Native size of morpheus_generation in bytes. */
|
|
548
|
+
generationSize;
|
|
549
|
+
|
|
550
|
+
/** The Emscripten module, for direct `FS` access. */
|
|
551
|
+
get module() {
|
|
552
|
+
return this.#module;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/** The Emscripten virtual file system. */
|
|
556
|
+
get fs() {
|
|
557
|
+
return this.#module.FS;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/** libmorpheus release that produced the module. */
|
|
561
|
+
get version() {
|
|
562
|
+
return this.#module.UTF8ToString(this.#module._morpheus_wasm_module_version());
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/** ABI version implemented by the module. */
|
|
566
|
+
get abiVersion() {
|
|
567
|
+
return this.#module._morpheus_abi_version();
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/** Human-readable description of a status code. */
|
|
571
|
+
statusMessage(status) {
|
|
572
|
+
return this.#module.UTF8ToString(this.#module._morpheus_status_message(status));
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/** @internal */
|
|
576
|
+
_check(status, context) {
|
|
577
|
+
if (status === MorpheusStatus.Ok) return;
|
|
578
|
+
throw new MorpheusError(status, `${context}: ${this.statusMessage(status)} (${statusName(status)})`);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/** @internal */
|
|
582
|
+
_forget(context) {
|
|
583
|
+
this.#contexts.delete(context);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
#ensureDirectory(path) {
|
|
587
|
+
const FS = this.#module.FS;
|
|
588
|
+
let current = "";
|
|
589
|
+
for (const part of path.split("/").filter(Boolean)) {
|
|
590
|
+
current += "/" + part;
|
|
591
|
+
if (!FS.analyzePath(current).exists) FS.mkdir(current);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Mount a host directory into the virtual file system (Node.js, Deno and
|
|
597
|
+
* Bun only). Returns the mount point to pass to `open()`.
|
|
598
|
+
*/
|
|
599
|
+
mountDirectory(hostPath, mountPoint = "/stemlib") {
|
|
600
|
+
const FS = this.#module.FS;
|
|
601
|
+
const NODEFS = FS.filesystems?.NODEFS;
|
|
602
|
+
if (!NODEFS) {
|
|
603
|
+
throw new Error("NODEFS is unavailable in this environment; use writeStemlib()");
|
|
604
|
+
}
|
|
605
|
+
this.#ensureDirectory(mountPoint);
|
|
606
|
+
FS.mount(NODEFS, { root: hostPath }, mountPoint);
|
|
607
|
+
return mountPoint;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Write stemlib files into the in-memory file system. `entries` maps paths
|
|
612
|
+
* relative to `root` (for example `Latin/steminds/nomind`) to bytes.
|
|
613
|
+
* Returns `root` to pass to `open()`.
|
|
614
|
+
*/
|
|
615
|
+
writeStemlib(entries, root = "/stemlib") {
|
|
616
|
+
const FS = this.#module.FS;
|
|
617
|
+
const iterable = entries instanceof Map || Array.isArray(entries)
|
|
618
|
+
? entries
|
|
619
|
+
: Object.entries(entries);
|
|
620
|
+
for (const [relativePath, data] of iterable) {
|
|
621
|
+
const fullPath = `${root}/${relativePath}`.replace(/\/+/g, "/");
|
|
622
|
+
const directory = fullPath.slice(0, fullPath.lastIndexOf("/"));
|
|
623
|
+
this.#ensureDirectory(directory);
|
|
624
|
+
const bytes = data instanceof Uint8Array
|
|
625
|
+
? data
|
|
626
|
+
: data instanceof ArrayBuffer
|
|
627
|
+
? new Uint8Array(data)
|
|
628
|
+
: typeof data === "string"
|
|
629
|
+
? encoder.encode(data)
|
|
630
|
+
: null;
|
|
631
|
+
if (!bytes) throw new TypeError(`unsupported data for ${relativePath}`);
|
|
632
|
+
FS.writeFile(fullPath, bytes);
|
|
633
|
+
}
|
|
634
|
+
return root;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Fetch stemlib files over HTTP into the in-memory file system. `paths`
|
|
639
|
+
* are relative to both `baseUrl` and `root`; pass the directory listing or
|
|
640
|
+
* manifest of the served stemlib. Returns `root` to pass to `open()`.
|
|
641
|
+
*/
|
|
642
|
+
async fetchStemlib(baseUrl, paths, { root = "/stemlib", fetch: fetcher = globalThis.fetch, concurrency = 8 } = {}) {
|
|
643
|
+
if (typeof fetcher !== "function") throw new Error("fetch is unavailable; pass options.fetch");
|
|
644
|
+
const base = String(baseUrl).endsWith("/") ? String(baseUrl) : `${baseUrl}/`;
|
|
645
|
+
const queue = [...paths];
|
|
646
|
+
const entries = [];
|
|
647
|
+
const worker = async () => {
|
|
648
|
+
while (queue.length) {
|
|
649
|
+
const relativePath = queue.shift();
|
|
650
|
+
const response = await fetcher(new URL(relativePath, base));
|
|
651
|
+
if (!response.ok) {
|
|
652
|
+
throw new MorpheusError(
|
|
653
|
+
MorpheusStatus.StemlibError,
|
|
654
|
+
`fetching ${relativePath} failed with HTTP ${response.status}`,
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
entries.push([relativePath, new Uint8Array(await response.arrayBuffer())]);
|
|
658
|
+
}
|
|
659
|
+
};
|
|
660
|
+
await Promise.all(Array.from({ length: Math.max(1, concurrency) }, worker));
|
|
661
|
+
return this.writeStemlib(entries, root);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/** Relative paths `open()` validates for a language. */
|
|
665
|
+
static requiredFiles(language) {
|
|
666
|
+
const directory = LANGUAGE_DIRECTORIES[language];
|
|
667
|
+
if (!directory) throw new RangeError(`unknown language: ${language}`);
|
|
668
|
+
const files = [...REQUIRED_STEMLIB_FILES.common];
|
|
669
|
+
if (language === MorpheusLanguage.Greek) files.push(...REQUIRED_STEMLIB_FILES.greek);
|
|
670
|
+
return files.map((file) => `${directory}/${file}`);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/** Create a context for `language` over the stemlib at `stemlib` (a virtual path). */
|
|
674
|
+
open({ stemlib = "/stemlib", language }) {
|
|
675
|
+
if (!Object.values(MorpheusLanguage).includes(language)) {
|
|
676
|
+
throw new RangeError(`unknown language: ${language}`);
|
|
677
|
+
}
|
|
678
|
+
const module = this.#module;
|
|
679
|
+
const path = encodeInput(stemlib, "stemlib");
|
|
680
|
+
return withArena(module, (arena) => {
|
|
681
|
+
const pathPointer = arena.bytes(path);
|
|
682
|
+
const contextSlot = arena.alloc(POINTER_SIZE);
|
|
683
|
+
module.setValue(contextSlot, 0, "i32");
|
|
684
|
+
const status = module._morpheus_open_path(
|
|
685
|
+
ABI_VERSION,
|
|
686
|
+
pathPointer,
|
|
687
|
+
path.length,
|
|
688
|
+
language,
|
|
689
|
+
contextSlot,
|
|
690
|
+
);
|
|
691
|
+
this._check(status, `opening ${stemlib} for language ${language} failed`);
|
|
692
|
+
const context = new MorpheusContext(this, module, module.getValue(contextSlot, "i32"), language);
|
|
693
|
+
this.#contexts.add(context);
|
|
694
|
+
return context;
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/** Close every context created by this instance. */
|
|
699
|
+
closeAll() {
|
|
700
|
+
for (const context of [...this.#contexts]) context.close();
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* Compile `morpheus.wasm` next to a remotely served `morpheus.mjs`. The
|
|
706
|
+
* Emscripten loader locates the `.wasm` through `node:fs` on Node-like
|
|
707
|
+
* runtimes and through `fetch` elsewhere; a module served from a registry
|
|
708
|
+
* (JSR's `https://jsr.io/...`) to Node.js, Deno or Bun falls between the two,
|
|
709
|
+
* so the facade fetches and compiles it itself.
|
|
710
|
+
*/
|
|
711
|
+
async function compileRemoteModule(wasmUrl) {
|
|
712
|
+
if (typeof fetch !== "function") {
|
|
713
|
+
throw new Error(`fetch is unavailable to load ${wasmUrl.href}`);
|
|
714
|
+
}
|
|
715
|
+
const response = await fetch(wasmUrl);
|
|
716
|
+
if (!response.ok) {
|
|
717
|
+
throw new MorpheusError(
|
|
718
|
+
MorpheusStatus.InternalError,
|
|
719
|
+
`fetching ${wasmUrl.href} failed with HTTP ${response.status}`,
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
if (
|
|
723
|
+
typeof WebAssembly.compileStreaming === "function" &&
|
|
724
|
+
response.headers.get("content-type")?.split(";")[0].trim() === "application/wasm"
|
|
725
|
+
) {
|
|
726
|
+
return await WebAssembly.compileStreaming(response);
|
|
727
|
+
}
|
|
728
|
+
return await WebAssembly.compile(await response.arrayBuffer());
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Instantiate the module. By default the factory is imported from
|
|
733
|
+
* `./dist/morpheus.mjs` next to this file; pass `moduleUrl` or an already
|
|
734
|
+
* imported `factory` to load it from elsewhere. When the module URL is not a
|
|
735
|
+
* `file:` URL (a package served from JSR, say), the facade fetches and
|
|
736
|
+
* compiles `morpheus.wasm` itself. `wasmModule` supplies an already compiled
|
|
737
|
+
* `WebAssembly.Module` (a Cloudflare Workers `.wasm` import, or one compiled
|
|
738
|
+
* once and shared with a worker pool) so the loader neither fetches nor
|
|
739
|
+
* compiles `morpheus.wasm`. `moduleOptions` reaches the Emscripten factory
|
|
740
|
+
* (for example `locateFile` or `wasmBinary`).
|
|
741
|
+
*/
|
|
742
|
+
export async function createMorpheus(
|
|
743
|
+
{ factory, moduleUrl, wasmModule, moduleOptions = {} } = {},
|
|
744
|
+
) {
|
|
745
|
+
let create = factory;
|
|
746
|
+
if (!create) {
|
|
747
|
+
// The literal specifier keeps the default import analyzable by bundlers
|
|
748
|
+
// and by JSR's publish-time rewriting; an explicit moduleUrl is loaded
|
|
749
|
+
// as an absolute URL, which JSR reports as unanalyzable and leaves as is.
|
|
750
|
+
const url = new URL(moduleUrl ?? new URL("./dist/morpheus.mjs", import.meta.url));
|
|
751
|
+
const imported = moduleUrl === undefined
|
|
752
|
+
? await import("./dist/morpheus.mjs")
|
|
753
|
+
: await import(/* @vite-ignore */ url.href);
|
|
754
|
+
create = imported.default;
|
|
755
|
+
if (!wasmModule && url.protocol !== "file:" && !moduleOptions.wasmBinary) {
|
|
756
|
+
wasmModule = await compileRemoteModule(new URL("./morpheus.wasm", url));
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
if (typeof create !== "function") {
|
|
760
|
+
throw new TypeError("the module factory must be the default export of morpheus.mjs");
|
|
761
|
+
}
|
|
762
|
+
const options = { noInitialRun: true, ...moduleOptions };
|
|
763
|
+
if (wasmModule) {
|
|
764
|
+
if (!(wasmModule instanceof WebAssembly.Module)) {
|
|
765
|
+
throw new TypeError("wasmModule must be a WebAssembly.Module");
|
|
766
|
+
}
|
|
767
|
+
options.instantiateWasm = (imports, receive) => {
|
|
768
|
+
WebAssembly.instantiate(wasmModule, imports).then(
|
|
769
|
+
(instance) => receive(instance, wasmModule),
|
|
770
|
+
(error) => {
|
|
771
|
+
throw error;
|
|
772
|
+
},
|
|
773
|
+
);
|
|
774
|
+
return {};
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
const module = await create(options);
|
|
778
|
+
return new Morpheus(module);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
export default createMorpheus;
|