diff --git a/lib/core.ts b/lib/core.ts index 0199814..7a0c5b4 100644 --- a/lib/core.ts +++ b/lib/core.ts @@ -2,12 +2,17 @@ interface Environment { global: typeof globalThis & Record; proto(name: string): object; setProto(name: string, val: object): void; + symbol(name: string): symbol; } interface Internals { + object: ObjectConstructor; + function: FunctionConstructor; + promise: typeof Promise; + markSpecial(...funcs: Function[]): void; getEnv(func: Function): Environment | undefined; setEnv(func: T, env: Environment): T; - apply(func: Function, thisArg: any, args: any[]): any; + apply(func: Function, thisArg: any, args: any[], env?: Environment): any; delay(timeout: number, callback: Function): () => void; pushMessage(micro: boolean, func: Function, thisArg: any, args: any[]): void; @@ -55,6 +60,7 @@ try { run('timeout'); env.global.log = log; + env.global.NewObject = internals.object; log('Loaded polyfills!'); } diff --git a/lib/promise.ts b/lib/promise.ts index 903c7c2..1f7e8b7 100644 --- a/lib/promise.ts +++ b/lib/promise.ts @@ -1,203 +1,208 @@ define("promise", () => { - const syms = { - callbacks: internals.symbol('Promise.callbacks'), - state: internals.symbol('Promise.state'), - value: internals.symbol('Promise.value'), - handled: internals.symbol('Promise.handled'), - } as { - readonly callbacks: unique symbol, - readonly state: unique symbol, - readonly value: unique symbol, - readonly handled: unique symbol, - } + var Promise = env.global.Promise = internals.promise + return; - type Callback = [ PromiseFulfillFunc, PromiseRejectFunc ]; - enum State { - Pending, - Fulfilled, - Rejected, - } + // const syms = { + // callbacks: internals.symbol('Promise.callbacks'), + // state: internals.symbol('Promise.state'), + // value: internals.symbol('Promise.value'), + // handled: internals.symbol('Promise.handled'), + // } as { + // readonly callbacks: unique symbol, + // readonly state: unique symbol, + // readonly value: unique symbol, + // readonly handled: unique symbol, + // } - function isAwaitable(val: unknown): val is Thenable { - return ( - typeof val === 'object' && - val !== null && - 'then' in val && - typeof val.then === 'function' - ); - } - function resolve(promise: Promise, v: any, state: State) { - if (promise[syms.state] === State.Pending) { - if (typeof v === 'object' && v !== null && 'then' in v && typeof v.then === 'function') { - v.then( - (res: any) => resolve(promise, res, state), - (res: any) => resolve(promise, res, State.Rejected) - ); - return; - } - promise[syms.value] = v; - promise[syms.state] = state; + // type Callback = [ PromiseFulfillFunc, PromiseRejectFunc ]; + // enum State { + // Pending, + // Fulfilled, + // Rejected, + // } - for (let i = 0; i < promise[syms.callbacks]!.length; i++) { - promise[syms.handled] = true; - promise[syms.callbacks]![i][state - 1](v); - } + // function isAwaitable(val: unknown): val is Thenable { + // return ( + // typeof val === 'object' && + // val !== null && + // 'then' in val && + // typeof val.then === 'function' + // ); + // } + // function resolve(promise: Promise, v: any, state: State) { + // if (promise[syms.state] === State.Pending) { + // if (typeof v === 'object' && v !== null && 'then' in v && typeof v.then === 'function') { + // v.then( + // (res: any) => resolve(promise, res, state), + // (res: any) => resolve(promise, res, State.Rejected) + // ); + // return; + // } + // promise[syms.value] = v; + // promise[syms.state] = state; - promise[syms.callbacks] = undefined; + // for (let i = 0; i < promise[syms.callbacks]!.length; i++) { + // promise[syms.handled] = true; + // promise[syms.callbacks]![i][state - 1](v); + // } - internals.pushMessage(true, internals.setEnv(() => { - if (!promise[syms.handled] && state === State.Rejected) { - log('Uncaught (in promise) ' + promise[syms.value]); - } - }, env), undefined, []); - } - } + // promise[syms.callbacks] = undefined; - class Promise { - public static isAwaitable(val: unknown): val is Thenable { - return isAwaitable(val); - } + // internals.pushMessage(true, internals.setEnv(() => { + // if (!promise[syms.handled] && state === State.Rejected) { + // log('Uncaught (in promise) ' + promise[syms.value]); + // } + // }, env), undefined, []); + // } + // } - public static resolve(val: T): Promise> { - return new Promise(res => res(val as any)); - } - public static reject(val: T): Promise> { - return new Promise((_, rej) => rej(val as any)); - } + // class _Promise { + // public static isAwaitable(val: unknown): val is Thenable { + // return isAwaitable(val); + // } - public static race(vals: T[]): Promise> { - if (typeof vals.length !== 'number') throw new TypeError('vals must be an array. Note that Promise.race is not variadic.'); - return new Promise((res, rej) => { - for (let i = 0; i < vals.length; i++) { - const val = vals[i]; - if (this.isAwaitable(val)) val.then(res, rej); - else res(val as any); - } - }); - } - public static any(vals: T[]): Promise> { - if (typeof vals.length !== 'number') throw new TypeError('vals must be an array. Note that Promise.any is not variadic.'); - return new Promise((res, rej) => { - let n = 0; + // public static resolve(val: T): Promise> { + // return new Promise(res => res(val as any)); + // } + // public static reject(val: T): Promise> { + // return new Promise((_, rej) => rej(val as any)); + // } - for (let i = 0; i < vals.length; i++) { - const val = vals[i]; - if (this.isAwaitable(val)) val.then(res, (err) => { - n++; - if (n === vals.length) throw Error('No promise resolved.'); - }); - else res(val as any); - } + // public static race(vals: T[]): Promise> { + // if (typeof vals.length !== 'number') throw new env.global.TypeError('vals must be an array. Note that Promise.race is not variadic.'); + // return new Promise((res, rej) => { + // for (let i = 0; i < vals.length; i++) { + // const val = vals[i]; + // if (this.isAwaitable(val)) val.then(res, rej); + // else res(val as any); + // } + // }); + // } + // public static any(vals: T[]): Promise> { + // if (typeof vals.length !== 'number') throw new env.global.TypeError('vals must be an array. Note that Promise.any is not variadic.'); + // return new Promise((res, rej) => { + // let n = 0; - if (vals.length === 0) throw Error('No promise resolved.'); - }); - } - public static all(vals: any[]): Promise { - if (typeof vals.length !== 'number') throw new TypeError('vals must be an array. Note that Promise.all is not variadic.'); - return new Promise((res, rej) => { - const result: any[] = []; - let n = 0; + // for (let i = 0; i < vals.length; i++) { + // const val = vals[i]; + // if (this.isAwaitable(val)) val.then(res, (err) => { + // n++; + // if (n === vals.length) throw Error('No promise resolved.'); + // }); + // else res(val as any); + // } - for (let i = 0; i < vals.length; i++) { - const val = vals[i]; - if (this.isAwaitable(val)) val.then( - val => { - n++; - result[i] = val; - if (n === vals.length) res(result); - }, - rej - ); - else { - n++; - result[i] = val; - } - } + // if (vals.length === 0) throw Error('No promise resolved.'); + // }); + // } + // public static all(vals: any[]): Promise { + // if (typeof vals.length !== 'number') throw new env.global.TypeError('vals must be an array. Note that Promise.all is not variadic.'); + // return new Promise((res, rej) => { + // const result: any[] = []; + // let n = 0; - if (vals.length === n) res(result); - }); - } - public static allSettled(vals: any[]): Promise { - if (typeof vals.length !== 'number') throw new TypeError('vals must be an array. Note that Promise.allSettled is not variadic.'); - return new Promise((res, rej) => { - const result: any[] = []; - let n = 0; + // for (let i = 0; i < vals.length; i++) { + // const val = vals[i]; + // if (this.isAwaitable(val)) val.then( + // val => { + // n++; + // result[i] = val; + // if (n === vals.length) res(result); + // }, + // rej + // ); + // else { + // n++; + // result[i] = val; + // } + // } - for (let i = 0; i < vals.length; i++) { - const value = vals[i]; - if (this.isAwaitable(value)) value.then( - value => { - n++; - result[i] = { status: 'fulfilled', value }; - if (n === vals.length) res(result); - }, - reason => { - n++; - result[i] = { status: 'rejected', reason }; - if (n === vals.length) res(result); - }, - ); - else { - n++; - result[i] = { status: 'fulfilled', value }; - } - } + // if (vals.length === n) res(result); + // }); + // } + // public static allSettled(vals: any[]): Promise { + // if (typeof vals.length !== 'number') throw new env.global.TypeError('vals must be an array. Note that Promise.allSettled is not variadic.'); + // return new Promise((res, rej) => { + // const result: any[] = []; + // let n = 0; - if (vals.length === n) res(result); - }); - } + // for (let i = 0; i < vals.length; i++) { + // const value = vals[i]; + // if (this.isAwaitable(value)) value.then( + // value => { + // n++; + // result[i] = { status: 'fulfilled', value }; + // if (n === vals.length) res(result); + // }, + // reason => { + // n++; + // result[i] = { status: 'rejected', reason }; + // if (n === vals.length) res(result); + // }, + // ); + // else { + // n++; + // result[i] = { status: 'fulfilled', value }; + // } + // } - [syms.callbacks]?: Callback[] = []; - [syms.handled] = false; - [syms.state] = State.Pending; - [syms.value]?: T | unknown; + // if (vals.length === n) res(result); + // }); + // } - public then(onFulfil?: PromiseFulfillFunc, onReject?: PromiseRejectFunc) { - return new Promise((resolve, reject) => { - onFulfil ??= v => v; - onReject ??= v => v; + // [syms.callbacks]?: Callback[] = []; + // [syms.handled] = false; + // [syms.state] = State.Pending; + // [syms.value]?: T | unknown; - const callback = (func: (val: any) => any) => (v: any) => { - try { resolve(func(v)); } - catch (e) { reject(e); } - } - switch (this[syms.state]) { - case State.Pending: - this[syms.callbacks]![this[syms.callbacks]!.length] = [callback(onFulfil), callback(onReject)]; - break; - case State.Fulfilled: - this[syms.handled] = true; - callback(onFulfil)(this[syms.value]); - break; - case State.Rejected: - this[syms.handled] = true; - callback(onReject)(this[syms.value]); - break; - } - }) - } - public catch(func: PromiseRejectFunc) { - return this.then(undefined, func); - } - public finally(func: () => void) { - return this.then( - v => { - func(); - return v; - }, - v => { - func(); - throw v; - } - ); - } + // public then(onFulfil?: PromiseFulfillFunc, onReject?: PromiseRejectFunc) { + // return new Promise((resolve, reject) => { + // onFulfil ??= v => v; + // onReject ??= v => v; - public constructor(func: PromiseFunc) { - internals.pushMessage(true, func, undefined, [ - ((v) => resolve(this, v, State.Fulfilled)) as PromiseFulfillFunc, - ((err) => resolve(this, err, State.Rejected)) as PromiseRejectFunc - ]); - } - } - env.global.Promise = Promise as any; + // const callback = (func: (val: any) => any) => (v: any) => { + // try { + // resolve(func(v)); + // } + // catch (e) { reject(e); } + // } + // switch (this[syms.state]) { + // case State.Pending: + // this[syms.callbacks]![this[syms.callbacks]!.length] = [callback(onFulfil), callback(onReject)]; + // break; + // case State.Fulfilled: + // this[syms.handled] = true; + // callback(onFulfil)(this[syms.value]); + // break; + // case State.Rejected: + // this[syms.handled] = true; + // callback(onReject)(this[syms.value]); + // break; + // } + // }) + // } + // public catch(func: PromiseRejectFunc) { + // return this.then(undefined, func); + // } + // public finally(func: () => void) { + // return this.then( + // v => { + // func(); + // return v; + // }, + // v => { + // func(); + // throw v; + // } + // ); + // } + + // public constructor(func: PromiseFunc) { + // internals.pushMessage(true, func, undefined, [ + // ((v) => resolve(this, v, State.Fulfilled)) as PromiseFulfillFunc, + // ((err) => resolve(this, err, State.Rejected)) as PromiseRejectFunc + // ]); + // } + // } + // env.global.Promise = Promise as any; }); diff --git a/lib/timeout.ts b/lib/timeout.ts index 8db82a0..e1f167a 100644 --- a/lib/timeout.ts +++ b/lib/timeout.ts @@ -4,14 +4,14 @@ define("timeout", () => { let timeoutI = 0, intervalI = 0; env.global.setTimeout = (func, delay, ...args) => { - if (typeof func !== 'function') throw new TypeError("func must be a function."); + if (typeof func !== 'function') throw new env.global.TypeError("func must be a function."); delay = (delay ?? 0) - 0; const cancelFunc = internals.delay(delay, () => internals.apply(func, undefined, args)); timeouts[++timeoutI] = cancelFunc; return timeoutI; }; env.global.setInterval = (func, delay, ...args) => { - if (typeof func !== 'function') throw new TypeError("func must be a function."); + if (typeof func !== 'function') throw new env.global.TypeError("func must be a function."); delay = (delay ?? 0) - 0; const i = ++intervalI; diff --git a/lib/values/function.ts b/lib/values/function.ts index 4a161d6..4636d5d 100644 --- a/lib/values/function.ts +++ b/lib/values/function.ts @@ -1,140 +1,5 @@ define("values/function", () => { - var Function = env.global.Function = function() { - throw 'Using the constructor Function() is forbidden.'; - } as unknown as FunctionConstructor; - + var Function = env.global.Function = internals.function; env.setProto('function', Function.prototype); setConstr(Function.prototype, Function); - - setProps(Function.prototype, { - apply(thisArg, args) { - if (typeof args !== 'object') throw 'Expected arguments to be an array-like object.'; - var len = args.length - 0; - let newArgs: any[]; - if (internals.isArray(args)) newArgs = args; - else { - newArgs = []; - - while (len >= 0) { - len--; - newArgs[len] = args[len]; - } - } - - return internals.apply(this, thisArg, newArgs); - }, - call(thisArg, ...args) { - return this.apply(thisArg, args); - }, - bind(thisArg, ...args) { - const func = this; - const res = function() { - const resArgs = []; - - for (let i = 0; i < args.length; i++) { - resArgs[i] = args[i]; - } - for (let i = 0; i < arguments.length; i++) { - resArgs[i + args.length] = arguments[i]; - } - - return func.apply(thisArg, resArgs); - }; - res.name = " " + func.name; - return res; - }, - toString() { - return 'function (...) { ... }'; - }, - }); - setProps(Function, { - async(func) { - if (typeof func !== 'function') throw new TypeError('Expected func to be function.'); - - return function (this: any) { - const args = arguments; - - return new Promise((res, rej) => { - const gen = internals.apply(internals.generator(func as any), this, args as any); - - (function next(type: 'none' | 'err' | 'ret', val?: any) { - try { - let result; - - switch (type) { - case 'err': result = gen.throw(val); break; - case 'ret': result = gen.next(val); break; - case 'none': result = gen.next(); break; - } - if (result.done) res(result.value); - else Promise.resolve(result.value).then( - v => next('ret', v), - v => next('err', v) - ) - } - catch (e) { - rej(e); - } - })('none'); - }); - }; - }, - asyncGenerator(func) { - if (typeof func !== 'function') throw new TypeError('Expected func to be function.'); - - return function(this: any, ...args: any[]) { - const gen = internals.apply(internals.generator((_yield) => func( - val => _yield(['await', val]) as any, - val => _yield(['yield', val]) - )), this, args) as Generator<['await' | 'yield', any]>; - - const next = (resolve: Function, reject: Function, type: 'none' | 'val' | 'ret' | 'err', val?: any) => { - let res; - - try { - switch (type) { - case 'val': res = gen.next(val); break; - case 'ret': res = gen.return(val); break; - case 'err': res = gen.throw(val); break; - default: res = gen.next(); break; - } - } - catch (e) { return reject(e); } - - if (res.done) return { done: true, res: res }; - else if (res.value[0] === 'await') Promise.resolve(res.value[1]).then( - v => next(resolve, reject, 'val', v), - v => next(resolve, reject, 'err', v), - ) - else resolve({ done: false, value: res.value[1] }); - }; - - return { - next() { - const args = arguments; - if (arguments.length === 0) return new Promise((res, rej) => next(res, rej, 'none')); - else return new Promise((res, rej) => next(res, rej, 'val', args[0])); - }, - return: (value) => new Promise((res, rej) => next(res, rej, 'ret', value)), - throw: (value) => new Promise((res, rej) => next(res, rej, 'err', value)), - [env.global.Symbol.asyncIterator]() { return this; } - } - } - }, - generator(func) { - if (typeof func !== 'function') throw new TypeError('Expected func to be function.'); - const gen = internals.generator(func); - return function(this: any, ...args: any[]) { - const it = internals.apply(gen, this, args); - - return { - next: (...args) => internals.apply(it.next, it, args), - return: (val) => internals.apply(it.next, it, [val]), - throw: (val) => internals.apply(it.next, it, [val]), - [env.global.Symbol.iterator]() { return this; } - } - } - } - }) - internals.markSpecial(Function); }); \ No newline at end of file diff --git a/lib/values/object.ts b/lib/values/object.ts index 475e4da..0473a1e 100644 --- a/lib/values/object.ts +++ b/lib/values/object.ts @@ -1,226 +1,5 @@ define("values/object", () => { - var Object = env.global.Object = function(arg: any) { - if (arg === undefined || arg === null) return {}; - else if (typeof arg === 'boolean') return new Boolean(arg); - else if (typeof arg === 'number') return new Number(arg); - else if (typeof arg === 'string') return new String(arg); - return arg; - } as ObjectConstructor; - - env.setProto('object', Object.prototype); + var Object = env.global.Object = internals.object; (Object.prototype as any).__proto__ = null; - setConstr(Object.prototype, Object as any); - - function throwNotObject(obj: any, name: string) { - if (obj === null || typeof obj !== 'object' && typeof obj !== 'function') { - throw new TypeError(`Object.${name} may only be used for objects.`); - } - } - function check(obj: any) { - return typeof obj === 'object' && obj !== null || typeof obj === 'function'; - } - - setProps(Object, { - assign(dst, ...src) { - throwNotObject(dst, 'assign'); - for (let i = 0; i < src.length; i++) { - const obj = src[i]; - throwNotObject(obj, 'assign'); - for (const key of Object.keys(obj)) { - (dst as any)[key] = (obj as any)[key]; - } - } - return dst; - }, - create(obj, props) { - props ??= {}; - return Object.defineProperties({ __proto__: obj }, props as any) as any; - }, - - defineProperty(obj, key, attrib) { - throwNotObject(obj, 'defineProperty'); - if (typeof attrib !== 'object') throw new TypeError('Expected attributes to be an object.'); - - if ('value' in attrib) { - if ('get' in attrib || 'set' in attrib) throw new TypeError('Cannot specify a value and accessors for a property.'); - if (!internals.defineField( - obj, key, - attrib.value, - !!attrib.writable, - !!attrib.enumerable, - !!attrib.configurable - )) throw new TypeError('Can\'t define property \'' + key + '\'.'); - } - else { - if (typeof attrib.get !== 'function' && attrib.get !== undefined) throw new TypeError('Get accessor must be a function.'); - if (typeof attrib.set !== 'function' && attrib.set !== undefined) throw new TypeError('Set accessor must be a function.'); - - if (!internals.defineProp( - obj, key, - attrib.get, - attrib.set, - !!attrib.enumerable, - !!attrib.configurable - )) throw new TypeError('Can\'t define property \'' + key + '\'.'); - } - - return obj; - }, - defineProperties(obj, attrib) { - throwNotObject(obj, 'defineProperties'); - if (typeof attrib !== 'object' && typeof attrib !== 'function') throw 'Expected second argument to be an object.'; - - for (var key in attrib) { - Object.defineProperty(obj, key, attrib[key]); - } - - return obj; - }, - - keys(obj, onlyString) { - return internals.keys(obj, !!(onlyString ?? true)); - }, - entries(obj, onlyString) { - const res = []; - const keys = internals.keys(obj, !!(onlyString ?? true)); - - for (let i = 0; i < keys.length; i++) { - res[i] = [ keys[i], (obj as any)[keys[i]] ]; - } - - return keys; - }, - values(obj, onlyString) { - const res = []; - const keys = internals.keys(obj, !!(onlyString ?? true)); - - for (let i = 0; i < keys.length; i++) { - res[i] = (obj as any)[keys[i]]; - } - - return keys; - }, - - getOwnPropertyDescriptor(obj, key) { - return internals.ownProp(obj, key) as any; - }, - getOwnPropertyDescriptors(obj) { - const res = []; - const keys = internals.ownPropKeys(obj); - - for (let i = 0; i < keys.length; i++) { - res[i] = internals.ownProp(obj, keys[i]); - } - - return res; - }, - - getOwnPropertyNames(obj) { - const arr = internals.ownPropKeys(obj); - const res = []; - - for (let i = 0; i < arr.length; i++) { - if (typeof arr[i] === 'symbol') continue; - res[res.length] = arr[i]; - } - - return res as any; - }, - getOwnPropertySymbols(obj) { - const arr = internals.ownPropKeys(obj); - const res = []; - - for (let i = 0; i < arr.length; i++) { - if (typeof arr[i] !== 'symbol') continue; - res[res.length] = arr[i]; - } - - return res as any; - }, - hasOwn(obj, key) { - const keys = internals.ownPropKeys(obj); - - for (let i = 0; i < keys.length; i++) { - if (keys[i] === key) return true; - } - - return false; - }, - - getPrototypeOf(obj) { - return obj.__proto__; - }, - setPrototypeOf(obj, proto) { - (obj as any).__proto__ = proto; - return obj; - }, - - fromEntries(iterable) { - const res = {} as any; - - for (const el of iterable) { - res[el[0]] = el[1]; - } - - return res; - }, - - preventExtensions(obj) { - throwNotObject(obj, 'preventExtensions'); - internals.lock(obj, 'ext'); - return obj; - }, - seal(obj) { - throwNotObject(obj, 'seal'); - internals.lock(obj, 'seal'); - return obj; - }, - freeze(obj) { - throwNotObject(obj, 'freeze'); - internals.lock(obj, 'freeze'); - return obj; - }, - - isExtensible(obj) { - if (!check(obj)) return false; - return internals.extensible(obj); - }, - isSealed(obj) { - if (!check(obj)) return true; - if (internals.extensible(obj)) return false; - const keys = internals.ownPropKeys(obj); - - for (let i = 0; i < keys.length; i++) { - if (internals.ownProp(obj, keys[i]).configurable) return false; - } - - return true; - }, - isFrozen(obj) { - if (!check(obj)) return true; - if (internals.extensible(obj)) return false; - const keys = internals.ownPropKeys(obj); - - for (let i = 0; i < keys.length; i++) { - const prop = internals.ownProp(obj, keys[i]); - if (prop.configurable) return false; - if ('writable' in prop && prop.writable) return false; - } - - return true; - } - }); - - setProps(Object.prototype, { - valueOf() { - return this; - }, - toString() { - return '[object ' + (this[env.global.Symbol.typeName] ?? 'Unknown') + ']'; - }, - hasOwnProperty(key) { - return Object.hasOwn(this, key); - }, - }); - internals.markSpecial(Object); + env.setProto('object', Object.prototype); }); \ No newline at end of file diff --git a/lib/values/symbol.ts b/lib/values/symbol.ts index 64b4d5e..dff4b4f 100644 --- a/lib/values/symbol.ts +++ b/lib/values/symbol.ts @@ -2,8 +2,8 @@ define("values/symbol", () => { const symbols: Record = { }; var Symbol = env.global.Symbol = function(this: any, val?: string) { - if (this !== undefined && this !== null) throw new TypeError("Symbol may not be called with 'new'."); - if (typeof val !== 'string' && val !== undefined) throw new TypeError('val must be a string or undefined.'); + if (this !== undefined && this !== null) throw new env.global.TypeError("Symbol may not be called with 'new'."); + if (typeof val !== 'string' && val !== undefined) throw new env.global.TypeError('val must be a string or undefined.'); return internals.symbol(val); } as SymbolConstructor; @@ -12,23 +12,23 @@ define("values/symbol", () => { setProps(Symbol, { for(key) { - if (typeof key !== 'string' && key !== undefined) throw new TypeError('key must be a string or undefined.'); + if (typeof key !== 'string' && key !== undefined) throw new env.global.TypeError('key must be a string or undefined.'); if (key in symbols) return symbols[key]; else return symbols[key] = internals.symbol(key); }, keyFor(sym) { - if (typeof sym !== 'symbol') throw new TypeError('sym must be a symbol.'); + if (typeof sym !== 'symbol') throw new env.global.TypeError('sym must be a symbol.'); return internals.symbolToString(sym); }, - typeName: Symbol("Symbol.name") as any, - replace: Symbol('Symbol.replace') as any, - match: Symbol('Symbol.match') as any, - matchAll: Symbol('Symbol.matchAll') as any, - split: Symbol('Symbol.split') as any, - search: Symbol('Symbol.search') as any, - iterator: Symbol('Symbol.iterator') as any, - asyncIterator: Symbol('Symbol.asyncIterator') as any, + typeName: env.symbol("Symbol.typeName") as any, + replace: env.symbol('Symbol.replace') as any, + match: env.symbol('Symbol.match') as any, + matchAll: env.symbol('Symbol.matchAll') as any, + split: env.symbol('Symbol.split') as any, + search: env.symbol('Symbol.search') as any, + iterator: env.symbol('Symbol.iterator') as any, + asyncIterator: env.symbol('Symbol.asyncIterator') as any, }); internals.defineField(env.global.Object.prototype, Symbol.typeName, 'Object', false, false, false); diff --git a/src/me/topchetoeu/jscript/Main.java b/src/me/topchetoeu/jscript/Main.java index 09657b8..6f580d1 100644 --- a/src/me/topchetoeu/jscript/Main.java +++ b/src/me/topchetoeu/jscript/Main.java @@ -10,7 +10,7 @@ import java.nio.file.Path; import me.topchetoeu.jscript.engine.MessageContext; import me.topchetoeu.jscript.engine.Context; import me.topchetoeu.jscript.engine.Engine; -import me.topchetoeu.jscript.engine.FunctionContext; +import me.topchetoeu.jscript.engine.Environment; import me.topchetoeu.jscript.engine.values.Values; import me.topchetoeu.jscript.events.Observer; import me.topchetoeu.jscript.exceptions.EngineException; @@ -21,7 +21,7 @@ import me.topchetoeu.jscript.polyfills.Internals; public class Main { static Thread task; static Engine engine; - static FunctionContext env; + static Environment env; public static String streamToString(InputStream in) { try { @@ -47,37 +47,14 @@ public class Main { private static Observer valuePrinter = new Observer() { public void next(Object data) { - try { - Values.printValue(null, data); - } + try { Values.printValue(null, data); } catch (InterruptedException e) { } System.out.println(); } public void error(RuntimeException err) { - try { - try { - if (err instanceof EngineException) { - System.out.println("Uncaught " + ((EngineException)err).toString(new Context(null, new MessageContext(engine)))); - } - else if (err instanceof SyntaxException) { - System.out.println("Syntax error:" + ((SyntaxException)err).msg); - } - else if (err.getCause() instanceof InterruptedException) return; - else { - System.out.println("Internal error ocurred:"); - err.printStackTrace(); - } - } - catch (EngineException ex) { - System.out.println("Uncaught "); - Values.printValue(null, ((EngineException)err).value); - System.out.println(); - } - } - catch (InterruptedException ex) { - return; - } + try { Values.printError(err, null); } + catch (InterruptedException ex) { return; } } }; @@ -85,8 +62,10 @@ public class Main { System.out.println(String.format("Running %s v%s by %s", Metadata.NAME, Metadata.VERSION, Metadata.AUTHOR)); var in = new BufferedReader(new InputStreamReader(System.in)); engine = new Engine(); - env = new FunctionContext(null, null, null); - var builderEnv = new FunctionContext(null, new NativeTypeRegister(), null); + + // TODO: Replace type register with safer accessor + env = new Environment(null, new NativeTypeRegister(), null); + var builderEnv = new Environment(null, new NativeTypeRegister(), null); var exited = new boolean[1]; env.global.define("exit", ctx -> { diff --git a/src/me/topchetoeu/jscript/engine/Context.java b/src/me/topchetoeu/jscript/engine/Context.java index 417b115..801cbc7 100644 --- a/src/me/topchetoeu/jscript/engine/Context.java +++ b/src/me/topchetoeu/jscript/engine/Context.java @@ -5,16 +5,16 @@ import me.topchetoeu.jscript.engine.values.Values; import me.topchetoeu.jscript.parsing.Parsing; public class Context { - public final FunctionContext function; + public final Environment env; public final MessageContext message; public FunctionValue compile(String filename, String raw) throws InterruptedException { - var res = Values.toString(this, function.compile.call(this, null, raw, filename)); - return Parsing.compile(function, filename, res); + var res = Values.toString(this, env.compile.call(this, null, raw, filename)); + return Parsing.compile(env, filename, res); } - public Context(FunctionContext funcCtx, MessageContext msgCtx) { - this.function = funcCtx; + public Context(Environment funcCtx, MessageContext msgCtx) { + this.env = funcCtx; this.message = msgCtx; } } diff --git a/src/me/topchetoeu/jscript/engine/Engine.java b/src/me/topchetoeu/jscript/engine/Engine.java index 3e0b388..7b87e73 100644 --- a/src/me/topchetoeu/jscript/engine/Engine.java +++ b/src/me/topchetoeu/jscript/engine/Engine.java @@ -11,7 +11,7 @@ public class Engine { private class UncompiledFunction extends FunctionValue { public final String filename; public final String raw; - public final FunctionContext ctx; + public final Environment ctx; @Override public Object call(Context ctx, Object thisArg, Object ...args) throws InterruptedException { @@ -19,7 +19,7 @@ public class Engine { return ctx.compile(filename, raw).call(ctx, thisArg, args); } - public UncompiledFunction(FunctionContext ctx, String filename, String raw) { + public UncompiledFunction(Environment ctx, String filename, String raw) { super(filename, 0); this.filename = filename; this.raw = raw; @@ -109,7 +109,7 @@ public class Engine { return msg.notifier; } public Awaitable pushMsg(boolean micro, Context ctx, String filename, String raw, Object thisArg, Object ...args) { - return pushMsg(micro, ctx.message, new UncompiledFunction(ctx.function, filename, raw), thisArg, args); + return pushMsg(micro, ctx.message, new UncompiledFunction(ctx.env, filename, raw), thisArg, args); } // public Engine() { diff --git a/src/me/topchetoeu/jscript/engine/FunctionContext.java b/src/me/topchetoeu/jscript/engine/Environment.java similarity index 81% rename from src/me/topchetoeu/jscript/engine/FunctionContext.java rename to src/me/topchetoeu/jscript/engine/Environment.java index e21b7d9..164112d 100644 --- a/src/me/topchetoeu/jscript/engine/FunctionContext.java +++ b/src/me/topchetoeu/jscript/engine/Environment.java @@ -6,15 +6,20 @@ import me.topchetoeu.jscript.engine.scope.GlobalScope; import me.topchetoeu.jscript.engine.values.FunctionValue; import me.topchetoeu.jscript.engine.values.NativeFunction; import me.topchetoeu.jscript.engine.values.ObjectValue; +import me.topchetoeu.jscript.engine.values.Symbol; import me.topchetoeu.jscript.exceptions.EngineException; import me.topchetoeu.jscript.interop.Native; import me.topchetoeu.jscript.interop.NativeGetter; import me.topchetoeu.jscript.interop.NativeSetter; -public class FunctionContext { +public class Environment { private HashMap prototypes = new HashMap<>(); public GlobalScope global; public WrappersProvider wrappersProvider; + /** + * NOTE: This is not the register for Symbol.for, but for the symbols like Symbol.iterator + */ + public HashMap symbols = new HashMap<>(); @Native public FunctionValue compile; @Native public FunctionValue regexConstructor = new NativeFunction("RegExp", (ctx, thisArg, args) -> { @@ -26,6 +31,16 @@ public class FunctionContext { @Native public void setProto(String name, ObjectValue val) { prototypes.put(name, val); } + + @Native public Symbol symbol(String name) { + if (symbols.containsKey(name)) return symbols.get(name); + else { + var res = new Symbol(name); + symbols.put(name, res); + return res; + } + } + // @Native public ObjectValue arrayPrototype = new ObjectValue(); // @Native public ObjectValue boolPrototype = new ObjectValue(); // @Native public ObjectValue functionPrototype = new ObjectValue(); @@ -48,21 +63,21 @@ public class FunctionContext { } @Native - public FunctionContext fork() { - var res = new FunctionContext(compile, wrappersProvider, global); + public Environment fork() { + var res = new Environment(compile, wrappersProvider, global); res.regexConstructor = regexConstructor; res.prototypes = new HashMap<>(prototypes); return res; } @Native - public FunctionContext child() { + public Environment child() { var res = fork(); res.global = res.global.globalChild(); return res; } - public FunctionContext(FunctionValue compile, WrappersProvider nativeConverter, GlobalScope global) { + public Environment(FunctionValue compile, WrappersProvider nativeConverter, GlobalScope global) { if (compile == null) compile = new NativeFunction("compile", (ctx, thisArg, args) -> args.length == 0 ? "" : args[0]); if (nativeConverter == null) nativeConverter = new WrappersProvider() { public ObjectValue getConstr(Class obj) { diff --git a/src/me/topchetoeu/jscript/engine/frame/CodeFrame.java b/src/me/topchetoeu/jscript/engine/frame/CodeFrame.java index aeaf264..96b8350 100644 --- a/src/me/topchetoeu/jscript/engine/frame/CodeFrame.java +++ b/src/me/topchetoeu/jscript/engine/frame/CodeFrame.java @@ -107,11 +107,11 @@ public class CodeFrame { return Runners.exec(ctx, instr, this); } catch (EngineException e) { - throw e.add(function.name, prevLoc); + throw e.add(function.name, prevLoc).setContext(ctx); } } - public Object next(Context ctx, Object prevReturn, Object prevError) throws InterruptedException { + public Object next(Context ctx, Object prevValue, Object prevReturn, Object prevError) throws InterruptedException { TryCtx tryCtx = null; if (prevError != Runners.NO_RETURN) prevReturn = Runners.NO_RETURN; @@ -196,6 +196,7 @@ public class CodeFrame { if (prevError != Runners.NO_RETURN) throw new EngineException(prevError); if (prevReturn != Runners.NO_RETURN) return prevReturn; + if (prevValue != Runners.NO_RETURN) push(ctx, prevValue); if (tryCtx == null) return nextNoTry(ctx); else if (tryCtx.state == TryCtx.STATE_TRY) { @@ -263,7 +264,7 @@ public class CodeFrame { try { ctx.message.pushFrame(this); while (true) { - var res = next(ctx, Runners.NO_RETURN, Runners.NO_RETURN); + var res = next(ctx, Runners.NO_RETURN, Runners.NO_RETURN, Runners.NO_RETURN); if (res != Runners.NO_RETURN) return res; } } diff --git a/src/me/topchetoeu/jscript/engine/frame/Runners.java b/src/me/topchetoeu/jscript/engine/frame/Runners.java index 37ce098..08d49e2 100644 --- a/src/me/topchetoeu/jscript/engine/frame/Runners.java +++ b/src/me/topchetoeu/jscript/engine/frame/Runners.java @@ -48,16 +48,18 @@ public class Runners { var callArgs = frame.take(instr.get(0)); var funcObj = frame.pop(); - if (Values.isFunction(funcObj) && Values.function(funcObj).special) { - frame.push(ctx, call(ctx, funcObj, null, callArgs)); - } - else { - var proto = Values.getMember(ctx, funcObj, "prototype"); - var obj = new ObjectValue(); - obj.setPrototype(ctx, proto); - call(ctx, funcObj, obj, callArgs); - frame.push(ctx, obj); - } + frame.push(ctx, Values.callNew(ctx, funcObj, callArgs)); + + // if (Values.isFunction(funcObj) && Values.function(funcObj).special) { + // frame.push(ctx, call(ctx, funcObj, null, callArgs)); + // } + // else { + // var proto = Values.getMember(ctx, funcObj, "prototype"); + // var obj = new ObjectValue(); + // obj.setPrototype(ctx, proto); + // call(ctx, funcObj, obj, callArgs); + // frame.push(ctx, obj); + // } frame.codePtr++; return NO_RETURN; @@ -65,7 +67,7 @@ public class Runners { public static Object execMakeVar(Context ctx, Instruction instr, CodeFrame frame) throws InterruptedException { var name = (String)instr.get(0); - ctx.function.global.define(name); + ctx.env.global.define(name); frame.codePtr++; return NO_RETURN; } @@ -160,7 +162,7 @@ public class Runners { public static Object execLoadVar(Context ctx, Instruction instr, CodeFrame frame) throws InterruptedException { var i = instr.get(0); - if (i instanceof String) frame.push(ctx, ctx.function.global.get(ctx, (String)i)); + if (i instanceof String) frame.push(ctx, ctx.env.global.get(ctx, (String)i)); else frame.push(ctx, frame.scope.get((int)i).get(ctx)); frame.codePtr++; @@ -172,7 +174,7 @@ public class Runners { return NO_RETURN; } public static Object execLoadGlob(Context ctx, Instruction instr, CodeFrame frame) { - frame.push(ctx, ctx.function.global.obj); + frame.push(ctx, ctx.env.global.obj); frame.codePtr++; return NO_RETURN; } @@ -198,7 +200,7 @@ public class Runners { var body = new Instruction[end - start]; System.arraycopy(frame.function.body, start, body, 0, end - start); - var func = new CodeFunction(ctx.function, "", localsN, len, captures, body); + var func = new CodeFunction(ctx.env, "", localsN, len, captures, body); frame.push(ctx, func); frame.codePtr += n; @@ -222,7 +224,7 @@ public class Runners { return execLoadMember(ctx, instr, frame); } public static Object execLoadRegEx(Context ctx, Instruction instr, CodeFrame frame) throws InterruptedException { - frame.push(ctx, ctx.function.regexConstructor.call(ctx, null, instr.get(0), instr.get(1))); + frame.push(ctx, ctx.env.regexConstructor.call(ctx, null, instr.get(0), instr.get(1))); frame.codePtr++; return NO_RETURN; } @@ -246,7 +248,7 @@ public class Runners { var val = (boolean)instr.get(1) ? frame.peek() : frame.pop(); var i = instr.get(0); - if (i instanceof String) ctx.function.global.set(ctx, (String)i, val); + if (i instanceof String) ctx.env.global.set(ctx, (String)i, val); else frame.scope.get((int)i).set(ctx, val); frame.codePtr++; @@ -293,8 +295,8 @@ public class Runners { Object obj; if (name != null) { - if (ctx.function.global.has(ctx, name)) { - obj = ctx.function.global.get(ctx, name); + if (ctx.env.global.has(ctx, name)) { + obj = ctx.env.global.get(ctx, name); } else obj = null; } diff --git a/src/me/topchetoeu/jscript/engine/values/CodeFunction.java b/src/me/topchetoeu/jscript/engine/values/CodeFunction.java index d70b20d..0b34425 100644 --- a/src/me/topchetoeu/jscript/engine/values/CodeFunction.java +++ b/src/me/topchetoeu/jscript/engine/values/CodeFunction.java @@ -3,7 +3,7 @@ package me.topchetoeu.jscript.engine.values; import me.topchetoeu.jscript.Location; import me.topchetoeu.jscript.compilation.Instruction; import me.topchetoeu.jscript.engine.Context; -import me.topchetoeu.jscript.engine.FunctionContext; +import me.topchetoeu.jscript.engine.Environment; import me.topchetoeu.jscript.engine.frame.CodeFrame; import me.topchetoeu.jscript.engine.scope.ValueVariable; @@ -12,7 +12,7 @@ public class CodeFunction extends FunctionValue { public final int length; public final Instruction[] body; public final ValueVariable[] captures; - public FunctionContext environment; + public Environment environment; public Location loc() { for (var instr : body) { @@ -32,7 +32,7 @@ public class CodeFunction extends FunctionValue { return new CodeFrame(ctx, thisArg, args, this).run(new Context(environment, ctx.message)); } - public CodeFunction(FunctionContext environment, String name, int localsN, int length, ValueVariable[] captures, Instruction[] body) { + public CodeFunction(Environment environment, String name, int localsN, int length, ValueVariable[] captures, Instruction[] body) { super(name, length); this.captures = captures; this.environment = environment; diff --git a/src/me/topchetoeu/jscript/engine/values/NativeWrapper.java b/src/me/topchetoeu/jscript/engine/values/NativeWrapper.java index 753521d..9005e5f 100644 --- a/src/me/topchetoeu/jscript/engine/values/NativeWrapper.java +++ b/src/me/topchetoeu/jscript/engine/values/NativeWrapper.java @@ -8,7 +8,8 @@ public class NativeWrapper extends ObjectValue { @Override public ObjectValue getPrototype(Context ctx) throws InterruptedException { - if (prototype == NATIVE_PROTO) return ctx.function.wrappersProvider.getProto(wrapped.getClass()); + if (prototype == NATIVE_PROTO) + return ctx.env.wrappersProvider.getProto(wrapped.getClass()); else return super.getPrototype(ctx); } diff --git a/src/me/topchetoeu/jscript/engine/values/ObjectValue.java b/src/me/topchetoeu/jscript/engine/values/ObjectValue.java index 9986cc8..054ab3e 100644 --- a/src/me/topchetoeu/jscript/engine/values/ObjectValue.java +++ b/src/me/topchetoeu/jscript/engine/values/ObjectValue.java @@ -55,7 +55,7 @@ public class ObjectValue { public final boolean memberWritable(Object key) { if (state == State.FROZEN) return false; - return !nonWritableSet.contains(key); + return values.containsKey(key) && !nonWritableSet.contains(key); } public final boolean memberConfigurable(Object key) { if (state == State.SEALED || state == State.FROZEN) return false; @@ -147,13 +147,13 @@ public class ObjectValue { public ObjectValue getPrototype(Context ctx) throws InterruptedException { try { - if (prototype == OBJ_PROTO) return ctx.function.proto("object"); - if (prototype == ARR_PROTO) return ctx.function.proto("array"); - if (prototype == FUNC_PROTO) return ctx.function.proto("function"); - if (prototype == ERR_PROTO) return ctx.function.proto("error"); - if (prototype == RANGE_ERR_PROTO) return ctx.function.proto("rangeErr"); - if (prototype == SYNTAX_ERR_PROTO) return ctx.function.proto("syntaxErr"); - if (prototype == TYPE_ERR_PROTO) return ctx.function.proto("typeErr"); + if (prototype == OBJ_PROTO) return ctx.env.proto("object"); + if (prototype == ARR_PROTO) return ctx.env.proto("array"); + if (prototype == FUNC_PROTO) return ctx.env.proto("function"); + if (prototype == ERR_PROTO) return ctx.env.proto("error"); + if (prototype == RANGE_ERR_PROTO) return ctx.env.proto("rangeErr"); + if (prototype == SYNTAX_ERR_PROTO) return ctx.env.proto("syntaxErr"); + if (prototype == TYPE_ERR_PROTO) return ctx.env.proto("typeErr"); } catch (NullPointerException e) { return null; @@ -172,14 +172,14 @@ public class ObjectValue { else if (Values.isObject(val)) { var obj = Values.object(val); - if (ctx != null && ctx.function != null) { - if (obj == ctx.function.proto("object")) prototype = OBJ_PROTO; - else if (obj == ctx.function.proto("array")) prototype = ARR_PROTO; - else if (obj == ctx.function.proto("function")) prototype = FUNC_PROTO; - else if (obj == ctx.function.proto("error")) prototype = ERR_PROTO; - else if (obj == ctx.function.proto("syntaxErr")) prototype = SYNTAX_ERR_PROTO; - else if (obj == ctx.function.proto("typeErr")) prototype = TYPE_ERR_PROTO; - else if (obj == ctx.function.proto("rangeErr")) prototype = RANGE_ERR_PROTO; + if (ctx != null && ctx.env != null) { + if (obj == ctx.env.proto("object")) prototype = OBJ_PROTO; + else if (obj == ctx.env.proto("array")) prototype = ARR_PROTO; + else if (obj == ctx.env.proto("function")) prototype = FUNC_PROTO; + else if (obj == ctx.env.proto("error")) prototype = ERR_PROTO; + else if (obj == ctx.env.proto("syntaxErr")) prototype = SYNTAX_ERR_PROTO; + else if (obj == ctx.env.proto("typeErr")) prototype = TYPE_ERR_PROTO; + else if (obj == ctx.env.proto("rangeErr")) prototype = RANGE_ERR_PROTO; else prototype = obj; } else prototype = obj; diff --git a/src/me/topchetoeu/jscript/engine/values/Values.java b/src/me/topchetoeu/jscript/engine/values/Values.java index 73f7029..839c6e3 100644 --- a/src/me/topchetoeu/jscript/engine/values/Values.java +++ b/src/me/topchetoeu/jscript/engine/values/Values.java @@ -13,7 +13,9 @@ import java.util.Map; import me.topchetoeu.jscript.engine.Context; import me.topchetoeu.jscript.engine.Operation; import me.topchetoeu.jscript.engine.frame.ConvertHint; +import me.topchetoeu.jscript.exceptions.ConvertException; import me.topchetoeu.jscript.exceptions.EngineException; +import me.topchetoeu.jscript.exceptions.SyntaxException; public class Values { public static final Object NULL = new Object(); @@ -321,10 +323,10 @@ public class Values { if (isObject(obj)) return object(obj).getPrototype(ctx); if (ctx == null) return null; - if (obj instanceof String) return ctx.function.proto("string"); - else if (obj instanceof Number) return ctx.function.proto("number"); - else if (obj instanceof Boolean) return ctx.function.proto("bool"); - else if (obj instanceof Symbol) return ctx.function.proto("symbol"); + if (obj instanceof String) return ctx.env.proto("string"); + else if (obj instanceof Number) return ctx.env.proto("number"); + else if (obj instanceof Boolean) return ctx.env.proto("bool"); + else if (obj instanceof Symbol) return ctx.env.proto("symbol"); return null; } @@ -352,12 +354,39 @@ public class Values { return res; } + public static ObjectValue getMemberDescriptor(Context ctx, Object obj, Object key) throws InterruptedException { + if (obj instanceof ObjectValue) return ((ObjectValue)obj).getMemberDescriptor(ctx, key); + else if (obj instanceof String && key instanceof Number) { + var i = ((Number)key).intValue(); + var _i = ((Number)key).doubleValue(); + if (i - _i != 0) return null; + if (i < 0 || i >= ((String)obj).length()) return null; + + return new ObjectValue(ctx, Map.of( + "value", ((String)obj).charAt(i) + "", + "writable", false, + "enumerable", true, + "configurable", false + )); + } + else return null; + } public static Object call(Context ctx, Object func, Object thisArg, Object ...args) throws InterruptedException { - if (!isFunction(func)) - throw EngineException.ofType("Tried to call a non-function value."); + if (!isFunction(func)) throw EngineException.ofType("Tried to call a non-function value."); return function(func).call(ctx, thisArg, args); } + public static Object callNew(Context ctx, Object func, Object ...args) throws InterruptedException { + if (func instanceof FunctionValue && ((FunctionValue)func).special) return ((FunctionValue)func).call(ctx, null, args); + + var res = new ObjectValue(); + var proto = Values.getMember(ctx, func, "prototype"); + res.setPrototype(ctx, proto); + + call(ctx, func, res, args); + + return res; + } public static boolean strictEquals(Context ctx, Object a, Object b) { a = normalize(ctx, a); b = normalize(ctx, b); @@ -420,7 +449,7 @@ public class Values { if (val instanceof Class) { if (ctx == null) return null; - else return ctx.function.wrappersProvider.getConstr((Class)val); + else return ctx.env.wrappersProvider.getConstr((Class)val); } return new NativeWrapper(val); @@ -429,17 +458,15 @@ public class Values { @SuppressWarnings("unchecked") public static T convert(Context ctx, Object obj, Class clazz) throws InterruptedException { if (clazz == Void.class) return null; - if (clazz == null || clazz == Object.class) return (T)obj; - - var err = new IllegalArgumentException(String.format("Cannot convert '%s' to '%s'.", type(obj), clazz.getName())); if (obj instanceof NativeWrapper) { var res = ((NativeWrapper)obj).wrapped; if (clazz.isInstance(res)) return (T)res; } + if (clazz == null || clazz == Object.class) return (T)obj; + if (obj instanceof ArrayValue) { - if (clazz.isAssignableFrom(ArrayList.class)) { var raw = array(obj).toArray(); var res = new ArrayList<>(); @@ -480,11 +507,9 @@ public class Values { if (clazz == Character.class || clazz == char.class) { if (obj instanceof Number) return (T)(Character)(char)number(obj); - else if (obj == NULL) throw new IllegalArgumentException("Cannot convert null to character."); - else if (obj == null) throw new IllegalArgumentException("Cannot convert undefined to character."); else { var res = toString(ctx, obj); - if (res.length() == 0) throw new IllegalArgumentException("Cannot convert empty string to character."); + if (res.length() == 0) throw new ConvertException("\"\"", "Character"); else return (T)(Character)res.charAt(0); } } @@ -492,20 +517,22 @@ public class Values { if (obj == null) return null; if (clazz.isInstance(obj)) return (T)obj; - throw err; + throw new ConvertException(type(obj), clazz.getName()); } public static Iterable toJavaIterable(Context ctx, Object obj) throws InterruptedException { return () -> { try { - var constr = getMember(ctx, ctx.function.proto("symbol"), "constructor"); - var symbol = getMember(ctx, constr, "iterator"); + var symbol = ctx.env.symbol("Symbol.iterator"); var iteratorFunc = getMember(ctx, obj, symbol); if (!isFunction(iteratorFunc)) return Collections.emptyIterator(); - var iterator = getMember(ctx, call(ctx, iteratorFunc, obj), "next"); - if (!isFunction(iterator)) return Collections.emptyIterator(); - var iterable = obj; + var iterator = iteratorFunc instanceof FunctionValue ? + ((FunctionValue)iteratorFunc).call(ctx, iteratorFunc, obj) : + iteratorFunc; + var nextFunc = getMember(ctx, call(ctx, iteratorFunc, obj), "next"); + + if (!isFunction(nextFunc)) return Collections.emptyIterator(); return new Iterator() { private Object value = null; @@ -515,7 +542,7 @@ public class Values { private void loadNext() throws InterruptedException { if (next == null) value = null; else if (consumed) { - var curr = object(next.call(ctx, iterable)); + var curr = object(next.call(ctx, iterator)); if (curr == null) { next = null; value = null; } if (toBoolean(curr.getMember(ctx, "done"))) { next = null; value = null; } else { @@ -567,7 +594,7 @@ public class Values { var it = iterable.iterator(); try { - var key = getMember(ctx, getMember(ctx, ctx.function.proto("symbol"), "constructor"), "iterator"); + var key = getMember(ctx, getMember(ctx, ctx.env.proto("symbol"), "constructor"), "iterator"); res.defineProperty(ctx, key, new NativeFunction("", (_ctx, thisArg, args) -> thisArg)); } catch (IllegalArgumentException | NullPointerException e) { } @@ -655,4 +682,25 @@ public class Values { public static void printValue(Context ctx, Object val) throws InterruptedException { printValue(ctx, val, new HashSet<>(), 0); } + public static void printError(RuntimeException err, String prefix) throws InterruptedException { + prefix = prefix == null ? "Uncauthg" : "Uncaught " + prefix; + try { + if (err instanceof EngineException) { + System.out.println(prefix + " " + ((EngineException)err).toString(((EngineException)err).ctx)); + } + else if (err instanceof SyntaxException) { + System.out.println("Syntax error:" + ((SyntaxException)err).msg); + } + else if (err.getCause() instanceof InterruptedException) return; + else { + System.out.println("Internal error ocurred:"); + err.printStackTrace(); + } + } + catch (EngineException ex) { + System.out.println("Uncaught "); + Values.printValue(null, ((EngineException)err).value); + System.out.println(); + } + } } diff --git a/src/me/topchetoeu/jscript/exceptions/ConvertException.java b/src/me/topchetoeu/jscript/exceptions/ConvertException.java new file mode 100644 index 0000000..967bf7c --- /dev/null +++ b/src/me/topchetoeu/jscript/exceptions/ConvertException.java @@ -0,0 +1,11 @@ +package me.topchetoeu.jscript.exceptions; + +public class ConvertException extends RuntimeException { + public final String source, target; + + public ConvertException(String source, String target) { + super(String.format("Cannot convert '%s' to '%s'.", source, target)); + this.source = source; + this.target = target; + } +} diff --git a/src/me/topchetoeu/jscript/exceptions/EngineException.java b/src/me/topchetoeu/jscript/exceptions/EngineException.java index 6119a73..e695426 100644 --- a/src/me/topchetoeu/jscript/exceptions/EngineException.java +++ b/src/me/topchetoeu/jscript/exceptions/EngineException.java @@ -12,6 +12,7 @@ import me.topchetoeu.jscript.engine.values.ObjectValue.PlaceholderProto; public class EngineException extends RuntimeException { public final Object value; public EngineException cause; + public Context ctx = null; public final List stackTrace = new ArrayList<>(); public EngineException add(String name, Location location) { @@ -27,6 +28,10 @@ public class EngineException extends RuntimeException { this.cause = cause; return this; } + public EngineException setContext(Context ctx) { + this.ctx = ctx; + return this; + } public String toString(Context ctx) throws InterruptedException { var ss = new StringBuilder(); diff --git a/src/me/topchetoeu/jscript/interop/Native.java b/src/me/topchetoeu/jscript/interop/Native.java index f98765a..c71992c 100644 --- a/src/me/topchetoeu/jscript/interop/Native.java +++ b/src/me/topchetoeu/jscript/interop/Native.java @@ -9,4 +9,5 @@ import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) public @interface Native { public String value() default ""; + public boolean raw() default false; } diff --git a/src/me/topchetoeu/jscript/interop/NativeConstructor.java b/src/me/topchetoeu/jscript/interop/NativeConstructor.java new file mode 100644 index 0000000..5603a77 --- /dev/null +++ b/src/me/topchetoeu/jscript/interop/NativeConstructor.java @@ -0,0 +1,13 @@ +package me.topchetoeu.jscript.interop; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +public @interface NativeConstructor { + public boolean raw() default false; +} + diff --git a/src/me/topchetoeu/jscript/interop/NativeGetter.java b/src/me/topchetoeu/jscript/interop/NativeGetter.java index ea5651b..5e94a5e 100644 --- a/src/me/topchetoeu/jscript/interop/NativeGetter.java +++ b/src/me/topchetoeu/jscript/interop/NativeGetter.java @@ -9,4 +9,5 @@ import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) public @interface NativeGetter { public String value(); + public boolean raw() default false; } diff --git a/src/me/topchetoeu/jscript/interop/NativeSetter.java b/src/me/topchetoeu/jscript/interop/NativeSetter.java index bcb5cd3..b460831 100644 --- a/src/me/topchetoeu/jscript/interop/NativeSetter.java +++ b/src/me/topchetoeu/jscript/interop/NativeSetter.java @@ -9,4 +9,5 @@ import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) public @interface NativeSetter { public String value(); + public boolean raw() default false; } diff --git a/src/me/topchetoeu/jscript/interop/NativeTypeRegister.java b/src/me/topchetoeu/jscript/interop/NativeTypeRegister.java index ac6eb85..f927415 100644 --- a/src/me/topchetoeu/jscript/interop/NativeTypeRegister.java +++ b/src/me/topchetoeu/jscript/interop/NativeTypeRegister.java @@ -13,25 +13,35 @@ public class NativeTypeRegister implements WrappersProvider { private final HashMap, FunctionValue> constructors = new HashMap<>(); private final HashMap, ObjectValue> prototypes = new HashMap<>(); + private static boolean isMember(Class[] args) { + return args.length == 3; + } + private static void applyMethods(boolean member, ObjectValue target, Class clazz) { for (var method : clazz.getDeclaredMethods()) { - if (!Modifier.isStatic(method.getModifiers()) != member) continue; - var nat = method.getAnnotation(Native.class); var get = method.getAnnotation(NativeGetter.class); var set = method.getAnnotation(NativeSetter.class); + var params = method.getParameterTypes(); + var memberMismatch = !Modifier.isStatic(method.getModifiers()) != member; if (nat != null) { + if (nat.raw()) { + if (isMember(params) != member) continue; + } + else if (memberMismatch) continue; + var name = nat.value(); var val = target.values.get(name); if (name.equals("")) name = method.getName(); if (!(val instanceof OverloadFunction)) target.defineProperty(null, name, val = new OverloadFunction(name)); - ((OverloadFunction)val).overloads.add(Overload.fromMethod(method)); + ((OverloadFunction)val).overloads.add(Overload.fromMethod(method, nat.raw())); } else { if (get != null) { + if (get.raw() && isMember(params) != member || memberMismatch) continue; var name = get.value(); var prop = target.properties.get(name); OverloadFunction getter = null; @@ -40,10 +50,11 @@ public class NativeTypeRegister implements WrappersProvider { if (prop != null && prop.getter instanceof OverloadFunction) getter = (OverloadFunction)prop.getter; else getter = new OverloadFunction("get " + name); - getter.overloads.add(Overload.fromMethod(method)); + getter.overloads.add(Overload.fromMethod(method, get.raw())); target.defineProperty(null, name, getter, setter, true, true); } if (set != null) { + if (set.raw() && isMember(params) != member || memberMismatch) continue; var name = set.value(); var prop = target.properties.get(name); var getter = prop == null ? null : prop.getter; @@ -52,7 +63,7 @@ public class NativeTypeRegister implements WrappersProvider { if (prop != null && prop.setter instanceof OverloadFunction) setter = (OverloadFunction)prop.setter; else setter = new OverloadFunction("set " + name); - setter.overloads.add(Overload.fromMethod(method)); + setter.overloads.add(Overload.fromMethod(method, set.raw())); target.defineProperty(null, name, getter, setter, true, true); } } @@ -66,8 +77,8 @@ public class NativeTypeRegister implements WrappersProvider { if (nat != null) { var name = nat.value(); if (name.equals("")) name = field.getName(); - var getter = new OverloadFunction("get " + name).add(Overload.getterFromField(field)); - var setter = new OverloadFunction("set " + name).add(Overload.setterFromField(field)); + var getter = new OverloadFunction("get " + name).add(Overload.getterFromField(field, nat.raw())); + var setter = new OverloadFunction("set " + name).add(Overload.setterFromField(field, nat.raw())); target.defineProperty(null, name, getter, setter, true, false); } } @@ -113,8 +124,14 @@ public class NativeTypeRegister implements WrappersProvider { FunctionValue func = new OverloadFunction(clazz.getName()); for (var overload : clazz.getConstructors()) { - if (overload.getAnnotation(Native.class) == null) continue; - ((OverloadFunction)func).add(Overload.fromConstructor(overload)); + var nat = overload.getAnnotation(Native.class); + if (nat == null) continue; + ((OverloadFunction)func).add(Overload.fromConstructor(overload, nat.raw())); + } + for (var overload : clazz.getMethods()) { + var constr = overload.getAnnotation(NativeConstructor.class); + if (constr == null) continue; + ((OverloadFunction)func).add(Overload.fromMethod(overload, constr.raw())); } if (((OverloadFunction)func).overloads.size() == 0) { diff --git a/src/me/topchetoeu/jscript/interop/Overload.java b/src/me/topchetoeu/jscript/interop/Overload.java index 191f0e4..740fe99 100644 --- a/src/me/topchetoeu/jscript/interop/Overload.java +++ b/src/me/topchetoeu/jscript/interop/Overload.java @@ -17,53 +17,63 @@ public class Overload { public final OverloadRunner runner; public final boolean variadic; + public final boolean raw; public final Class thisArg; public final Class[] params; - public static Overload fromMethod(Method method) { + public static Overload fromMethod(Method method, boolean raw) { return new Overload( (ctx, th, args) -> method.invoke(th, args), - method.isVarArgs(), + method.isVarArgs(), raw, Modifier.isStatic(method.getModifiers()) ? null : method.getDeclaringClass(), method.getParameterTypes() ); } - public static Overload fromConstructor(Constructor method) { + public static Overload fromConstructor(Constructor method, boolean raw) { return new Overload( (ctx, th, args) -> method.newInstance(args), - method.isVarArgs(), + method.isVarArgs(), raw, Modifier.isStatic(method.getModifiers()) ? null : method.getDeclaringClass(), method.getParameterTypes() ); } - public static Overload getterFromField(Field field) { + public static Overload getterFromField(Field field, boolean raw) { return new Overload( - (ctx, th, args) -> field.get(th), false, + (ctx, th, args) -> field.get(th), false, raw, Modifier.isStatic(field.getModifiers()) ? null : field.getDeclaringClass(), new Class[0] ); } - public static Overload setterFromField(Field field) { + public static Overload setterFromField(Field field, boolean raw) { if (Modifier.isFinal(field.getModifiers())) return null; return new Overload( - (ctx, th, args) -> { field.set(th, args[0]); return null; }, false, + (ctx, th, args) -> { field.set(th, args[0]); return null; }, false, raw, Modifier.isStatic(field.getModifiers()) ? null : field.getDeclaringClass(), new Class[0] ); } - public static Overload getter(Class thisArg, OverloadRunner runner) { + public static Overload getter(Class thisArg, OverloadRunner runner, boolean raw) { return new Overload( - (ctx, th, args) -> runner.run(ctx, th, args), false, + (ctx, th, args) -> runner.run(ctx, th, args), false, raw, thisArg, new Class[0] ); } - public Overload(OverloadRunner runner, boolean variadic, Class thisArg, Class args[]) { + public Overload(OverloadRunner runner, boolean variadic, boolean raw, Class thisArg, Class args[]) { this.runner = runner; this.variadic = variadic; + this.raw = raw; this.thisArg = thisArg; this.params = args; + + if (raw) { + if (!( + thisArg == null && ( + args.length == 3 && args[0] == Context.class && args[1] == Object.class && args[2] == Object[].class || + args.length == 2 && args[0] == Context.class && args[1] == Object[].class + ))) throw new IllegalArgumentException("Invalid signature for raw method."); + } } } \ No newline at end of file diff --git a/src/me/topchetoeu/jscript/interop/OverloadFunction.java b/src/me/topchetoeu/jscript/interop/OverloadFunction.java index 321b04d..89a66d9 100644 --- a/src/me/topchetoeu/jscript/interop/OverloadFunction.java +++ b/src/me/topchetoeu/jscript/interop/OverloadFunction.java @@ -8,41 +8,61 @@ import java.util.List; import me.topchetoeu.jscript.engine.Context; import me.topchetoeu.jscript.engine.values.FunctionValue; import me.topchetoeu.jscript.engine.values.Values; +import me.topchetoeu.jscript.exceptions.ConvertException; import me.topchetoeu.jscript.exceptions.EngineException; public class OverloadFunction extends FunctionValue { public final List overloads = new ArrayList<>(); public Object call(Context ctx, Object thisArg, Object ...args) throws InterruptedException { - for (var overload : overloads) { - boolean consumesEngine = overload.params.length > 0 && overload.params[0] == Context.class; - int start = consumesEngine ? 1 : 0; - int end = overload.params.length - (overload.variadic ? 1 : 0); - + loop: for (var overload : overloads) { Object[] newArgs = new Object[overload.params.length]; - for (var i = start; i < end; i++) { - Object val; - - if (i - start >= args.length) val = null; - else val = args[i - start]; - - newArgs[i] = Values.convert(ctx, val, overload.params[i]); + if (overload.raw) { + newArgs[0] = ctx; + newArgs[1] = thisArg; + newArgs[2] = args; } + else { + boolean consumesEngine = overload.params.length > 0 && overload.params[0] == Context.class; + int start = consumesEngine ? 1 : 0; + int end = overload.params.length - (overload.variadic ? 1 : 0); - if (overload.variadic) { - var type = overload.params[overload.params.length - 1].getComponentType(); - var n = Math.max(args.length - end + start, 0); - Object varArg = Array.newInstance(type, n); + for (var i = start; i < end; i++) { + Object val; - for (var i = 0; i < n; i++) { - Array.set(varArg, i, Values.convert(ctx, args[i + end - start], type)); + if (i - start >= args.length) val = null; + else val = args[i - start]; + + try { + newArgs[i] = Values.convert(ctx, val, overload.params[i]); + } + catch (ConvertException e) { + if (overloads.size() > 1) continue loop; + else throw EngineException.ofType(String.format("Argument %d can't be converted from %s to %s", i - start, e.source, e.target)); + } } - newArgs[newArgs.length - 1] = varArg; - } + if (overload.variadic) { + var type = overload.params[overload.params.length - 1].getComponentType(); + var n = Math.max(args.length - end + start, 0); + Object varArg = Array.newInstance(type, n); - if (consumesEngine) newArgs[0] = ctx; + for (var i = 0; i < n; i++) { + try { + Array.set(varArg, i, Values.convert(ctx, args[i + end - start], type)); + } + catch (ConvertException e) { + if (overloads.size() > 1) continue loop; + else throw EngineException.ofType(String.format("Element in variadic argument can't be converted from %s to %s", e.source, e.target)); + } + } + + newArgs[newArgs.length - 1] = varArg; + } + + if (consumesEngine) newArgs[0] = ctx; + } Object _this = overload.thisArg == null ? null : Values.convert(ctx, thisArg, overload.thisArg); diff --git a/src/me/topchetoeu/jscript/parsing/Parsing.java b/src/me/topchetoeu/jscript/parsing/Parsing.java index 0672cb5..54725b9 100644 --- a/src/me/topchetoeu/jscript/parsing/Parsing.java +++ b/src/me/topchetoeu/jscript/parsing/Parsing.java @@ -13,7 +13,7 @@ import me.topchetoeu.jscript.compilation.VariableDeclareStatement.Pair; import me.topchetoeu.jscript.compilation.control.*; import me.topchetoeu.jscript.compilation.control.SwitchStatement.SwitchCase; import me.topchetoeu.jscript.compilation.values.*; -import me.topchetoeu.jscript.engine.FunctionContext; +import me.topchetoeu.jscript.engine.Environment; import me.topchetoeu.jscript.engine.Operation; import me.topchetoeu.jscript.engine.scope.ValueVariable; import me.topchetoeu.jscript.engine.values.CodeFunction; @@ -1875,7 +1875,7 @@ public class Parsing { return list.toArray(Statement[]::new); } - public static CodeFunction compile(FunctionContext environment, Statement ...statements) { + public static CodeFunction compile(Environment environment, Statement ...statements) { var target = environment.global.globalChild(); var subscope = target.child(); var res = new ArrayList(); @@ -1905,7 +1905,7 @@ public class Parsing { return new CodeFunction(environment, "", subscope.localsCount(), 0, new ValueVariable[0], res.toArray(Instruction[]::new)); } - public static CodeFunction compile(FunctionContext environment, String filename, String raw) { + public static CodeFunction compile(Environment environment, String filename, String raw) { try { return compile(environment, parse(filename, raw)); } diff --git a/src/me/topchetoeu/jscript/polyfills/AsyncFunctionPolyfill.java b/src/me/topchetoeu/jscript/polyfills/AsyncFunctionPolyfill.java new file mode 100644 index 0000000..0661995 --- /dev/null +++ b/src/me/topchetoeu/jscript/polyfills/AsyncFunctionPolyfill.java @@ -0,0 +1,76 @@ +package me.topchetoeu.jscript.polyfills; + +import me.topchetoeu.jscript.engine.Context; +import me.topchetoeu.jscript.engine.frame.CodeFrame; +import me.topchetoeu.jscript.engine.frame.Runners; +import me.topchetoeu.jscript.engine.values.CodeFunction; +import me.topchetoeu.jscript.engine.values.FunctionValue; +import me.topchetoeu.jscript.engine.values.NativeFunction; +import me.topchetoeu.jscript.exceptions.EngineException; + +public class AsyncFunctionPolyfill extends FunctionValue { + public final FunctionValue factory; + + public static class AsyncHelper { + public PromisePolyfill promise = new PromisePolyfill(); + public CodeFrame frame; + + private boolean awaiting = false; + + private void next(Context ctx, Object inducedValue, Object inducedError) throws InterruptedException { + Object res = null; + ctx.message.pushFrame(frame); + + awaiting = false; + while (!awaiting) { + try { + res = frame.next(ctx, inducedValue, Runners.NO_RETURN, inducedError); + inducedValue = inducedError = Runners.NO_RETURN; + if (res != Runners.NO_RETURN) { + promise.fulfill(ctx, res); + break; + } + } + catch (EngineException e) { + promise.reject(ctx, e.value); + break; + } + } + + ctx.message.popFrame(frame); + + if (awaiting) { + PromisePolyfill.then(ctx, frame.pop(), new NativeFunction(this::fulfill), new NativeFunction(this::reject)); + } + } + + public Object fulfill(Context ctx, Object thisArg, Object ...args) throws InterruptedException { + next(ctx, args.length > 0 ? args[0] : null, Runners.NO_RETURN); + return null; + } + public Object reject(Context ctx, Object thisArg, Object ...args) throws InterruptedException { + next(ctx, Runners.NO_RETURN, args.length > 0 ? args[0] : null); + return null; + } + + public Object await(Context ctx, Object thisArg, Object[] args) { + this.awaiting = true; + return args.length > 0 ? args[0] : null; + } + } + + @Override + public Object call(Context ctx, Object thisArg, Object ...args) throws InterruptedException { + var handler = new AsyncHelper(); + var func = factory.call(ctx, thisArg, new NativeFunction("await", handler::await)); + if (!(func instanceof CodeFunction)) throw EngineException.ofType("Return value of argument must be a js function."); + handler.frame = new CodeFrame(ctx, thisArg, args, (CodeFunction)func); + handler.next(ctx, Runners.NO_RETURN, Runners.NO_RETURN); + return handler.promise; + } + + public AsyncFunctionPolyfill(FunctionValue factory) { + super(factory.name, factory.length); + this.factory = factory; + } +} diff --git a/src/me/topchetoeu/jscript/polyfills/AsyncGeneratorPolyfill.java b/src/me/topchetoeu/jscript/polyfills/AsyncGeneratorPolyfill.java new file mode 100644 index 0000000..4328d61 --- /dev/null +++ b/src/me/topchetoeu/jscript/polyfills/AsyncGeneratorPolyfill.java @@ -0,0 +1,133 @@ +package me.topchetoeu.jscript.polyfills; + +import java.util.Map; + +import me.topchetoeu.jscript.engine.Context; +import me.topchetoeu.jscript.engine.frame.CodeFrame; +import me.topchetoeu.jscript.engine.frame.Runners; +import me.topchetoeu.jscript.engine.values.CodeFunction; +import me.topchetoeu.jscript.engine.values.FunctionValue; +import me.topchetoeu.jscript.engine.values.NativeFunction; +import me.topchetoeu.jscript.engine.values.ObjectValue; +import me.topchetoeu.jscript.exceptions.EngineException; +import me.topchetoeu.jscript.interop.Native; + +public class AsyncGeneratorPolyfill extends FunctionValue { + public final FunctionValue factory; + + public static class AsyncGenerator { + private int state = 0; + private boolean done = false; + private PromisePolyfill currPromise; + public CodeFrame frame; + + private void next(Context ctx, Object inducedValue, Object inducedReturn, Object inducedError) throws InterruptedException { + if (done) { + if (inducedError != Runners.NO_RETURN) + throw new EngineException(inducedError); + currPromise.fulfill(ctx, new ObjectValue(ctx, Map.of( + "done", true, + "value", inducedReturn == Runners.NO_RETURN ? null : inducedReturn + ))); + return; + } + + Object res = null; + ctx.message.pushFrame(frame); + state = 0; + + while (state == 0) { + try { + res = frame.next(ctx, inducedValue, inducedReturn, inducedError); + inducedValue = inducedReturn = inducedError = Runners.NO_RETURN; + if (res != Runners.NO_RETURN) { + var obj = new ObjectValue(); + obj.defineProperty(ctx, "done", true); + obj.defineProperty(ctx, "value", res); + currPromise.fulfill(ctx, obj); + break; + } + } + catch (EngineException e) { + currPromise.reject(ctx, e.value); + break; + } + } + + ctx.message.popFrame(frame); + + if (state == 1) { + PromisePolyfill.then(ctx, frame.pop(), new NativeFunction(this::fulfill), new NativeFunction(this::reject)); + } + else if (state == 2) { + var obj = new ObjectValue(); + obj.defineProperty(ctx, "done", false); + obj.defineProperty(ctx, "value", frame.pop()); + currPromise.fulfill(ctx, obj); + } + } + + @Override + public String toString() { + if (done) return "Generator [closed]"; + if (state != 0) return "Generator [suspended]"; + return "Generator [running]"; + } + + public Object fulfill(Context ctx, Object thisArg, Object ...args) throws InterruptedException { + next(ctx, args.length > 0 ? args[0] : null, Runners.NO_RETURN, Runners.NO_RETURN); + return null; + } + public Object reject(Context ctx, Object thisArg, Object ...args) throws InterruptedException { + next(ctx, Runners.NO_RETURN, args.length > 0 ? args[0] : null, Runners.NO_RETURN); + return null; + } + + @Native + public PromisePolyfill next(Context ctx, Object ...args) throws InterruptedException { + this.currPromise = new PromisePolyfill(); + if (args.length == 0) next(ctx, Runners.NO_RETURN, Runners.NO_RETURN, Runners.NO_RETURN); + else next(ctx, args[0], Runners.NO_RETURN, Runners.NO_RETURN); + return this.currPromise; + } + @Native("throw") + public PromisePolyfill _throw(Context ctx, Object error) throws InterruptedException { + this.currPromise = new PromisePolyfill(); + next(ctx, Runners.NO_RETURN, Runners.NO_RETURN, error); + return this.currPromise; + } + @Native("return") + public PromisePolyfill _return(Context ctx, Object value) throws InterruptedException { + this.currPromise = new PromisePolyfill(); + next(ctx, Runners.NO_RETURN, value, Runners.NO_RETURN); + return this.currPromise; + } + + + public Object await(Context ctx, Object thisArg, Object[] args) { + this.state = 1; + return args.length > 0 ? args[0] : null; + } + public Object yield(Context ctx, Object thisArg, Object[] args) { + this.state = 2; + return args.length > 0 ? args[0] : null; + } + } + + @Override + public Object call(Context ctx, Object thisArg, Object ...args) throws InterruptedException { + var handler = new AsyncGenerator(); + var func = factory.call(ctx, thisArg, + new NativeFunction("await", handler::await), + new NativeFunction("yield", handler::yield) + ); + if (!(func instanceof CodeFunction)) throw EngineException.ofType("Return value of argument must be a js function."); + handler.frame = new CodeFrame(ctx, thisArg, args, (CodeFunction)func); + return handler; + } + + public AsyncGeneratorPolyfill(FunctionValue factory) { + super(factory.name, factory.length); + this.factory = factory; + } +} diff --git a/src/me/topchetoeu/jscript/polyfills/FunctionPolyfill.java b/src/me/topchetoeu/jscript/polyfills/FunctionPolyfill.java new file mode 100644 index 0000000..89253d0 --- /dev/null +++ b/src/me/topchetoeu/jscript/polyfills/FunctionPolyfill.java @@ -0,0 +1,60 @@ +package me.topchetoeu.jscript.polyfills; + +import me.topchetoeu.jscript.engine.Context; +import me.topchetoeu.jscript.engine.values.ArrayValue; +import me.topchetoeu.jscript.engine.values.FunctionValue; +import me.topchetoeu.jscript.engine.values.NativeFunction; +import me.topchetoeu.jscript.exceptions.EngineException; +import me.topchetoeu.jscript.interop.Native; + +public class FunctionPolyfill { + @Native(raw=true) public static Object apply(Context ctx, Object func, Object[] args) throws InterruptedException { + var thisArg = args.length > 0 ? args[0] : null; + var _args = args.length > 1 ? args[1] : null; + + if (!(_args instanceof ArrayValue)) throw EngineException.ofError("Expected arguments to be an array."); + if (!(func instanceof FunctionValue)) throw EngineException.ofError("Expected this to be a function."); + + return ((FunctionValue)func).call(ctx, thisArg, ((ArrayValue)_args).toArray()); + } + @Native(raw=true) public static Object call(Context ctx, Object func, Object[] args) throws InterruptedException { + var thisArg = args.length > 0 ? args[0] : null; + var _args = new Object[args.length > 1 ? args.length - 1 : 0]; + + if (!(func instanceof FunctionValue)) throw EngineException.ofError("Expected this to be a function."); + + if (_args.length != 0) System.arraycopy(args, 1, _args, 0, _args.length); + + return ((FunctionValue)func).call(ctx, thisArg, _args); + } + @Native(raw=true) public static Object bind(Context ctx, Object func, Object[] args) { + var thisArg = args.length > 0 ? args[0] : null; + var _args = new Object[args.length > 1 ? args.length - 1 : 0]; + FunctionValue _func = (FunctionValue)func; + + if (!(func instanceof FunctionValue)) throw EngineException.ofError("Expected this to be a function."); + + if (_args.length != 0) System.arraycopy(args, 1, _args, 0, _args.length); + + return new NativeFunction(_func.name + " (bound)", (callCtx, _0, callArgs) -> { + var resArgs = new Object[_args.length + callArgs.length]; + System.arraycopy(_args, 0, resArgs, 0, _args.length); + System.arraycopy(callArgs, 0, resArgs, _args.length, resArgs.length - _args.length); + + return _func.call(ctx, thisArg, resArgs); + }); + } + @Native(raw=true) public static String toString(Context ctx, Object func, Object[] args) { + return "function (...) { ... }"; + } + + @Native public static FunctionValue async(FunctionValue func) { + return new AsyncFunctionPolyfill(func); + } + @Native public static FunctionValue asyncGenerator(FunctionValue func) { + return new AsyncGeneratorPolyfill(func); + } + @Native public static FunctionValue generator(FunctionValue func) { + return new GeneratorPolyfill(func); + } +} diff --git a/src/me/topchetoeu/jscript/polyfills/GeneratorFunction.java b/src/me/topchetoeu/jscript/polyfills/GeneratorPolyfill.java similarity index 91% rename from src/me/topchetoeu/jscript/polyfills/GeneratorFunction.java rename to src/me/topchetoeu/jscript/polyfills/GeneratorPolyfill.java index 17b098f..47f5080 100644 --- a/src/me/topchetoeu/jscript/polyfills/GeneratorFunction.java +++ b/src/me/topchetoeu/jscript/polyfills/GeneratorPolyfill.java @@ -10,7 +10,7 @@ import me.topchetoeu.jscript.engine.values.ObjectValue; import me.topchetoeu.jscript.exceptions.EngineException; import me.topchetoeu.jscript.interop.Native; -public class GeneratorFunction extends FunctionValue { +public class GeneratorPolyfill extends FunctionValue { public final FunctionValue factory; public static class Generator { @@ -28,12 +28,12 @@ public class GeneratorFunction extends FunctionValue { } Object res = null; - if (inducedValue != Runners.NO_RETURN) frame.push(ctx, inducedValue); ctx.message.pushFrame(frame); yielding = false; + while (!yielding) { try { - res = frame.next(ctx, inducedReturn, inducedError); + res = frame.next(ctx, inducedValue, inducedReturn, inducedError); inducedReturn = inducedError = Runners.NO_RETURN; if (res != Runners.NO_RETURN) { done = true; @@ -74,7 +74,7 @@ public class GeneratorFunction extends FunctionValue { public String toString() { if (done) return "Generator [closed]"; if (yielding) return "Generator [suspended]"; - return "Generator " + (done ? "[closed]" : "[suspended]"); + return "Generator [running]"; } public Object yield(Context ctx, Object thisArg, Object[] args) { @@ -92,7 +92,7 @@ public class GeneratorFunction extends FunctionValue { return handler; } - public GeneratorFunction(FunctionValue factory) { + public GeneratorPolyfill(FunctionValue factory) { super(factory.name, factory.length); this.factory = factory; } diff --git a/src/me/topchetoeu/jscript/polyfills/Internals.java b/src/me/topchetoeu/jscript/polyfills/Internals.java index 71fbbab..e584be7 100644 --- a/src/me/topchetoeu/jscript/polyfills/Internals.java +++ b/src/me/topchetoeu/jscript/polyfills/Internals.java @@ -1,7 +1,7 @@ package me.topchetoeu.jscript.polyfills; import me.topchetoeu.jscript.engine.Context; -import me.topchetoeu.jscript.engine.FunctionContext; +import me.topchetoeu.jscript.engine.Environment; import me.topchetoeu.jscript.engine.values.ArrayValue; import me.topchetoeu.jscript.engine.values.CodeFunction; import me.topchetoeu.jscript.engine.values.FunctionValue; @@ -12,20 +12,25 @@ import me.topchetoeu.jscript.engine.values.Values; import me.topchetoeu.jscript.interop.Native; public class Internals { + @Native public final Class object = ObjectPolyfill.class; + @Native public final Class function = FunctionPolyfill.class; + @Native public final Class promise = PromisePolyfill.class; + @Native public void markSpecial(FunctionValue ...funcs) { for (var func : funcs) { func.special = true; } } - @Native public FunctionContext getEnv(Object func) { + @Native public Environment getEnv(Object func) { if (func instanceof CodeFunction) return ((CodeFunction)func).environment; else return null; } - @Native public Object setEnv(Object func, FunctionContext env) { + @Native public Object setEnv(Object func, Environment env) { if (func instanceof CodeFunction) ((CodeFunction)func).environment = env; return func; } - @Native public Object apply(Context ctx, FunctionValue func, Object thisArg, ArrayValue args) throws InterruptedException { + @Native public Object apply(Context ctx, FunctionValue func, Object thisArg, ArrayValue args, Environment env) throws InterruptedException { + if (env != null) ctx = new Context(env, ctx.message); return func.call(ctx, thisArg, args.toArray()); } @Native public FunctionValue delay(Context ctx, double delay, FunctionValue callback) throws InterruptedException { @@ -84,8 +89,8 @@ public class Internals { @Native public boolean isArray(Object obj) { return obj instanceof ArrayValue; } - @Native public GeneratorFunction generator(FunctionValue obj) { - return new GeneratorFunction(obj); + @Native public GeneratorPolyfill generator(FunctionValue obj) { + return new GeneratorPolyfill(obj); } @Native public boolean defineField(Context ctx, ObjectValue obj, Object key, Object val, boolean writable, boolean enumerable, boolean configurable) { diff --git a/src/me/topchetoeu/jscript/polyfills/ObjectPolyfill.java b/src/me/topchetoeu/jscript/polyfills/ObjectPolyfill.java new file mode 100644 index 0000000..10226a4 --- /dev/null +++ b/src/me/topchetoeu/jscript/polyfills/ObjectPolyfill.java @@ -0,0 +1,212 @@ +package me.topchetoeu.jscript.polyfills; + +import me.topchetoeu.jscript.engine.Context; +import me.topchetoeu.jscript.engine.values.ArrayValue; +import me.topchetoeu.jscript.engine.values.FunctionValue; +import me.topchetoeu.jscript.engine.values.ObjectValue; +import me.topchetoeu.jscript.engine.values.Symbol; +import me.topchetoeu.jscript.engine.values.Values; +import me.topchetoeu.jscript.exceptions.EngineException; +import me.topchetoeu.jscript.interop.Native; +import me.topchetoeu.jscript.interop.NativeConstructor; + +public class ObjectPolyfill { + @Native public static ObjectValue assign(Context ctx, ObjectValue dst, Object... src) throws InterruptedException { + for (var obj : src) { + for (var key : Values.getMembers(ctx, obj, true, true)) { + Values.setMember(ctx, dst, key, Values.getMember(ctx, obj, key)); + } + } + return dst; + } + @Native public static ObjectValue create(Context ctx, ObjectValue proto, ObjectValue props) throws InterruptedException { + var obj = new ObjectValue(); + obj.setPrototype(ctx, proto); + return defineProperties(ctx, obj, props); + } + + @Native public static ObjectValue defineProperty(Context ctx, ObjectValue obj, Object key, ObjectValue attrib) throws InterruptedException { + var hasVal = attrib.hasMember(ctx, "value", false); + var hasGet = attrib.hasMember(ctx, "get", false); + var hasSet = attrib.hasMember(ctx, "set", false); + + if (hasVal) { + if (hasGet || hasSet) throw EngineException.ofType("Cannot specify a value and accessors for a property."); + if (!obj.defineProperty( + ctx, key, + attrib.getMember(ctx, "value"), + Values.toBoolean(attrib.getMember(ctx, "writable")), + Values.toBoolean(attrib.getMember(ctx, "configurable")), + Values.toBoolean(attrib.getMember(ctx, "enumerable")) + )) throw EngineException.ofType("Can't define property '" + key + "'."); + } + else { + var get = attrib.getMember(ctx, "get"); + var set = attrib.getMember(ctx, "set"); + if (get != null && !(get instanceof FunctionValue)) throw EngineException.ofType("Get accessor must be a function."); + if (set != null && !(set instanceof FunctionValue)) throw EngineException.ofType("Set accessor must be a function."); + + if (!obj.defineProperty( + ctx, key, + (FunctionValue)get, (FunctionValue)set, + Values.toBoolean(attrib.getMember(ctx, "configurable")), + Values.toBoolean(attrib.getMember(ctx, "enumerable")) + )) throw EngineException.ofType("Can't define property '" + key + "'."); + } + + return obj; + } + @Native public static ObjectValue defineProperties(Context ctx, ObjectValue obj, ObjectValue attrib) throws InterruptedException { + for (var key : Values.getMembers(null, obj, false, false)) { + obj.defineProperty(ctx, key, attrib.getMember(ctx, key)); + } + + return obj; + } + + @Native public static ArrayValue keys(Context ctx, Object obj, Object all) throws InterruptedException { + var res = new ArrayValue(); + var _all = Values.toBoolean(all); + + for (var key : Values.getMembers(ctx, obj, true, false)) { + if (_all || !(key instanceof Symbol)) res.set(ctx, res.size(), key); + } + + return res; + } + @Native public static ArrayValue entries(Context ctx, Object obj, Object all) throws InterruptedException { + var res = new ArrayValue(); + var _all = Values.toBoolean(all); + + for (var key : Values.getMembers(ctx, obj, true, false)) { + if (_all || !(key instanceof Symbol)) res.set(ctx, res.size(), new ArrayValue(ctx, key, Values.getMember(ctx, obj, key))); + } + + return res; + } + @Native public static ArrayValue values(Context ctx, Object obj, Object all) throws InterruptedException { + var res = new ArrayValue(); + var _all = Values.toBoolean(all); + + for (var key : Values.getMembers(ctx, obj, true, false)) { + if (_all || key instanceof String) res.set(ctx, res.size(), Values.getMember(ctx, obj, key)); + } + + return res; + } + + @Native public static ObjectValue getOwnPropertyDescriptor(Context ctx, Object obj, Object key) throws InterruptedException { + return Values.getMemberDescriptor(ctx, obj, key); + } + @Native public static ObjectValue getOwnPropertyDescriptors(Context ctx, Object obj) throws InterruptedException { + var res = new ObjectValue(); + for (var key : Values.getMembers(ctx, obj, true, true)) { + res.defineProperty(ctx, key, getOwnPropertyDescriptor(ctx, obj, key)); + } + return res; + } + + @Native public static ArrayValue getOwnPropertyNames(Context ctx, Object obj, Object all) throws InterruptedException { + var res = new ArrayValue(); + var _all = Values.toBoolean(all); + + for (var key : Values.getMembers(ctx, obj, true, true)) { + if (_all || !(key instanceof Symbol)) res.set(ctx, res.size(), key); + } + + return res; + } + @Native public static ArrayValue getOwnPropertySymbols(Context ctx, Object obj) throws InterruptedException { + var res = new ArrayValue(); + + for (var key : Values.getMembers(ctx, obj, true, true)) { + if (key instanceof Symbol) res.set(ctx, res.size(), key); + } + + return res; + } + @Native public static boolean hasOwn(Context ctx, Object obj, Object key) throws InterruptedException { + return Values.hasMember(ctx, obj, key, true); + } + + @Native public static ObjectValue getPrototypeOf(Context ctx, Object obj) throws InterruptedException { + return Values.getPrototype(ctx, obj); + } + @Native public static Object setPrototypeOf(Context ctx, Object obj, Object proto) throws InterruptedException { + Values.setPrototype(ctx, obj, proto); + return obj; + } + + @Native public static ObjectValue fromEntries(Context ctx, Object iterable) throws InterruptedException { + var res = new ObjectValue(); + + for (var el : Values.toJavaIterable(ctx, iterable)) { + if (el instanceof ArrayValue) { + res.defineProperty(ctx, ((ArrayValue)el).get(0), ((ArrayValue)el).get(1)); + } + } + + return res; + } + + @Native public static Object preventExtensions(Context ctx, Object obj) throws InterruptedException { + if (obj instanceof ObjectValue) ((ObjectValue)obj).preventExtensions(); + return obj; + } + @Native public static Object seal(Context ctx, Object obj) throws InterruptedException { + if (obj instanceof ObjectValue) ((ObjectValue)obj).seal(); + return obj; + } + @Native public static Object freeze(Context ctx, Object obj) throws InterruptedException { + if (obj instanceof ObjectValue) ((ObjectValue)obj).freeze(); + return obj; + } + + @Native public static boolean isExtensible(Context ctx, Object obj) throws InterruptedException { + return obj instanceof ObjectValue && ((ObjectValue)obj).extensible(); + } + @Native public static boolean isSealed(Context ctx, Object obj) throws InterruptedException { + if (obj instanceof ObjectValue && ((ObjectValue)obj).extensible()) { + var _obj = (ObjectValue)obj; + for (var key : _obj.keys(true)) { + if (_obj.memberConfigurable(key)) return false; + } + } + + return true; + } + @Native public static boolean isFrozen(Context ctx, Object obj) throws InterruptedException { + if (obj instanceof ObjectValue && ((ObjectValue)obj).extensible()) { + var _obj = (ObjectValue)obj; + for (var key : _obj.keys(true)) { + if (_obj.memberConfigurable(key)) return false; + if (_obj.memberWritable(key)) return false; + } + } + + return true; + } + + @Native(raw = true) public static Object valueOf(Context ctx, Object thisArg, Object[] args) { + return thisArg; + } + @Native(raw = true) public static String toString(Context ctx, Object thisArg, Object[] args) throws InterruptedException { + var name = Values.getMember(ctx, thisArg, ctx.env.symbol("Symbol.typeName")); + if (name == null) name = "Unknown"; + else name = Values.toString(ctx, name); + + return "[object " + name + "]"; + } + @Native(raw = true) public static boolean hasOwnProperty(Context ctx, Object thisArg, Object[] args) throws InterruptedException { + return ObjectPolyfill.hasOwn(ctx, thisArg, Values.convert(ctx, args.length == 0 ? null : args[0], String.class)); + } + + @NativeConstructor public static Object constructor(Context ctx, Object arg) throws InterruptedException { + if (arg == null || arg == Values.NULL) return new ObjectValue(); + else if (arg instanceof Boolean) return Values.callNew(ctx, ctx.env.global.get(ctx, "Boolean"), arg); + else if (arg instanceof Number) return Values.callNew(ctx, ctx.env.global.get(ctx, "Number"), arg); + else if (arg instanceof String) return Values.callNew(ctx, ctx.env.global.get(ctx, "String"), arg); + else if (arg instanceof Symbol) return Values.callNew(ctx, ctx.env.global.get(ctx, "Symbol"), arg); + else return arg; + } +} diff --git a/src/me/topchetoeu/jscript/polyfills/PromisePolyfill.java b/src/me/topchetoeu/jscript/polyfills/PromisePolyfill.java new file mode 100644 index 0000000..a640572 --- /dev/null +++ b/src/me/topchetoeu/jscript/polyfills/PromisePolyfill.java @@ -0,0 +1,346 @@ +package me.topchetoeu.jscript.polyfills; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import me.topchetoeu.jscript.engine.Context; +import me.topchetoeu.jscript.engine.MessageContext; +import me.topchetoeu.jscript.engine.values.ArrayValue; +import me.topchetoeu.jscript.engine.values.FunctionValue; +import me.topchetoeu.jscript.engine.values.NativeFunction; +import me.topchetoeu.jscript.engine.values.NativeWrapper; +import me.topchetoeu.jscript.engine.values.ObjectValue; +import me.topchetoeu.jscript.engine.values.Values; +import me.topchetoeu.jscript.exceptions.EngineException; +import me.topchetoeu.jscript.interop.Native; + +public class PromisePolyfill { + private static class Handle { + public final Context ctx; + public final FunctionValue fulfilled; + public final FunctionValue rejected; + + public Handle(Context ctx, FunctionValue fulfilled, FunctionValue rejected) { + this.ctx = ctx; + this.fulfilled = fulfilled; + this.rejected = rejected; + } + } + + @Native("resolve") + public static PromisePolyfill ofResolved(Context ctx, Object val) throws InterruptedException { + var res = new PromisePolyfill(); + res.fulfill(ctx, val); + return res; + } + @Native("reject") + public static PromisePolyfill ofRejected(Context ctx, Object val) throws InterruptedException { + var res = new PromisePolyfill(); + res.reject(ctx, val); + return res; + } + + @Native public static PromisePolyfill any(Context ctx, Object _promises) throws InterruptedException { + if (!Values.isArray(_promises)) throw EngineException.ofType("Expected argument for any to be an array."); + var promises = Values.array(_promises); + if (promises.size() == 0) return ofResolved(ctx, new ArrayValue()); + var n = new int[] { promises.size() }; + var res = new PromisePolyfill(); + + var errors = new ArrayValue(); + + for (var i = 0; i < promises.size(); i++) { + var index = i; + var val = promises.get(i); + then(ctx, val, + new NativeFunction(null, (e, th, args) -> { res.fulfill(e, args[0]); return null; }), + new NativeFunction(null, (e, th, args) -> { + errors.set(ctx, index, args[0]); + n[0]--; + if (n[0] <= 0) res.reject(e, errors); + return null; + }) + ); + } + + return res; + } + @Native public static PromisePolyfill race(Context ctx, Object _promises) throws InterruptedException { + if (!Values.isArray(_promises)) throw EngineException.ofType("Expected argument for any to be an array."); + var promises = Values.array(_promises); + if (promises.size() == 0) return ofResolved(ctx, new ArrayValue()); + var res = new PromisePolyfill(); + + for (var i = 0; i < promises.size(); i++) { + var val = promises.get(i); + then(ctx, val, + new NativeFunction(null, (e, th, args) -> { res.fulfill(e, args[0]); return null; }), + new NativeFunction(null, (e, th, args) -> { res.reject(e, args[0]); return null; }) + ); + } + + return res; + } + @Native public static PromisePolyfill all(Context ctx, Object _promises) throws InterruptedException { + if (!Values.isArray(_promises)) throw EngineException.ofType("Expected argument for any to be an array."); + var promises = Values.array(_promises); + if (promises.size() == 0) return ofResolved(ctx, new ArrayValue()); + var n = new int[] { promises.size() }; + var res = new PromisePolyfill(); + + var result = new ArrayValue(); + + for (var i = 0; i < promises.size(); i++) { + var index = i; + var val = promises.get(i); + then(ctx, val, + new NativeFunction(null, (e, th, args) -> { + result.set(ctx, index, args[0]); + n[0]--; + if (n[0] <= 0) res.fulfill(e, result); + return null; + }), + new NativeFunction(null, (e, th, args) -> { res.reject(e, args[0]); return null; }) + ); + } + + if (n[0] <= 0) res.fulfill(ctx, result); + + return res; + } + @Native public static PromisePolyfill allSettled(Context ctx, Object _promises) throws InterruptedException { + if (!Values.isArray(_promises)) throw EngineException.ofType("Expected argument for any to be an array."); + var promises = Values.array(_promises); + if (promises.size() == 0) return ofResolved(ctx, new ArrayValue()); + var n = new int[] { promises.size() }; + var res = new PromisePolyfill(); + + var result = new ArrayValue(); + + for (var i = 0; i < promises.size(); i++) { + var index = i; + var val = promises.get(i); + then(ctx, val, + new NativeFunction(null, (e, th, args) -> { + result.set(ctx, index, new ObjectValue(ctx, Map.of( + "status", "fulfilled", + "value", args[0] + ))); + n[0]--; + if (n[0] <= 0) res.fulfill(e, result); + return null; + }), + new NativeFunction(null, (e, th, args) -> { + result.set(ctx, index, new ObjectValue(ctx, Map.of( + "status", "rejected", + "reason", args[0] + ))); + n[0]--; + if (n[0] <= 0) res.fulfill(e, result); + return null; + }) + ); + } + + if (n[0] <= 0) res.fulfill(ctx, result); + + return res; + } + + /** + * Thread safe - you can call this from anywhere + * HOWEVER, it's strongly recommended to use this only in javascript + */ + @Native(raw = true) public static Object then(Context ctx, Object thisArg, Object ...args) throws InterruptedException { + var onFulfill = args.length > 0 ? args[0] : null; + var onReject = args.length > 1 ? args[1]: null; + + if (!(onFulfill instanceof FunctionValue)) onFulfill = null; + if (!(onReject instanceof FunctionValue)) onReject = null; + + var res = new PromisePolyfill(); + + var fulfill = onFulfill == null ? new NativeFunction((_ctx, _thisArg, _args) -> _args.length > 0 ? _args[0] : null) : (FunctionValue)onFulfill; + var reject = onReject == null ? new NativeFunction((_ctx, _thisArg, _args) -> { + throw new EngineException(_args.length > 0 ? _args[0] : null); + }) : (FunctionValue)onReject; + + if (thisArg instanceof NativeWrapper && ((NativeWrapper)thisArg).wrapped instanceof PromisePolyfill) { + thisArg = ((NativeWrapper)thisArg).wrapped; + } + + var fulfillHandle = new NativeFunction(null, (_ctx, th, a) -> { + try { + res.fulfill(ctx, Values.convert(ctx, fulfill.call(ctx, null, a[0]), Object.class)); + } + catch (EngineException err) { res.reject(ctx, err.value); } + return null; + }); + var rejectHandle = new NativeFunction(null, (_ctx, th, a) -> { + try { res.fulfill(ctx, reject.call(ctx, null, a[0])); } + catch (EngineException err) { res.reject(ctx, err.value); } + return null; + }); + + if (thisArg instanceof PromisePolyfill) ((PromisePolyfill)thisArg).handle(ctx, fulfillHandle, rejectHandle); + else { + Object next; + try { + next = Values.getMember(ctx, thisArg, "then"); + } + catch (IllegalArgumentException e) { next = null; } + + try { + if (next instanceof FunctionValue) ((FunctionValue)next).call(ctx, thisArg, fulfillHandle, rejectHandle); + else res.fulfill(ctx, fulfill.call(ctx, null, thisArg)); + } + catch (EngineException err) { + res.reject(ctx, fulfill.call(ctx, null, err.value)); + } + } + + return res; + } + /** + * Thread safe - you can call this from anywhere + * HOWEVER, it's strongly recommended to use this only in javascript + */ + @Native(value = "catch", raw = true) public static Object _catch(Context ctx, Object thisArg, Object ...args) throws InterruptedException { + return then(ctx, thisArg, null, args.length > 0 ? args[0] : null); + } + /** + * Thread safe - you can call this from anywhere + * HOWEVER, it's strongly recommended to use this only in javascript + */ + @Native(value = "finally", raw = true) public static Object _finally(Context ctx, Object thisArg, Object ...args) throws InterruptedException { + var handleFunc = args.length > 0 ? args[0] : null; + + return then(ctx, thisArg, + new NativeFunction(null, (e, th, _args) -> { + if (handleFunc instanceof FunctionValue) ((FunctionValue)handleFunc).call(ctx); + return args[0]; + }), + new NativeFunction(null, (e, th, _args) -> { + if (handleFunc instanceof FunctionValue) ((FunctionValue)handleFunc).call(ctx); + throw new EngineException(_args[0]); + }) + ); + } + + private List handles = new ArrayList<>(); + + private static final int STATE_PENDING = 0; + private static final int STATE_FULFILLED = 1; + private static final int STATE_REJECTED = 2; + + private int state = STATE_PENDING; + private boolean handled = false; + private Object val; + + private void resolve(Context ctx, Object val, int state) throws InterruptedException { + if (this.state != STATE_PENDING) return; + + if (val instanceof PromisePolyfill) ((PromisePolyfill)val).handle(ctx, + new NativeFunction(null, (e, th, a) -> { this.resolve(ctx, a[0], state); return null; }), + new NativeFunction(null, (e, th, a) -> { this.resolve(ctx, a[0], STATE_REJECTED); return null; }) + ); + else { + Object next; + try { next = Values.getMember(ctx, val, "next"); } + catch (IllegalArgumentException e) { next = null; } + + try { + if (next instanceof FunctionValue) ((FunctionValue)next).call(ctx, val, + new NativeFunction((e, _thisArg, a) -> { this.resolve(ctx, a.length > 0 ? a[0] : null, state); return null; }), + new NativeFunction((e, _thisArg, a) -> { this.resolve(ctx, a.length > 0 ? a[0] : null, STATE_REJECTED); return null; }) + ); + else { + this.val = val; + this.state = state; + + if (state == STATE_FULFILLED) { + for (var handle : handles) handle.fulfilled.call(handle.ctx, null, val); + } + else if (state == STATE_REJECTED) { + for (var handle : handles) handle.rejected.call(handle.ctx, null, val); + if (handles.size() == 0) { + ctx.message.engine.pushMsg(true, ctx.message, new NativeFunction((_ctx, _thisArg, _args) -> { + if (!handled) { + try { Values.printError(new EngineException(val), "(in promise)"); } + catch (InterruptedException ex) { } + } + + return null; + }), null); + } + } + + handles = null; + } + } + catch (EngineException err) { + this.reject(ctx, err.value); + } + } + } + + /** + * Thread safe - call from any thread + */ + public void fulfill(Context ctx, Object val) throws InterruptedException { + resolve(ctx, val, STATE_FULFILLED); + } + /** + * Thread safe - call from any thread + */ + public void reject(Context ctx, Object val) throws InterruptedException { + resolve(ctx, val, STATE_REJECTED); + } + + private void handle(Context ctx, FunctionValue fulfill, FunctionValue reject) { + if (state == STATE_FULFILLED) ctx.message.engine.pushMsg(true, new MessageContext(ctx.message.engine), fulfill, null, val); + else if (state == STATE_REJECTED) { + ctx.message.engine.pushMsg(true, new MessageContext(ctx.message.engine), reject, null, val); + handled = true; + } + else handles.add(new Handle(ctx, fulfill, reject)); + } + + @Override @Native public String toString() { + if (state == STATE_PENDING) return "Promise (pending)"; + else if (state == STATE_FULFILLED) return "Promise (fulfilled)"; + else return "Promise (rejected)"; + } + + /** + * NOT THREAD SAFE - must be called from the engine executor thread + */ + @Native public PromisePolyfill(Context ctx, FunctionValue func) throws InterruptedException { + if (!(func instanceof FunctionValue)) throw EngineException.ofType("A function must be passed to the promise constructor."); + try { + func.call( + ctx, null, + new NativeFunction(null, (e, th, args) -> { + fulfill(e, args.length > 0 ? args[0] : null); + return null; + }), + new NativeFunction(null, (e, th, args) -> { + reject(e, args.length > 0 ? args[0] : null); + return null; + }) + ); + } + catch (EngineException e) { + reject(ctx, e.value); + } + } + + private PromisePolyfill(int state, Object val) { + this.state = state; + this.val = val; + } + public PromisePolyfill() { + this(STATE_PENDING, null); + } +}