@mikrojs/native 0.12.0-next.9.g2e06437 → 0.13.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/CMakeLists.txt +3 -1
- package/include/mikrojs/mikrojs.h +7 -0
- package/include/mikrojs/platform.h +9 -0
- package/include/mikrojs/private.h +32 -5
- package/package.json +2 -2
- package/prebuilds/darwin-arm64/mikrojs.napi.node +0 -0
- package/prebuilds/linux-arm64/mikrojs.napi.node +0 -0
- package/prebuilds/linux-x64/mikrojs.napi.node +0 -0
- package/runtime/abort/abort.ts +120 -0
- package/runtime/internal.d.ts +2 -1
- package/runtime/sys/sys.ts +2 -0
- package/runtime/sys/types.ts +27 -1
- package/runtime/test/test.ts +54 -6
- package/src/builtins.cpp +10 -5
- package/src/mik_abort.cpp +40 -107
- package/src/mik_console.cpp +26 -24
- package/src/mik_inspect.cpp +3 -3
- package/src/mik_repl.cpp +4 -0
- package/src/mik_sys.cpp +11 -1
- package/src/mikrojs.cpp +224 -78
- package/src/modules.cpp +7 -16
- package/src/platform_posix.cpp +5 -0
package/CMakeLists.txt
CHANGED
|
@@ -81,7 +81,7 @@ endif()
|
|
|
81
81
|
include(cmake/mikrojs_bytecode.cmake)
|
|
82
82
|
mikrojs_generate_bytecode(
|
|
83
83
|
RUNTIME_DIR "${CMAKE_CURRENT_SOURCE_DIR}/runtime"
|
|
84
|
-
MODULES cbor env result schema fs http/helpers http/request i2c kv/nvs kv/rtc kv/shared module neopixel observable observable/lazy observable/operators pin pwm reader sleep spi sntp stdio stream sys test uart udp wifi
|
|
84
|
+
MODULES abort cbor env result schema fs http/helpers http/request i2c kv/nvs kv/rtc kv/shared module neopixel observable observable/lazy observable/operators pin pwm reader sleep spi sntp stdio stream sys test uart udp wifi
|
|
85
85
|
MODULE_PREFIX "mikro"
|
|
86
86
|
SYMBOL_PREFIX "mikro"
|
|
87
87
|
TARGET gen_bytecode
|
|
@@ -152,6 +152,7 @@ if(BUILD_TESTING)
|
|
|
152
152
|
test/main.cpp
|
|
153
153
|
test/runtime_test.cpp
|
|
154
154
|
test/modules_test.cpp
|
|
155
|
+
test/virtual_modules_test.cpp
|
|
155
156
|
test/text_encoding_test.cpp
|
|
156
157
|
test/cbor_test.cpp
|
|
157
158
|
test/abort_test.cpp
|
|
@@ -163,6 +164,7 @@ if(BUILD_TESTING)
|
|
|
163
164
|
test/runtime_recycle_test.cpp
|
|
164
165
|
test/udp_test.cpp
|
|
165
166
|
test/observable_test.cpp
|
|
167
|
+
test/unhandled_rejection_test.cpp
|
|
166
168
|
)
|
|
167
169
|
|
|
168
170
|
target_link_libraries(mikrojs_tests PRIVATE mikrojs)
|
|
@@ -86,6 +86,13 @@ void MIK_FreeRuntime(MIKRuntime* mik_rt);
|
|
|
86
86
|
* Emits no diagnostic output — callers decide whether and how to log. */
|
|
87
87
|
int MIK_RunEntry(MIKRuntime* mik_rt, const char* entry);
|
|
88
88
|
|
|
89
|
+
/* MIK_RunEntry variant that, on -EFAULT, copies the thrown exception's
|
|
90
|
+
* string form (or the sync-rejected promise's rejection value) into
|
|
91
|
+
* err_buf so callers can surface the real failure instead of a generic
|
|
92
|
+
* "evaluation threw". err_buf may be NULL; when given it is always
|
|
93
|
+
* NUL-terminated (empty when no message could be captured). */
|
|
94
|
+
int MIK_RunEntryErr(MIKRuntime* mik_rt, const char* entry, char* err_buf, size_t err_buf_size);
|
|
95
|
+
|
|
89
96
|
JSContext* MIK_GetJSContext(MIKRuntime* mik_rt);
|
|
90
97
|
MIKRuntime* MIK_GetRuntime(JSContext* ctx);
|
|
91
98
|
void MIK_SetFSBasePath(MIKRuntime* mik_rt, const char* base_path);
|
|
@@ -64,6 +64,15 @@ typedef struct MIKPlatform {
|
|
|
64
64
|
* original 6 MAC bytes. The returned pointer must remain valid for the
|
|
65
65
|
* lifetime of the platform. */
|
|
66
66
|
const char* (*get_device_id)(void);
|
|
67
|
+
/** Reason the chip last reset, as a stable lowercase string. On ESP32
|
|
68
|
+
* this maps esp_reset_reason(): "power-on", "software", "panic",
|
|
69
|
+
* "watchdog", "interrupt-watchdog", "task-watchdog", "brownout",
|
|
70
|
+
* "deep-sleep", "external", "sdio", "usb", "jtag", "efuse",
|
|
71
|
+
* "power-glitch", "cpu-lockup", or "unknown". A clean restart()
|
|
72
|
+
* reports "software". Host platforms have no reset concept and return
|
|
73
|
+
* "unknown". The returned pointer must remain valid for the lifetime
|
|
74
|
+
* of the platform. */
|
|
75
|
+
const char* (*get_reset_reason)(void);
|
|
67
76
|
} MIKPlatform;
|
|
68
77
|
|
|
69
78
|
/* Log levels (matching ESP-IDF ESP_LOG_xxx values) */
|
|
@@ -48,6 +48,15 @@ struct MIKLoopConsumerEntry {
|
|
|
48
48
|
MIKLoopDestroyFn destroy_fn;
|
|
49
49
|
};
|
|
50
50
|
|
|
51
|
+
/* A promise that rejected without a handler, awaiting the end-of-turn
|
|
52
|
+
* unhandled-rejection check. Mirrors quickjs-libc's JSRejectedPromiseEntry:
|
|
53
|
+
* we hold a reference to both the promise (matched by identity on `handle`)
|
|
54
|
+
* and its reason (reported at flush time). */
|
|
55
|
+
struct MIKRejectedPromise {
|
|
56
|
+
JSValue promise;
|
|
57
|
+
JSValue reason;
|
|
58
|
+
};
|
|
59
|
+
|
|
51
60
|
struct MIKRuntime {
|
|
52
61
|
MIKRunOptions options;
|
|
53
62
|
MIKConfig config;
|
|
@@ -83,10 +92,16 @@ struct MIKRuntime {
|
|
|
83
92
|
MIKTimerRegistry* timers;
|
|
84
93
|
std::vector<MIKNativeModuleEntry> native_modules;
|
|
85
94
|
std::vector<MIKLoopConsumerEntry> loop_consumers;
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
95
|
+
/* Promises that rejected without a handler, awaiting the end-of-turn
|
|
96
|
+
* unhandled-rejection check. The host rejection tracker adds a promise
|
|
97
|
+
* here on `reject` and removes it on `handle`; whatever remains after a
|
|
98
|
+
* microtask drain (mik__execute_jobs) is a genuine unhandled rejection
|
|
99
|
+
* and gets reported. This deferral mirrors the HTML/WinterCG algorithm
|
|
100
|
+
* and is what prevents a transiently-unhandled promise (e.g. a module's
|
|
101
|
+
* evaluation promise, rejected before the import loader attaches its
|
|
102
|
+
* .then) from being reported. Entries hold dup'd references owned by this
|
|
103
|
+
* vector and freed when removed or flushed. */
|
|
104
|
+
std::vector<MIKRejectedPromise> pending_rejections;
|
|
90
105
|
/* Shared prototype for Result objects ({ok, value} / {ok, error}) created
|
|
91
106
|
* by mik__result_ok/mik__result_err and the native:result ok()/err()
|
|
92
107
|
* functions. Holds .map/.mapErr/.andThen/.match/.orDefault/.orPanic +
|
|
@@ -167,6 +182,10 @@ JSValue mik_new_error(JSContext* ctx, int err);
|
|
|
167
182
|
JSValue mik_throw_errno(JSContext* ctx, int err);
|
|
168
183
|
|
|
169
184
|
void mik__execute_jobs(JSContext* ctx);
|
|
185
|
+
/* End-of-turn unhandled-rejection check; called after each microtask drain. */
|
|
186
|
+
void mik__flush_unhandled_rejections(JSContext* ctx);
|
|
187
|
+
/* Drop a promise from the pending-rejection queue without reporting it. */
|
|
188
|
+
void mik__forget_rejection(JSContext* ctx, JSValue promise);
|
|
170
189
|
JSModuleDef* mik__load_builtin(JSContext* ctx, const char* name);
|
|
171
190
|
int mik__load_file(JSContext* ctx, DynBuf* dbuf, const char* filename);
|
|
172
191
|
void mik__resolve_fs_path(JSContext* ctx, const char* module_name, char* out, size_t out_size);
|
|
@@ -221,7 +240,10 @@ void mik__stdin_consume(JSContext* ctx);
|
|
|
221
240
|
|
|
222
241
|
/* Console global (mik_console.cpp) */
|
|
223
242
|
void mik__console_init(JSContext* ctx, JSValue global_obj);
|
|
224
|
-
|
|
243
|
+
/* Reports an uncaught error/rejection. Dedups by error-object identity:
|
|
244
|
+
* returns true if it reported, false if this same object was already
|
|
245
|
+
* reported. */
|
|
246
|
+
bool mik__report_uncaught(JSContext* ctx, JSValue exc, bool in_promise = false);
|
|
225
247
|
|
|
226
248
|
/* Inspect (mik_inspect.cpp) */
|
|
227
249
|
std::string mik_inspect(JSContext* ctx, JSValue value, int depth = 2, bool colors = false,
|
|
@@ -313,6 +335,11 @@ void mik__repl_set_paused(bool paused);
|
|
|
313
335
|
* Device replies with zero-or-more MIK_MSG_FS_CHUNK frames followed by
|
|
314
336
|
* MIK_MSG_OK on EOF, or MIK_MSG_ERR if the path can't be opened. */
|
|
315
337
|
#define MIK_CMD_FS_GET 0x2B
|
|
338
|
+
/* Clear the on-device log files (log.txt + log.txt.1). No payload.
|
|
339
|
+
* Device suspends the logger, deletes both files, reopens a fresh
|
|
340
|
+
* log.txt, and replies MIK_MSG_OK. No-op (still OK) when file logging
|
|
341
|
+
* is disabled. */
|
|
342
|
+
#define MIK_CMD_LOG_RESET 0x2C
|
|
316
343
|
|
|
317
344
|
#define MIK_CMD_CONFIG_LIST 0x40
|
|
318
345
|
#define MIK_CMD_CONFIG_SET 0x41
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mikrojs/native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Mikro.js C++ runtime library and Node.js native addon",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"esp32",
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
"cmake-js": "^8.0.0",
|
|
81
81
|
"node-addon-api": "^8.7.0",
|
|
82
82
|
"node-gyp-build": "^4.8.4",
|
|
83
|
-
"@mikrojs/quickjs": "0.
|
|
83
|
+
"@mikrojs/quickjs": "0.13.0"
|
|
84
84
|
},
|
|
85
85
|
"devDependencies": {
|
|
86
86
|
"@swc/core": "^1.15.30",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/* Minimal AbortController / AbortSignal implementation.
|
|
2
|
+
*
|
|
3
|
+
* This is a lightweight subset of the WHATWG DOM spec: just enough
|
|
4
|
+
* for fetch timeouts and cooperative cancellation. No EventTarget
|
|
5
|
+
* dependency: listeners are stored in a simple array.
|
|
6
|
+
*
|
|
7
|
+
* Evaluating this module installs AbortController, AbortSignal,
|
|
8
|
+
* AbortError, and TimeoutError on globalThis. It is loaded lazily by
|
|
9
|
+
* the native lazy getters in mik_abort.cpp on first access to any of
|
|
10
|
+
* the four globals; shipping it as precompiled bytecode keeps the
|
|
11
|
+
* QuickJS parser out of the install path (parsing at runtime caused
|
|
12
|
+
* OOM on low-memory chips).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
class AbortError extends Error {
|
|
16
|
+
constructor(message?: string) {
|
|
17
|
+
super(message || 'The operation was aborted')
|
|
18
|
+
}
|
|
19
|
+
get name() {
|
|
20
|
+
return 'AbortError'
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
class TimeoutError extends Error {
|
|
25
|
+
constructor(message?: string) {
|
|
26
|
+
super(message || 'The operation timed out')
|
|
27
|
+
}
|
|
28
|
+
get name() {
|
|
29
|
+
return 'TimeoutError'
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const _a: unique symbol = Symbol()
|
|
34
|
+
const _r: unique symbol = Symbol()
|
|
35
|
+
const _l: unique symbol = Symbol()
|
|
36
|
+
const _s: unique symbol = Symbol()
|
|
37
|
+
|
|
38
|
+
function doAbort(signal: AbortSignal, reason: unknown): void {
|
|
39
|
+
if (signal[_a]) return
|
|
40
|
+
signal[_a] = true
|
|
41
|
+
signal[_r] = reason
|
|
42
|
+
if (typeof signal.onabort === 'function') signal.onabort()
|
|
43
|
+
for (const fn of signal[_l]) fn()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
class AbortSignal {
|
|
47
|
+
[_a] = false;
|
|
48
|
+
[_r]: unknown = undefined;
|
|
49
|
+
[_l]: (() => void)[] = []
|
|
50
|
+
onabort: (() => void) | null = null
|
|
51
|
+
|
|
52
|
+
get aborted(): boolean {
|
|
53
|
+
return this[_a]
|
|
54
|
+
}
|
|
55
|
+
get reason(): unknown {
|
|
56
|
+
return this[_r]
|
|
57
|
+
}
|
|
58
|
+
throwIfAborted(): void {
|
|
59
|
+
if (this[_a]) throw this[_r]
|
|
60
|
+
}
|
|
61
|
+
addEventListener(type: 'abort', fn: () => void): void {
|
|
62
|
+
if (type === 'abort' && typeof fn === 'function') this[_l].push(fn)
|
|
63
|
+
}
|
|
64
|
+
removeEventListener(type: 'abort', fn: () => void): void {
|
|
65
|
+
if (type === 'abort') this[_l] = this[_l].filter((f) => f !== fn)
|
|
66
|
+
}
|
|
67
|
+
static abort(reason?: unknown): AbortSignal {
|
|
68
|
+
const s = new AbortSignal()
|
|
69
|
+
doAbort(s, reason !== undefined ? reason : new AbortError())
|
|
70
|
+
return s
|
|
71
|
+
}
|
|
72
|
+
static timeout(ms: number): AbortSignal {
|
|
73
|
+
const s = new AbortSignal()
|
|
74
|
+
setTimeout(() => doAbort(s, new TimeoutError()), ms)
|
|
75
|
+
return s
|
|
76
|
+
}
|
|
77
|
+
static any(signals: AbortSignal[]): AbortSignal {
|
|
78
|
+
const s = new AbortSignal()
|
|
79
|
+
for (const i of signals) {
|
|
80
|
+
if (i.aborted) {
|
|
81
|
+
doAbort(s, i.reason)
|
|
82
|
+
return s
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
for (const i of signals) {
|
|
86
|
+
i.addEventListener('abort', () => doAbort(s, i.reason))
|
|
87
|
+
}
|
|
88
|
+
return s
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
class AbortController {
|
|
93
|
+
[_s] = new AbortSignal()
|
|
94
|
+
|
|
95
|
+
get signal(): AbortSignal {
|
|
96
|
+
return this[_s]
|
|
97
|
+
}
|
|
98
|
+
abort(reason?: unknown): void {
|
|
99
|
+
doAbort(this[_s], reason !== undefined ? reason : new AbortError())
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/* Object.defineProperties, NOT Object.assign: when this module is
|
|
104
|
+
* evaluated via a direct `import 'mikro/abort'` (it is in the builtins
|
|
105
|
+
* table), the four lazy getters installed by mik_abort.cpp are still
|
|
106
|
+
* present as setter-less accessors, and Object.assign's [[Set]] would
|
|
107
|
+
* throw "no setter for property". Defining replaces the accessors with
|
|
108
|
+
* plain data properties, matching what the lazy-getter path produces. */
|
|
109
|
+
const descriptor = (value: unknown) => ({
|
|
110
|
+
value,
|
|
111
|
+
writable: true,
|
|
112
|
+
enumerable: true,
|
|
113
|
+
configurable: true,
|
|
114
|
+
})
|
|
115
|
+
Object.defineProperties(globalThis, {
|
|
116
|
+
AbortError: descriptor(AbortError),
|
|
117
|
+
TimeoutError: descriptor(TimeoutError),
|
|
118
|
+
AbortSignal: descriptor(AbortSignal),
|
|
119
|
+
AbortController: descriptor(AbortController),
|
|
120
|
+
})
|
package/runtime/internal.d.ts
CHANGED
|
@@ -34,7 +34,7 @@ declare module 'native:observable' {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
declare module 'native:sys' {
|
|
37
|
-
import type {JsMemoryUsage} from './sys/types.js'
|
|
37
|
+
import type {JsMemoryUsage, ResetReason} from './sys/types.js'
|
|
38
38
|
export function evalScript(code: string): Promise<{value: unknown}>
|
|
39
39
|
export function memoryUsage(): {
|
|
40
40
|
heapUsed: number
|
|
@@ -67,6 +67,7 @@ declare module 'native:sys' {
|
|
|
67
67
|
}
|
|
68
68
|
export const firmware: {hash: string; date: string; idfVersion: string | undefined}
|
|
69
69
|
export const deviceId: string
|
|
70
|
+
export const resetReason: ResetReason
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
declare module 'native:fs' {
|
package/runtime/sys/sys.ts
CHANGED
package/runtime/sys/types.ts
CHANGED
|
@@ -73,7 +73,10 @@ export interface JsMemoryUsage {
|
|
|
73
73
|
arrayCount: number
|
|
74
74
|
/** Total element slots across all fast arrays */
|
|
75
75
|
fastArrayElements: number
|
|
76
|
-
/**
|
|
76
|
+
/** Cumulative bytes deserialized via JS_ReadObject over this
|
|
77
|
+
* runtime's lifetime (bytecode of imported modules, builtins, and
|
|
78
|
+
* bjson). A monotonic counter, NOT live memory — it never goes
|
|
79
|
+
* down, so don't read it as retained ArrayBuffer storage. */
|
|
77
80
|
binaryObjectSize: number
|
|
78
81
|
}
|
|
79
82
|
|
|
@@ -122,6 +125,29 @@ export declare const firmware: {
|
|
|
122
125
|
readonly idfVersion: string | undefined
|
|
123
126
|
}
|
|
124
127
|
|
|
128
|
+
/** Why the device last reset. A clean `restart()` reports `'software'`;
|
|
129
|
+
* a crash reports `'panic'`. Host/Node builds always report `'unknown'`. */
|
|
130
|
+
export type ResetReason =
|
|
131
|
+
| 'power-on'
|
|
132
|
+
| 'software'
|
|
133
|
+
| 'panic'
|
|
134
|
+
| 'watchdog'
|
|
135
|
+
| 'interrupt-watchdog'
|
|
136
|
+
| 'task-watchdog'
|
|
137
|
+
| 'brownout'
|
|
138
|
+
| 'deep-sleep'
|
|
139
|
+
| 'external'
|
|
140
|
+
| 'sdio'
|
|
141
|
+
| 'usb'
|
|
142
|
+
| 'jtag'
|
|
143
|
+
| 'efuse'
|
|
144
|
+
| 'power-glitch'
|
|
145
|
+
| 'cpu-lockup'
|
|
146
|
+
| 'unknown'
|
|
147
|
+
|
|
148
|
+
/** Reason the device last reset, determined once at boot. */
|
|
149
|
+
export declare const resetReason: ResetReason
|
|
150
|
+
|
|
125
151
|
export declare function restart(): never
|
|
126
152
|
|
|
127
153
|
/** Reason the device woke up this boot. Set after deep sleep (or first
|
package/runtime/test/test.ts
CHANGED
|
@@ -30,6 +30,17 @@ interface Suite {
|
|
|
30
30
|
const suites: Suite[] = []
|
|
31
31
|
let currentSuite: Suite | null = null
|
|
32
32
|
let heapBaseline = 0
|
|
33
|
+
/** Free system heap (bytes) at the baseline point, or 0 on the host (no
|
|
34
|
+
* system heap). Reference point for the "peak" figure: baseline free minus
|
|
35
|
+
* the run's low-water is the most the suite needed at once. Recaptured after
|
|
36
|
+
* each suite's beforeAll, in step with heapBaseline, so warmup is excluded
|
|
37
|
+
* from the figure. */
|
|
38
|
+
let sysFreeStart = 0
|
|
39
|
+
/** Lowest free system heap (bytes) sampled (post-gc) this run, or 0 on the
|
|
40
|
+
* host. The suite's closest sampled approach to OOM. The module re-inits
|
|
41
|
+
* for each test file (fresh runtime per file), so this is a true per-file
|
|
42
|
+
* figure, not a process-lifetime watermark shared across files. */
|
|
43
|
+
let sysFreeFloor = 0
|
|
33
44
|
|
|
34
45
|
/**
|
|
35
46
|
* Recapture the heap baseline. Called by the harness after each suite's
|
|
@@ -43,7 +54,15 @@ let heapBaseline = 0
|
|
|
43
54
|
async function captureHeapBaseline(): Promise<void> {
|
|
44
55
|
await Promise.resolve()
|
|
45
56
|
gc()
|
|
46
|
-
|
|
57
|
+
const {heapUsed, systemFree} = memoryUsage()
|
|
58
|
+
heapBaseline = heapUsed
|
|
59
|
+
// Recapture the system-heap baseline and reset the per-file low-water in
|
|
60
|
+
// step with heapBaseline, so warmup (module loads, TLS, wifi) is excluded
|
|
61
|
+
// from sysUsed the same way it is from heapDelta.
|
|
62
|
+
if (systemFree > 0) {
|
|
63
|
+
sysFreeStart = systemFree
|
|
64
|
+
sysFreeFloor = systemFree
|
|
65
|
+
}
|
|
47
66
|
}
|
|
48
67
|
|
|
49
68
|
function newSuite(name: string, flags: {skip?: boolean; only?: boolean; todo?: boolean}): Suite {
|
|
@@ -363,6 +382,8 @@ type TestEvent =
|
|
|
363
382
|
d: number
|
|
364
383
|
hb?: number
|
|
365
384
|
ha?: number
|
|
385
|
+
su?: number
|
|
386
|
+
sf?: number
|
|
366
387
|
tb?: number
|
|
367
388
|
ta?: number
|
|
368
389
|
pb?: number
|
|
@@ -401,7 +422,10 @@ function emitHeap(): void {
|
|
|
401
422
|
gc()
|
|
402
423
|
const mem = memoryUsage()
|
|
403
424
|
const evt: TestEvent = {e: 8, u: mem.heapUsed, t: mem.heapTotal}
|
|
404
|
-
if (mem.systemFree > 0)
|
|
425
|
+
if (mem.systemFree > 0) {
|
|
426
|
+
evt.f = mem.systemFree
|
|
427
|
+
if (sysFreeFloor === 0 || mem.systemFree < sysFreeFloor) sysFreeFloor = mem.systemFree
|
|
428
|
+
}
|
|
405
429
|
if (mem.systemMinFree > 0) evt.mf = mem.systemMinFree
|
|
406
430
|
emit(evt)
|
|
407
431
|
}
|
|
@@ -420,7 +444,14 @@ async function run(): Promise<void> {
|
|
|
420
444
|
// warmup costs (module loads, TLS lazy-init, wifi connection) get
|
|
421
445
|
// folded into the baseline automatically.
|
|
422
446
|
gc()
|
|
423
|
-
|
|
447
|
+
// Destructure to primitives so the live memoryUsage() object isn't held
|
|
448
|
+
// while heapBaseline is captured (it would otherwise be counted in it).
|
|
449
|
+
const {heapUsed: startHeap, systemFree: startFree} = memoryUsage()
|
|
450
|
+
heapBaseline = startHeap
|
|
451
|
+
if (startFree > 0) {
|
|
452
|
+
sysFreeStart = startFree
|
|
453
|
+
sysFreeFloor = startFree
|
|
454
|
+
}
|
|
424
455
|
const timersBefore = activeTimers()
|
|
425
456
|
const pendingBefore = pendingHttpCount()
|
|
426
457
|
|
|
@@ -569,11 +600,23 @@ async function run(): Promise<void> {
|
|
|
569
600
|
}
|
|
570
601
|
|
|
571
602
|
gc()
|
|
572
|
-
|
|
603
|
+
// Destructure to primitives (see baseline capture above) so the live
|
|
604
|
+
// memoryUsage() object isn't held while we build the run_done event.
|
|
605
|
+
const {heapUsed: heapAfter, systemFree: endFree} = memoryUsage()
|
|
606
|
+
if (endFree > 0 && (sysFreeFloor === 0 || endFree < sysFreeFloor)) {
|
|
607
|
+
sysFreeFloor = endFree
|
|
608
|
+
}
|
|
573
609
|
const timersAfter = activeTimers()
|
|
574
610
|
const pendingAfter = pendingHttpCount()
|
|
575
611
|
|
|
576
|
-
|
|
612
|
+
// Per-file system-heap figures. sysFreeFloor is the lowest free heap we
|
|
613
|
+
// sampled (post-gc) this run; sysUsed is how far free heap fell from the
|
|
614
|
+
// baseline to that low. They sum back to the baseline free, so "peak" and
|
|
615
|
+
// "min free" read as one story. Samples are taken between tests, so a
|
|
616
|
+
// transient peak inside a single test can dip below what sysFreeFloor saw.
|
|
617
|
+
const sysUsed = sysFreeStart > sysFreeFloor ? sysFreeStart - sysFreeFloor : 0
|
|
618
|
+
|
|
619
|
+
const doneEvt: TestEvent = {
|
|
577
620
|
e: 6,
|
|
578
621
|
p: passed,
|
|
579
622
|
f: failed,
|
|
@@ -586,7 +629,12 @@ async function run(): Promise<void> {
|
|
|
586
629
|
ta: timersAfter,
|
|
587
630
|
pb: pendingBefore,
|
|
588
631
|
pa: pendingAfter,
|
|
589
|
-
}
|
|
632
|
+
}
|
|
633
|
+
if (sysFreeStart > 0) {
|
|
634
|
+
doneEvt.su = sysUsed
|
|
635
|
+
doneEvt.sf = sysFreeFloor
|
|
636
|
+
}
|
|
637
|
+
emit(doneEvt)
|
|
590
638
|
|
|
591
639
|
// Signal the supervisor (if any) that this file is complete so it can
|
|
592
640
|
// swap in the next runtime. No-op in non-test-harness contexts.
|
package/src/builtins.cpp
CHANGED
|
@@ -32,11 +32,16 @@ static JSModuleDef* mik__deserialize_builtin(JSContext* ctx, const char* name,
|
|
|
32
32
|
JSValue obj = JS_ReadObject(ctx, data, data_size, JS_READ_OBJ_BYTECODE);
|
|
33
33
|
|
|
34
34
|
if (JS_IsException(obj)) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
/* Peek the exception for the log, then re-throw it so the caller
|
|
36
|
+
* can still propagate the real error (e.g. "Maximum call stack
|
|
37
|
+
* size exceeded") instead of a generic "not available" message. */
|
|
38
|
+
JSValue exc = JS_GetException(ctx);
|
|
39
|
+
const char* msg = JS_ToCString(ctx, exc);
|
|
40
|
+
platform->log(MIK_LOG_ERROR, "mikrojs",
|
|
41
|
+
"Failed to deserialize bytecode for builtin '%s' (%u bytes): %s", name,
|
|
42
|
+
data_size, msg ? msg : "unknown error");
|
|
43
|
+
if (msg) JS_FreeCString(ctx, msg);
|
|
44
|
+
JS_Throw(ctx, exc);
|
|
40
45
|
return NULL;
|
|
41
46
|
}
|
|
42
47
|
|
package/src/mik_abort.cpp
CHANGED
|
@@ -1,110 +1,32 @@
|
|
|
1
1
|
#include <quickjs.h>
|
|
2
2
|
|
|
3
|
+
#include "mikrojs/private.h"
|
|
3
4
|
#include "mikrojs/utils.h"
|
|
4
5
|
|
|
5
|
-
/*
|
|
6
|
+
/* AbortController / AbortSignal globals.
|
|
6
7
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* The implementation lives in runtime/abort/abort.ts and ships as
|
|
9
|
+
* precompiled bytecode in the builtins table ("mikro/abort").
|
|
10
|
+
* Evaluating that module installs AbortController, AbortSignal,
|
|
11
|
+
* AbortError, and TimeoutError on globalThis.
|
|
10
12
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* AbortSignal.any(signals) — aborts when any input signal aborts
|
|
17
|
-
* AbortController: signal, abort(reason)
|
|
18
|
-
*
|
|
19
|
-
* Lazy-initialized: the JS class hierarchy is compiled and installed
|
|
20
|
-
* on first access to any of the four globals (AbortController,
|
|
21
|
-
* AbortSignal, AbortError, TimeoutError).
|
|
13
|
+
* Lazy-initialized: this file installs lazy getters for the four
|
|
14
|
+
* globals; the first access evaluates the bytecode module. Bytecode
|
|
15
|
+
* (rather than JS source evaluated at runtime) keeps the QuickJS
|
|
16
|
+
* parser out of the install path — the parse peak pushed low-memory
|
|
17
|
+
* chips into OOM when the heap was already near full.
|
|
22
18
|
*/
|
|
23
19
|
|
|
24
|
-
|
|
25
|
-
(function(g) {
|
|
26
|
-
"use strict";
|
|
27
|
-
|
|
28
|
-
class AbortError extends Error {
|
|
29
|
-
constructor(message) { super(message || "The operation was aborted"); }
|
|
30
|
-
get name() { return "AbortError"; }
|
|
31
|
-
}
|
|
32
|
-
class TimeoutError extends Error {
|
|
33
|
-
constructor(message) { super(message || "The operation timed out"); }
|
|
34
|
-
get name() { return "TimeoutError"; }
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const _a = Symbol(), _r = Symbol(), _l = Symbol(), _s = Symbol();
|
|
38
|
-
const ABORT_REASON = () => new AbortError();
|
|
39
|
-
|
|
40
|
-
function doAbort(signal, reason) {
|
|
41
|
-
if (signal[_a]) return;
|
|
42
|
-
signal[_a] = true;
|
|
43
|
-
signal[_r] = reason;
|
|
44
|
-
if (typeof signal.onabort === "function") signal.onabort();
|
|
45
|
-
for (const fn of signal[_l]) fn();
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
class AbortSignal {
|
|
49
|
-
constructor() {
|
|
50
|
-
this[_a] = false;
|
|
51
|
-
this[_r] = undefined;
|
|
52
|
-
this[_l] = [];
|
|
53
|
-
this.onabort = null;
|
|
54
|
-
}
|
|
55
|
-
get aborted() { return this[_a]; }
|
|
56
|
-
get reason() { return this[_r]; }
|
|
57
|
-
throwIfAborted() { if (this[_a]) throw this[_r]; }
|
|
58
|
-
addEventListener(type, fn) {
|
|
59
|
-
if (type === "abort" && typeof fn === "function") this[_l].push(fn);
|
|
60
|
-
}
|
|
61
|
-
removeEventListener(type, fn) {
|
|
62
|
-
if (type === "abort") this[_l] = this[_l].filter(f => f !== fn);
|
|
63
|
-
}
|
|
64
|
-
static abort(reason) {
|
|
65
|
-
const s = new AbortSignal();
|
|
66
|
-
doAbort(s, reason !== undefined ? reason : ABORT_REASON());
|
|
67
|
-
return s;
|
|
68
|
-
}
|
|
69
|
-
static timeout(ms) {
|
|
70
|
-
const s = new AbortSignal();
|
|
71
|
-
setTimeout(() => doAbort(s, new TimeoutError()), ms);
|
|
72
|
-
return s;
|
|
73
|
-
}
|
|
74
|
-
static any(signals) {
|
|
75
|
-
const s = new AbortSignal();
|
|
76
|
-
for (const i of signals) {
|
|
77
|
-
if (i.aborted) { doAbort(s, i.reason); return s; }
|
|
78
|
-
}
|
|
79
|
-
for (const i of signals) {
|
|
80
|
-
i.addEventListener("abort", () => doAbort(s, i.reason));
|
|
81
|
-
}
|
|
82
|
-
return s;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
class AbortController {
|
|
87
|
-
constructor() { this[_s] = new AbortSignal(); }
|
|
88
|
-
get signal() { return this[_s]; }
|
|
89
|
-
abort(reason) { doAbort(this[_s], reason !== undefined ? reason : ABORT_REASON()); }
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
g.AbortError = AbortError;
|
|
93
|
-
g.TimeoutError = TimeoutError;
|
|
94
|
-
g.AbortSignal = AbortSignal;
|
|
95
|
-
g.AbortController = AbortController;
|
|
96
|
-
})
|
|
97
|
-
)JS";
|
|
98
|
-
|
|
99
|
-
/* Names of the globals this module installs */
|
|
20
|
+
/* Names of the globals the abort module installs */
|
|
100
21
|
static const char* const abort_global_names[] = {"AbortController", "AbortSignal", "AbortError",
|
|
101
22
|
"TimeoutError"};
|
|
102
23
|
static constexpr int ABORT_GLOBAL_COUNT = 4;
|
|
103
24
|
|
|
104
25
|
/**
|
|
105
26
|
* Lazy getter: on first access to any of the four globals, delete all
|
|
106
|
-
* lazy getters, evaluate the
|
|
107
|
-
* the requested one.
|
|
27
|
+
* lazy getters, evaluate the bytecode module to install real values,
|
|
28
|
+
* then return the requested one. The magic parameter identifies which
|
|
29
|
+
* global.
|
|
108
30
|
*
|
|
109
31
|
* Signature must match JS_CFUNC_getter_magic: (ctx, this_val, magic).
|
|
110
32
|
*/
|
|
@@ -112,29 +34,40 @@ static JSValue mik__abort_lazy_get(JSContext* ctx, JSValue this_val, int magic)
|
|
|
112
34
|
|
|
113
35
|
JSValue global_obj = JS_GetGlobalObject(ctx);
|
|
114
36
|
|
|
115
|
-
/* Remove all four lazy getters so the
|
|
37
|
+
/* Remove all four lazy getters so the module's globalThis
|
|
38
|
+
* assignments define plain properties */
|
|
116
39
|
for (int i = 0; i < ABORT_GLOBAL_COUNT; i++) {
|
|
117
40
|
JSAtom prop = JS_NewAtom(ctx, abort_global_names[i]);
|
|
118
41
|
JS_DeleteProperty(ctx, global_obj, prop, 0);
|
|
119
42
|
JS_FreeAtom(ctx, prop);
|
|
120
43
|
}
|
|
121
44
|
|
|
122
|
-
/* Evaluate
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
45
|
+
/* Evaluate the precompiled module; its side effect installs the
|
|
46
|
+
* globals. The module is synchronous, so the returned promise is
|
|
47
|
+
* already settled. */
|
|
48
|
+
JSModuleDef* m = mik__load_builtin(ctx, "mikro/abort");
|
|
49
|
+
if (m == NULL) {
|
|
50
|
+
/* A table miss returns NULL with no pending exception (only
|
|
51
|
+
* deserialize failures throw); without this guard the getter
|
|
52
|
+
* would surface a bare 'null'. Mirrors the module loader's
|
|
53
|
+
* missing-builtin fallback. */
|
|
54
|
+
if (!JS_HasException(ctx)) {
|
|
55
|
+
JS_ThrowReferenceError(ctx,
|
|
56
|
+
"Builtin module 'mikro/abort' is not available in this build");
|
|
57
|
+
}
|
|
128
58
|
JS_FreeValue(ctx, global_obj);
|
|
129
|
-
return
|
|
59
|
+
return JS_EXCEPTION;
|
|
130
60
|
}
|
|
131
|
-
JSValue ret =
|
|
132
|
-
JS_FreeValue(ctx, wrapper);
|
|
61
|
+
JSValue ret = JS_EvalFunction(ctx, JS_DupValue(ctx, JS_MKPTR(JS_TAG_MODULE, m)));
|
|
133
62
|
if (JS_IsException(ret)) {
|
|
134
|
-
JSValue exc = JS_GetException(ctx);
|
|
135
|
-
JS_FreeValue(ctx, exc);
|
|
136
63
|
JS_FreeValue(ctx, global_obj);
|
|
137
|
-
return
|
|
64
|
+
return JS_EXCEPTION;
|
|
65
|
+
}
|
|
66
|
+
if (JS_PromiseState(ctx, ret) == JS_PROMISE_REJECTED) {
|
|
67
|
+
JSValue err = JS_PromiseResult(ctx, ret);
|
|
68
|
+
JS_FreeValue(ctx, ret);
|
|
69
|
+
JS_FreeValue(ctx, global_obj);
|
|
70
|
+
return JS_Throw(ctx, err);
|
|
138
71
|
}
|
|
139
72
|
JS_FreeValue(ctx, ret);
|
|
140
73
|
|
|
@@ -145,7 +78,7 @@ static JSValue mik__abort_lazy_get(JSContext* ctx, JSValue this_val, int magic)
|
|
|
145
78
|
}
|
|
146
79
|
|
|
147
80
|
void mik__abort_init(JSContext* ctx, JSValue global_obj) {
|
|
148
|
-
/* Install lazy getters instead of eagerly evaluating the
|
|
81
|
+
/* Install lazy getters instead of eagerly evaluating the module.
|
|
149
82
|
* Each getter shares the same C function, distinguished by magic. */
|
|
150
83
|
for (int i = 0; i < ABORT_GLOBAL_COUNT; i++) {
|
|
151
84
|
JSAtom prop = JS_NewAtom(ctx, abort_global_names[i]);
|
package/src/mik_console.cpp
CHANGED
|
@@ -236,31 +236,32 @@ static JSValue mik__console_error_warn(JSContext* ctx, JSValue this_val, int arg
|
|
|
236
236
|
|
|
237
237
|
/* ── Uncaught error reporting ─────────────────────────────────────── */
|
|
238
238
|
|
|
239
|
-
/* Dedup:
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
239
|
+
/* Dedup across report paths: one error can reach this function more than
|
|
240
|
+
* once for a single failure. A failed dynamic import leaves two never-handled
|
|
241
|
+
* promises with the SAME reason (QuickJS runs a module body as an async
|
|
242
|
+
* function and reads its rejected result by value, plus the rejection
|
|
243
|
+
* propagated up the await chain), and at the REPL the throw path reports an
|
|
244
|
+
* error the deferred flush then sees again. Mark the error object the first
|
|
245
|
+
* time it is reported and skip it afterwards. Keying on object identity is
|
|
246
|
+
* exact and time-independent; a recycled address belongs to a fresh object
|
|
247
|
+
* with no marker, so distinct errors are never suppressed. Primitive
|
|
248
|
+
* rejections (e.g. a null OOM rejection) can't hold a marker and are always
|
|
249
|
+
* reported. Returns true if it reported, false if it was a duplicate. */
|
|
250
|
+
bool mik__report_uncaught(JSContext* ctx, JSValue exc, bool in_promise) {
|
|
250
251
|
if (JS_IsObject(exc)) {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
252
|
+
JSAtom marker = JS_NewAtom(ctx, "\xff" "mik_reported");
|
|
253
|
+
if (marker != JS_ATOM_NULL) {
|
|
254
|
+
if (JS_HasProperty(ctx, exc, marker) > 0) {
|
|
255
|
+
JS_FreeAtom(ctx, marker);
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
/* Non-enumerable, non-writable, non-configurable so it stays
|
|
259
|
+
* invisible to user code. Best-effort: JS_* calls return error
|
|
260
|
+
* codes (no C++ throw), so under OOM we just report without
|
|
261
|
+
* dedup rather than abort. */
|
|
262
|
+
JS_DefinePropertyValue(ctx, exc, marker, JS_TRUE, 0);
|
|
263
|
+
JS_FreeAtom(ctx, marker);
|
|
259
264
|
}
|
|
260
|
-
last_reported_ptr = ptr;
|
|
261
|
-
last_reported_time = now;
|
|
262
|
-
} else {
|
|
263
|
-
last_reported_ptr = nullptr;
|
|
264
265
|
}
|
|
265
266
|
|
|
266
267
|
/* Fixed-size stack buffer so this function never allocates from the
|
|
@@ -360,11 +361,12 @@ void mik__report_uncaught(JSContext* ctx, JSValue exc, bool in_promise) {
|
|
|
360
361
|
|
|
361
362
|
if (mik__repl_is_protocol_mode()) {
|
|
362
363
|
mik__repl_proto_send_output(MIK_MSG_ERROR, buf, pos);
|
|
363
|
-
return;
|
|
364
|
+
return true;
|
|
364
365
|
}
|
|
365
366
|
|
|
366
367
|
append_cstr("\r\n");
|
|
367
368
|
MIK_GetPlatform()->stderr_write(buf, pos);
|
|
369
|
+
return true;
|
|
368
370
|
}
|
|
369
371
|
|
|
370
372
|
/* ── Test emit ────────────────────────────────────────────────────── */
|
package/src/mik_inspect.cpp
CHANGED
|
@@ -805,7 +805,7 @@ std::string mik_inspect(JSContext* ctx, JSValue value, int depth, bool colors, b
|
|
|
805
805
|
return inspect_value(ctx, value, opts, depth);
|
|
806
806
|
}
|
|
807
807
|
|
|
808
|
-
/* ── JS-callable inspect function (for
|
|
808
|
+
/* ── JS-callable inspect function (for mikro/inspect module) ───── */
|
|
809
809
|
|
|
810
810
|
static JSValue mik__inspect_js(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
|
|
811
811
|
if (argc < 1) return JS_NewString(ctx, "undefined");
|
|
@@ -836,14 +836,14 @@ static JSValue mik__inspect_js(JSContext* ctx, JSValue this_val, int argc, JSVal
|
|
|
836
836
|
return JS_NewString(ctx, result.c_str());
|
|
837
837
|
}
|
|
838
838
|
|
|
839
|
-
/* Module init for '
|
|
839
|
+
/* Module init for 'mikro/inspect' native C module */
|
|
840
840
|
int mik__inspect_module_init(JSContext* ctx, JSModuleDef* m) {
|
|
841
841
|
return JS_SetModuleExport(ctx, m, "inspect",
|
|
842
842
|
JS_NewCFunction(ctx, mik__inspect_js, "inspect", 1));
|
|
843
843
|
}
|
|
844
844
|
|
|
845
845
|
void mik__inspect_register(JSContext* ctx) {
|
|
846
|
-
JSModuleDef* m = JS_NewCModule(ctx, "
|
|
846
|
+
JSModuleDef* m = JS_NewCModule(ctx, "mikro/inspect", mik__inspect_module_init);
|
|
847
847
|
if (m) {
|
|
848
848
|
JS_AddModuleExport(ctx, m, "inspect");
|
|
849
849
|
}
|
package/src/mik_repl.cpp
CHANGED
|
@@ -263,6 +263,10 @@ static JSValue repl_eval_and_pump(JSContext* ctx, const char* code, size_t len)
|
|
|
263
263
|
return pr;
|
|
264
264
|
}
|
|
265
265
|
if (state == JS_PROMISE_REJECTED) {
|
|
266
|
+
/* This promise's rejection is about to be reported via the throw
|
|
267
|
+
* path below; drop it from the deferred unhandled-rejection queue
|
|
268
|
+
* so the end-of-turn flush doesn't report it a second time. */
|
|
269
|
+
mik__forget_rejection(ctx, result);
|
|
266
270
|
JSValue reason = JS_PromiseResult(ctx, result);
|
|
267
271
|
JS_FreeValue(ctx, result);
|
|
268
272
|
JS_Throw(ctx, reason);
|
package/src/mik_sys.cpp
CHANGED
|
@@ -260,7 +260,17 @@ void mik__sys_api_init(JSContext* ctx, JSValue ns) {
|
|
|
260
260
|
JS_SetPropertyStr(ctx, ns, "board", mik__sys_board(ctx));
|
|
261
261
|
JS_SetPropertyStr(ctx, ns, "firmware", mik__sys_firmware(ctx));
|
|
262
262
|
|
|
263
|
-
|
|
263
|
+
/* Guard the platform function POINTERS, not just their results: a
|
|
264
|
+
* platform that omits an optional hook leaves a NULL pointer, and
|
|
265
|
+
* calling it is a segfault with no diagnostics (the sim was down for
|
|
266
|
+
* weeks because platform_node lacked get_reset_reason). */
|
|
267
|
+
const MIKPlatform* platform = MIK_GetPlatform();
|
|
268
|
+
const char* device_id = platform->get_device_id ? platform->get_device_id() : NULL;
|
|
264
269
|
JS_SetPropertyStr(ctx, ns, "deviceId",
|
|
265
270
|
device_id ? JS_NewString(ctx, device_id) : JS_UNDEFINED);
|
|
271
|
+
|
|
272
|
+
const char* reset_reason =
|
|
273
|
+
platform->get_reset_reason ? platform->get_reset_reason() : NULL;
|
|
274
|
+
JS_SetPropertyStr(ctx, ns, "resetReason",
|
|
275
|
+
JS_NewString(ctx, reset_reason ? reset_reason : "unknown"));
|
|
266
276
|
}
|
package/src/mikrojs.cpp
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
#include <quickjs.h>
|
|
4
4
|
|
|
5
|
+
#include <atomic>
|
|
5
6
|
#include <cerrno>
|
|
6
7
|
#include <cstdio>
|
|
7
8
|
#include <fcntl.h>
|
|
@@ -91,7 +92,7 @@ static void mik__add_exports(JSContext* ctx, JSModuleDef* m, const char* const*
|
|
|
91
92
|
static const char* const sys_exports[] = {
|
|
92
93
|
"evalScript", "memoryUsage", "jsMemoryUsage", "gc", "setTime",
|
|
93
94
|
"uptime", "restart", "version", "board", "firmware",
|
|
94
|
-
"deviceId", "
|
|
95
|
+
"deviceId", "resetReason", "activeTimers", "unloadNamespace", "isUnloadableNamespace"};
|
|
95
96
|
|
|
96
97
|
static int mik__sys_module_init(JSContext* ctx, JSModuleDef* m) {
|
|
97
98
|
JSValue ns = JS_NewObjectProto(ctx, JS_NULL);
|
|
@@ -111,21 +112,19 @@ static int mik__stdio_module_init(JSContext* ctx, JSModuleDef* m) {
|
|
|
111
112
|
return 0;
|
|
112
113
|
}
|
|
113
114
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
|
|
115
|
+
/* Host promise-rejection tracker. Rather than report eagerly the instant a
|
|
116
|
+
* promise rejects, defer the decision to the end-of-turn flush
|
|
117
|
+
* (mik__flush_unhandled_rejections): a promise that rejects without a
|
|
118
|
+
* handler is queued, and a promise that later gets a handler is removed
|
|
119
|
+
* from the queue. This mirrors the HTML/WinterCG unhandledrejection
|
|
120
|
+
* algorithm. It matters because some promises are born rejected before a
|
|
121
|
+
* handler can be attached. The clearest example is a module's evaluation
|
|
122
|
+
* promise, which the dynamic-import loader rejects (when the module body
|
|
123
|
+
* throws) and only then attaches its .then to. Reporting eagerly surfaced
|
|
124
|
+
* that transient rejection as a spurious second "Uncaught (in promise)".
|
|
125
|
+
*
|
|
126
|
+
* This is the same add-on-reject / remove-on-handle / report-after-drain
|
|
127
|
+
* pattern quickjs-libc's js_std_promise_rejection_tracker uses. */
|
|
129
128
|
static void mik__promise_rejection_tracker(JSContext* ctx, JSValue promise, JSValue reason,
|
|
130
129
|
bool is_handled, void* opaque) {
|
|
131
130
|
MIKRuntime* mik_rt = MIK_GetRuntime(ctx);
|
|
@@ -135,68 +134,109 @@ static void mik__promise_rejection_tracker(JSContext* ctx, JSValue promise, JSVa
|
|
|
135
134
|
return;
|
|
136
135
|
}
|
|
137
136
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
* Don't JS_Throw here — we're inside a QuickJS callback
|
|
151
|
-
* during promise resolution; throwing would corrupt engine
|
|
152
|
-
* state and cause mik__execute_jobs to dump the error a
|
|
153
|
-
* second time. MIK_Stop gates on whether we're inside an
|
|
154
|
-
* interactive REPL eval (skipping both the stop request and
|
|
155
|
-
* the restart) so a typo at the prompt doesn't reboot the
|
|
156
|
-
* device. */
|
|
157
|
-
if (mik_rt->error_handler_fn) {
|
|
158
|
-
mik_rt->error_handler_fn(ctx, reason, mik_rt->error_handler_opaque);
|
|
137
|
+
auto& pending = mik_rt->pending_rejections;
|
|
138
|
+
void* key = JS_VALUE_GET_PTR(promise);
|
|
139
|
+
|
|
140
|
+
if (is_handled) {
|
|
141
|
+
/* A handler was attached to a previously-rejected promise: cancel its
|
|
142
|
+
* pending report. */
|
|
143
|
+
for (size_t i = 0; i < pending.size(); i++) {
|
|
144
|
+
if (JS_VALUE_GET_PTR(pending[i].promise) == key) {
|
|
145
|
+
JS_FreeValue(ctx, pending[i].promise);
|
|
146
|
+
JS_FreeValue(ctx, pending[i].reason);
|
|
147
|
+
pending.erase(pending.begin() + i);
|
|
148
|
+
return;
|
|
159
149
|
}
|
|
160
|
-
MIK_Stop(mik_rt);
|
|
161
|
-
return;
|
|
162
150
|
}
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/* Rejected with no handler (yet): queue it. Hold references to both the
|
|
155
|
+
* promise and the reason so they stay alive until the end-of-turn check. */
|
|
156
|
+
pending.push_back({JS_DupValue(ctx, promise), JS_DupValue(ctx, reason)});
|
|
157
|
+
}
|
|
163
158
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
159
|
+
/* Drop a promise from the pending-rejection queue without reporting it. The
|
|
160
|
+
* REPL eval pump calls this for the result promise it polls and reports via
|
|
161
|
+
* the throw path, so the end-of-turn flush doesn't report it a second time. */
|
|
162
|
+
void mik__forget_rejection(JSContext* ctx, JSValue promise) {
|
|
163
|
+
MIKRuntime* mik_rt = MIK_GetRuntime(ctx);
|
|
164
|
+
if (!mik_rt) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
auto& pending = mik_rt->pending_rejections;
|
|
168
|
+
void* key = JS_VALUE_GET_PTR(promise);
|
|
169
|
+
for (size_t i = 0; i < pending.size(); i++) {
|
|
170
|
+
if (JS_VALUE_GET_PTR(pending[i].promise) == key) {
|
|
171
|
+
JS_FreeValue(ctx, pending[i].promise);
|
|
172
|
+
JS_FreeValue(ctx, pending[i].reason);
|
|
173
|
+
pending.erase(pending.begin() + i);
|
|
175
174
|
return;
|
|
176
175
|
}
|
|
177
|
-
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
178
|
|
|
179
|
-
|
|
180
|
-
|
|
179
|
+
/* End-of-turn unhandled-rejection check. Any promise still queued after a
|
|
180
|
+
* microtask drain rejected and never got a handler: report it, notify the
|
|
181
|
+
* host error handler, and halt. */
|
|
182
|
+
void mik__flush_unhandled_rejections(JSContext* ctx) {
|
|
183
|
+
MIKRuntime* mik_rt = MIK_GetRuntime(ctx);
|
|
184
|
+
if (!mik_rt || mik_rt->freeing || mik_rt->pending_rejections.empty()) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
/* During interactive REPL eval, the eval pump polls its result promise
|
|
188
|
+
* and reports rejections via the throw path; leave the queue alone until
|
|
189
|
+
* the eval finishes so we don't fight that model. */
|
|
190
|
+
if (mik__repl_is_evaluating()) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
181
193
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
194
|
+
/* Move the queue aside before reporting. Reporting doesn't enqueue JS
|
|
195
|
+
* jobs, but this keeps the runtime's vector in a clean state regardless. */
|
|
196
|
+
std::vector<MIKRejectedPromise> rejected;
|
|
197
|
+
rejected.swap(mik_rt->pending_rejections);
|
|
198
|
+
|
|
199
|
+
/* One failed dynamic import leaves two never-handled promises with the
|
|
200
|
+
* SAME reason: the module's internal sync-evaluation promise (QuickJS runs
|
|
201
|
+
* a module body as an async function and reads its rejected result by
|
|
202
|
+
* value, never attaching a handler) and the promise the error propagates
|
|
203
|
+
* to up the await chain. mik__report_uncaught dedups by reason-object
|
|
204
|
+
* identity, so the second is a no-op; only act on a fresh report so the
|
|
205
|
+
* host bridge and the halt don't fire twice. (quickjs-libc reports every
|
|
206
|
+
* entry; we collapse same-error duplicates.) */
|
|
207
|
+
for (const MIKRejectedPromise& rp : rejected) {
|
|
208
|
+
if (mik__report_uncaught(ctx, rp.reason, true)) {
|
|
209
|
+
/* Notify the error handler (e.g. host bridge) directly. Don't
|
|
210
|
+
* JS_Throw here: we're between jobs, and throwing would be picked
|
|
211
|
+
* up as a spurious pending exception. */
|
|
212
|
+
if (mik_rt->error_handler_fn) {
|
|
213
|
+
mik_rt->error_handler_fn(ctx, rp.reason, mik_rt->error_handler_opaque);
|
|
196
214
|
}
|
|
215
|
+
MIK_Stop(mik_rt);
|
|
197
216
|
}
|
|
217
|
+
JS_FreeValue(ctx, rp.promise);
|
|
218
|
+
JS_FreeValue(ctx, rp.reason);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
198
221
|
|
|
199
|
-
|
|
222
|
+
/* QuickJS only runs the cycle collector automatically once malloc_size
|
|
223
|
+
* crosses the runtime's GC threshold (256 KB by default), and js_malloc_rt
|
|
224
|
+
* fails against the memory limit without attempting a GC first. On devices
|
|
225
|
+
* where mem_limit is below the default threshold the collector would never
|
|
226
|
+
* fire before allocations start failing, so cyclic garbage (promise chains,
|
|
227
|
+
* closures) accumulates straight into OOM with collectable memory still
|
|
228
|
+
* alive. Cap the threshold below the limit so collection happens first.
|
|
229
|
+
* QuickJS raises the threshold to 1.5x the live size after every GC pass,
|
|
230
|
+
* which can push it back above the limit, so MIK_Loop re-applies the cap
|
|
231
|
+
* each turn. */
|
|
232
|
+
static void mik__clamp_gc_threshold(MIKRuntime* mik_rt) {
|
|
233
|
+
if (mik_rt->options.mem_limit <= 0) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
size_t limit = (size_t)mik_rt->options.mem_limit;
|
|
237
|
+
size_t cap = limit - limit / 8;
|
|
238
|
+
if (JS_GetGCThreshold(mik_rt->rt) > cap) {
|
|
239
|
+
JS_SetGCThreshold(mik_rt->rt, cap);
|
|
200
240
|
}
|
|
201
241
|
}
|
|
202
242
|
|
|
@@ -289,12 +329,25 @@ MIKRuntime* MIK_NewRuntimeInternal(MIKRunOptions* options) {
|
|
|
289
329
|
* exhaust the heap. Overridden via MIK_SetFSReadMax. */
|
|
290
330
|
mik_rt->fs_read_max = 65536;
|
|
291
331
|
|
|
332
|
+
/* Reserve the unhandled-rejection queue up-front so the rejection tracker
|
|
333
|
+
* never has to grow a std::vector on the rejection path. That path fires
|
|
334
|
+
* for a QuickJS null-OOM rejection, and a reallocation there could throw
|
|
335
|
+
* std::bad_alloc — which, with CONFIG_COMPILER_CXX_EXCEPTIONS=n on ESP32,
|
|
336
|
+
* aborts and masks the very OOM we're trying to report. Four slots covers
|
|
337
|
+
* any realistic per-turn rejection count; the runtime halts on the first
|
|
338
|
+
* one anyway. */
|
|
339
|
+
mik_rt->pending_rejections.reserve(4);
|
|
340
|
+
|
|
292
341
|
/* Check for duplicate native module registrations (global registry) */
|
|
293
342
|
mik__check_module_collisions();
|
|
294
343
|
|
|
295
344
|
/* Set memory limit */
|
|
296
345
|
JS_SetMemoryLimit(rt, options->mem_limit);
|
|
297
346
|
|
|
347
|
+
/* Make sure the cycle collector fires before the memory limit does
|
|
348
|
+
* (see mik__clamp_gc_threshold). */
|
|
349
|
+
mik__clamp_gc_threshold(mik_rt);
|
|
350
|
+
|
|
298
351
|
/* Set stack size */
|
|
299
352
|
JS_SetMaxStackSize(rt, options->stack_size);
|
|
300
353
|
/* loader for ES modules */
|
|
@@ -337,11 +390,6 @@ MIKRuntime* MIK_NewRuntimeInternal(MIKRunOptions* options) {
|
|
|
337
390
|
mik__text_encoding_init(ctx, global_obj);
|
|
338
391
|
mik__abort_init(ctx, global_obj);
|
|
339
392
|
|
|
340
|
-
/* Load some builtin references for easy access */
|
|
341
|
-
mik_rt->builtins.dispatch_event_func = JS_GetPropertyStr(ctx, global_obj, "dispatchEvent");
|
|
342
|
-
mik_rt->builtins.promise_event_ctor =
|
|
343
|
-
JS_GetPropertyStr(mik_rt->ctx, global_obj, "PromiseRejectionEvent");
|
|
344
|
-
|
|
345
393
|
/* Timers */
|
|
346
394
|
mik_rt->timers = MIK_NewTimerRegistry();
|
|
347
395
|
mik__timers_init(ctx, global_obj);
|
|
@@ -369,6 +417,13 @@ void MIK_FreeRuntime(MIKRuntime* mik_rt) {
|
|
|
369
417
|
mik_rt->stdin_state.on_data = JS_UNDEFINED;
|
|
370
418
|
}
|
|
371
419
|
|
|
420
|
+
/* Release any still-pending unhandled-rejection entries. */
|
|
421
|
+
for (const MIKRejectedPromise& rp : mik_rt->pending_rejections) {
|
|
422
|
+
JS_FreeValue(mik_rt->ctx, rp.promise);
|
|
423
|
+
JS_FreeValue(mik_rt->ctx, rp.reason);
|
|
424
|
+
}
|
|
425
|
+
mik_rt->pending_rejections.clear();
|
|
426
|
+
|
|
372
427
|
/* Destroy registered loop consumers */
|
|
373
428
|
for (const auto& consumer : mik_rt->loop_consumers) {
|
|
374
429
|
if (consumer.destroy_fn) {
|
|
@@ -384,10 +439,6 @@ void MIK_FreeRuntime(MIKRuntime* mik_rt) {
|
|
|
384
439
|
/* Destroy the JS engine. */
|
|
385
440
|
JS_FreeValue(mik_rt->ctx, mik_rt->env_obj);
|
|
386
441
|
mik_rt->env_obj = JS_UNDEFINED;
|
|
387
|
-
JS_FreeValue(mik_rt->ctx, mik_rt->builtins.dispatch_event_func);
|
|
388
|
-
mik_rt->builtins.dispatch_event_func = JS_UNDEFINED;
|
|
389
|
-
JS_FreeValue(mik_rt->ctx, mik_rt->builtins.promise_event_ctor);
|
|
390
|
-
mik_rt->builtins.promise_event_ctor = JS_UNDEFINED;
|
|
391
442
|
JS_FreeValue(mik_rt->ctx, mik_rt->result_ok_void_singleton);
|
|
392
443
|
mik_rt->result_ok_void_singleton = JS_UNDEFINED;
|
|
393
444
|
JS_FreeValue(mik_rt->ctx, mik_rt->result_proto);
|
|
@@ -510,9 +561,43 @@ static void mik__check_module_collisions(void) {
|
|
|
510
561
|
}
|
|
511
562
|
}
|
|
512
563
|
|
|
564
|
+
/* A chain of already-settled promises drains as one uninterrupted storm of
|
|
565
|
+
* jobs, and the test supervisor strings whole files of such chains together
|
|
566
|
+
* with runtime recycles in between. On ESP32 the main task runs above the
|
|
567
|
+
* idle task, so a multi-second stretch like that starves idle: the task
|
|
568
|
+
* watchdog (5 s, watching IDLE0) prints a register dump, and FreeRTOS
|
|
569
|
+
* housekeeping (freeing TCBs of exited tasks, e.g. per-request HTTP tasks)
|
|
570
|
+
* stalls. Force one platform yield per second of continuous job execution
|
|
571
|
+
* so idle always gets a slice. The timestamp is global on purpose: the
|
|
572
|
+
* starvation accumulates across MIK_Loop calls and across runtime recycles.
|
|
573
|
+
* Costs at most one tick (~10 ms) per second of busy work. */
|
|
574
|
+
#define MIK__YIELD_INTERVAL_US (1000 * 1000)
|
|
575
|
+
|
|
576
|
+
/* Atomic because the Node addon can pump two runtimes' loops from
|
|
577
|
+
* different worker threads; relaxed ordering is enough — a missed or
|
|
578
|
+
* extra yield is harmless, a torn 64-bit write is not. */
|
|
579
|
+
static std::atomic<int64_t> mik__last_yield_us{0};
|
|
580
|
+
|
|
581
|
+
static void mik__maybe_yield(void) {
|
|
582
|
+
const MIKPlatform* platform = MIK_GetPlatform();
|
|
583
|
+
int64_t now = platform->get_boot_us();
|
|
584
|
+
int64_t last = mik__last_yield_us.load(std::memory_order_relaxed);
|
|
585
|
+
/* now < last happens when the boot clock resets (deep sleep) or a test
|
|
586
|
+
* swaps in a platform with a different clock; restart the interval. */
|
|
587
|
+
if (last == 0 || now < last) {
|
|
588
|
+
mik__last_yield_us.store(now, std::memory_order_relaxed);
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
if (now - last >= MIK__YIELD_INTERVAL_US) {
|
|
592
|
+
platform->yield();
|
|
593
|
+
mik__last_yield_us.store(platform->get_boot_us(), std::memory_order_relaxed);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
513
597
|
void mik__execute_jobs(JSContext* ctx) {
|
|
514
598
|
JSContext* ctx1;
|
|
515
599
|
int err;
|
|
600
|
+
bool ran_any = false;
|
|
516
601
|
|
|
517
602
|
/* execute the pending jobs */
|
|
518
603
|
for (;;) {
|
|
@@ -529,7 +614,31 @@ void mik__execute_jobs(JSContext* ctx) {
|
|
|
529
614
|
}
|
|
530
615
|
break;
|
|
531
616
|
}
|
|
617
|
+
ran_any = true;
|
|
618
|
+
/* A GC pass inside this job raises the threshold to 1.5x live size,
|
|
619
|
+
* which lands above mem_limit whenever live >= ~2/3 of the limit —
|
|
620
|
+
* silently disabling cycle GC for the rest of the storm. Re-clamp
|
|
621
|
+
* between jobs so collection keeps firing; the check is a field
|
|
622
|
+
* read + compare when no GC ran. */
|
|
623
|
+
MIKRuntime* job_rt = MIK_GetRuntime(ctx1);
|
|
624
|
+
if (job_rt) {
|
|
625
|
+
mik__clamp_gc_threshold(job_rt);
|
|
626
|
+
}
|
|
627
|
+
mik__maybe_yield();
|
|
532
628
|
}
|
|
629
|
+
|
|
630
|
+
/* The yield interval measures continuous BUSY work. An empty drain
|
|
631
|
+
* means the loop is idling (the idle task is getting time between
|
|
632
|
+
* turns), so re-arm the interval — otherwise the first job after a
|
|
633
|
+
* quiet period pays a spurious ~10 ms yield for wall-clock time spent
|
|
634
|
+
* doing nothing. */
|
|
635
|
+
if (!ran_any) {
|
|
636
|
+
mik__last_yield_us.store(MIK_GetPlatform()->get_boot_us(), std::memory_order_relaxed);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/* Microtask checkpoint: every reject/handle transition for this turn has
|
|
640
|
+
* now run, so whatever is still queued is a genuine unhandled rejection. */
|
|
641
|
+
mik__flush_unhandled_rejections(ctx);
|
|
533
642
|
}
|
|
534
643
|
|
|
535
644
|
/* main loop which calls the user JS callbacks */
|
|
@@ -576,7 +685,18 @@ int MIK_Loop(MIKRuntime* mik_rt) {
|
|
|
576
685
|
}
|
|
577
686
|
|
|
578
687
|
mik__execute_jobs(mik_rt->ctx);
|
|
579
|
-
|
|
688
|
+
|
|
689
|
+
/* Backstop for GC passes outside the job drain (timer callbacks, loop
|
|
690
|
+
* consumers, top-level eval): a pass there may have raised the
|
|
691
|
+
* threshold above the memory limit; pull it back so the next turn's
|
|
692
|
+
* collection still fires before allocations fail. Within the job
|
|
693
|
+
* drain, mik__execute_jobs re-clamps after every job. */
|
|
694
|
+
mik__clamp_gc_threshold(mik_rt);
|
|
695
|
+
|
|
696
|
+
/* The end-of-turn unhandled-rejection flush inside mik__execute_jobs may
|
|
697
|
+
* have requested a stop; surface it this iteration rather than making the
|
|
698
|
+
* caller loop once more to notice. */
|
|
699
|
+
return mik_rt->stop_requested ? 1 : 0;
|
|
580
700
|
}
|
|
581
701
|
|
|
582
702
|
void MIK_SetConfig(MIKRuntime* mik_rt, const MIKConfig* config) {
|
|
@@ -961,6 +1081,13 @@ JSValue MIK_EvalModule(JSContext* ctx, const char* filename, bool is_main) {
|
|
|
961
1081
|
}
|
|
962
1082
|
|
|
963
1083
|
int MIK_RunEntry(MIKRuntime* mik_rt, const char* entry) {
|
|
1084
|
+
return MIK_RunEntryErr(mik_rt, entry, NULL, 0);
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
int MIK_RunEntryErr(MIKRuntime* mik_rt, const char* entry, char* err_buf, size_t err_buf_size) {
|
|
1088
|
+
if (err_buf && err_buf_size > 0) {
|
|
1089
|
+
err_buf[0] = '\0';
|
|
1090
|
+
}
|
|
964
1091
|
if (!mik_rt || !entry || entry[0] == '\0') {
|
|
965
1092
|
return -EINVAL;
|
|
966
1093
|
}
|
|
@@ -987,6 +1114,7 @@ int MIK_RunEntry(MIKRuntime* mik_rt, const char* entry) {
|
|
|
987
1114
|
|
|
988
1115
|
JSValue result = MIK_EvalModule(ctx, entry, true);
|
|
989
1116
|
bool failed = JS_IsException(result);
|
|
1117
|
+
bool rejected = false;
|
|
990
1118
|
/* Modules with top-level await return a Promise. A module body that
|
|
991
1119
|
* throws synchronously rejects that promise before JS_EvalFunction
|
|
992
1120
|
* returns, but the value itself is still an Object (rejected Promise),
|
|
@@ -998,7 +1126,25 @@ int MIK_RunEntry(MIKRuntime* mik_rt, const char* entry) {
|
|
|
998
1126
|
JSPromiseStateEnum state = JS_PromiseState(ctx, result);
|
|
999
1127
|
if (state == JS_PROMISE_REJECTED) {
|
|
1000
1128
|
failed = true;
|
|
1129
|
+
rejected = true;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
if (failed && err_buf && err_buf_size > 0) {
|
|
1133
|
+
/* Capture the failure's string form for the caller. Without an
|
|
1134
|
+
* err_buf the pending exception is intentionally left on ctx,
|
|
1135
|
+
* matching MIK_RunEntry's historical behavior. */
|
|
1136
|
+
JSValue exc = rejected ? JS_PromiseResult(ctx, result) : JS_GetException(ctx);
|
|
1137
|
+
const char* msg = JS_ToCString(ctx, exc);
|
|
1138
|
+
if (msg) {
|
|
1139
|
+
snprintf(err_buf, err_buf_size, "%s", msg);
|
|
1140
|
+
JS_FreeCString(ctx, msg);
|
|
1141
|
+
} else {
|
|
1142
|
+
/* Stringifying the exception itself threw (e.g. OOM) — drop
|
|
1143
|
+
* the secondary exception so it can't leak into later evals. */
|
|
1144
|
+
JSValue stray = JS_GetException(ctx);
|
|
1145
|
+
JS_FreeValue(ctx, stray);
|
|
1001
1146
|
}
|
|
1147
|
+
JS_FreeValue(ctx, exc);
|
|
1002
1148
|
}
|
|
1003
1149
|
JS_FreeValue(ctx, result);
|
|
1004
1150
|
return failed ? -EFAULT : 0;
|
package/src/modules.cpp
CHANGED
|
@@ -245,11 +245,11 @@ static JSModuleDef* mik_module_loader_inner(JSContext* ctx, const char* module_n
|
|
|
245
245
|
/* mik__load_builtin returns NULL either because the module isn't in
|
|
246
246
|
* the table, or because deserialization failed (e.g. stack overflow
|
|
247
247
|
* from deeply nested .bjs loading). If deserialization failed, the
|
|
248
|
-
* real exception is already on ctx — don't replace it.
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
248
|
+
* real exception is already on ctx — don't replace it. Probe with
|
|
249
|
+
* JS_HasException, NOT JS_GetException + JS_IsNull: quickjs-ng
|
|
250
|
+
* returns JS_UNINITIALIZED from an empty exception slot, and
|
|
251
|
+
* re-throwing that surfaced as a bare "[uninitialized]" error. */
|
|
252
|
+
if (!JS_HasException(ctx)) {
|
|
253
253
|
JS_ThrowReferenceError(ctx, "Builtin module '%s' is not available in this build",
|
|
254
254
|
module_name);
|
|
255
255
|
}
|
|
@@ -285,13 +285,8 @@ JSModuleDef* mik_module_loader(JSContext* ctx, const char* module_name, void* op
|
|
|
285
285
|
JSModuleDef* result = mik_module_loader_inner(ctx, module_name, opaque);
|
|
286
286
|
if (debug) {
|
|
287
287
|
if (result == nullptr) {
|
|
288
|
-
JSValue exc = JS_GetException(ctx);
|
|
289
|
-
bool has_exc = !JS_IsNull(exc) && !JS_IsUndefined(exc);
|
|
290
288
|
fprintf(stderr, "[mik-modules] FAIL: %s (exception=%s)\n", module_name,
|
|
291
|
-
|
|
292
|
-
/* Re-throw so the caller still sees the exception. */
|
|
293
|
-
if (has_exc) JS_Throw(ctx, exc);
|
|
294
|
-
else JS_FreeValue(ctx, exc);
|
|
289
|
+
JS_HasException(ctx) ? "set" : "NULL");
|
|
295
290
|
} else {
|
|
296
291
|
fprintf(stderr, "[mik-modules] ok: %s\n", module_name);
|
|
297
292
|
}
|
|
@@ -319,12 +314,8 @@ JSModuleDef* mik_module_loader(JSContext* ctx, const char* module_name, void* op
|
|
|
319
314
|
JSModuleDef* m = mik_module_loader_inner(ctx, module_name, opaque);
|
|
320
315
|
if (debug) {
|
|
321
316
|
if (m == nullptr) {
|
|
322
|
-
JSValue exc = JS_GetException(ctx);
|
|
323
|
-
bool has_exc = !JS_IsNull(exc) && !JS_IsUndefined(exc);
|
|
324
317
|
fprintf(stderr, "[mik-modules] FAIL: %s (exception=%s)\n", module_name,
|
|
325
|
-
|
|
326
|
-
if (has_exc) JS_Throw(ctx, exc);
|
|
327
|
-
else JS_FreeValue(ctx, exc);
|
|
318
|
+
JS_HasException(ctx) ? "set" : "NULL");
|
|
328
319
|
} else {
|
|
329
320
|
fprintf(stderr, "[mik-modules] ok: %s\n", module_name);
|
|
330
321
|
}
|
package/src/platform_posix.cpp
CHANGED
|
@@ -116,6 +116,10 @@ static const char* posix_get_device_id(void) {
|
|
|
116
116
|
return id;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
static const char* posix_get_reset_reason(void) {
|
|
120
|
+
return "unknown"; /* Desktop processes have no chip reset concept */
|
|
121
|
+
}
|
|
122
|
+
|
|
119
123
|
static void posix_log(int level, const char* tag, const char* fmt, ...) {
|
|
120
124
|
if (level < MIK_LOG_ERROR || level > MIK_LOG_VERBOSE) return;
|
|
121
125
|
fprintf(stderr, "[%s] %s: ", mik_log_level_name(level), tag);
|
|
@@ -147,6 +151,7 @@ static const MIKPlatform posix_platform = {
|
|
|
147
151
|
.stderr_write = posix_stderr_write,
|
|
148
152
|
.stdin_read = posix_stdin_read,
|
|
149
153
|
.get_device_id = posix_get_device_id,
|
|
154
|
+
.get_reset_reason = posix_get_reset_reason,
|
|
150
155
|
};
|
|
151
156
|
|
|
152
157
|
static const MIKPlatform* current_platform = &posix_platform;
|