@live-codes/ocaml 0.0.0-stage → 0.1.1

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.
Files changed (130) hide show
  1. package/LICENSE +220 -0
  2. package/README.md +165 -2
  3. package/THIRD-PARTY-NOTICES.md +52 -0
  4. package/assets/compiler/compile_stdout.browser.js +27856 -0
  5. package/assets/compiler/jsoo.js +12631 -0
  6. package/assets/compiler/package.json +3 -0
  7. package/assets/compiler/runtime/bigarray.js +1072 -0
  8. package/assets/compiler/runtime/bigstring.js +137 -0
  9. package/assets/compiler/runtime/domain.js +393 -0
  10. package/assets/compiler/runtime/dynlink.js +91 -0
  11. package/assets/compiler/runtime/float32.js +486 -0
  12. package/assets/compiler/runtime/fs.js +416 -0
  13. package/assets/compiler/runtime/fs_fake.js +600 -0
  14. package/assets/compiler/runtime/fs_node.js +609 -0
  15. package/assets/compiler/runtime/fs_quickjs.js +532 -0
  16. package/assets/compiler/runtime/gc.js +201 -0
  17. package/assets/compiler/runtime/graphics.js +1053 -0
  18. package/assets/compiler/runtime/ieee_754.js +621 -0
  19. package/assets/compiler/runtime/int64.js +516 -0
  20. package/assets/compiler/runtime/io.js +739 -0
  21. package/assets/compiler/runtime/jslib.js +553 -0
  22. package/assets/compiler/runtime/jslib_js_of_ocaml.js +81 -0
  23. package/assets/compiler/runtime/lexing.js +240 -0
  24. package/assets/compiler/runtime/manifest.txt +31 -0
  25. package/assets/compiler/runtime/marshal.js +863 -0
  26. package/assets/compiler/runtime/md5.js +236 -0
  27. package/assets/compiler/runtime/mlBytes.js +1007 -0
  28. package/assets/compiler/runtime/nat.js +469 -0
  29. package/assets/compiler/runtime/parsing.js +317 -0
  30. package/assets/compiler/runtime/prng.js +84 -0
  31. package/assets/compiler/runtime/promise.js +45 -0
  32. package/assets/compiler/runtime/runtime.js +16688 -0
  33. package/assets/compiler/runtime/runtime_events.js +90 -0
  34. package/assets/compiler/runtime/stdlib.js +363 -0
  35. package/assets/compiler/runtime/sync.js +35 -0
  36. package/assets/compiler/runtime/toplevel.js +150 -0
  37. package/assets/compiler/runtime/unix.js +914 -0
  38. package/assets/compiler/runtime/weak.js +247 -0
  39. package/assets/compiler/stdlib.js +254 -0
  40. package/assets/compiler/worker.js +46 -0
  41. package/assets/interpreter/libs/greet.cma +0 -0
  42. package/assets/interpreter/libs/greet.cmi +0 -0
  43. package/assets/interpreter/libs/manifest.txt +1 -0
  44. package/assets/interpreter/package.json +3 -0
  45. package/assets/interpreter/stdlib/camlinternalFormat.cmi +0 -0
  46. package/assets/interpreter/stdlib/camlinternalFormatBasics.cmi +0 -0
  47. package/assets/interpreter/stdlib/camlinternalLazy.cmi +0 -0
  48. package/assets/interpreter/stdlib/camlinternalMod.cmi +0 -0
  49. package/assets/interpreter/stdlib/camlinternalOO.cmi +0 -0
  50. package/assets/interpreter/stdlib/std_exit.cmi +0 -0
  51. package/assets/interpreter/stdlib/stdlib.cmi +0 -0
  52. package/assets/interpreter/stdlib/stdlib__Arg.cmi +0 -0
  53. package/assets/interpreter/stdlib/stdlib__Array.cmi +0 -0
  54. package/assets/interpreter/stdlib/stdlib__ArrayLabels.cmi +0 -0
  55. package/assets/interpreter/stdlib/stdlib__Atomic.cmi +0 -0
  56. package/assets/interpreter/stdlib/stdlib__Bigarray.cmi +0 -0
  57. package/assets/interpreter/stdlib/stdlib__Bool.cmi +0 -0
  58. package/assets/interpreter/stdlib/stdlib__Buffer.cmi +0 -0
  59. package/assets/interpreter/stdlib/stdlib__Bytes.cmi +0 -0
  60. package/assets/interpreter/stdlib/stdlib__BytesLabels.cmi +0 -0
  61. package/assets/interpreter/stdlib/stdlib__Callback.cmi +0 -0
  62. package/assets/interpreter/stdlib/stdlib__Char.cmi +0 -0
  63. package/assets/interpreter/stdlib/stdlib__Complex.cmi +0 -0
  64. package/assets/interpreter/stdlib/stdlib__Condition.cmi +0 -0
  65. package/assets/interpreter/stdlib/stdlib__Digest.cmi +0 -0
  66. package/assets/interpreter/stdlib/stdlib__Domain.cmi +0 -0
  67. package/assets/interpreter/stdlib/stdlib__Dynarray.cmi +0 -0
  68. package/assets/interpreter/stdlib/stdlib__Effect.cmi +0 -0
  69. package/assets/interpreter/stdlib/stdlib__Either.cmi +0 -0
  70. package/assets/interpreter/stdlib/stdlib__Ephemeron.cmi +0 -0
  71. package/assets/interpreter/stdlib/stdlib__Filename.cmi +0 -0
  72. package/assets/interpreter/stdlib/stdlib__Float.cmi +0 -0
  73. package/assets/interpreter/stdlib/stdlib__Format.cmi +0 -0
  74. package/assets/interpreter/stdlib/stdlib__Fun.cmi +0 -0
  75. package/assets/interpreter/stdlib/stdlib__Gc.cmi +0 -0
  76. package/assets/interpreter/stdlib/stdlib__Hashtbl.cmi +0 -0
  77. package/assets/interpreter/stdlib/stdlib__Iarray.cmi +0 -0
  78. package/assets/interpreter/stdlib/stdlib__In_channel.cmi +0 -0
  79. package/assets/interpreter/stdlib/stdlib__Int.cmi +0 -0
  80. package/assets/interpreter/stdlib/stdlib__Int32.cmi +0 -0
  81. package/assets/interpreter/stdlib/stdlib__Int64.cmi +0 -0
  82. package/assets/interpreter/stdlib/stdlib__Lazy.cmi +0 -0
  83. package/assets/interpreter/stdlib/stdlib__Lexing.cmi +0 -0
  84. package/assets/interpreter/stdlib/stdlib__List.cmi +0 -0
  85. package/assets/interpreter/stdlib/stdlib__ListLabels.cmi +0 -0
  86. package/assets/interpreter/stdlib/stdlib__Map.cmi +0 -0
  87. package/assets/interpreter/stdlib/stdlib__Marshal.cmi +0 -0
  88. package/assets/interpreter/stdlib/stdlib__MoreLabels.cmi +0 -0
  89. package/assets/interpreter/stdlib/stdlib__Mutex.cmi +0 -0
  90. package/assets/interpreter/stdlib/stdlib__Nativeint.cmi +0 -0
  91. package/assets/interpreter/stdlib/stdlib__Obj.cmi +0 -0
  92. package/assets/interpreter/stdlib/stdlib__Oo.cmi +0 -0
  93. package/assets/interpreter/stdlib/stdlib__Option.cmi +0 -0
  94. package/assets/interpreter/stdlib/stdlib__Out_channel.cmi +0 -0
  95. package/assets/interpreter/stdlib/stdlib__Pair.cmi +0 -0
  96. package/assets/interpreter/stdlib/stdlib__Parsing.cmi +0 -0
  97. package/assets/interpreter/stdlib/stdlib__Pqueue.cmi +0 -0
  98. package/assets/interpreter/stdlib/stdlib__Printexc.cmi +0 -0
  99. package/assets/interpreter/stdlib/stdlib__Printf.cmi +0 -0
  100. package/assets/interpreter/stdlib/stdlib__Queue.cmi +0 -0
  101. package/assets/interpreter/stdlib/stdlib__Random.cmi +0 -0
  102. package/assets/interpreter/stdlib/stdlib__Repr.cmi +0 -0
  103. package/assets/interpreter/stdlib/stdlib__Result.cmi +0 -0
  104. package/assets/interpreter/stdlib/stdlib__Scanf.cmi +0 -0
  105. package/assets/interpreter/stdlib/stdlib__Semaphore.cmi +0 -0
  106. package/assets/interpreter/stdlib/stdlib__Seq.cmi +0 -0
  107. package/assets/interpreter/stdlib/stdlib__Set.cmi +0 -0
  108. package/assets/interpreter/stdlib/stdlib__Stack.cmi +0 -0
  109. package/assets/interpreter/stdlib/stdlib__StdLabels.cmi +0 -0
  110. package/assets/interpreter/stdlib/stdlib__String.cmi +0 -0
  111. package/assets/interpreter/stdlib/stdlib__StringLabels.cmi +0 -0
  112. package/assets/interpreter/stdlib/stdlib__Sys.cmi +0 -0
  113. package/assets/interpreter/stdlib/stdlib__Type.cmi +0 -0
  114. package/assets/interpreter/stdlib/stdlib__Uchar.cmi +0 -0
  115. package/assets/interpreter/stdlib/stdlib__Unit.cmi +0 -0
  116. package/assets/interpreter/stdlib/stdlib__Weak.cmi +0 -0
  117. package/assets/interpreter/stdlib/topdirs.cmi +0 -0
  118. package/assets/interpreter/stdlib/units.txt +75 -0
  119. package/assets/interpreter/stdlib/unix.cmi +0 -0
  120. package/assets/interpreter/stdlib/unixLabels.cmi +0 -0
  121. package/assets/interpreter/worker-entry.js +39 -0
  122. package/assets/interpreter/worker.js +42356 -0
  123. package/bin/copy-assets.mjs +75 -0
  124. package/package.json +41 -4
  125. package/src/compiler/compile.js +195 -0
  126. package/src/compiler/index.js +46 -0
  127. package/src/compiler/worker.js +46 -0
  128. package/src/index.js +12 -0
  129. package/src/interpreter/api.js +182 -0
  130. package/src/interpreter/index.js +5 -0
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env node
2
+ // Copies the assets an implementation needs into a directory you serve.
3
+ //
4
+ // A page cannot read a file out of node_modules, so the runtime assets have to
5
+ // be published by whatever serves the page. This writes the set for one
6
+ // implementation at a time, so using the compiler does not mean serving the
7
+ // interpreter's assets or the other way round.
8
+ import { cp, mkdir, readdir } from 'node:fs/promises';
9
+ import { resolve } from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+
12
+ const ASSETS = fileURLToPath(new URL('../assets/', import.meta.url));
13
+
14
+ const USAGE = `Copy the OCaml assets that ship with this package into a directory you serve.
15
+
16
+ ocaml-copy-assets [directory] [--compiler|--interpreter|--all]
17
+
18
+ directory where to write them (default: ./ocaml)
19
+ --compiler the compiler: the two compiler programs and the stdlib (default)
20
+ --interpreter the toplevel: its runtime, interfaces and libraries
21
+ --all both, each in its own subdirectory
22
+ --print-path print the packaged assets directory and exit
23
+ --help print this
24
+
25
+ The default is --compiler, so a compiler user serves nothing they do not need.
26
+ `;
27
+
28
+ const args = process.argv.slice(2);
29
+ if (args.includes('--help') || args.includes('-h')) {
30
+ console.log(USAGE);
31
+ process.exit(0);
32
+ }
33
+ if (args.includes('--print-path')) {
34
+ console.log(ASSETS);
35
+ process.exit(0);
36
+ }
37
+
38
+ const which = args.includes('--all')
39
+ ? ['compiler', 'interpreter']
40
+ : args.includes('--interpreter')
41
+ ? ['interpreter']
42
+ : ['compiler'];
43
+
44
+ const root = resolve(args.find((arg) => !arg.startsWith('-')) ?? 'ocaml');
45
+
46
+ // One implementation is written straight into the target directory so that
47
+ // `baseUrl` is simply that directory; both are nested, because each has its own
48
+ // `stdlib/` and they would collide.
49
+ const targetFor = (implementation) =>
50
+ which.length === 1 ? root : resolve(root, implementation);
51
+
52
+ for (const implementation of which) {
53
+ const source = resolve(ASSETS, implementation);
54
+ const target = targetFor(implementation);
55
+ await mkdir(target, { recursive: true });
56
+ for (const entry of await readdir(source, { withFileTypes: true })) {
57
+ await cp(resolve(source, entry.name), resolve(target, entry.name), {
58
+ recursive: true,
59
+ force: true
60
+ });
61
+ }
62
+ console.log(`${implementation} assets copied to ${target}`);
63
+ }
64
+
65
+ console.log('');
66
+ console.log(`Serve ${which.length === 1 ? 'that directory' : 'those directories'} and pass the URL as baseUrl,`);
67
+ console.log('for example:');
68
+ for (const implementation of which) {
69
+ console.log(
70
+ ` ${implementation}: ` +
71
+ "new URL('" +
72
+ (which.length === 1 ? '/ocaml/' : `/${implementation}/`) +
73
+ "', location.href)"
74
+ );
75
+ }
package/package.json CHANGED
@@ -1,6 +1,43 @@
1
1
  {
2
2
  "name": "@live-codes/ocaml",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "description": "Run OCaml in the browser with no server: a compiler that emits JavaScript, and a toplevel that runs OCaml directly",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/live-codes/browser-ocaml.git",
8
+ "directory": "packages/ocaml"
9
+ },
10
+ "homepage": "https://github.com/live-codes/browser-ocaml#readme",
11
+ "bugs": "https://github.com/live-codes/browser-ocaml/issues",
12
+ "type": "module",
13
+ "license": "LGPL-2.1-or-later WITH OCaml-LGPL-linking-exception",
14
+ "exports": {
15
+ ".": "./src/index.js",
16
+ "./compiler": "./src/compiler/index.js",
17
+ "./interpreter": "./src/interpreter/index.js"
18
+ },
19
+ "bin": {
20
+ "ocaml-copy-assets": "./bin/copy-assets.mjs"
21
+ },
22
+ "files": [
23
+ "src",
24
+ "bin",
25
+ "assets",
26
+ "README.md",
27
+ "LICENSE",
28
+ "THIRD-PARTY-NOTICES.md"
29
+ ],
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "engines": {
34
+ "node": ">=20"
35
+ },
36
+ "keywords": [
37
+ "ocaml",
38
+ "compiler",
39
+ "toplevel",
40
+ "playground",
41
+ "browser"
42
+ ]
43
+ }
@@ -0,0 +1,195 @@
1
+ // Compile OCaml source to JavaScript.
2
+ //
3
+ // Two compiler programs, each in its own worker: the compiler-libs driver turns
4
+ // source into bytecode, js_of_ocaml turns bytecode into JavaScript. A worker per
5
+ // step because a js_of_ocaml program runs its main on load and returns -- it
6
+ // cannot be reused.
7
+ //
8
+ // Everything crosses between the page and the compilers through the virtual
9
+ // filesystem; nothing is written to disk. The result travels as hex, because a
10
+ // raw bytecode stream would not survive being put on a text channel.
11
+
12
+ const lines = (text) => text.split('\n').filter((line) => line.trim() !== '');
13
+
14
+ // Two kinds of upstream noise reach the worker's stderr, and neither says anything
15
+ // about the program being compiled:
16
+ //
17
+ // - js_of_ocaml is a command-line program and calls `exit` after writing its
18
+ // output -- and in a browser-target build `exit` is not implemented, so it
19
+ // raises. The output has already arrived by then.
20
+ // - js_of_ocaml's runtime tries to `importScripts` its own chunk files, which
21
+ // cannot work once the worker is created from a data URL -- the trick that lets
22
+ // the compiler run from a CDN, since a worker script must otherwise be
23
+ // same-origin. It falls back to the files it was handed and the bundle is
24
+ // correct. A genuinely fatal load leaves no output at all, and that is reported
25
+ // separately by `compile`.
26
+ const PHANTOM =
27
+ /Function 'exit' not implemented|Failed to execute 'importScripts'|^Uncaught NetworkError:/;
28
+ const diagnosticsOf = (text) => lines(text).filter((line) => !PHANTOM.test(line));
29
+
30
+ // Byte-for-byte, and deliberately not TextDecoder: in browsers "latin1" and
31
+ // "iso-8859-1" are both aliases for windows-1252, which remaps bytes 0x80-0x9F.
32
+ // That is enough to make a .cmi unreadable -- "Corrupted compiled interface" --
33
+ // so bytes are copied one at a time, chunked because a 1.6 MB .cma would
34
+ // overflow the argument list.
35
+ function bytesToString(bytes) {
36
+ let s = '';
37
+ const CHUNK = 0x8000;
38
+ for (let i = 0; i < bytes.length; i += CHUNK) {
39
+ s += String.fromCharCode.apply(null, bytes.subarray(i, i + CHUNK));
40
+ }
41
+ return s;
42
+ }
43
+
44
+ function hexToBytes(hex) {
45
+ const bytes = new Uint8Array(hex.length / 2);
46
+ for (let i = 0; i < bytes.length; i++) bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
47
+ return bytes;
48
+ }
49
+
50
+ // A worker script has to be same-origin with whatever creates it: `new Worker()`
51
+ // refuses a cross-origin URL, and CORS does not help -- it is the script fetch the
52
+ // browser will not make. So the worker is created from a small data URL whose only
53
+ // job is to `importScripts` the harness, which *is* allowed across origins and then
54
+ // loads everything else itself.
55
+ //
56
+ // A data URL rather than a blob because the compiler may itself be running inside a
57
+ // worker, where `URL.createObjectURL` is not dependable. The URL is interpolated
58
+ // into JavaScript, so it is quoted with `JSON.stringify` and base64-encoded: the
59
+ // base64 form needs no escaping of its own.
60
+ const toDataUrl = (content, type = 'text/javascript') =>
61
+ `data:${type};charset=UTF-8;base64,` + btoa(content);
62
+
63
+ const workerDataUrlFor = (script) =>
64
+ toDataUrl(`importScripts(${JSON.stringify(String(script))});`);
65
+
66
+ async function runInWorker(baseUrl, script, files, env, argv, fs = []) {
67
+ const worker = new Worker(workerDataUrlFor(new URL('worker.js', baseUrl).href));
68
+ return new Promise((resolve, reject) => {
69
+ worker.onmessage = (event) => {
70
+ worker.terminate();
71
+ resolve(event.data);
72
+ };
73
+ worker.onerror = (event) => {
74
+ worker.terminate();
75
+ reject(new Error(event.message || 'the compiler worker failed to load'));
76
+ };
77
+ worker.postMessage({ script, files, env, argv, fs });
78
+ });
79
+ }
80
+
81
+ // The compiler finds parts of its own runtime through a filesystem -- `Fs` --
82
+ // while it emits a program. Outside a browser those files are on disk beside it;
83
+ // inside one there is no filesystem, and the program it emits is then missing
84
+ // pieces of its own runtime, silently: a chunk that provides classes or helpers
85
+ // rather than primitives leaves no dummy behind. So they are shipped and
86
+ // registered, under both the plain and the `+`-prefixed name the compiler may
87
+ // ask for. Loaded once per page, not once per compile.
88
+ let runtimeChunks;
89
+
90
+ async function loadRuntime(baseUrl) {
91
+ if (runtimeChunks) return runtimeChunks;
92
+ const manifest = await (await fetch(new URL('runtime/manifest.txt', baseUrl))).text();
93
+ runtimeChunks = (
94
+ await Promise.all(
95
+ lines(manifest).map(async (name) => {
96
+ const content = bytesToString(
97
+ new Uint8Array(await (await fetch(new URL(`runtime/${name}`, baseUrl))).arrayBuffer())
98
+ );
99
+ return [
100
+ { name, content },
101
+ { name: `+${name}`, content }
102
+ ];
103
+ })
104
+ )
105
+ ).flat();
106
+ return runtimeChunks;
107
+ }
108
+
109
+ // The standard library the driver reads, as one `js_of_ocaml build-fs` artifact:
110
+ // a script that registers every file in the pseudo filesystem, through
111
+ // `jsoo_create_file` when a runtime is already up and by queueing on
112
+ // `jsoo_fs_tmp` when not. The worker mounts it before the compiler program, and
113
+ // the program's runtime replays the queue as it starts.
114
+ //
115
+ // It used to be 71 files, listed in a manifest and fetched one at a time on
116
+ // every compile. One artifact is one request, and no JavaScript has to assemble
117
+ // it. The files register at `/stdlib`, which is why `OCAMLLIB` below is
118
+ // absolute rather than the bare name it used to be.
119
+ const STDLIB = 'stdlib.js';
120
+ const STDLIB_DIR = '/stdlib';
121
+
122
+ /**
123
+ * Compile OCaml source to JavaScript.
124
+ *
125
+ * @param {string} source
126
+ * @param {object} options
127
+ * @param {string|URL} options.baseUrl - where the compiler assets are served from.
128
+ * @param {string} [options.driver] - the source-to-bytecode program in that directory.
129
+ * @param {string} [options.jsoo] - the bytecode-to-JavaScript program.
130
+ * @param {string} [options.stdlib] - the filesystem artifact carrying the standard library.
131
+ * @returns {Promise<{js: string|null, diagnostics: string[], error?: string}>}
132
+ */
133
+ export async function compile(source, { baseUrl, driver = 'compile_stdout.browser.js', jsoo = 'jsoo.js', stdlib = STDLIB }) {
134
+ if (typeof source !== 'string') throw new TypeError('compile: source must be a string');
135
+ const runtimeFiles = await loadRuntime(baseUrl);
136
+ const files = [{ name: 'main.ml', content: source }, ...runtimeFiles];
137
+
138
+ // Source -> bytecode, as hex.
139
+ const front = await runInWorker(
140
+ baseUrl,
141
+ new URL(driver, baseUrl).href,
142
+ files,
143
+ { OCAMLLIB: STDLIB_DIR },
144
+ ['node', 'compile_stdout', 'main.ml'],
145
+ [new URL(stdlib, baseUrl).href]
146
+ );
147
+ const diagnostics = diagnosticsOf(front.err.join('\n'));
148
+ // The driver prints its hex with no newlines anywhere, so its chunks are
149
+ // pieces of one line and go back together without separators.
150
+ const hex = front.out.join('').trim();
151
+ if (!/^[0-9a-f]+$/.test(hex)) {
152
+ return { js: null, diagnostics, error: 'compilation failed' };
153
+ }
154
+
155
+ // Bytecode -> JavaScript, on stdout.
156
+ //
157
+ // `--target-env=browser` because that is where the result runs.
158
+ //
159
+ // `--pretty` is for the reader -- the emitted JavaScript is what a Compiled
160
+ // Code pane shows -- and it also keeps the output line-oriented, which the
161
+ // joining below depends on.
162
+ const back = await runInWorker(
163
+ baseUrl,
164
+ new URL(jsoo, baseUrl).href,
165
+ [{ name: 'main.byte', content: bytesToString(hexToBytes(hex)) }, ...runtimeFiles],
166
+ {},
167
+ ['node', 'js_of_ocaml', 'main.byte', '-o', '-', '--target-env=browser', '--pretty']
168
+ );
169
+ const backDiagnostics = diagnosticsOf(back.err.join('\n'));
170
+ // js_of_ocaml's stdout is line-buffered, so it hands over one console call
171
+ // per line: the chunks have to be rejoined with newlines, not concatenated.
172
+ // Concatenated, the whole program lands on a single line and ends up inside
173
+ // the header comment it begins with -- it then runs nothing, prints nothing
174
+ // and raises nothing. That is a miserable failure to debug: 64 KB of
175
+ // plausible-looking, commented-out code.
176
+ const js = back.out.join('\n');
177
+ // A bundle that arrives as one line would be swallowed by the header comment
178
+ // it starts with, and would then run nothing at all. Fail loudly rather than
179
+ // hand back a program that quietly does nothing.
180
+ if (!js.includes('\n')) {
181
+ return {
182
+ js: null,
183
+ diagnostics: [...diagnostics, ...backDiagnostics],
184
+ error: 'the compiler produced a single-line bundle, which would not run'
185
+ };
186
+ }
187
+ if (js.trim() === '') {
188
+ return {
189
+ js: null,
190
+ diagnostics: [...diagnostics, ...backDiagnostics],
191
+ error: 'js_of_ocaml produced no output'
192
+ };
193
+ }
194
+ return { js, diagnostics: [...diagnostics, ...backDiagnostics] };
195
+ }
@@ -0,0 +1,46 @@
1
+ // The compiler entry point.
2
+ //
3
+ // import { createCompiler } from '@live-codes/ocaml';
4
+ //
5
+ // const compiler = await createCompiler({ baseUrl });
6
+ // const { js, diagnostics } = await compiler.compile(source);
7
+ //
8
+ // A page cannot read files out of node_modules, so the compiler assets have to
9
+ // be published by whatever serves the page -- `ocaml-copy-assets` writes a copy
10
+ // into a directory you serve, and `baseUrl` points at it. Creating a compiler
11
+ // checks the assets are actually there, so a bad baseUrl fails here rather than
12
+ // at the first compile.
13
+ import { compile as compileSource } from './compile.js';
14
+
15
+ export async function createCompiler({ baseUrl } = {}) {
16
+ if (!baseUrl) {
17
+ throw new Error(
18
+ 'baseUrl is required: serve the compiler assets and pass their URL. ' +
19
+ 'Run `ocaml-copy-assets --compiler <dir>` to write them, then ' +
20
+ "`createCompiler({ baseUrl: new URL('/ocaml-compiler/', location.href) })`."
21
+ );
22
+ }
23
+ const base = new URL(baseUrl, globalThis.location?.href ?? 'http://localhost/');
24
+
25
+ // The standard library is one 5 MB artifact, so it is not fetched just to
26
+ // prove it is reachable -- a HEAD is enough to catch a wrong baseUrl, and
27
+ // the worker's `importScripts` would fail much less legibly later.
28
+ const stdlib = new URL('stdlib.js', base);
29
+ const response = await fetch(stdlib, { method: 'HEAD' }).catch(() => null);
30
+ if (!response || !response.ok) {
31
+ throw new Error(
32
+ `No compiler assets at ${base}: ${stdlib} is not there. ` +
33
+ 'Run `ocaml-copy-assets --compiler <dir>` and point baseUrl at the directory you serve.'
34
+ );
35
+ }
36
+
37
+ return {
38
+ banner: 'ocaml compiler',
39
+ /** Compile a program. Returns `{ js, diagnostics, error? }`. */
40
+ compile(source) {
41
+ return compileSource(source, { baseUrl: base });
42
+ }
43
+ };
44
+ }
45
+
46
+ export { compileSource as compile };
@@ -0,0 +1,46 @@
1
+ // The compiler worker: one compiler per worker.
2
+ //
3
+ // A js_of_ocaml program runs its main on load and returns, so a worker cannot
4
+ // run two of them -- each compile needs a fresh one, created and terminated.
5
+ //
6
+ // Everything the compiler needs arrives through the virtual filesystem and the
7
+ // environment; its output is collected by overriding console, because with the
8
+ // browser filesystem fd 1 and fd 2 are wired to console.log and console.error
9
+ // (runtime/js/fs_fake.js). A worker has no other way in: the output never
10
+ // reaches a file the page can read.
11
+ self.onmessage = (event) => {
12
+ const { script, files, env, argv, fs = [] } = event.data;
13
+ const out = [];
14
+ const err = [];
15
+ const realLog = console.log;
16
+ const realError = console.error;
17
+
18
+ console.log = (...args) => out.push(args.join(' '));
19
+ console.error = (...args) => err.push(args.join(' '));
20
+
21
+ try {
22
+ globalThis.jsoo_fs_tmp = files;
23
+ globalThis.jsoo_env = env;
24
+ // Browsers have no `process`; js_of_ocaml reads `process?.argv` for argv,
25
+ // so a fake one is enough. Note Sys.argv.(0) is argv[1].
26
+ globalThis.process = { argv };
27
+ // Filesystem artifacts first. Nothing has defined `jsoo_create_file` yet,
28
+ // so they queue their files on `jsoo_fs_tmp` -- and the program's runtime
29
+ // replays that queue at start-up. Loading them after the program would
30
+ // take the other branch and register the files directly, so the order is
31
+ // not load-bearing, but this is the cheaper one.
32
+ for (const url of fs) importScripts(url);
33
+ importScripts(script);
34
+ // The chunks go back as they arrived, not joined: the caller knows how to
35
+ // put them together, and joining here would lose the newlines between
36
+ // them. js_of_ocaml's stdout is line-buffered, so it hands over one call
37
+ // per line, and a program that arrives as a single line is a program
38
+ // swallowed by its own header comment.
39
+ self.postMessage({ out, err });
40
+ } catch (error) {
41
+ self.postMessage({ out, err: [...err, String((error && error.message) || error)] });
42
+ } finally {
43
+ console.log = realLog;
44
+ console.error = realError;
45
+ }
46
+ };
package/src/index.js ADDED
@@ -0,0 +1,12 @@
1
+ // @live-codes/ocaml
2
+ //
3
+ // Two implementations of OCaml in the browser, each behind its own entry point
4
+ // so that using one costs nothing for the other:
5
+ //
6
+ // @live-codes/ocaml this file -- the compiler
7
+ // @live-codes/ocaml/compiler OCaml source -> JavaScript
8
+ // @live-codes/ocaml/interpreter a toplevel that runs OCaml directly
9
+ //
10
+ // The compiler is the default because that is the model the other LiveCodes
11
+ // languages use: the source becomes JavaScript, which the page then runs.
12
+ export * from './compiler/index.js';
@@ -0,0 +1,182 @@
1
+ // One implementation behind the package entry point.
2
+ //
3
+ // There are two ways to run it, and the caller's situation decides which:
4
+ //
5
+ // - If the runtime is already loaded in this context (a host that called
6
+ // `importScripts('worker.js')`, as LiveCodes' compiler worker would), it is
7
+ // used in place. A host that is already a worker should not start a nested
8
+ // one just to hold a runtime it could call directly.
9
+ // - Otherwise a worker is started for you, for standalone use where nobody
10
+ // else owns the message loop.
11
+ //
12
+ // `baseUrl` is what makes the first case work: the runtime cannot discover
13
+ // where its assets are once it has been imported into someone else's worker,
14
+ // so it is told rather than left to infer.
15
+
16
+ const shape = (raw, runMs) => {
17
+ const failed = raw.exitCode === null;
18
+ const lines = raw.diagnostics ? raw.diagnostics.split('\n').filter(Boolean) : [];
19
+ return {
20
+ stdout: raw.stdout,
21
+ stderr: raw.stderr,
22
+ output: raw.output,
23
+ diagnostics: lines,
24
+ // Empty on success, so `errors.length` is a reliable failure test. Warnings
25
+ // from a successful run are in `diagnostics`, not here.
26
+ errors: failed ? lines : [],
27
+ exitCode: failed ? null : Number(raw.exitCode),
28
+ outcome: raw.outcome,
29
+ // A runtime failure (a missing asset, a failed start) rather than the
30
+ // program's diagnostics. Null when nothing went wrong at that level.
31
+ error: raw.error ?? null,
32
+ runMs
33
+ };
34
+ };
35
+
36
+ const absolute = (baseUrl) =>
37
+ new URL(baseUrl, globalThis.location?.href ?? 'http://localhost/').href;
38
+
39
+ export function createApi() {
40
+ /**
41
+ * Start an OCaml runtime.
42
+ *
43
+ * @param {object} options
44
+ * @param {string|URL} options.baseUrl - where the runtime assets are served from. Required:
45
+ * the assets ship in this package but a page cannot read them off disk, so they have to be
46
+ * published somewhere fetchable (see `ocaml-copy-assets`).
47
+ * @param {object} [options.runtime] - an already-loaded `self.ocamlWasm` to use in place,
48
+ * instead of starting a worker. Defaults to one already present in this context.
49
+ * @param {string[]} [options.args] - default program argv. `Sys.argv.(0)` is `"main.ml"`.
50
+ * @param {{name: string, contents: string}[]} [options.files] - default files, written into the
51
+ * runtime's filesystem so `open_in "name"` finds them.
52
+ */
53
+ async function createCompiler(options = {}) {
54
+ const { baseUrl, args: defaultArgs = [], files: defaultFiles = [] } = options;
55
+ if (!baseUrl) {
56
+ throw new Error(
57
+ 'baseUrl is required: serve the runtime assets and pass their URL. ' +
58
+ 'Run `ocaml-copy-assets <dir>` to write them, then ' +
59
+ "`createCompiler({ baseUrl: new URL('/ocaml/', location.href) })`."
60
+ );
61
+ }
62
+
63
+ const resolvedBase = absolute(baseUrl);
64
+ let disposed = false;
65
+
66
+ const runtime = options.runtime ?? globalThis.ocamlWasm;
67
+ if (runtime) {
68
+ runtime.setBaseUrl(resolvedBase);
69
+ runtime.init();
70
+ return {
71
+ banner: 'in-thread',
72
+ /** Run a program in this context. Synchronous underneath; the Promise is for parity. */
73
+ run(code, input = '', runOptions = {}) {
74
+ if (disposed) return Promise.reject(new Error('This compiler has been disposed.'));
75
+ if (typeof code !== 'string') {
76
+ return Promise.reject(
77
+ new Error('run() needs the program source as its first argument.')
78
+ );
79
+ }
80
+ const started = performance.now();
81
+ // `run` does not throw: a failure comes back as `error`.
82
+ const raw = runtime.run({
83
+ code,
84
+ input,
85
+ args: runOptions.args ?? defaultArgs,
86
+ files: runOptions.files ?? defaultFiles
87
+ });
88
+ return Promise.resolve(shape(raw, performance.now() - started));
89
+ },
90
+ dispose() {
91
+ disposed = true;
92
+ }
93
+ };
94
+ }
95
+
96
+ const workerUrl = new URL('worker-entry.js', resolvedBase);
97
+ const worker = new Worker(workerUrl);
98
+
99
+ // `ready` is posted once the toplevel has initialised, so a bad baseUrl fails here rather
100
+ // than at the first run.
101
+ const banner = await new Promise((resolve, reject) => {
102
+ const onMessage = (event) => {
103
+ const message = event.data;
104
+ if (message && message[0] === 'ready') {
105
+ worker.removeEventListener('message', onMessage);
106
+ resolve(message[1]);
107
+ }
108
+ };
109
+ worker.addEventListener('message', onMessage);
110
+ worker.addEventListener('error', (event) => {
111
+ reject(
112
+ new Error(
113
+ `Failed to load the OCaml runtime from ${workerUrl}: ${event.message ?? 'unknown error'}`
114
+ )
115
+ );
116
+ });
117
+ });
118
+
119
+ // One toplevel, one environment: runs share it, so they are serialised rather than
120
+ // interleaved. A queued run still resolves - it just waits.
121
+ let queue = Promise.resolve();
122
+
123
+ /**
124
+ * Run a program.
125
+ *
126
+ * @param {string} code - OCaml source.
127
+ * @param {string} [input] - stdin, handed to the program once and then closed.
128
+ * @param {object} [runOptions] - per-run overrides: `args`, `files`.
129
+ * @returns {Promise<{stdout: string, stderr: string, output: string, diagnostics: string[],
130
+ * errors: string[], exitCode: number|null, outcome: string, error: string|null,
131
+ * runMs: number}>}
132
+ */
133
+ function run(code, input = '', runOptions = {}) {
134
+ if (disposed) return Promise.reject(new Error('This compiler has been disposed.'));
135
+ if (typeof code !== 'string') {
136
+ return Promise.reject(new Error('run() needs the program source as its first argument.'));
137
+ }
138
+
139
+ const started = performance.now();
140
+ const done = queue.then(
141
+ () =>
142
+ new Promise((resolve) => {
143
+ const onMessage = (event) => {
144
+ const message = event.data;
145
+ if (!message || message[0] !== 'result') return;
146
+ worker.removeEventListener('message', onMessage);
147
+ const [, exitCode, stdout, stderr, outcome, output, diagnostics] = message;
148
+ resolve(
149
+ shape(
150
+ { exitCode, stdout, stderr, outcome, output, diagnostics, error: null },
151
+ performance.now() - started
152
+ )
153
+ );
154
+ };
155
+ worker.addEventListener('message', onMessage);
156
+ worker.postMessage({
157
+ code,
158
+ input,
159
+ args: runOptions.args ?? defaultArgs,
160
+ files: runOptions.files ?? defaultFiles
161
+ });
162
+ })
163
+ );
164
+ queue = done.then(
165
+ () => undefined,
166
+ () => undefined
167
+ );
168
+ return done;
169
+ }
170
+
171
+ /** Stop the worker. Further runs reject. */
172
+ function dispose() {
173
+ if (disposed) return;
174
+ disposed = true;
175
+ worker.terminate();
176
+ }
177
+
178
+ return { banner, run, dispose };
179
+ }
180
+
181
+ return { createCompiler };
182
+ }
@@ -0,0 +1,5 @@
1
+ import { createApi } from './api.js';
2
+
3
+ const api = createApi();
4
+
5
+ export const createCompiler = api.createCompiler;