@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.
- package/LICENSE +220 -0
- package/README.md +165 -2
- package/THIRD-PARTY-NOTICES.md +52 -0
- package/assets/compiler/compile_stdout.browser.js +27856 -0
- package/assets/compiler/jsoo.js +12631 -0
- package/assets/compiler/package.json +3 -0
- package/assets/compiler/runtime/bigarray.js +1072 -0
- package/assets/compiler/runtime/bigstring.js +137 -0
- package/assets/compiler/runtime/domain.js +393 -0
- package/assets/compiler/runtime/dynlink.js +91 -0
- package/assets/compiler/runtime/float32.js +486 -0
- package/assets/compiler/runtime/fs.js +416 -0
- package/assets/compiler/runtime/fs_fake.js +600 -0
- package/assets/compiler/runtime/fs_node.js +609 -0
- package/assets/compiler/runtime/fs_quickjs.js +532 -0
- package/assets/compiler/runtime/gc.js +201 -0
- package/assets/compiler/runtime/graphics.js +1053 -0
- package/assets/compiler/runtime/ieee_754.js +621 -0
- package/assets/compiler/runtime/int64.js +516 -0
- package/assets/compiler/runtime/io.js +739 -0
- package/assets/compiler/runtime/jslib.js +553 -0
- package/assets/compiler/runtime/jslib_js_of_ocaml.js +81 -0
- package/assets/compiler/runtime/lexing.js +240 -0
- package/assets/compiler/runtime/manifest.txt +31 -0
- package/assets/compiler/runtime/marshal.js +863 -0
- package/assets/compiler/runtime/md5.js +236 -0
- package/assets/compiler/runtime/mlBytes.js +1007 -0
- package/assets/compiler/runtime/nat.js +469 -0
- package/assets/compiler/runtime/parsing.js +317 -0
- package/assets/compiler/runtime/prng.js +84 -0
- package/assets/compiler/runtime/promise.js +45 -0
- package/assets/compiler/runtime/runtime.js +16688 -0
- package/assets/compiler/runtime/runtime_events.js +90 -0
- package/assets/compiler/runtime/stdlib.js +363 -0
- package/assets/compiler/runtime/sync.js +35 -0
- package/assets/compiler/runtime/toplevel.js +150 -0
- package/assets/compiler/runtime/unix.js +914 -0
- package/assets/compiler/runtime/weak.js +247 -0
- package/assets/compiler/stdlib.js +254 -0
- package/assets/compiler/worker.js +46 -0
- package/assets/interpreter/libs/greet.cma +0 -0
- package/assets/interpreter/libs/greet.cmi +0 -0
- package/assets/interpreter/libs/manifest.txt +1 -0
- package/assets/interpreter/package.json +3 -0
- package/assets/interpreter/stdlib/camlinternalFormat.cmi +0 -0
- package/assets/interpreter/stdlib/camlinternalFormatBasics.cmi +0 -0
- package/assets/interpreter/stdlib/camlinternalLazy.cmi +0 -0
- package/assets/interpreter/stdlib/camlinternalMod.cmi +0 -0
- package/assets/interpreter/stdlib/camlinternalOO.cmi +0 -0
- package/assets/interpreter/stdlib/std_exit.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__Arg.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__Array.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__ArrayLabels.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__Atomic.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__Bigarray.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__Bool.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__Buffer.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__Bytes.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__BytesLabels.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__Callback.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__Char.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__Complex.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__Condition.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__Digest.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__Domain.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__Dynarray.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__Effect.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__Either.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__Ephemeron.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__Filename.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__Float.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__Format.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__Fun.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__Gc.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__Hashtbl.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__Iarray.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__In_channel.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__Int.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__Int32.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__Int64.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__Lazy.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__Lexing.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__List.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__ListLabels.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__Map.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__Marshal.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__MoreLabels.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__Mutex.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__Nativeint.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__Obj.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__Oo.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__Option.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__Out_channel.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__Pair.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__Parsing.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__Pqueue.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__Printexc.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__Printf.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__Queue.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__Random.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__Repr.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__Result.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__Scanf.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__Semaphore.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__Seq.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__Set.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__Stack.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__StdLabels.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__String.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__StringLabels.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__Sys.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__Type.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__Uchar.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__Unit.cmi +0 -0
- package/assets/interpreter/stdlib/stdlib__Weak.cmi +0 -0
- package/assets/interpreter/stdlib/topdirs.cmi +0 -0
- package/assets/interpreter/stdlib/units.txt +75 -0
- package/assets/interpreter/stdlib/unix.cmi +0 -0
- package/assets/interpreter/stdlib/unixLabels.cmi +0 -0
- package/assets/interpreter/worker-entry.js +39 -0
- package/assets/interpreter/worker.js +42356 -0
- package/bin/copy-assets.mjs +75 -0
- package/package.json +41 -4
- package/src/compiler/compile.js +195 -0
- package/src/compiler/index.js +46 -0
- package/src/compiler/worker.js +46 -0
- package/src/index.js +12 -0
- package/src/interpreter/api.js +182 -0
- 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.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
+
}
|