-
Notifications
You must be signed in to change notification settings - Fork 9
/
main.js
540 lines (493 loc) · 15.5 KB
/
main.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
// global classes
function bootstrap_runtime () {
class TextEncoder {
encoding = 'utf-8'
encode (input = '') {
// todo: empty string
// todo: result cache
return utf8Encode(input)
}
encodeInto (src, dest) {
// todo: pass a u32array(2) handle in here so we can return read, written
return utf8EncodeInto(src, dest)
}
}
class TextDecoder {
encoding = 'utf-8'
decode (u8) {
// todo: result cache
if (!u8.ptr) ptr(u8)
return utf8Decode(u8.ptr, u8.size)
}
}
// externally exposed functions
function assert (condition, message, ErrorType = Error) {
if (!condition) {
if (message && message.constructor.name === 'Function') {
throw new ErrorType(message(condition))
}
throw new ErrorType(message || "Assertion failed")
}
return condition
}
function wrap (handle, fn, plen = 0) {
const call = fn
const params = (new Array(plen)).fill(0).map((_, i) => `p${i}`).join(', ')
// TODO: Number.IsSafeInteger check - return BigInt if not safe
const f = new Function(
'handle',
'call',
`return function ${fn.name} (${params}) {
call(${params}${plen > 0 ? ', ' : ''}handle);
const v = handle[0] + ((2 ** 32) * handle[1])
return handle[0] + ((2 ** 32) * handle[1]);
}`,)
const fun = f(handle, call)
if (fn.state) fun.state = fn.state
return fun
}
function ptr (u8) {
u8.ptr = lo.getAddress(u8)
u8.size = u8.byteLength
return u8
}
function cstr (str) {
const buf = ptr(encoder.encode(`${str}\0`))
buf.size = buf.size - 1
return buf
}
function addr (u32) {
return u32[0] + ((2 ** 32) * u32[1])
}
function check_mode (val, mode) {
return (val & S_IFMT) === mode
}
function is_file (path) {
const fd = open(path, O_RDONLY)
if (fd <= 2) return false
if (fstat(fd, stat) !== 0) return false
close(fd)
return check_mode(stat32[MODE_WORD], S_IFREG)
}
function read_file (path, flags = O_RDONLY, size = 0) {
const fd = open(path, flags)
assert(fd > 0, `failed to open ${path} with flags ${flags}`)
if (size === 0) {
assert(fstat(fd, stat) === 0)
if (core.os === 'mac') {
size = Number(st[12])
} else {
size = Number(st[6])
}
}
let off = 0
let len = 0
// todo - check for max size
const u8 = new Uint8Array(size)
while ((len = read(fd, u8, size - off)) > 0) off += len
close(fd)
return u8
}
function write_file (path, u8, flags = defaultWriteFlags,
mode = defaultWriteMode) {
const len = u8.length
if (!len) return -1
const fd = open(path, flags, mode)
assert(fd > 0)
const chunks = Math.ceil(len / 4096)
let total = 0
let bytes = 0
for (let i = 0, off = 0; i < chunks; ++i, off += 4096) {
const towrite = Math.min(len - off, 4096)
bytes = write(fd, u8.subarray(off, off + towrite), towrite)
if (bytes <= 0) break
total += bytes
}
assert(bytes > 0)
close(fd)
return total
}
// this is called to load a binding linked into runtime or from an external
// shared library
function load (name) {
if (libCache.has(name)) return libCache.get(name)
let lib
if (core.binding_loader) {
lib = core.binding_loader(name)
if (!lib) lib = library(name)
} else {
lib = library(name)
}
if (lib) {
lib.internal = true
libCache.set(name, lib)
return lib
}
// todo: we leak this handle - need to be able to unload
const handle = core.dlopen(`lib/${name}/${name}.so`, RTLD_LAZY) ||
core.dlopen(`${LO_HOME}/lib/${name}/${name}.so`, RTLD_LAZY)
if (!handle) return
const sym = core.dlsym(handle, `_register_${name}`)
if (!sym) return
lib = library(sym)
lib.handle = handle
if (!lib) return
lib.fileName = `lib/${name}/${name}.so`
libCache.set(name, lib)
return lib
}
// internal functions
// this is called by the synchronous CJS require() loader
function load_source_sync (specifier, resource) {
let src = ''
if (core.sync_loader) {
src = core.sync_loader(specifier, resource)
if (src) return src
}
src = lo.builtin(specifier)
if (!src || (LO_CACHE === 1)) {
// todo: path.join
try {
src = decoder.decode(read_file(specifier))
} catch (err) {
src = decoder.decode(read_file(`${LO_HOME}/${specifier}`))
}
}
return src
}
// this is called when async ESM modules are loaded
async function load_source (specifier, resource) {
let src = ''
if (core.loader) {
src = await core.loader(specifier, resource)
if (src) return src
}
src = lo.builtin(specifier)
if (!src || (LO_CACHE === 1)) {
// todo: path.join
try {
src = decoder.decode(read_file(specifier))
} catch (err) {
src = decoder.decode(read_file(`${LO_HOME}/${specifier}`))
}
}
return src
}
async function on_module_load (specifier, resource) {
if (!specifier) return
if (moduleCache.has(specifier)) {
const mod = moduleCache.get(specifier)
if (!mod.evaluated) {
mod.namespace = await evaluateModule(mod.identity)
mod.evaluated = true
}
return mod.namespace
}
// todo: allow overriding loadSource - return a promise
// todo: this should be async
const src = await load_source(specifier, resource)
const mod = loadModule(src, specifier)
mod.resource = resource
moduleCache.set(specifier, mod)
const { requests } = mod
for (const request of requests) {
const src = await load_source(request, resource)
const mod = loadModule(src, request)
moduleCache.set(request, mod)
}
if (!mod.evaluated) {
mod.namespace = await evaluateModule(mod.identity)
mod.evaluated = true
}
return mod.namespace
}
function on_module_instantiate (specifier) {
//lo.print(`on_module_instantiate: ${specifier}\n`)
if (moduleCache.has(specifier)) {
return moduleCache.get(specifier).identity
}
// todo: why is this function synchronous only??
// todo: we need to specify the calling module path here
const src = load_source_sync(specifier)
const mod = loadModule(src, specifier)
moduleCache.set(specifier, mod)
return mod.identity
}
/**
* an approximation of node.js synchronous require. not sure if this should
* be here at all but it's useful for compatibility testing
* todo: add parent path support like it was in just
* ```
* @param file_path {string} path to the file to be required
*/
function require (file_path) {
if (requireCache.has(file_path)) {
return requireCache.get(file_path).exports
}
// todo: this is now async
const src = load_source_sync(file_path)
const f = new Function('exports', 'module', 'require', src)
const mod = { exports: {} }
f.call(globalThis, mod.exports, mod, require)
moduleCache.set(file_path, mod)
return mod.exports
}
/**
* handle any exceptions in async code that did not have a handler
* the best thing to do is die gracefully and log as much as possible
* we should make what happens here configurable
* @param err { Error } a javascript Error object
*/
function on_unhandled_rejection (err) {
console.error(`${AR}Unhandled Rejection${AD}`)
//console.error(err.stack)
die(err, true)
}
const builtin_cache = new Map()
function on_load_builtin (identifier) {
if (builtin_cache.has(identifier)) return builtin_cache.get(identifier)
// todo: use the actual index.js specified for the compiled runtime if we are in a compiled runtime
if (identifier === 'worker_source.js') {
builtin_cache.set(identifier, workerSource)
return workerSource
}
builtin_cache.set(identifier, builtin(identifier))
return builtin(identifier)
}
function wrap_getenv () {
const getenv = wrap(handle, core.getenv, 1)
return str => {
const ptr = getenv(str)
if (!ptr) return ''
// console.error(ptr)
const len = strnlen(ptr, MAX_ENV)
// console.error(len === 0)
if (len === 0) return ''
return lo.utf8Decode(ptr, len)
}
}
function wrap_getcwd () {
const getcwd = wrap(handle, core.getcwd, 2)
const cwdbuf = new Uint8Array(MAX_DIR)
return () => {
const ptr = getcwd(cwdbuf, cwdbuf.length)
if (!ptr) return ''
const len = strnlen(ptr, MAX_DIR)
if (len === 0) return ''
return utf8Decode(ptr, -1)
}
}
function die (err, hide_fatal = false) {
if (!hide_fatal) console.error(`${AR}Fatal Exception${AD}`)
console.error(err.stack)
console.error(`${AY}process will exit${AD}`)
exit(1)
}
const {
utf8EncodeInto, utf8Encode, utf8Decode, getAddress, args, exit, builtin,
library, workerSource, loadModule, evaluateModule, hrtime, wrapMemory
} = lo
const { core: core_library } = library('core')
const {
O_WRONLY, O_CREAT, O_TRUNC, O_RDONLY, S_IWUSR, S_IRUSR, S_IRGRP, S_IROTH,
S_IFREG, STDOUT, STDERR, S_IFMT, RTLD_LAZY
} = core_library
const {
write_string, open, fstat, read, write, close, strnlen
} = core_library
// this is sad (same problem with libs extending base object)
// direct extensions don't work well with types
// !!! overrides give us false types
// so lo.core is one type and library/lo.load is another
// but in JS they will be the same after override
// to avoid that we can copy props from base object
// and types for both cases will be valid
const core_unknown = /**@type {unknown} */(core_library);
const core = /**@type {Runtime['core']} */(core_unknown);
function little_endian () {
const buffer = new ArrayBuffer(2)
new DataView(buffer).setInt16(0, 256, true)
return new Int16Array(buffer)[0] === 256
}
const MAX_ENV = 65536 // maximum environment variable size - todo
const MAX_DIR = 65536 // maximum path len - todo
const isatty = core.isatty(STDOUT)
const AD = isatty ? '\u001b[0m' : '' // ANSI Default
const A0 = isatty ? '\u001b[30m' : '' // ANSI Black
const AR = isatty ? '\u001b[31m' : '' // ANSI Red
const AG = isatty ? '\u001b[32m' : '' // ANSI Green
const AY = isatty ? '\u001b[33m' : '' // ANSI Yellow
const AB = isatty ? '\u001b[34m' : '' // ANSI Blue
const AM = isatty ? '\u001b[35m' : '' // ANSI Magenta
const AC = isatty ? '\u001b[36m' : '' // ANSI Cyan
const AW = isatty ? '\u001b[37m' : '' // ANSI White
const defaultWriteFlags = O_WRONLY | O_CREAT | O_TRUNC
const defaultWriteMode = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH
core.defaultWriteFlags = defaultWriteFlags
core.defaultWriteMode = defaultWriteMode
const encoder = new TextEncoder()
const decoder = new TextDecoder()
const handle = new Uint32Array(2)
const stat = new Uint8Array(160)
const stat32 = new Uint32Array(stat.buffer)
const st = new BigUint64Array(stat.buffer)
const moduleCache = new Map()
const requireCache = new Map()
const libCache = new Map()
// todo: check errors
globalThis.console = {
log: str => write_string(STDOUT, `${str}\n`),
error: str => write_string(STDERR, `${str}\n`)
}
globalThis.onUnhandledRejection = on_unhandled_rejection
globalThis.require = require
globalThis.TextEncoder = TextEncoder
globalThis.TextDecoder = TextDecoder
lo.colors = { AD, A0, AR, AG, AY, AB, AM, AC, AW }
lo.builtin = on_load_builtin
lo.utf8Encode = utf8Encode
lo.load = load
lo.hrtime = wrap(handle, hrtime, 0)
lo.getAddress = wrap(handle, getAddress, 1)
lo.assert = assert
lo.moduleCache = moduleCache
lo.libCache = libCache
lo.requireCache = requireCache
lo.wrap = wrap
lo.wrapMemory = (ptr, len, free = 0) =>
new Uint8Array(wrapMemory(ptr, len, free))
lo.cstr = cstr
lo.ptr = ptr
lo.addr = addr
lo.core = core
// todo: should we just overwrite the existing ones and not put these on "lo"?
lo.getenv = wrap_getenv()
lo.setenv = lo.core.setenv
lo.getcwd = wrap_getcwd()
const LO_HOME = lo.getenv('LO_HOME') || lo.getcwd()
const LO_CACHE = parseInt(lo.getenv('LO_CACHE') || '0', 10)
core.homedir = LO_HOME
core.dlopen = wrap(handle, core.dlopen, 2)
core.dlsym = wrap(handle, core.dlsym, 2)
core.mmap = wrap(handle, core.mmap, 6)
core.calloc = wrap(handle, core.calloc, 2)
core.memcpy = wrap(handle, core.memcpy, 3)
core.memmove = wrap(handle, core.memmove, 3)
core.aligned_alloc = wrap(handle, core.aligned_alloc, 2)
core.isFile = is_file
core.read_file = read_file
core.write_file = write_file
core.little_endian = little_endian()
// todo: optimize this - return numbers and make a single call to get both
core.os = lo.os()
core.arch = lo.arch()
const MODE_WORD = core.arch === 'arm64' ? 4 : 6
const _builtins = lo.builtins()
lo.builtins = () => _builtins
const _libraries = lo.libraries()
lo.libraries = () => _libraries
// todo: depracte lo.libraries() in favour of lo.bindings()
lo.bindings = lo.libraries
//delete lo.library
//const noop = () => {}
//core.loader = core.sync_loader = noop
lo.setModuleCallbacks(on_module_load, on_module_instantiate)
// TODO: remove camel case names when we do a cleanup
core.readFile = read_file
core.writeFile = write_file
lo.evaluate_module = lo.evaluateModule
lo.get_address = lo.getAddress
lo.latin1_decode = lo.latin1Decode
lo.lib_cache = lo.libCache
lo.load_module = lo.loadModule
lo.module_cache = lo.moduleCache
lo.next_tick = lo.nextTick
lo.pump_message_loop = lo.pumpMessageLoop
lo.read_memory = lo.readMemory
lo.read_memory_at_offset = lo.readMemoryAtOffset
lo.register_callback = lo.registerCallback
lo.require_cache = lo.requireCache
lo.run_microtasks = lo.runMicroTasks
lo.run_script = lo.runScript
lo.set_flags = lo.setFlags
lo.set_module_callbacks = lo.setModuleCallbacks
lo.unwrap_memory = lo.unwrapMemory
lo.utf8_decode = lo.utf8Decode
lo.utf8_encode = lo.utf8Encode
lo.utf8_encode_into = lo.utf8EncodeInto
lo.utf8_encode_into_ptr = lo.utf8EncodeIntoPtr
lo.utf8_encode_into_at_offset = lo.utf8EncodeIntoAtOffset
lo.utf8_length = lo.utf8Length
lo.wrap_memory = lo.wrapMemory
lo.latin1_decode = lo.latin1Decode
// todo: fix this and write up/decide exactly what module resolution does
// currently we check/open each file twice
/*
core.loader = specifier => {
if (is_file(specifier)) return
const home_path = `${LO_HOME}/${specifier}`
if (is_file(home_path)) return decoder.decode(read_file(home_path))
}
core.binding_loader = name => {
const handle = core.dlopen(`${LO_HOME}/lib/${name}/${name}.so`, 1)
if (!handle) return
const sym = core.dlsym(handle, `_register_${name}`)
if (!sym) return
const lib = library(sym)
if (!lib) return
lib.fileName = `lib/${name}/${name}.so`
lib.handle = handle
libCache.set(name, lib)
return lib
}
*/
function show_usage () {
// console.log('show_usage not implemented')
}
async function global_main () {
// todo: upgrade, install etc. maybe install these as command scripts, but that would not be very secure
if (args.length === 1) {
show_usage()
return Promise.resolve()
}
const command = args[1]
if (command === 'gen') {
(await import('lib/gen.js')).gen(args.slice(2))
} else if (command === 'build') {
//todo: should be awaited
(await import('lib/build.js')).build(args.slice(2))
} else if (command === 'install') {
(await import('lib/build.js')).install(args.slice(2))
} else if (command === 'init') {
(await import('lib/build.js')).init(args.slice(2))
} else if (command === 'uninstall') {
(await import('lib/build.js')).uninstall(args.slice(2))
} else if (command === 'upgrade') {
(await import('lib/build.js')).upgrade(args.slice(2))
} else if (command === 'repl') {
(await import('lib/repl.js')).repl().catch(die)
} else if (command === 'eval') {
await (new AsyncFunction(args[2]))()
} else if (command === 'type') {
(await import('lib/types.js')).add_app_type_deps(args[2])
} else {
let filePath = command
const { main, serve, test, bench } = await import(filePath)
const pargs = args.slice(2)
if (test) await test(...pargs)
if (bench) await bench(...pargs)
if (main) await main(...pargs)
if (serve) await serve(...pargs)
}
}
const AsyncFunction = async function () {}.constructor
if (workerSource) {
import('worker_source.js').catch(die)
} else if (args.length === 1) {
show_usage()
} else {
global_main().catch(die)
}
} // end bootstrap_runtime
bootstrap_runtime()
export {}