@@ -216,6 +216,105 @@ class $JSArray { constructor(values) { this.values = values; } }
216216 if (value instanceof $JSArray && name === 'length') return value.values.length;
217217 $fail('TypeError', 'Attribute is outside the approved JavaScript registry.', location);
218218}
219+ ''' ),
220+ 'async' : (('call' , 'javascript' ), r'''
221+ if (signal !== null && (typeof signal !== 'object' || typeof signal.aborted !== 'boolean' ||
222+ typeof signal.addEventListener !== 'function' || typeof signal.removeEventListener !== 'function')) {
223+ throw new TypeError('signal must be an AbortSignal or null');
224+ }
225+ function $cancelled(location) {
226+ const error = new $PyError('AbortError', 'Compiled operation cancelled.', location);
227+ error.code = 'CANCELLED'; return error;
228+ }
229+ function $checkpoint(location) { if (signal?.aborted) throw $cancelled(location); }
230+ class $Coroutine {
231+ constructor(start) { this.start = start; this.started = false; }
232+ }
233+ class $PromiseValue {
234+ constructor(promise) { this.promise = promise; promise.catch(() => {}); Object.freeze(this); }
235+ }
236+ const $asyncFunctions = new WeakSet();
237+ let $activeAsync = 0;
238+ function $cfn(name, count, defaults, body) {
239+ const fn = $fn(name, count, defaults, (args, location) => new $Coroutine(() => body(args, location)));
240+ $asyncFunctions.add(fn); return fn;
241+ }
242+ function $wait(promise, location) {
243+ $checkpoint(location);
244+ return new Promise((resolve, reject) => {
245+ let finished = false;
246+ const cleanup = () => signal?.removeEventListener('abort', abort);
247+ const abort = () => { if (!finished) { finished = true; cleanup(); reject($cancelled(location)); } };
248+ signal?.addEventListener('abort', abort, {once: true});
249+ if (signal?.aborted) { abort(); return; }
250+ promise.then(value => {
251+ if (finished) return; finished = true; cleanup();
252+ try { $checkpoint(location); resolve(value); } catch (error) { reject(error); }
253+ }, error => {
254+ if (finished) return; finished = true; cleanup();
255+ reject(error instanceof $PyError ? error : new $PyError('RuntimeError', 'Unhandled Promise rejection.', location));
256+ });
257+ });
258+ }
259+ function $await(value, location) {
260+ $checkpoint(location);
261+ if (value instanceof $PromiseValue) return $wait(value.promise, location);
262+ if (!(value instanceof $Coroutine)) $fail('TypeError', 'await requires an adopted coroutine or Promise.', location);
263+ if (value.started) $fail('RuntimeError', 'cannot reuse already awaited coroutine', location);
264+ value.started = true;
265+ if ($activeAsync >= 128) $fail('RecursionError', 'Maximum active async calls (128) exceeded.', location);
266+ $activeAsync++;
267+ let promise;
268+ try { promise = Promise.resolve(value.start()); } catch (error) { promise = Promise.reject(error); }
269+ const pending = promise.finally(() => { $activeAsync--; });
270+ pending.catch(() => {});
271+ return $wait(pending, location);
272+ }
273+ const $PromiseRegistry = Object.freeze({
274+ resolve: $fn('Promise.resolve', 1, [], (args, location) => {
275+ if (args[0] instanceof $Coroutine) $fail('TypeError', 'Await a coroutine before Promise conversion.', location);
276+ return args[0] instanceof $PromiseValue ? args[0] : new $PromiseValue(Promise.resolve(args[0]));
277+ }),
278+ reject: $fn('Promise.reject', 1, [], (args, location) => {
279+ if (!(args[0] instanceof $PyError)) $fail('TypeError', 'Promise.reject requires an adopted exception.', location);
280+ return new $PromiseValue(Promise.reject(args[0]));
281+ })
282+ });
283+ function $asyncAttribute(value, name, location) {
284+ if (value === $JS && name === 'Promise') return $PromiseRegistry;
285+ if (value === $PromiseRegistry && (name === 'resolve' || name === 'reject')) return value[name];
286+ if (value instanceof $PromiseValue && name === 'then') return $fn('Promise.then', null, [], (args, where) => {
287+ if (args.length > 2 || args.some(fn => fn !== null && (!(fn instanceof $Callable) || $asyncFunctions.has(fn)))) {
288+ $fail('TypeError', 'then requires up to two synchronous Python callbacks or None.', where);
289+ }
290+ const callback = fn => fn == null ? undefined : result => {
291+ $checkpoint(where);
292+ const returned = $call(fn, [result], where);
293+ if (returned instanceof $Coroutine) $fail('TypeError', 'Async Promise callbacks are outside this profile.', where);
294+ return returned instanceof $PromiseValue ? returned.promise : returned;
295+ };
296+ return new $PromiseValue(value.promise.then(callback(args[0]), callback(args[1])));
297+ });
298+ $fail('TypeError', 'Attribute is outside the adopted async registry.', location);
299+ }
300+ ''' ),
301+ 'async-expose' : (('async' , 'input' ), r'''
302+ const $asyncExposed = new WeakMap();
303+ function $asyncExpose(value, location) {
304+ if (!(value instanceof $Callable)) return value;
305+ if (!$asyncExposed.has(value)) {
306+ const invoke = args => {
307+ $checkpoint(location);
308+ return $call(value, args.map(argument => $input(argument, location)), location);
309+ };
310+ $asyncExposed.set(value, $asyncFunctions.has(value) ?
311+ (...args) => Promise.resolve().then(() => $await(invoke(args), location)) : (...args) => {
312+ const result = invoke(args);
313+ return result instanceof $PromiseValue ? $await(result, location) : result;
314+ });
315+ }
316+ return $asyncExposed.get(value);
317+ }
219318''' ),
220319 'input' : (('call' , 'str' , 'tuple' ), r'''
221320function $input(value, location, depth = 0, seen = new Set()) {
@@ -248,6 +347,8 @@ class $JSArray { constructor(values) { this.values = values; } }
248347 'types' : ('$types' , '$isinstance' ), 'exception' : ('$TypeError' ,),
249348 'repr' : ('$repr' ,), 'print' : ('$print' ,),
250349 'javascript' : ('$JS' , '$JArray' , '$attribute' ), 'input' : ('$input' , '$expose' ),
350+ 'async' : ('$checkpoint' , '$await' , '$cfn' , '$PromiseRegistry' , '$asyncAttribute' ),
351+ 'async-expose' : ('$asyncExpose' ,),
251352}
252353
253354
0 commit comments