diff --git a/build.js b/build.js index 20a1f23..774fabe 100644 --- a/build.js +++ b/build.js @@ -54,7 +54,7 @@ async function compileJava() { .replace('${AUTHOR}', conf.author) ); const args = ['--release', '11', ]; - if (argv[1] === 'debug') args.push('-g'); + if (argv[2] === 'debug') args.push('-g'); args.push('-d', 'dst/classes', 'Metadata.java'); for await (const path of find('src', undefined, v => v.endsWith('.java') && !v.endsWith('Metadata.java'))) args.push(path); diff --git a/lib/core.ts b/lib/core.ts deleted file mode 100644 index 2dc232a..0000000 --- a/lib/core.ts +++ /dev/null @@ -1,71 +0,0 @@ -interface Environment { - global: typeof globalThis & Record; - proto(name: string): object; - setProto(name: string, val: object): void; -} -interface Internals { - markSpecial(...funcs: Function[]): void; - getEnv(func: Function): Environment | undefined; - setEnv(func: T, env: Environment): T; - apply(func: Function, thisArg: any, args: any[]): any; - delay(timeout: number, callback: Function): () => void; - pushMessage(micro: boolean, func: Function, thisArg: any, args: any[]): void; - - strlen(val: string): number; - char(val: string): number; - stringFromStrings(arr: string[]): string; - stringFromChars(arr: number[]): string; - symbol(name?: string): symbol; - symbolToString(sym: symbol): string; - - isArray(obj: any): boolean; - generator(func: (_yield: (val: T) => unknown) => (...args: any[]) => unknown): GeneratorFunction; - defineField(obj: object, key: any, val: any, writable: boolean, enumerable: boolean, configurable: boolean): boolean; - defineProp(obj: object, key: any, get: Function | undefined, set: Function | undefined, enumerable: boolean, configurable: boolean): boolean; - keys(obj: object, onlyString: boolean): any[]; - ownProp(obj: any, key: string): PropertyDescriptor; - ownPropKeys(obj: any): any[]; - lock(obj: object, type: 'ext' | 'seal' | 'freeze'): void; - extensible(obj: object): boolean; - - sort(arr: any[], comaprator: (a: any, b: any) => number): void; - - constructor: { - log(...args: any[]): void; - } -} - -var env: Environment = arguments[0], internals: Internals = arguments[1]; -globalThis.log = internals.constructor.log; - -try { - run('values/object'); - run('values/symbol'); - run('values/function'); - run('values/errors'); - run('values/string'); - run('values/number'); - run('values/boolean'); - run('values/array'); - run('promise'); - run('map'); - run('set'); - run('regex'); - run('timeout'); - - env.global.log = log; - - log('Loaded polyfills!'); -} -catch (e: any) { - let err = 'Uncaught error while loading polyfills: '; - - if (typeof Error !== 'undefined' && e instanceof Error && e.toString !== {}.toString) err += e; - else if ('message' in e) { - if ('name' in e) err += e.name + ": " + e.message; - else err += 'Error: ' + e.message; - } - else err += e; - - log(e); -} \ No newline at end of file diff --git a/lib/map.ts b/lib/map.ts deleted file mode 100644 index b7745af..0000000 --- a/lib/map.ts +++ /dev/null @@ -1,93 +0,0 @@ -define("map", () => { - const syms = { values: internals.symbol('Map.values') } as { readonly values: unique symbol }; - const Object = env.global.Object; - - class Map { - [syms.values]: any = {}; - - public [env.global.Symbol.iterator](): IterableIterator<[KeyT, ValueT]> { - return this.entries(); - } - - public clear() { - this[syms.values] = {}; - } - public delete(key: KeyT) { - if ((key as any) in this[syms.values]) { - delete this[syms.values]; - return true; - } - else return false; - } - - public entries(): IterableIterator<[KeyT, ValueT]> { - const keys = internals.ownPropKeys(this[syms.values]); - let i = 0; - - return { - next: () => { - if (i >= keys.length) return { done: true }; - else return { done: false, value: [ keys[i], this[syms.values][keys[i++]] ] } - }, - [env.global.Symbol.iterator]() { return this; } - } - } - public keys(): IterableIterator { - const keys = internals.ownPropKeys(this[syms.values]); - let i = 0; - - return { - next: () => { - if (i >= keys.length) return { done: true }; - else return { done: false, value: keys[i] } - }, - [env.global.Symbol.iterator]() { return this; } - } - } - public values(): IterableIterator { - const keys = internals.ownPropKeys(this[syms.values]); - let i = 0; - - return { - next: () => { - if (i >= keys.length) return { done: true }; - else return { done: false, value: this[syms.values][keys[i++]] } - }, - [env.global.Symbol.iterator]() { return this; } - } - } - - public get(key: KeyT) { - return this[syms.values][key]; - } - public set(key: KeyT, val: ValueT) { - this[syms.values][key] = val; - return this; - } - public has(key: KeyT) { - return (key as any) in this[syms.values][key]; - } - - public get size() { - return internals.ownPropKeys(this[syms.values]).length; - } - - public forEach(func: (key: KeyT, val: ValueT, map: Map) => void, thisArg?: any) { - const keys = internals.ownPropKeys(this[syms.values]); - - for (let i = 0; i < keys.length; i++) { - func(keys[i], this[syms.values][keys[i]], this); - } - } - - public constructor(iterable: Iterable<[KeyT, ValueT]>) { - const it = iterable[env.global.Symbol.iterator](); - - for (let el = it.next(); !el.done; el = it.next()) { - this[syms.values][el.value[0]] = el.value[1]; - } - } - } - - env.global.Map = Map; -}); diff --git a/lib/modules.ts b/lib/modules.ts deleted file mode 100644 index c15e496..0000000 --- a/lib/modules.ts +++ /dev/null @@ -1,13 +0,0 @@ -var { define, run } = (() => { - const modules: Record = {}; - - function define(name: string, func: Function) { - modules[name] = func; - } - function run(name: string) { - if (typeof modules[name] === 'function') return modules[name](); - else throw "The module '" + name + "' doesn't exist."; - } - - return { define, run }; -})(); diff --git a/lib/promise.ts b/lib/promise.ts deleted file mode 100644 index 903c7c2..0000000 --- a/lib/promise.ts +++ /dev/null @@ -1,203 +0,0 @@ -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, - } - - type Callback = [ PromiseFulfillFunc, PromiseRejectFunc ]; - enum State { - Pending, - Fulfilled, - Rejected, - } - - 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; - - for (let i = 0; i < promise[syms.callbacks]!.length; i++) { - promise[syms.handled] = true; - promise[syms.callbacks]![i][state - 1](v); - } - - promise[syms.callbacks] = undefined; - - internals.pushMessage(true, internals.setEnv(() => { - if (!promise[syms.handled] && state === State.Rejected) { - log('Uncaught (in promise) ' + promise[syms.value]); - } - }, env), undefined, []); - } - } - - class Promise { - public static isAwaitable(val: unknown): val is Thenable { - return isAwaitable(val); - } - - 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)); - } - - 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; - - 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); - } - - 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( - val => { - n++; - result[i] = val; - if (n === vals.length) res(result); - }, - rej - ); - else { - n++; - result[i] = val; - } - } - - 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 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); - }); - } - - [syms.callbacks]?: Callback[] = []; - [syms.handled] = false; - [syms.state] = State.Pending; - [syms.value]?: T | unknown; - - public then(onFulfil?: PromiseFulfillFunc, onReject?: PromiseRejectFunc) { - return new Promise((resolve, reject) => { - onFulfil ??= v => v; - onReject ??= v => v; - - 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/regex.ts b/lib/regex.ts deleted file mode 100644 index ed5ce84..0000000 --- a/lib/regex.ts +++ /dev/null @@ -1,143 +0,0 @@ -define("regex", () => { - // var RegExp = env.global.RegExp = env.internals.RegExp; - - // setProps(RegExp.prototype as RegExp, env, { - // [Symbol.typeName]: 'RegExp', - - // test(val) { - // return !!this.exec(val); - // }, - // toString() { - // return '/' + this.source + '/' + this.flags; - // }, - - // [Symbol.match](target) { - // if (this.global) { - // const res: string[] = []; - // let val; - // while (val = this.exec(target)) { - // res.push(val[0]); - // } - // this.lastIndex = 0; - // return res; - // } - // else { - // const res = this.exec(target); - // if (!this.sticky) this.lastIndex = 0; - // return res; - // } - // }, - // [Symbol.matchAll](target) { - // let pattern: RegExp | undefined = new this.constructor(this, this.flags + "g") as RegExp; - - // return { - // next: (): IteratorResult => { - // const val = pattern?.exec(target); - - // if (val === null || val === undefined) { - // pattern = undefined; - // return { done: true }; - // } - // else return { value: val }; - // }, - // [Symbol.iterator]() { return this; } - // } - // }, - // [Symbol.split](target, limit, sensible) { - // const pattern = new this.constructor(this, this.flags + "g") as RegExp; - // let match: RegExpResult | null; - // let lastEnd = 0; - // const res: string[] = []; - - // while ((match = pattern.exec(target)) !== null) { - // let added: string[] = []; - - // if (match.index >= target.length) break; - - // if (match[0].length === 0) { - // added = [ target.substring(lastEnd, pattern.lastIndex), ]; - // if (pattern.lastIndex < target.length) added.push(...match.slice(1)); - // } - // else if (match.index - lastEnd > 0) { - // added = [ target.substring(lastEnd, match.index), ...match.slice(1) ]; - // } - // else { - // for (let i = 1; i < match.length; i++) { - // res[res.length - match.length + i] = match[i]; - // } - // } - - // if (sensible) { - // if (limit !== undefined && res.length + added.length >= limit) break; - // else res.push(...added); - // } - // else { - // for (let i = 0; i < added.length; i++) { - // if (limit !== undefined && res.length >= limit) return res; - // else res.push(added[i]); - // } - // } - - // lastEnd = pattern.lastIndex; - // } - - // if (lastEnd < target.length) { - // res.push(target.substring(lastEnd)); - // } - - // return res; - // }, - // [Symbol.replace](target, replacement) { - // const pattern = new this.constructor(this, this.flags + "d") as RegExp; - // let match: RegExpResult | null; - // let lastEnd = 0; - // const res: string[] = []; - - // // log(pattern.toString()); - - // while ((match = pattern.exec(target)) !== null) { - // const indices = match.indices![0]; - // res.push(target.substring(lastEnd, indices[0])); - // if (replacement instanceof Function) { - // res.push(replacement(target.substring(indices[0], indices[1]), ...match.slice(1), indices[0], target)); - // } - // else { - // res.push(replacement); - // } - // lastEnd = indices[1]; - // if (!pattern.global) break; - // } - - // if (lastEnd < target.length) { - // res.push(target.substring(lastEnd)); - // } - - // return res.join(''); - // }, - // [Symbol.search](target, reverse, start) { - // const pattern: RegExp | undefined = new this.constructor(this, this.flags + "g") as RegExp; - - - // if (!reverse) { - // pattern.lastIndex = (start as any) | 0; - // const res = pattern.exec(target); - // if (res) return res.index; - // else return -1; - // } - // else { - // start ??= target.length; - // start |= 0; - // let res: RegExpResult | null = null; - - // while (true) { - // const tmp = pattern.exec(target); - // if (tmp === null || tmp.index > start) break; - // res = tmp; - // } - - // if (res && res.index <= start) return res.index; - // else return -1; - // } - // }, - // }); -}); \ No newline at end of file diff --git a/lib/set.ts b/lib/set.ts deleted file mode 100644 index a0ac277..0000000 --- a/lib/set.ts +++ /dev/null @@ -1,81 +0,0 @@ -define("set", () => { - const syms = { values: internals.symbol('Map.values') } as { readonly values: unique symbol }; - const Object = env.global.Object; - - class Set { - [syms.values]: any = {}; - - public [env.global.Symbol.iterator](): IterableIterator<[T, T]> { - return this.entries(); - } - - public clear() { - this[syms.values] = {}; - } - public delete(key: T) { - if ((key as any) in this[syms.values]) { - delete this[syms.values]; - return true; - } - else return false; - } - - public entries(): IterableIterator<[T, T]> { - const keys = internals.ownPropKeys(this[syms.values]); - let i = 0; - - return { - next: () => { - if (i >= keys.length) return { done: true }; - else return { done: false, value: [ keys[i], keys[i] ] } - }, - [env.global.Symbol.iterator]() { return this; } - } - } - public keys(): IterableIterator { - const keys = internals.ownPropKeys(this[syms.values]); - let i = 0; - - return { - next: () => { - if (i >= keys.length) return { done: true }; - else return { done: false, value: keys[i] } - }, - [env.global.Symbol.iterator]() { return this; } - } - } - public values(): IterableIterator { - return this.keys(); - } - - public add(val: T) { - this[syms.values][val] = undefined; - return this; - } - public has(key: T) { - return (key as any) in this[syms.values][key]; - } - - public get size() { - return internals.ownPropKeys(this[syms.values]).length; - } - - public forEach(func: (key: T, val: T, map: Set) => void, thisArg?: any) { - const keys = internals.ownPropKeys(this[syms.values]); - - for (let i = 0; i < keys.length; i++) { - func(keys[i], this[syms.values][keys[i]], this); - } - } - - public constructor(iterable: Iterable) { - const it = iterable[env.global.Symbol.iterator](); - - for (let el = it.next(); !el.done; el = it.next()) { - this[syms.values][el.value] = undefined; - } - } - } - - env.global.Set = Set; -}); diff --git a/lib/timeout.ts b/lib/timeout.ts deleted file mode 100644 index 8db82a0..0000000 --- a/lib/timeout.ts +++ /dev/null @@ -1,38 +0,0 @@ -define("timeout", () => { - const timeouts: Record void> = { }; - const intervals: Record void> = { }; - let timeoutI = 0, intervalI = 0; - - env.global.setTimeout = (func, delay, ...args) => { - if (typeof func !== 'function') throw new 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."); - delay = (delay ?? 0) - 0; - - const i = ++intervalI; - intervals[i] = internals.delay(delay, callback); - - return i; - - function callback() { - internals.apply(func, undefined, args); - intervals[i] = internals.delay(delay!, callback); - } - }; - - env.global.clearTimeout = (id) => { - const func = timeouts[id]; - if (func) func(); - timeouts[id] = undefined!; - }; - env.global.clearInterval = (id) => { - const func = intervals[id]; - if (func) func(); - intervals[id] = undefined!; - }; -}); \ No newline at end of file diff --git a/lib/tsconfig.json b/lib/tsconfig.json deleted file mode 100644 index 8c3e2af..0000000 --- a/lib/tsconfig.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "files": [ - "lib.d.ts", - "modules.ts", - "utils.ts", - "values/object.ts", - "values/symbol.ts", - "values/function.ts", - "values/errors.ts", - "values/string.ts", - "values/number.ts", - "values/boolean.ts", - "values/array.ts", - "promise.ts", - "map.ts", - "set.ts", - "regex.ts", - "timeout.ts", - "core.ts" - ], - "compilerOptions": { - "outFile": "../bin/me/topchetoeu/jscript/js/core.js", - // "declarationDir": "", - // "declarationDir": "bin/me/topchetoeu/jscript/dts", - "target": "ES5", - "lib": [], - "module": "None", - "stripInternal": true, - "downlevelIteration": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "strict": true, - } -} diff --git a/lib/utils.ts b/lib/utils.ts deleted file mode 100644 index 6fdf694..0000000 --- a/lib/utils.ts +++ /dev/null @@ -1,38 +0,0 @@ -function setProps< - TargetT extends object, - DescT extends { - [x in Exclude ]?: TargetT[x] extends ((...args: infer ArgsT) => infer RetT) ? - ((this: TargetT, ...args: ArgsT) => RetT) : - TargetT[x] - } ->(target: TargetT, desc: DescT) { - var props = internals.keys(desc, false); - for (var i = 0; i < props.length; i++) { - var key = props[i]; - internals.defineField( - target, key, (desc as any)[key], - true, // writable - false, // enumerable - true // configurable - ); - } -} -function setConstr(target: object, constr: Function) { - internals.defineField( - target, 'constructor', constr, - true, // writable - false, // enumerable - true // configurable - ); -} - -function wrapI(max: number, i: number) { - i |= 0; - if (i < 0) i = max + i; - return i; -} -function clampI(max: number, i: number) { - if (i < 0) i = 0; - if (i > max) i = max; - return i; -} \ No newline at end of file diff --git a/lib/values/array.ts b/lib/values/array.ts deleted file mode 100644 index 12928ba..0000000 --- a/lib/values/array.ts +++ /dev/null @@ -1,336 +0,0 @@ -define("values/array", () => { - var Array = env.global.Array = function(len?: number) { - var res = []; - - if (typeof len === 'number' && arguments.length === 1) { - if (len < 0) throw 'Invalid array length.'; - res.length = len; - } - else { - for (var i = 0; i < arguments.length; i++) { - res[i] = arguments[i]; - } - } - - return res; - } as ArrayConstructor; - - env.setProto('array', Array.prototype); - (Array.prototype as any)[env.global.Symbol.typeName] = "Array"; - setConstr(Array.prototype, Array); - - setProps(Array.prototype, { - [env.global.Symbol.iterator]: function() { - return this.values(); - }, - [env.global.Symbol.typeName]: "Array", - - values() { - var i = 0; - - return { - next: () => { - while (i < this.length) { - if (i++ in this) return { done: false, value: this[i - 1] }; - } - return { done: true, value: undefined }; - }, - [env.global.Symbol.iterator]() { return this; } - }; - }, - keys() { - var i = 0; - - return { - next: () => { - while (i < this.length) { - if (i++ in this) return { done: false, value: i - 1 }; - } - return { done: true, value: undefined }; - }, - [env.global.Symbol.iterator]() { return this; } - }; - }, - entries() { - var i = 0; - - return { - next: () => { - while (i < this.length) { - if (i++ in this) return { done: false, value: [i - 1, this[i - 1]] }; - } - return { done: true, value: undefined }; - }, - [env.global.Symbol.iterator]() { return this; } - }; - }, - concat() { - var res = [] as any[]; - res.push.apply(res, this); - - for (var i = 0; i < arguments.length; i++) { - var arg = arguments[i]; - if (arg instanceof Array) { - res.push.apply(res, arg); - } - else { - res.push(arg); - } - } - - return res; - }, - every(func, thisArg) { - if (typeof func !== 'function') throw new TypeError("Given argument not a function."); - func = func.bind(thisArg); - - for (var i = 0; i < this.length; i++) { - if (!func(this[i], i, this)) return false; - } - - return true; - }, - some(func, thisArg) { - if (typeof func !== 'function') throw new TypeError("Given argument not a function."); - func = func.bind(thisArg); - - for (var i = 0; i < this.length; i++) { - if (func(this[i], i, this)) return true; - } - - return false; - }, - fill(val, start, end) { - if (arguments.length < 3) end = this.length; - if (arguments.length < 2) start = 0; - - start = clampI(this.length, wrapI(this.length + 1, start ?? 0)); - end = clampI(this.length, wrapI(this.length + 1, end ?? this.length)); - - for (; start < end; start++) { - this[start] = val; - } - - return this; - }, - filter(func, thisArg) { - if (typeof func !== 'function') throw new TypeError("Given argument is not a function."); - - var res = []; - for (var i = 0; i < this.length; i++) { - if (i in this && func.call(thisArg, this[i], i, this)) res.push(this[i]); - } - return res; - }, - find(func, thisArg) { - if (typeof func !== 'function') throw new TypeError("Given argument is not a function."); - - for (var i = 0; i < this.length; i++) { - if (i in this && func.call(thisArg, this[i], i, this)) return this[i]; - } - - return undefined; - }, - findIndex(func, thisArg) { - if (typeof func !== 'function') throw new TypeError("Given argument is not a function."); - - for (var i = 0; i < this.length; i++) { - if (i in this && func.call(thisArg, this[i], i, this)) return i; - } - - return -1; - }, - findLast(func, thisArg) { - if (typeof func !== 'function') throw new TypeError("Given argument is not a function."); - - for (var i = this.length - 1; i >= 0; i--) { - if (i in this && func.call(thisArg, this[i], i, this)) return this[i]; - } - - return undefined; - }, - findLastIndex(func, thisArg) { - if (typeof func !== 'function') throw new TypeError("Given argument is not a function."); - - for (var i = this.length - 1; i >= 0; i--) { - if (i in this && func.call(thisArg, this[i], i, this)) return i; - } - - return -1; - }, - flat(depth) { - var res = [] as any[]; - var buff = []; - res.push(...this); - - for (var i = 0; i < (depth ?? 1); i++) { - var anyArrays = false; - for (var el of res) { - if (el instanceof Array) { - buff.push(...el); - anyArrays = true; - } - else buff.push(el); - } - - res = buff; - buff = []; - if (!anyArrays) break; - } - - return res; - }, - flatMap(func, th) { - return this.map(func, th).flat(); - }, - forEach(func, thisArg) { - for (var i = 0; i < this.length; i++) { - if (i in this) func.call(thisArg, this[i], i, this); - } - }, - map(func, thisArg) { - if (typeof func !== 'function') throw new TypeError("Given argument is not a function."); - - var res = []; - for (var i = 0; i < this.length; i++) { - if (i in this) res[i] = func.call(thisArg, this[i], i, this); - } - return res; - }, - pop() { - if (this.length === 0) return undefined; - var val = this[this.length - 1]; - this.length--; - return val; - }, - push() { - for (var i = 0; i < arguments.length; i++) { - this[this.length] = arguments[i]; - } - return arguments.length; - }, - shift() { - if (this.length === 0) return undefined; - var res = this[0]; - - for (var i = 0; i < this.length - 1; i++) { - this[i] = this[i + 1]; - } - - this.length--; - - return res; - }, - unshift() { - for (var i = this.length - 1; i >= 0; i--) { - this[i + arguments.length] = this[i]; - } - for (var i = 0; i < arguments.length; i++) { - this[i] = arguments[i]; - } - - return arguments.length; - }, - slice(start, end) { - start = clampI(this.length, wrapI(this.length + 1, start ?? 0)); - end = clampI(this.length, wrapI(this.length + 1, end ?? this.length)); - - var res: any[] = []; - var n = end - start; - if (n <= 0) return res; - - for (var i = 0; i < n; i++) { - res[i] = this[start + i]; - } - - return res; - }, - toString() { - let res = ''; - for (let i = 0; i < this.length; i++) { - if (i > 0) res += ','; - if (i in this && this[i] !== undefined && this[i] !== null) res += this[i]; - } - - return res; - }, - indexOf(el, start) { - start = start! | 0; - for (var i = Math.max(0, start); i < this.length; i++) { - if (i in this && this[i] == el) return i; - } - - return -1; - }, - lastIndexOf(el, start) { - start = start! | 0; - for (var i = this.length; i >= start; i--) { - if (i in this && this[i] == el) return i; - } - - return -1; - }, - includes(el, start) { - return this.indexOf(el, start) >= 0; - }, - join(val = ',') { - let res = '', first = true; - - for (let i = 0; i < this.length; i++) { - if (!(i in this)) continue; - if (!first) res += val; - first = false; - res += this[i]; - } - return res; - }, - sort(func) { - func ??= (a, b) => { - const _a = a + ''; - const _b = b + ''; - - if (_a > _b) return 1; - if (_a < _b) return -1; - return 0; - }; - - if (typeof func !== 'function') throw new TypeError('Expected func to be undefined or a function.'); - - internals.sort(this, func); - return this; - }, - splice(start, deleteCount, ...items) { - start = clampI(this.length, wrapI(this.length, start ?? 0)); - deleteCount = (deleteCount ?? Infinity | 0); - if (start + deleteCount >= this.length) deleteCount = this.length - start; - - const res = this.slice(start, start + deleteCount); - const moveN = items.length - deleteCount; - const len = this.length; - - if (moveN < 0) { - for (let i = start - moveN; i < len; i++) { - this[i + moveN] = this[i]; - } - } - else if (moveN > 0) { - for (let i = len - 1; i >= start; i--) { - this[i + moveN] = this[i]; - } - } - - for (let i = 0; i < items.length; i++) { - this[i + start] = items[i]; - } - - this.length = len + moveN; - - return res; - } - }); - - setProps(Array, { - isArray(val: any) { return internals.isArray(val); } - }); - internals.markSpecial(Array); -}); \ No newline at end of file diff --git a/lib/values/boolean.ts b/lib/values/boolean.ts deleted file mode 100644 index c0d45a2..0000000 --- a/lib/values/boolean.ts +++ /dev/null @@ -1,12 +0,0 @@ -define("values/boolean", () => { - var Boolean = env.global.Boolean = function (this: Boolean | undefined, arg) { - var val; - if (arguments.length === 0) val = false; - else val = !!arg; - if (this === undefined || this === null) return val; - else (this as any).value = val; - } as BooleanConstructor; - - env.setProto('bool', Boolean.prototype); - setConstr(Boolean.prototype, Boolean); -}); diff --git a/lib/values/errors.ts b/lib/values/errors.ts deleted file mode 100644 index 4bfc7e5..0000000 --- a/lib/values/errors.ts +++ /dev/null @@ -1,46 +0,0 @@ -define("values/errors", () => { - var Error = env.global.Error = function Error(msg: string) { - if (msg === undefined) msg = ''; - else msg += ''; - - return Object.setPrototypeOf({ - message: msg, - stack: [] as string[], - }, Error.prototype); - } as ErrorConstructor; - - setConstr(Error.prototype, Error); - setProps(Error.prototype, { - name: 'Error', - toString: internals.setEnv(function(this: Error) { - if (!(this instanceof Error)) return ''; - - if (this.message === '') return this.name; - else return this.name + ': ' + this.message; - }, env) - }); - env.setProto('error', Error.prototype); - internals.markSpecial(Error); - - function makeError(name: string, proto: string): T1 { - function constr (msg: string) { - var res = new Error(msg); - (res as any).__proto__ = constr.prototype; - return res; - } - - (constr as any).__proto__ = Error; - (constr.prototype as any).__proto__ = env.proto('error'); - setConstr(constr.prototype, constr as ErrorConstructor); - setProps(constr.prototype, { name: name }); - - internals.markSpecial(constr); - env.setProto(proto, constr.prototype); - - return constr as T1; - } - - env.global.RangeError = makeError('RangeError', 'rangeErr'); - env.global.TypeError = makeError('TypeError', 'typeErr'); - env.global.SyntaxError = makeError('SyntaxError', 'syntaxErr'); -}); \ No newline at end of file diff --git a/lib/values/function.ts b/lib/values/function.ts deleted file mode 100644 index 4a161d6..0000000 --- a/lib/values/function.ts +++ /dev/null @@ -1,140 +0,0 @@ -define("values/function", () => { - var Function = env.global.Function = function() { - throw 'Using the constructor Function() is forbidden.'; - } as unknown as FunctionConstructor; - - 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/number.ts b/lib/values/number.ts deleted file mode 100644 index 57e8d48..0000000 --- a/lib/values/number.ts +++ /dev/null @@ -1,33 +0,0 @@ -define("values/number", () => { - var Number = env.global.Number = function(this: Number | undefined, arg: any) { - var val; - if (arguments.length === 0) val = 0; - else val = arg - 0; - if (this === undefined || this === null) return val; - else (this as any).value = val; - } as NumberConstructor; - - env.setProto('number', Number.prototype); - setConstr(Number.prototype, Number); - - setProps(Number.prototype, { - valueOf() { - if (typeof this === 'number') return this; - else return (this as any).value; - }, - toString() { - if (typeof this === 'number') return this + ''; - else return (this as any).value + ''; - } - }); - - setProps(Number, { - parseInt(val) { return Math.trunc(val as any - 0); }, - parseFloat(val) { return val as any - 0; }, - }); - - env.global.parseInt = Number.parseInt; - env.global.parseFloat = Number.parseFloat; - env.global.Object.defineProperty(env.global, 'NaN', { value: 0 / 0, writable: false }); - env.global.Object.defineProperty(env.global, 'Infinity', { value: 1 / 0, writable: false }); -}); \ No newline at end of file diff --git a/lib/values/object.ts b/lib/values/object.ts deleted file mode 100644 index 475e4da..0000000 --- a/lib/values/object.ts +++ /dev/null @@ -1,226 +0,0 @@ -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); - (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); -}); \ No newline at end of file diff --git a/lib/values/string.ts b/lib/values/string.ts deleted file mode 100644 index bfce164..0000000 --- a/lib/values/string.ts +++ /dev/null @@ -1,267 +0,0 @@ -define("values/string", () => { - var String = env.global.String = function(this: String | undefined, arg: any) { - var val; - if (arguments.length === 0) val = ''; - else val = arg + ''; - if (this === undefined || this === null) return val; - else (this as any).value = val; - } as StringConstructor; - - env.setProto('string', String.prototype); - setConstr(String.prototype, String); - - setProps(String.prototype, { - toString() { - if (typeof this === 'string') return this; - else return (this as any).value; - }, - valueOf() { - if (typeof this === 'string') return this; - else return (this as any).value; - }, - - substring(start, end) { - if (typeof this !== 'string') { - if (this instanceof String) return (this as any).value.substring(start, end); - else throw new Error('This function may be used only with primitive or object strings.'); - } - start = start ?? 0 | 0; - end = (end ?? this.length) | 0; - - const res = []; - - for (let i = start; i < end; i++) { - if (i >= 0 && i < this.length) res[res.length] = this[i]; - } - - return internals.stringFromStrings(res); - }, - substr(start, length) { - start = start ?? 0 | 0; - - if (start >= this.length) start = this.length - 1; - if (start < 0) start = 0; - - length = (length ?? this.length - start) | 0; - const end = length + start; - const res = []; - - for (let i = start; i < end; i++) { - if (i >= 0 && i < this.length) res[res.length] = this[i]; - } - - return internals.stringFromStrings(res); - }, - - toLowerCase() { - // TODO: Implement localization - const res = []; - - for (let i = 0; i < this.length; i++) { - const c = internals.char(this[i]); - - if (c >= 65 && c <= 90) res[i] = c - 65 + 97; - else res[i] = c; - } - - return internals.stringFromChars(res); - }, - toUpperCase() { - // TODO: Implement localization - const res = []; - - for (let i = 0; i < this.length; i++) { - const c = internals.char(this[i]); - - if (c >= 97 && c <= 122) res[i] = c - 97 + 65; - else res[i] = c; - } - - return internals.stringFromChars(res); - }, - - charAt(pos) { - if (typeof this !== 'string') { - if (this instanceof String) return (this as any).value.charAt(pos); - else throw new Error('This function may be used only with primitive or object strings.'); - } - - pos = pos | 0; - if (pos < 0 || pos >= this.length) return ''; - return this[pos]; - }, - charCodeAt(pos) { - if (typeof this !== 'string') { - if (this instanceof String) return (this as any).value.charAt(pos); - else throw new Error('This function may be used only with primitive or object strings.'); - } - - pos = pos | 0; - if (pos < 0 || pos >= this.length) return 0 / 0; - return internals.char(this[pos]); - }, - - startsWith(term, pos) { - if (typeof this !== 'string') { - if (this instanceof String) return (this as any).value.startsWith(term, pos); - else throw new Error('This function may be used only with primitive or object strings.'); - } - pos = pos! | 0; - term = term + ""; - - if (pos < 0 || this.length < term.length + pos) return false; - - for (let i = 0; i < term.length; i++) { - if (this[i + pos] !== term[i]) return false; - } - - return true; - }, - endsWith(term, pos) { - if (typeof this !== 'string') { - if (this instanceof String) return (this as any).value.endsWith(term, pos); - else throw new Error('This function may be used only with primitive or object strings.'); - } - pos = (pos ?? this.length) | 0; - term = term + ""; - - const start = pos - term.length; - - if (start < 0 || this.length < term.length + start) return false; - - for (let i = 0; i < term.length; i++) { - if (this[i + start] !== term[i]) return false; - } - - return true; - }, - - indexOf(term: any, start) { - if (typeof this !== 'string') { - if (this instanceof String) return (this as any).value.indexOf(term, start); - else throw new Error('This function may be used only with primitive or object strings.'); - } - - if (typeof term[env.global.Symbol.search] !== 'function') term = RegExp.escape(term); - - return term[env.global.Symbol.search](this, false, start); - }, - lastIndexOf(term: any, start) { - if (typeof this !== 'string') { - if (this instanceof String) return (this as any).value.indexOf(term, start); - else throw new Error('This function may be used only with primitive or object strings.'); - } - - if (typeof term[env.global.Symbol.search] !== 'function') term = RegExp.escape(term); - - return term[env.global.Symbol.search](this, true, start); - }, - includes(term, start) { - return this.indexOf(term, start) >= 0; - }, - - replace(pattern: any, val) { - if (typeof this !== 'string') { - if (this instanceof String) return (this as any).value.replace(pattern, val); - else throw new Error('This function may be used only with primitive or object strings.'); - } - - if (typeof pattern[env.global.Symbol.replace] !== 'function') pattern = RegExp.escape(pattern); - - return pattern[env.global.Symbol.replace](this, val); - }, - replaceAll(pattern: any, val) { - if (typeof this !== 'string') { - if (this instanceof String) return (this as any).value.replace(pattern, val); - else throw new Error('This function may be used only with primitive or object strings.'); - } - - if (typeof pattern[env.global.Symbol.replace] !== 'function') pattern = RegExp.escape(pattern, "g"); - if (pattern instanceof RegExp && !pattern.global) pattern = new pattern.constructor(pattern.source, pattern.flags + "g"); - - return pattern[env.global.Symbol.replace](this, val); - }, - - match(pattern: any) { - if (typeof this !== 'string') { - if (this instanceof String) return (this as any).value.match(pattern); - else throw new Error('This function may be used only with primitive or object strings.'); - } - - if (typeof pattern[env.global.Symbol.match] !== 'function') pattern = RegExp.escape(pattern); - - return pattern[env.global.Symbol.match](this); - }, - matchAll(pattern: any) { - if (typeof this !== 'string') { - if (this instanceof String) return (this as any).value.matchAll(pattern); - else throw new Error('This function may be used only with primitive or object strings.'); - } - - if (typeof pattern[env.global.Symbol.match] !== 'function') pattern = RegExp.escape(pattern, "g"); - if (pattern instanceof RegExp && !pattern.global) pattern = new pattern.constructor(pattern.source, pattern.flags + "g"); - - return pattern[env.global.Symbol.match](this); - }, - - split(pattern: any, lim, sensible) { - if (typeof this !== 'string') { - if (this instanceof String) return (this as any).value.split(pattern, lim, sensible); - else throw new Error('This function may be used only with primitive or object strings.'); - } - - if (typeof pattern[env.global.Symbol.split] !== 'function') pattern = RegExp.escape(pattern, "g"); - - return pattern[env.global.Symbol.split](this, lim, sensible); - }, - slice(start, end) { - if (typeof this !== 'string') { - if (this instanceof String) return (this as any).value.slice(start, end); - else throw new Error('This function may be used only with primitive or object strings.'); - } - - start = wrapI(this.length, start ?? 0 | 0); - end = wrapI(this.length, end ?? this.length | 0); - - if (start > end) return ''; - - return this.substring(start, end); - }, - - concat(...args) { - if (typeof this !== 'string') { - if (this instanceof String) return (this as any).value.concat(...args); - else throw new Error('This function may be used only with primitive or object strings.'); - } - - var res = this; - for (var arg of args) res += arg; - return res; - }, - - trim() { - return this - .replace(/^\s+/g, '') - .replace(/\s+$/g, ''); - } - }); - - setProps(String, { - fromCharCode(val) { - return internals.stringFromChars([val | 0]); - }, - }) - - env.global.Object.defineProperty(String.prototype, 'length', { - get() { - if (typeof this !== 'string') { - if (this instanceof String) return (this as any).value.length; - else throw new Error('This function may be used only with primitive or object strings.'); - } - - return internals.strlen(this); - }, - configurable: true, - enumerable: false, - }); -}); \ No newline at end of file diff --git a/lib/values/symbol.ts b/lib/values/symbol.ts deleted file mode 100644 index 64b4d5e..0000000 --- a/lib/values/symbol.ts +++ /dev/null @@ -1,36 +0,0 @@ -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.'); - return internals.symbol(val); - } as SymbolConstructor; - - env.setProto('symbol', Symbol.prototype); - setConstr(Symbol.prototype, Symbol); - - setProps(Symbol, { - for(key) { - if (typeof key !== 'string' && key !== undefined) throw new 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.'); - 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, - }); - - internals.defineField(env.global.Object.prototype, Symbol.typeName, 'Object', false, false, false); - internals.defineField(env.global, Symbol.typeName, 'Window', false, false, false); -}); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 4bbacb0..0000000 --- a/package-lock.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "java-jscript", - "lockfileVersion": 3, - "requires": true, - "packages": {} -} diff --git a/package.json b/package.json deleted file mode 100644 index 0967ef4..0000000 --- a/package.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/src/me/topchetoeu/jscript/Main.java b/src/me/topchetoeu/jscript/Main.java index 09657b8..b72caea 100644 --- a/src/me/topchetoeu/jscript/Main.java +++ b/src/me/topchetoeu/jscript/Main.java @@ -7,21 +7,20 @@ import java.io.InputStreamReader; import java.nio.file.Files; import java.nio.file.Path; -import me.topchetoeu.jscript.engine.MessageContext; -import me.topchetoeu.jscript.engine.Context; +import me.topchetoeu.jscript.engine.Message; import me.topchetoeu.jscript.engine.Engine; -import me.topchetoeu.jscript.engine.FunctionContext; +import me.topchetoeu.jscript.engine.Environment; +import me.topchetoeu.jscript.engine.values.NativeFunction; import me.topchetoeu.jscript.engine.values.Values; import me.topchetoeu.jscript.events.Observer; import me.topchetoeu.jscript.exceptions.EngineException; import me.topchetoeu.jscript.exceptions.SyntaxException; -import me.topchetoeu.jscript.interop.NativeTypeRegister; 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 +46,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,26 +61,30 @@ 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); + + env = new Environment(null, null, null); var exited = new boolean[1]; - env.global.define("exit", ctx -> { - exited[0] = true; - task.interrupt(); - throw new InterruptedException(); - }); - env.global.define("go", ctx -> { - try { - var func = ctx.compile("do.js", new String(Files.readAllBytes(Path.of("do.js")))); - return func.call(ctx); - } - catch (IOException e) { - throw new EngineException("Couldn't open do.js"); - } - }); + engine.pushMsg(false, new Message(engine), new NativeFunction((ctx, thisArg, _a) -> { + new Internals().apply(env); - engine.pushMsg(false, new Context(builderEnv, new MessageContext(engine)), "core.js", resourceToString("js/core.js"), null, env, new Internals()).toObservable().on(valuePrinter); + env.global.define("exit", _ctx -> { + exited[0] = true; + task.interrupt(); + throw new InterruptedException(); + }); + env.global.define("go", _ctx -> { + try { + var func = _ctx.compile("do.js", new String(Files.readAllBytes(Path.of("do.js")))); + return func.call(_ctx); + } + catch (IOException e) { + throw new EngineException("Couldn't open do.js"); + } + }); + + return null; + }), null); task = engine.start(); var reader = new Thread(() -> { @@ -114,7 +94,7 @@ public class Main { var raw = in.readLine(); if (raw == null) break; - engine.pushMsg(false, new Context(env, new MessageContext(engine)), "", raw, null).toObservable().once(valuePrinter); + engine.pushMsg(false, env.context(new Message(engine)), "", raw, null).toObservable().once(valuePrinter); } catch (EngineException e) { try { diff --git a/src/me/topchetoeu/jscript/engine/Context.java b/src/me/topchetoeu/jscript/engine/Context.java index 417b115..882e7ce 100644 --- a/src/me/topchetoeu/jscript/engine/Context.java +++ b/src/me/topchetoeu/jscript/engine/Context.java @@ -5,16 +5,23 @@ import me.topchetoeu.jscript.engine.values.Values; import me.topchetoeu.jscript.parsing.Parsing; public class Context { - public final FunctionContext function; - public final MessageContext message; + public final Environment env; + public final Message 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; - this.message = msgCtx; + public Context setEnv(Environment env) { + return new Context(env, message); + } + public Context setMsg(Message msg) { + return new Context(env, msg); + } + + public Context(Environment env, Message msg) { + this.env = env; + this.message = msg; } } diff --git a/src/me/topchetoeu/jscript/engine/Data.java b/src/me/topchetoeu/jscript/engine/Data.java new file mode 100644 index 0000000..1db4ba8 --- /dev/null +++ b/src/me/topchetoeu/jscript/engine/Data.java @@ -0,0 +1,60 @@ +package me.topchetoeu.jscript.engine; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map.Entry; + +@SuppressWarnings("unchecked") +public class Data implements Iterable, ?>> { + private HashMap, Object> data = new HashMap<>(); + + public Data copy() { + return new Data().addAll(this); + } + + public Data addAll(Iterable, ?>> data) { + for (var el : data) { + add((DataKey)el.getKey(), (Object)el.getValue()); + } + return this; + } + + public Data set(DataKey key, T val) { + if (val == null) data.remove(key); + else data.put((DataKey)key, (Object)val); + return this; + } + public T add(DataKey key, T val) { + if (data.containsKey(key)) return (T)data.get(key); + else { + if (val == null) data.remove(key); + else data.put((DataKey)key, (Object)val); + return val; + } + } + public T get(DataKey key) { + return get(key, null); + } + public T get(DataKey key, T defaultVal) { + if (!has(key)) return defaultVal; + else return (T)data.get(key); + } + public boolean has(DataKey key) { return data.containsKey(key); } + + public int increase(DataKey key, int n, int start) { + int res; + set(key, res = get(key, start) + n); + return res; + } + public int increase(DataKey key, int n) { + return increase(key, n, 0); + } + public int increase(DataKey key) { + return increase(key, 1, 0); + } + + @Override + public Iterator, ?>> iterator() { + return (Iterator, ?>>)data.entrySet(); + } +} diff --git a/src/me/topchetoeu/jscript/engine/DataKey.java b/src/me/topchetoeu/jscript/engine/DataKey.java new file mode 100644 index 0000000..017fa2a --- /dev/null +++ b/src/me/topchetoeu/jscript/engine/DataKey.java @@ -0,0 +1,3 @@ +package me.topchetoeu.jscript.engine; + +public class DataKey { } \ No newline at end of file diff --git a/src/me/topchetoeu/jscript/engine/Engine.java b/src/me/topchetoeu/jscript/engine/Engine.java index 3e0b388..013b66e 100644 --- a/src/me/topchetoeu/jscript/engine/Engine.java +++ b/src/me/topchetoeu/jscript/engine/Engine.java @@ -11,19 +11,19 @@ public class Engine { private class UncompiledFunction extends FunctionValue { public final String filename; public final String raw; - public final FunctionContext ctx; + public final Environment env; @Override public Object call(Context ctx, Object thisArg, Object ...args) throws InterruptedException { - ctx = new Context(this.ctx, ctx.message); + ctx = ctx.setEnv(env); return ctx.compile(filename, raw).call(ctx, thisArg, args); } - public UncompiledFunction(FunctionContext ctx, String filename, String raw) { + public UncompiledFunction(Environment env, String filename, String raw) { super(filename, 0); this.filename = filename; this.raw = raw; - this.ctx = ctx; + this.env = env; } } @@ -32,10 +32,10 @@ public class Engine { public final Object thisArg; public final Object[] args; public final DataNotifier notifier = new DataNotifier<>(); - public final MessageContext ctx; + public final Message msg; - public Task(MessageContext ctx, FunctionValue func, Object thisArg, Object[] args) { - this.ctx = ctx; + public Task(Message ctx, FunctionValue func, Object thisArg, Object[] args) { + this.msg = ctx; this.func = func; this.thisArg = thisArg; this.args = args; @@ -52,7 +52,7 @@ public class Engine { private void runTask(Task task) throws InterruptedException { try { - task.notifier.next(task.func.call(new Context(null, task.ctx), task.thisArg, task.args)); + task.notifier.next(task.func.call(task.msg.context(null), task.thisArg, task.args)); } catch (InterruptedException e) { task.notifier.error(new RuntimeException(e)); @@ -102,14 +102,14 @@ public class Engine { return this.thread != null; } - public Awaitable pushMsg(boolean micro, MessageContext ctx, FunctionValue func, Object thisArg, Object ...args) { + public Awaitable pushMsg(boolean micro, Message ctx, FunctionValue func, Object thisArg, Object ...args) { var msg = new Task(ctx, func, thisArg, args); if (micro) microTasks.addLast(msg); else macroTasks.addLast(msg); 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/Environment.java b/src/me/topchetoeu/jscript/engine/Environment.java new file mode 100644 index 0000000..c8136cf --- /dev/null +++ b/src/me/topchetoeu/jscript/engine/Environment.java @@ -0,0 +1,84 @@ +package me.topchetoeu.jscript.engine; + +import java.util.HashMap; + +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; +import me.topchetoeu.jscript.interop.NativeWrapperProvider; + +public class Environment { + private HashMap prototypes = new HashMap<>(); + + public final Data data = new Data(); + public final HashMap symbols = new HashMap<>(); + + public GlobalScope global; + public WrappersProvider wrappersProvider; + + @Native public FunctionValue compile; + @Native public FunctionValue regexConstructor = new NativeFunction("RegExp", (ctx, thisArg, args) -> { + throw EngineException.ofError("Regular expressions not supported.").setContext(ctx); + }); + + public Environment addData(Data data) { + this.data.addAll(data); + return this; + } + + @Native public ObjectValue proto(String name) { + return prototypes.get(name); + } + @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; + } + } + + @NativeGetter("global") public ObjectValue getGlobal() { + return global.obj; + } + @NativeSetter("global") public void setGlobal(ObjectValue val) { + global = new GlobalScope(val); + } + + @Native public Environment fork() { + var res = new Environment(compile, wrappersProvider, global); + res.regexConstructor = regexConstructor; + res.prototypes = new HashMap<>(prototypes); + return res; + } + @Native public Environment child() { + var res = fork(); + res.global = res.global.globalChild(); + return res; + } + + public Context context(Message msg) { + return new Context(this, msg); + } + + 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 NativeWrapperProvider(this); + if (global == null) global = new GlobalScope(); + + this.wrappersProvider = nativeConverter; + this.compile = compile; + this.global = global; + } +} diff --git a/src/me/topchetoeu/jscript/engine/FunctionContext.java b/src/me/topchetoeu/jscript/engine/FunctionContext.java deleted file mode 100644 index e21b7d9..0000000 --- a/src/me/topchetoeu/jscript/engine/FunctionContext.java +++ /dev/null @@ -1,81 +0,0 @@ -package me.topchetoeu.jscript.engine; - -import java.util.HashMap; - -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.exceptions.EngineException; -import me.topchetoeu.jscript.interop.Native; -import me.topchetoeu.jscript.interop.NativeGetter; -import me.topchetoeu.jscript.interop.NativeSetter; - -public class FunctionContext { - private HashMap prototypes = new HashMap<>(); - public GlobalScope global; - public WrappersProvider wrappersProvider; - - @Native public FunctionValue compile; - @Native public FunctionValue regexConstructor = new NativeFunction("RegExp", (ctx, thisArg, args) -> { - throw EngineException.ofError("Regular expressions not supported."); - }); - @Native public ObjectValue proto(String name) { - return prototypes.get(name); - } - @Native public void setProto(String name, ObjectValue val) { - prototypes.put(name, val); - } - // @Native public ObjectValue arrayPrototype = new ObjectValue(); - // @Native public ObjectValue boolPrototype = new ObjectValue(); - // @Native public ObjectValue functionPrototype = new ObjectValue(); - // @Native public ObjectValue numberPrototype = new ObjectValue(); - // @Native public ObjectValue objectPrototype = new ObjectValue(PlaceholderProto.NONE); - // @Native public ObjectValue stringPrototype = new ObjectValue(); - // @Native public ObjectValue symbolPrototype = new ObjectValue(); - // @Native public ObjectValue errorPrototype = new ObjectValue(); - // @Native public ObjectValue syntaxErrPrototype = new ObjectValue(PlaceholderProto.ERROR); - // @Native public ObjectValue typeErrPrototype = new ObjectValue(PlaceholderProto.ERROR); - // @Native public ObjectValue rangeErrPrototype = new ObjectValue(PlaceholderProto.ERROR); - - @NativeGetter("global") - public ObjectValue getGlobal() { - return global.obj; - } - @NativeSetter("global") - public void setGlobal(ObjectValue val) { - global = new GlobalScope(val); - } - - @Native - public FunctionContext fork() { - var res = new FunctionContext(compile, wrappersProvider, global); - res.regexConstructor = regexConstructor; - res.prototypes = new HashMap<>(prototypes); - return res; - } - - @Native - public FunctionContext child() { - var res = fork(); - res.global = res.global.globalChild(); - return res; - } - - public FunctionContext(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) { - throw EngineException.ofType("Java objects not passable to Javascript."); - } - public ObjectValue getProto(Class obj) { - throw EngineException.ofType("Java objects not passable to Javascript."); - } - }; - if (global == null) global = new GlobalScope(); - - this.wrappersProvider = nativeConverter; - this.compile = compile; - this.global = global; - } -} diff --git a/src/me/topchetoeu/jscript/engine/MessageContext.java b/src/me/topchetoeu/jscript/engine/Message.java similarity index 50% rename from src/me/topchetoeu/jscript/engine/MessageContext.java rename to src/me/topchetoeu/jscript/engine/Message.java index 2ac19b4..b61f522 100644 --- a/src/me/topchetoeu/jscript/engine/MessageContext.java +++ b/src/me/topchetoeu/jscript/engine/Message.java @@ -7,15 +7,22 @@ import java.util.List; import me.topchetoeu.jscript.engine.frame.CodeFrame; import me.topchetoeu.jscript.exceptions.EngineException; -public class MessageContext { +public class Message { public final Engine engine; private final ArrayList frames = new ArrayList<>(); public int maxStackFrames = 1000; + public final Data data = new Data(); + public List frames() { return Collections.unmodifiableList(frames); } - public MessageContext pushFrame(CodeFrame frame) { + public Message addData(Data data) { + this.data.addAll(data); + return this; + } + + public Message pushFrame(Context ctx, CodeFrame frame) throws InterruptedException { this.frames.add(frame); if (this.frames.size() > maxStackFrames) throw EngineException.ofRange("Stack overflow!"); return this; @@ -27,7 +34,30 @@ public class MessageContext { return true; } - public MessageContext(Engine engine) { + public List stackTrace() { + var res = new ArrayList(); + + for (var el : frames) { + var name = el.function.name; + var loc = el.function.loc(); + var trace = ""; + + if (loc != null) trace += "at " + loc.toString() + " "; + if (name != null && !name.equals("")) trace += "in " + name + " "; + + trace = trace.trim(); + + if (!res.equals("")) res.add(trace); + } + + return res; + } + + public Context context(Environment env) { + return new Context(env, this); + } + + public Message(Engine engine) { this.engine = engine; } } diff --git a/src/me/topchetoeu/jscript/engine/WrappersProvider.java b/src/me/topchetoeu/jscript/engine/WrappersProvider.java index 6205da8..dd27353 100644 --- a/src/me/topchetoeu/jscript/engine/WrappersProvider.java +++ b/src/me/topchetoeu/jscript/engine/WrappersProvider.java @@ -1,8 +1,9 @@ package me.topchetoeu.jscript.engine; +import me.topchetoeu.jscript.engine.values.FunctionValue; import me.topchetoeu.jscript.engine.values.ObjectValue; public interface WrappersProvider { public ObjectValue getProto(Class obj); - public ObjectValue getConstr(Class obj); + public FunctionValue 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..2ab02a3 100644 --- a/src/me/topchetoeu/jscript/engine/frame/CodeFrame.java +++ b/src/me/topchetoeu/jscript/engine/frame/CodeFrame.java @@ -1,7 +1,6 @@ package me.topchetoeu.jscript.engine.frame; -import java.util.ArrayList; -import java.util.List; +import java.util.Stack; import me.topchetoeu.jscript.Location; import me.topchetoeu.jscript.engine.Context; @@ -9,6 +8,7 @@ import me.topchetoeu.jscript.engine.scope.LocalScope; import me.topchetoeu.jscript.engine.scope.ValueVariable; import me.topchetoeu.jscript.engine.values.ArrayValue; import me.topchetoeu.jscript.engine.values.CodeFunction; +import me.topchetoeu.jscript.engine.values.ObjectValue; import me.topchetoeu.jscript.engine.values.Values; import me.topchetoeu.jscript.exceptions.EngineException; @@ -24,8 +24,8 @@ public class CodeFrame { public final int tryStart, catchStart, finallyStart, end; public int state; public Object retVal; - public int jumpPtr; public EngineException err; + public int jumpPtr; public TryCtx(int tryStart, int tryN, int catchN, int finallyN) { hasCatch = catchN >= 0; @@ -45,7 +45,7 @@ public class CodeFrame { public final LocalScope scope; public final Object thisArg; public final Object[] args; - public final List tryStack = new ArrayList<>(); + public final Stack tryStack = new Stack<>(); public final CodeFunction function; public Object[] stack = new Object[32]; @@ -93,6 +93,12 @@ public class CodeFrame { stack[stackPtr++] = Values.normalize(ctx, val); } + private void setCause(Context ctx, EngineException err, EngineException cause) throws InterruptedException { + if (err.value instanceof ObjectValue) { + Values.setMember(ctx, err, ctx.env.symbol("Symbol.cause"), cause); + } + err.cause = cause; + } private Object nextNoTry(Context ctx) throws InterruptedException { if (Thread.currentThread().isInterrupted()) throw new InterruptedException(); if (codePtr < 0 || codePtr >= function.body.length) return null; @@ -107,163 +113,125 @@ 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 { - TryCtx tryCtx = null; - if (prevError != Runners.NO_RETURN) prevReturn = Runners.NO_RETURN; + public Object next(Context ctx, Object value, Object returnValue, EngineException error) throws InterruptedException { + if (value != Runners.NO_RETURN) push(ctx, value); - while (!tryStack.isEmpty()) { - var tmp = tryStack.get(tryStack.size() - 1); - var remove = false; - - if (prevError != Runners.NO_RETURN) { - remove = true; - if (tmp.state == TryCtx.STATE_TRY) { - tmp.jumpPtr = tmp.end; - - if (tmp.hasCatch) { - tmp.state = TryCtx.STATE_CATCH; - scope.catchVars.add(new ValueVariable(false, prevError)); - prevError = Runners.NO_RETURN; - codePtr = tmp.catchStart; - remove = false; - } - else if (tmp.hasFinally) { - tmp.state = TryCtx.STATE_FINALLY_THREW; - tmp.err = new EngineException(prevError); - prevError = Runners.NO_RETURN; - codePtr = tmp.finallyStart; - remove = false; - } - } - } - else if (prevReturn != Runners.NO_RETURN) { - remove = true; - if (tmp.hasFinally && tmp.state <= TryCtx.STATE_CATCH) { - tmp.state = TryCtx.STATE_FINALLY_RETURNED; - tmp.retVal = prevReturn; - prevReturn = Runners.NO_RETURN; - codePtr = tmp.finallyStart; - remove = false; - } - } - else if (tmp.state == TryCtx.STATE_TRY) { - if (codePtr < tmp.tryStart || codePtr >= tmp.catchStart) { - if (jumpFlag) tmp.jumpPtr = codePtr; - else tmp.jumpPtr = tmp.end; - - if (tmp.hasFinally) { - tmp.state = TryCtx.STATE_FINALLY_JUMPED; - codePtr = tmp.finallyStart; - } - else codePtr = tmp.jumpPtr; - remove = !tmp.hasFinally; - } - } - else if (tmp.state == TryCtx.STATE_CATCH) { - if (codePtr < tmp.catchStart || codePtr >= tmp.finallyStart) { - if (jumpFlag) tmp.jumpPtr = codePtr; - else tmp.jumpPtr = tmp.end; - scope.catchVars.remove(scope.catchVars.size() - 1); - - if (tmp.hasFinally) { - tmp.state = TryCtx.STATE_FINALLY_JUMPED; - codePtr = tmp.finallyStart; - } - else codePtr = tmp.jumpPtr; - remove = !tmp.hasFinally; - } - } - else if (codePtr < tmp.finallyStart || codePtr >= tmp.end) { - if (!jumpFlag) { - if (tmp.state == TryCtx.STATE_FINALLY_THREW) throw tmp.err; - else if (tmp.state == TryCtx.STATE_FINALLY_RETURNED) return tmp.retVal; - else if (tmp.state == TryCtx.STATE_FINALLY_JUMPED) codePtr = tmp.jumpPtr; - } - else codePtr = tmp.jumpPtr; - remove = true; - } - - if (remove) tryStack.remove(tryStack.size() - 1); - else { - tryCtx = tmp; - break; - } + if (returnValue == Runners.NO_RETURN && error == null) { + try { returnValue = nextNoTry(ctx); } + catch (EngineException e) { error = e; } } - if (prevError != Runners.NO_RETURN) throw new EngineException(prevError); - if (prevReturn != Runners.NO_RETURN) return prevReturn; + while (!tryStack.empty()) { + var tryCtx = tryStack.peek(); + var newState = -1; - if (tryCtx == null) return nextNoTry(ctx); - else if (tryCtx.state == TryCtx.STATE_TRY) { - try { - var res = nextNoTry(ctx); - if (res != Runners.NO_RETURN && tryCtx.hasFinally) { - tryCtx.retVal = res; - tryCtx.state = TryCtx.STATE_FINALLY_RETURNED; - } + switch (tryCtx.state) { + case TryCtx.STATE_TRY: + if (error != null) { + if (tryCtx.hasCatch) { + tryCtx.err = error; + newState = TryCtx.STATE_CATCH; + } + else if (tryCtx.hasFinally) { + tryCtx.err = error; + newState = TryCtx.STATE_FINALLY_THREW; + } + break; + } + else if (returnValue != Runners.NO_RETURN) { + if (tryCtx.hasFinally) { + tryCtx.retVal = error; + newState = TryCtx.STATE_FINALLY_RETURNED; + } + break; + } + else if (codePtr >= tryCtx.tryStart && codePtr < tryCtx.catchStart) return Runners.NO_RETURN; - else return res; + if (tryCtx.hasFinally) { + if (jumpFlag) tryCtx.jumpPtr = codePtr; + else tryCtx.jumpPtr = tryCtx.end; + newState = TryCtx.STATE_FINALLY_JUMPED; + } + else codePtr = tryCtx.end; + break; + case TryCtx.STATE_CATCH: + if (error != null) { + if (tryCtx.hasFinally) { + tryCtx.err = error; + newState = TryCtx.STATE_FINALLY_THREW; + } + break; + } + else if (returnValue != Runners.NO_RETURN) { + if (tryCtx.hasFinally) { + tryCtx.retVal = returnValue; + newState = TryCtx.STATE_FINALLY_RETURNED; + } + break; + } + else if (codePtr >= tryCtx.catchStart && codePtr < tryCtx.finallyStart) return Runners.NO_RETURN; + + if (tryCtx.hasFinally) { + if (jumpFlag) tryCtx.jumpPtr = codePtr; + else tryCtx.jumpPtr = tryCtx.end; + newState = TryCtx.STATE_FINALLY_JUMPED; + } + else codePtr = tryCtx.end; + break; + case TryCtx.STATE_FINALLY_THREW: + if (error != null) setCause(ctx, error, tryCtx.err); + else if (codePtr < tryCtx.finallyStart || codePtr >= tryCtx.end) error = tryCtx.err; + else return Runners.NO_RETURN; + break; + case TryCtx.STATE_FINALLY_RETURNED: + if (returnValue == Runners.NO_RETURN) { + if (codePtr < tryCtx.finallyStart || codePtr >= tryCtx.end) returnValue = tryCtx.retVal; + else return Runners.NO_RETURN; + } + break; + case TryCtx.STATE_FINALLY_JUMPED: + if (codePtr < tryCtx.finallyStart || codePtr >= tryCtx.end) { + if (!jumpFlag) codePtr = tryCtx.jumpPtr; + else codePtr = tryCtx.end; + } + else return Runners.NO_RETURN; + break; } - catch (EngineException e) { - if (tryCtx.hasCatch) { - tryCtx.state = TryCtx.STATE_CATCH; - tryCtx.err = e; + + if (tryCtx.state == TryCtx.STATE_CATCH) scope.catchVars.remove(scope.catchVars.size() - 1); + + if (newState == -1) { + tryStack.pop(); + continue; + } + + tryCtx.state = newState; + switch (newState) { + case TryCtx.STATE_CATCH: + scope.catchVars.add(new ValueVariable(false, tryCtx.err.value)); codePtr = tryCtx.catchStart; - scope.catchVars.add(new ValueVariable(false, e.value)); - return Runners.NO_RETURN; - } - else if (tryCtx.hasFinally) { - tryCtx.err = e; - tryCtx.state = TryCtx.STATE_FINALLY_THREW; - } - else throw e; + break; + default: + codePtr = tryCtx.finallyStart; } - codePtr = tryCtx.finallyStart; return Runners.NO_RETURN; } - else if (tryCtx.state == TryCtx.STATE_CATCH) { - try { - var res = nextNoTry(ctx); - if (res != Runners.NO_RETURN && tryCtx.hasFinally) { - tryCtx.retVal = res; - tryCtx.state = TryCtx.STATE_FINALLY_RETURNED; - } - else return res; - } - catch (EngineException e) { - e.cause = tryCtx.err; - if (tryCtx.hasFinally) { - tryCtx.err = e; - tryCtx.state = TryCtx.STATE_FINALLY_THREW; - } - else throw e; - } - - codePtr = tryCtx.finallyStart; - return Runners.NO_RETURN; - } - else if (tryCtx.state == TryCtx.STATE_FINALLY_THREW) { - try { - return nextNoTry(ctx); - } - catch (EngineException e) { - e.cause = tryCtx.err; - throw e; - } - } - else return nextNoTry(ctx); + + if (error != null) throw error.setContext(ctx); + if (returnValue != Runners.NO_RETURN) return returnValue; + return Runners.NO_RETURN; } public Object run(Context ctx) throws InterruptedException { try { - ctx.message.pushFrame(this); + ctx.message.pushFrame(ctx, this); while (true) { - var res = next(ctx, Runners.NO_RETURN, Runners.NO_RETURN); + var res = next(ctx, Runners.NO_RETURN, Runners.NO_RETURN, null); 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..09f3f18 100644 --- a/src/me/topchetoeu/jscript/engine/frame/Runners.java +++ b/src/me/topchetoeu/jscript/engine/frame/Runners.java @@ -26,7 +26,7 @@ public class Runners { public static Object execThrow(Context ctx, Instruction instr, CodeFrame frame) { throw new EngineException(frame.pop()); } - public static Object execThrowSyntax(Context ctx, Instruction instr, CodeFrame frame) { + public static Object execThrowSyntax(Context ctx, Instruction instr, CodeFrame frame) throws InterruptedException { throw EngineException.ofSyntax((String)instr.get(0)); } @@ -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; } @@ -305,7 +307,7 @@ public class Runners { frame.codePtr++; return NO_RETURN; } - public static Object execNop(Context ctx, Instruction instr, CodeFrame frame) { + public static Object execNop(Context ctx, Instruction instr, CodeFrame frame) throws InterruptedException { if (instr.is(0, "dbg_names")) { var names = new String[instr.params.length - 1]; for (var i = 0; i < instr.params.length - 1; i++) { diff --git a/src/me/topchetoeu/jscript/engine/scope/LocalScope.java b/src/me/topchetoeu/jscript/engine/scope/LocalScope.java index 3232f47..4ec8991 100644 --- a/src/me/topchetoeu/jscript/engine/scope/LocalScope.java +++ b/src/me/topchetoeu/jscript/engine/scope/LocalScope.java @@ -9,7 +9,8 @@ public class LocalScope { public final ArrayList catchVars = new ArrayList<>(); public ValueVariable get(int i) { - if (i >= locals.length) return catchVars.get(i - locals.length); + if (i >= locals.length) + return catchVars.get(i - locals.length); if (i >= 0) return locals[i]; else return captures[~i]; } diff --git a/src/me/topchetoeu/jscript/engine/values/ArrayValue.java b/src/me/topchetoeu/jscript/engine/values/ArrayValue.java index 0bd59ea..006c8d4 100644 --- a/src/me/topchetoeu/jscript/engine/values/ArrayValue.java +++ b/src/me/topchetoeu/jscript/engine/values/ArrayValue.java @@ -1,90 +1,128 @@ package me.topchetoeu.jscript.engine.values; -import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Comparator; +import java.util.Iterator; import java.util.List; import me.topchetoeu.jscript.engine.Context; -public class ArrayValue extends ObjectValue { - private static final Object EMPTY = new Object(); - private final ArrayList values = new ArrayList<>(); +// TODO: Make methods generic +public class ArrayValue extends ObjectValue implements Iterable { + private static final Object UNDEFINED = new Object(); + private Object[] values; + private int size; - public int size() { return values.size(); } + private void alloc(int index) { + if (index < values.length) return; + if (index < values.length * 2) index = values.length * 2; + + var arr = new Object[index]; + System.arraycopy(values, 0, arr, 0, values.length); + values = arr; + } + + public int size() { return size; } public boolean setSize(int val) { if (val < 0) return false; - while (size() > val) { - values.remove(values.size() - 1); - } - while (size() < val) { - values.add(EMPTY); + if (size > val) shrink(size - val); + else { + alloc(val); + size = val; } return true; } public Object get(int i) { - if (i < 0 || i >= values.size()) return null; - var res = values.get(i); - if (res == EMPTY) return null; + if (i < 0 || i >= size) return null; + var res = values[i]; + if (res == UNDEFINED) return null; else return res; } public void set(Context ctx, int i, Object val) { if (i < 0) return; - while (values.size() <= i) { - values.add(EMPTY); - } + alloc(i); - values.set(i, Values.normalize(ctx, val)); + val = Values.normalize(ctx, val); + if (val == null) val = UNDEFINED; + values[i] = val; + if (i >= size) size = i + 1; } public boolean has(int i) { - return i >= 0 && i < values.size() && values.get(i) != EMPTY; + return i >= 0 && i < values.length && values[i] != null; } public void remove(int i) { - if (i < 0 || i >= values.size()) return; - values.set(i, EMPTY); + if (i < 0 || i >= values.length) return; + values[i] = null; } public void shrink(int n) { - if (n > values.size()) values.clear(); + if (n >= values.length) { + values = new Object[16]; + size = 0; + } else { - for (int i = 0; i < n && values.size() > 0; i++) { - values.remove(values.size() - 1); + for (int i = 0; i < n; i++) { + values[--size] = null; } } } + public Object[] toArray() { + Object[] res = new Object[size]; + copyTo(res, 0, 0, size); + return res; + } + public void copyTo(Object[] arr, int sourceStart, int destStart, int count) { + for (var i = 0; i < count; i++) { + if (i + sourceStart < 0 || i + sourceStart >= size) arr[i + destStart] = null; + if (values[i + sourceStart] == UNDEFINED) arr[i + destStart] = null; + else arr[i + sourceStart] = values[i + destStart]; + } + } + public void copyTo(Context ctx, ArrayValue arr, int sourceStart, int destStart, int count) { + // Iterate in reverse to reallocate at most once + for (var i = count - 1; i >= 0; i--) { + if (i + sourceStart < 0 || i + sourceStart >= size) arr.set(ctx, i + destStart, null); + if (values[i + sourceStart] == UNDEFINED) arr.set(ctx, i + destStart, null); + else arr.set(ctx, i + destStart, values[i + sourceStart]); + } + } + + public void copyFrom(Context ctx, Object[] arr, int sourceStart, int destStart, int count) { + for (var i = 0; i < count; i++) { + set(ctx, i + destStart, arr[i + sourceStart]); + } + } + + public void move(int srcI, int dstI, int n) { + alloc(dstI + n); + + System.arraycopy(values, srcI, values, dstI, n); + + if (dstI + n >= size) size = dstI + n; + } + public void sort(Comparator comparator) { - values.sort((a, b) -> { + Arrays.sort(values, 0, size, (a, b) -> { var _a = 0; var _b = 0; - if (a == null) _a = 1; - if (a == EMPTY) _a = 2; + if (a == UNDEFINED) _a = 1; + if (a == null) _a = 2; - if (b == null) _b = 1; - if (b == EMPTY) _b = 2; + if (b == UNDEFINED) _b = 1; + if (b == null) _b = 2; - if (Integer.compare(_a, _b) != 0) return Integer.compare(_a, _b); + if (_a != 0 || _b != 0) return Integer.compare(_a, _b); return comparator.compare(a, b); }); } - public Object[] toArray() { - Object[] res = new Object[values.size()]; - - for (var i = 0; i < values.size(); i++) { - if (values.get(i) == EMPTY) res[i] = null; - else res[i] = values.get(i); - } - - return res; - } - @Override protected Object getField(Context ctx, Object key) throws InterruptedException { - if (key.equals("length")) return values.size(); if (key instanceof Number) { var i = ((Number)key).doubleValue(); if (i >= 0 && i - Math.floor(i) == 0) { @@ -96,9 +134,6 @@ public class ArrayValue extends ObjectValue { } @Override protected boolean setField(Context ctx, Object key, Object val) throws InterruptedException { - if (key.equals("length")) { - return setSize((int)Values.toNumber(ctx, val)); - } if (key instanceof Number) { var i = Values.number(key); if (i >= 0 && i - Math.floor(i) == 0) { @@ -111,7 +146,6 @@ public class ArrayValue extends ObjectValue { } @Override protected boolean hasField(Context ctx, Object key) throws InterruptedException { - if (key.equals("length")) return true; if (key instanceof Number) { var i = Values.number(key); if (i >= 0 && i - Math.floor(i) == 0) { @@ -140,18 +174,42 @@ public class ArrayValue extends ObjectValue { for (var i = 0; i < size(); i++) { if (has(i)) res.add(i); } - if (includeNonEnumerable) res.add("length"); return res; } + @Override + public Iterator iterator() { + return new Iterator() { + private int i = 0; + + @Override + public boolean hasNext() { + return i < size(); + } + @Override + public Object next() { + if (!hasNext()) return null; + return get(i++); + } + }; + } + public ArrayValue() { super(PlaceholderProto.ARRAY); - nonEnumerableSet.add("length"); - nonConfigurableSet.add("length"); + values = new Object[16]; + size = 0; + } + public ArrayValue(int cap) { + super(PlaceholderProto.ARRAY); + values = new Object[cap]; + size = 0; } public ArrayValue(Context ctx, Object ...values) { this(); - for (var i = 0; i < values.length; i++) this.values.add(Values.normalize(ctx, values[i])); + this.values = new Object[values.length]; + size = values.length; + + for (var i = 0; i < size; i++) this.values[i] = Values.normalize(ctx, values[i]); } public static ArrayValue of(Context ctx, Collection values) { diff --git a/src/me/topchetoeu/jscript/engine/values/CodeFunction.java b/src/me/topchetoeu/jscript/engine/values/CodeFunction.java index d70b20d..c0025b3 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) { @@ -29,10 +29,10 @@ public class CodeFunction extends FunctionValue { @Override public Object call(Context ctx, Object thisArg, Object ...args) throws InterruptedException { - return new CodeFrame(ctx, thisArg, args, this).run(new Context(environment, ctx.message)); + return new CodeFrame(ctx, thisArg, args, this).run(ctx.setEnv(environment)); } - 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..b7dfe2e 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; @@ -233,7 +233,7 @@ public class ObjectValue { public final Object getMember(Context ctx, Object key, Object thisArg) throws InterruptedException { key = Values.normalize(ctx, key); - if (key.equals("__proto__")) { + if ("__proto__".equals(key)) { var res = getPrototype(ctx); return res == null ? Values.NULL : res; } diff --git a/src/me/topchetoeu/jscript/engine/values/Symbol.java b/src/me/topchetoeu/jscript/engine/values/Symbol.java index 5715430..116ae0b 100644 --- a/src/me/topchetoeu/jscript/engine/values/Symbol.java +++ b/src/me/topchetoeu/jscript/engine/values/Symbol.java @@ -10,6 +10,6 @@ public final class Symbol { @Override public String toString() { if (value == null) return "Symbol"; - else return "Symbol(" + value + ")"; + else return "@@" + value; } } diff --git a/src/me/topchetoeu/jscript/engine/values/Values.java b/src/me/topchetoeu/jscript/engine/values/Values.java index 73f7029..07a0036 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(); @@ -115,8 +117,8 @@ public class Values { public static double toNumber(Context ctx, Object obj) throws InterruptedException { var val = toPrimitive(ctx, obj, ConvertHint.VALUEOF); - if (val instanceof Number) return number(obj); - if (val instanceof Boolean) return ((Boolean)obj) ? 1 : 0; + if (val instanceof Number) return number(val); + if (val instanceof Boolean) return ((Boolean)val) ? 1 : 0; if (val instanceof String) { try { return Double.parseDouble((String)val); @@ -132,7 +134,7 @@ public class Values { if (val == NULL) return "null"; if (val instanceof Number) { - var d = number(obj); + var d = number(val); if (d == Double.NEGATIVE_INFINITY) return "-Infinity"; if (d == Double.POSITIVE_INFINITY) return "Infinity"; if (Double.isNaN(d)) return "NaN"; @@ -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,38 @@ 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 { + var res = new ObjectValue(); + var proto = Values.getMember(ctx, func, "prototype"); + res.setPrototype(ctx, proto); + + var ret = call(ctx, func, res, args); + + if (ret != null && func instanceof FunctionValue && ((FunctionValue)func).special) return ret; + return res; + } public static boolean strictEquals(Context ctx, Object a, Object b) { a = normalize(ctx, a); b = normalize(ctx, b); @@ -420,7 +448,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 +457,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 +506,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,30 +516,32 @@ public class Values { if (obj == null) return null; if (clazz.isInstance(obj)) return (T)obj; - throw err; + throw new ConvertException(type(obj), clazz.getSimpleName()); } - public static Iterable toJavaIterable(Context ctx, Object obj) throws InterruptedException { + public static Iterable toJavaIterable(Context ctx, Object obj) { 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, obj, obj) : + iteratorFunc; + var nextFunc = getMember(ctx, call(ctx, iteratorFunc, obj), "next"); + + if (!isFunction(nextFunc)) return Collections.emptyIterator(); return new Iterator() { private Object value = null; public boolean consumed = true; - private FunctionValue next = function(iterator); + private FunctionValue next = (FunctionValue)nextFunc; 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 { @@ -562,12 +588,11 @@ public class Values { }; } - public static ObjectValue fromJavaIterable(Context ctx, Iterable iterable) throws InterruptedException { + public static ObjectValue fromJavaIterator(Context ctx, Iterator it) throws InterruptedException { var res = new ObjectValue(); - 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) { } @@ -580,6 +605,10 @@ public class Values { return res; } + public static ObjectValue fromJavaIterable(Context ctx, Iterable it) throws InterruptedException { + return fromJavaIterator(ctx, it.iterator()); + } + private static void printValue(Context ctx, Object val, HashSet passed, int tab) throws InterruptedException { if (passed.contains(val)) { System.out.print("[circular]"); @@ -604,7 +633,7 @@ public class Values { if (i != 0) System.out.print(", "); else System.out.print(" "); if (obj.has(i)) printValue(ctx, obj.get(i), passed, tab); - else System.out.print(", "); + else System.out.print(""); } System.out.print(" ] "); } @@ -655,4 +684,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 ? "Uncaught" : "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 75adb6a..fab5276 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(); @@ -36,16 +41,17 @@ public class EngineException extends RuntimeException { catch (EngineException e) { ss.append("[Error while stringifying]\n"); } - for (var line : stackTrace) { - ss.append(" ").append(line).append('\n'); - } - if (cause != null) ss.append("Caused by ").append(cause.toString(ctx)).append('\n'); + // for (var line : stackTrace) { + // ss.append(" ").append(line).append('\n'); + // } + // if (cause != null) ss.append("Caused by ").append(cause.toString(ctx)).append('\n'); ss.deleteCharAt(ss.length() - 1); return ss.toString(); } - private static Object err(String msg, PlaceholderProto proto) { + private static Object err(String name, String msg, PlaceholderProto proto) { var res = new ObjectValue(proto); + if (name != null) res.defineProperty(null, "name", name); res.defineProperty(null, "message", msg); return res; } @@ -57,19 +63,22 @@ public class EngineException extends RuntimeException { this.cause = null; } + public static EngineException ofError(String name, String msg) { + return new EngineException(err(name, msg, PlaceholderProto.ERROR)); + } public static EngineException ofError(String msg) { - return new EngineException(err(msg, PlaceholderProto.ERROR)); + return new EngineException(err(null, msg, PlaceholderProto.ERROR)); } public static EngineException ofSyntax(SyntaxException e) { - return new EngineException(err(e.msg, PlaceholderProto.SYNTAX_ERROR)).add(null, e.loc); + return new EngineException(err(null, e.msg, PlaceholderProto.SYNTAX_ERROR)).add(null, e.loc); } public static EngineException ofSyntax(String msg) { - return new EngineException(err(msg, PlaceholderProto.SYNTAX_ERROR)); + return new EngineException(err(null, msg, PlaceholderProto.SYNTAX_ERROR)); } public static EngineException ofType(String msg) { - return new EngineException(err(msg, PlaceholderProto.TYPE_ERROR)); + return new EngineException(err(null, msg, PlaceholderProto.TYPE_ERROR)); } public static EngineException ofRange(String msg) { - return new EngineException(err(msg, PlaceholderProto.RANGE_ERROR)); + return new EngineException(err(null, msg, PlaceholderProto.RANGE_ERROR)); } } diff --git a/src/me/topchetoeu/jscript/filesystem/EntryType.java b/src/me/topchetoeu/jscript/filesystem/EntryType.java new file mode 100644 index 0000000..26e255a --- /dev/null +++ b/src/me/topchetoeu/jscript/filesystem/EntryType.java @@ -0,0 +1,7 @@ +package me.topchetoeu.jscript.filesystem; + +public enum EntryType { + NONE, + FILE, + FOLDER, +} \ No newline at end of file diff --git a/src/me/topchetoeu/jscript/filesystem/File.java b/src/me/topchetoeu/jscript/filesystem/File.java new file mode 100644 index 0000000..b7a14a8 --- /dev/null +++ b/src/me/topchetoeu/jscript/filesystem/File.java @@ -0,0 +1,27 @@ +package me.topchetoeu.jscript.filesystem; + +import java.io.IOException; + +public interface File { + int read() throws IOException, InterruptedException; + boolean write(byte val) throws IOException, InterruptedException; + long tell() throws IOException, InterruptedException; + void seek(long offset, int pos) throws IOException, InterruptedException; + void close() throws IOException, InterruptedException; + Permissions perms(); + + default String readToString() throws IOException, InterruptedException { + seek(0, 2); + long len = tell(); + if (len < 0) return null; + + seek(0, 0); + byte[] res = new byte[(int)len]; + + for (var i = 0; i < len; i++) { + res[i] = (byte)read(); + } + + return new String(res); + } +} \ No newline at end of file diff --git a/src/me/topchetoeu/jscript/filesystem/Filesystem.java b/src/me/topchetoeu/jscript/filesystem/Filesystem.java new file mode 100644 index 0000000..02ae9d5 --- /dev/null +++ b/src/me/topchetoeu/jscript/filesystem/Filesystem.java @@ -0,0 +1,10 @@ +package me.topchetoeu.jscript.filesystem; + +import java.io.IOException; + +public interface Filesystem { + File open(String path) throws IOException, InterruptedException; + boolean mkdir(String path) throws IOException, InterruptedException; + EntryType type(String path) throws IOException, InterruptedException; + boolean rm(String path) throws IOException, InterruptedException; +} diff --git a/src/me/topchetoeu/jscript/filesystem/InaccessibleFile.java b/src/me/topchetoeu/jscript/filesystem/InaccessibleFile.java new file mode 100644 index 0000000..2cc59b4 --- /dev/null +++ b/src/me/topchetoeu/jscript/filesystem/InaccessibleFile.java @@ -0,0 +1,35 @@ +package me.topchetoeu.jscript.filesystem; + +import java.io.IOException; + +public class InaccessibleFile implements File { + public static final InaccessibleFile INSTANCE = new InaccessibleFile(); + + @Override + public int read() throws IOException, InterruptedException { + return -1; + } + + @Override + public boolean write(byte val) throws IOException, InterruptedException { + return false; + } + + @Override + public long tell() throws IOException, InterruptedException { + return 0; + } + + @Override + public void seek(long offset, int pos) throws IOException, InterruptedException { } + + @Override + public void close() throws IOException, InterruptedException { } + + @Override + public Permissions perms() { + return Permissions.NONE; + } + + private InaccessibleFile() { } +} diff --git a/src/me/topchetoeu/jscript/filesystem/MemoryFile.java b/src/me/topchetoeu/jscript/filesystem/MemoryFile.java new file mode 100644 index 0000000..c414a7e --- /dev/null +++ b/src/me/topchetoeu/jscript/filesystem/MemoryFile.java @@ -0,0 +1,53 @@ +package me.topchetoeu.jscript.filesystem; + +import java.io.IOException; + +public class MemoryFile implements File { + private int ptr; + private Permissions mode; + public final byte[] data; + + @Override + public int read() throws IOException, InterruptedException { + if (data == null || !mode.readable || ptr >= data.length) return -1; + return data[ptr++]; + } + + @Override + public boolean write(byte val) throws IOException, InterruptedException { + if (data == null || !mode.writable || ptr >= data.length) return false; + data[ptr++] = val; + return true; + } + + @Override + public long tell() throws IOException, InterruptedException { + return ptr; + } + + @Override + public void seek(long offset, int pos) throws IOException, InterruptedException { + if (data == null) return; + + if (pos == 0) ptr = (int)offset; + else if (pos == 1) ptr += (int)offset; + else if (pos == 2) ptr = data.length - (int)offset; + } + + @Override + public void close() throws IOException, InterruptedException { + mode = null; + ptr = 0; + } + + @Override + public Permissions perms() { + if (data == null) return Permissions.NONE; + return mode; + } + + public MemoryFile(byte[] buff, Permissions mode) { + this.data = buff; + this.mode = mode; + } +} diff --git a/src/me/topchetoeu/jscript/filesystem/Permissions.java b/src/me/topchetoeu/jscript/filesystem/Permissions.java new file mode 100644 index 0000000..5a5ff34 --- /dev/null +++ b/src/me/topchetoeu/jscript/filesystem/Permissions.java @@ -0,0 +1,17 @@ +package me.topchetoeu.jscript.filesystem; + +public enum Permissions { + NONE("", false, false), + READ("r", true, false), + READ_WRITE("rw", true, true); + + public final String readMode; + public final boolean readable; + public final boolean writable; + + private Permissions(String mode, boolean r, boolean w) { + this.readMode = mode; + this.readable = r; + this.writable = w; + } +} diff --git a/src/me/topchetoeu/jscript/filesystem/PhysicalFile.java b/src/me/topchetoeu/jscript/filesystem/PhysicalFile.java new file mode 100644 index 0000000..cb5d067 --- /dev/null +++ b/src/me/topchetoeu/jscript/filesystem/PhysicalFile.java @@ -0,0 +1,51 @@ +package me.topchetoeu.jscript.filesystem; + +import java.io.IOException; +import java.io.RandomAccessFile; + +public class PhysicalFile implements File { + private RandomAccessFile file; + private Permissions perms; + + @Override + public int read() throws IOException, InterruptedException { + if (file == null || !perms.readable) return -1; + else return file.read(); + } + + @Override + public boolean write(byte val) throws IOException, InterruptedException { + if (file == null || !perms.writable) return false; + file.write(val); + return true; + } + + @Override + public long tell() throws IOException, InterruptedException { + if (file == null) return 0; + return file.getFilePointer(); + } + @Override + public void seek(long offset, int pos) throws IOException, InterruptedException { + if (file == null) return; + if (pos == 0) file.seek(pos); + else if (pos == 1) file.seek(file.getFilePointer() + pos); + else if (pos == 2) file.seek(file.length() + pos); + } + + @Override + public void close() throws IOException, InterruptedException { + if (file == null) return; + file.close(); + } + + @Override + public Permissions perms() { return perms; } + + public PhysicalFile(String path, Permissions mode) throws IOException { + if (mode == Permissions.NONE) file = null; + else file = new RandomAccessFile(path, mode.readMode); + + perms = mode; + } +} diff --git a/src/me/topchetoeu/jscript/filesystem/PhysicalFilesystem.java b/src/me/topchetoeu/jscript/filesystem/PhysicalFilesystem.java new file mode 100644 index 0000000..0241676 --- /dev/null +++ b/src/me/topchetoeu/jscript/filesystem/PhysicalFilesystem.java @@ -0,0 +1,74 @@ +package me.topchetoeu.jscript.filesystem; + +import java.io.IOException; +import java.nio.file.Path; + +public class PhysicalFilesystem implements Filesystem { + public final Path root; + + private Permissions getPerms(Path path) { + var file = path.toFile(); + if (!path.startsWith(root)) return Permissions.NONE; + if (file.canRead() && file.canWrite()) return Permissions.READ_WRITE; + if (file.canRead()) return Permissions.READ; + + return Permissions.NONE; + } + private Path getPath(String name) { + return root.resolve(name); + } + + @Override + public File open(String path) throws IOException, InterruptedException { + var _path = root.resolve(path); + + var perms = getPerms(_path); + if (perms == Permissions.NONE) return InaccessibleFile.INSTANCE; + + var f = _path.toFile(); + + if (f.isDirectory()) { + var res = new StringBuilder(); + + for (var child : f.listFiles()) res.append(child.toString()).append('\n'); + + return new MemoryFile(res.toString().getBytes(), Permissions.READ); + } + else return new PhysicalFile(path, perms); + } + + @Override + public boolean mkdir(String path) throws IOException, InterruptedException { + var _path = getPath(path); + var perms = getPerms(_path); + var f = _path.toFile(); + + if (!perms.writable) return false; + else return f.mkdir(); + } + + @Override + public EntryType type(String path) throws IOException, InterruptedException { + var _path = getPath(path); + var perms = getPerms(_path); + var f = _path.toFile(); + + if (perms == Permissions.NONE) return EntryType.NONE; + else if (f.isFile()) return EntryType.FILE; + else return EntryType.FOLDER; + } + + @Override + public boolean rm(String path) throws IOException, InterruptedException { + var _path = getPath(path); + var perms = getPerms(_path); + var f = _path.toFile(); + + if (!perms.writable) return false; + else return f.delete(); + } + + public PhysicalFilesystem(Path root) { + this.root = root; + } +} diff --git a/src/me/topchetoeu/jscript/interop/InitType.java b/src/me/topchetoeu/jscript/interop/InitType.java new file mode 100644 index 0000000..89793f8 --- /dev/null +++ b/src/me/topchetoeu/jscript/interop/InitType.java @@ -0,0 +1,7 @@ +package me.topchetoeu.jscript.interop; + +public enum InitType { + CONSTRUCTOR, + PROTOTYPE, + NAMESPACE, +} \ No newline at end of file diff --git a/src/me/topchetoeu/jscript/interop/Native.java b/src/me/topchetoeu/jscript/interop/Native.java index f98765a..b0465a6 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 thisArg() 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..e9aa462 --- /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 thisArg() default false; +} + diff --git a/src/me/topchetoeu/jscript/interop/NativeGetter.java b/src/me/topchetoeu/jscript/interop/NativeGetter.java index ea5651b..7a625ad 100644 --- a/src/me/topchetoeu/jscript/interop/NativeGetter.java +++ b/src/me/topchetoeu/jscript/interop/NativeGetter.java @@ -8,5 +8,6 @@ import java.lang.annotation.Target; @Target({ ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) public @interface NativeGetter { - public String value(); + public String value() default ""; + public boolean thisArg() default false; } diff --git a/src/me/topchetoeu/jscript/interop/NativeInit.java b/src/me/topchetoeu/jscript/interop/NativeInit.java new file mode 100644 index 0000000..66d13fe --- /dev/null +++ b/src/me/topchetoeu/jscript/interop/NativeInit.java @@ -0,0 +1,12 @@ +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 NativeInit { + InitType value(); +} diff --git a/src/me/topchetoeu/jscript/interop/NativeSetter.java b/src/me/topchetoeu/jscript/interop/NativeSetter.java index bcb5cd3..225eccb 100644 --- a/src/me/topchetoeu/jscript/interop/NativeSetter.java +++ b/src/me/topchetoeu/jscript/interop/NativeSetter.java @@ -8,5 +8,6 @@ import java.lang.annotation.Target; @Target({ ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) public @interface NativeSetter { - public String value(); + public String value() default ""; + public boolean thisArg() default false; } diff --git a/src/me/topchetoeu/jscript/interop/NativeTypeRegister.java b/src/me/topchetoeu/jscript/interop/NativeWrapperProvider.java similarity index 55% rename from src/me/topchetoeu/jscript/interop/NativeTypeRegister.java rename to src/me/topchetoeu/jscript/interop/NativeWrapperProvider.java index ac6eb85..7affa94 100644 --- a/src/me/topchetoeu/jscript/interop/NativeTypeRegister.java +++ b/src/me/topchetoeu/jscript/interop/NativeWrapperProvider.java @@ -1,199 +1,247 @@ -package me.topchetoeu.jscript.interop; - -import java.lang.reflect.Modifier; -import java.util.HashMap; - -import me.topchetoeu.jscript.engine.WrappersProvider; -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; - -public class NativeTypeRegister implements WrappersProvider { - private final HashMap, FunctionValue> constructors = new HashMap<>(); - private final HashMap, ObjectValue> prototypes = new HashMap<>(); - - 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); - - if (nat != null) { - 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)); - } - else { - if (get != null) { - var name = get.value(); - var prop = target.properties.get(name); - OverloadFunction getter = null; - var setter = prop == null ? null : prop.setter; - - if (prop != null && prop.getter instanceof OverloadFunction) getter = (OverloadFunction)prop.getter; - else getter = new OverloadFunction("get " + name); - - getter.overloads.add(Overload.fromMethod(method)); - target.defineProperty(null, name, getter, setter, true, true); - } - if (set != null) { - var name = set.value(); - var prop = target.properties.get(name); - var getter = prop == null ? null : prop.getter; - OverloadFunction setter = null; - - if (prop != null && prop.setter instanceof OverloadFunction) setter = (OverloadFunction)prop.setter; - else setter = new OverloadFunction("set " + name); - - setter.overloads.add(Overload.fromMethod(method)); - target.defineProperty(null, name, getter, setter, true, true); - } - } - } - } - private static void applyFields(boolean member, ObjectValue target, Class clazz) { - for (var field : clazz.getDeclaredFields()) { - if (!Modifier.isStatic(field.getModifiers()) != member) continue; - var nat = field.getAnnotation(Native.class); - - 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)); - target.defineProperty(null, name, getter, setter, true, false); - } - } - } - private static void applyClasses(boolean member, ObjectValue target, Class clazz) { - for (var cl : clazz.getDeclaredClasses()) { - if (!Modifier.isStatic(cl.getModifiers()) != member) continue; - var nat = cl.getAnnotation(Native.class); - - if (nat != null) { - var name = nat.value(); - if (name.equals("")) name = cl.getSimpleName(); - - var getter = new NativeFunction("get " + name, (ctx, thisArg, args) -> cl); - - target.defineProperty(null, name, getter, null, true, false); - } - } - } - - /** - * Generates a prototype for the given class. - * The returned object will have appropriate wrappers for all instance members. - * All accessors and methods will expect the this argument to be a native wrapper of the given class type. - * @param clazz The class for which a prototype should be generated - */ - public static ObjectValue makeProto(Class clazz) { - var res = new ObjectValue(); - - applyMethods(true, res, clazz); - applyFields(true, res, clazz); - applyClasses(true, res, clazz); - - return res; - } - /** - * Generates a constructor for the given class. - * The returned function will have appropriate wrappers for all static members. - * When the function gets called, the underlying constructor will get called, unless the constructor is inaccessible. - * @param clazz The class for which a constructor should be generated - */ - public static FunctionValue makeConstructor(Class clazz) { - FunctionValue func = new OverloadFunction(clazz.getName()); - - for (var overload : clazz.getConstructors()) { - if (overload.getAnnotation(Native.class) == null) continue; - ((OverloadFunction)func).add(Overload.fromConstructor(overload)); - } - - if (((OverloadFunction)func).overloads.size() == 0) { - func = new NativeFunction(clazz.getName(), (a, b, c) -> { throw EngineException.ofError("This constructor is not invokable."); }); - } - - applyMethods(false, func, clazz); - applyFields(false, func, clazz); - applyClasses(false, func, clazz); - - func.special = true; - - return func; - } - /** - * Generates a namespace for the given class. - * The returned function will have appropriate wrappers for all static members. - * This method behaves almost like {@link NativeTypeRegister#makeConstructor}, but will return an object instead. - * @param clazz The class for which a constructor should be generated - */ - public static ObjectValue makeNamespace(Class clazz) { - ObjectValue res = new ObjectValue(); - - applyMethods(false, res, clazz); - applyFields(false, res, clazz); - applyClasses(false, res, clazz); - - return res; - } - - private void initType(Class clazz, FunctionValue constr, ObjectValue proto) { - if (constr != null && proto != null) return; - // i vomit - if ( - clazz == Object.class || - clazz == Void.class || - clazz == Number.class || clazz == Double.class || clazz == Float.class || - clazz == Long.class || clazz == Integer.class || clazz == Short.class || - clazz == Character.class || clazz == Byte.class || clazz == Boolean.class || - clazz.isPrimitive() || - clazz.isArray() || - clazz.isAnonymousClass() || - clazz.isEnum() || - clazz.isInterface() || - clazz.isSynthetic() - ) return; - - if (constr == null) constr = makeConstructor(clazz); - if (proto == null) proto = makeProto(clazz); - - proto.values.put("constructor", constr); - constr.values.put("prototype", proto); - - prototypes.put(clazz, proto); - constructors.put(clazz, constr); - - var parent = clazz.getSuperclass(); - if (parent == null) return; - - var parentProto = getProto(parent); - var parentConstr = getConstr(parent); - - if (parentProto != null) proto.setPrototype(null, parentProto); - if (parentConstr != null) constr.setPrototype(null, parentConstr); - } - - public ObjectValue getProto(Class clazz) { - initType(clazz, constructors.get(clazz), prototypes.get(clazz)); - return prototypes.get(clazz); - } - public FunctionValue getConstr(Class clazz) { - initType(clazz, constructors.get(clazz), prototypes.get(clazz)); - return constructors.get(clazz); - } - - public void setProto(Class clazz, ObjectValue value) { - prototypes.put(clazz, value); - } - public void setConstr(Class clazz, FunctionValue value) { - constructors.put(clazz, value); - } -} +package me.topchetoeu.jscript.interop; + +import java.lang.reflect.Modifier; +import java.util.HashMap; + +import me.topchetoeu.jscript.engine.Environment; +import me.topchetoeu.jscript.engine.WrappersProvider; +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; + +public class NativeWrapperProvider implements WrappersProvider { + private final HashMap, FunctionValue> constructors = new HashMap<>(); + private final HashMap, ObjectValue> prototypes = new HashMap<>(); + private final Environment env; + + private static void applyMethods(Environment env, boolean member, ObjectValue target, Class clazz) { + for (var method : clazz.getDeclaredMethods()) { + var nat = method.getAnnotation(Native.class); + var get = method.getAnnotation(NativeGetter.class); + var set = method.getAnnotation(NativeSetter.class); + var memberMatch = !Modifier.isStatic(method.getModifiers()) == member; + + if (nat != null) { + if (nat.thisArg() && !member || !nat.thisArg() && !memberMatch) continue; + + Object name = nat.value(); + if (((String)name).startsWith("@@")) name = env.symbol(((String)name).substring(2)); + else if (name.equals("")) name = method.getName(); + + var val = target.values.get(name); + + if (!(val instanceof OverloadFunction)) target.defineProperty(null, name, val = new OverloadFunction(name.toString())); + + ((OverloadFunction)val).add(Overload.fromMethod(method, nat.thisArg())); + } + else { + if (get != null) { + if (get.thisArg() && !member || !get.thisArg() && !memberMatch) continue; + + Object name = get.value(); + if (((String)name).startsWith("@@")) name = env.symbol(((String)name).substring(2)); + else if (name.equals("")) name = method.getName(); + + var prop = target.properties.get(name); + OverloadFunction getter = null; + var setter = prop == null ? null : prop.setter; + + if (prop != null && prop.getter instanceof OverloadFunction) getter = (OverloadFunction)prop.getter; + else getter = new OverloadFunction("get " + name); + + getter.add(Overload.fromMethod(method, get.thisArg())); + target.defineProperty(null, name, getter, setter, true, true); + } + if (set != null) { + if (set.thisArg() && !member || !set.thisArg() && !memberMatch) continue; + + Object name = set.value(); + if (((String)name).startsWith("@@")) name = env.symbol(((String)name).substring(2)); + else if (name.equals("")) name = method.getName(); + + var prop = target.properties.get(name); + var getter = prop == null ? null : prop.getter; + OverloadFunction setter = null; + + if (prop != null && prop.setter instanceof OverloadFunction) setter = (OverloadFunction)prop.setter; + else setter = new OverloadFunction("set " + name); + + setter.add(Overload.fromMethod(method, set.thisArg())); + target.defineProperty(null, name, getter, setter, true, true); + } + } + } + } + private static void applyFields(Environment env, boolean member, ObjectValue target, Class clazz) { + for (var field : clazz.getDeclaredFields()) { + if (!Modifier.isStatic(field.getModifiers()) != member) continue; + var nat = field.getAnnotation(Native.class); + + if (nat != null) { + Object name = nat.value(); + if (((String)name).startsWith("@@")) name = env.symbol(((String)name).substring(2)); + else if (name.equals("")) name = field.getName(); + + var getter = OverloadFunction.of("get " + name, Overload.getterFromField(field)); + var setter = OverloadFunction.of("set " + name, Overload.setterFromField(field)); + target.defineProperty(null, name, getter, setter, true, false); + } + } + } + private static void applyClasses(Environment env, boolean member, ObjectValue target, Class clazz) { + for (var cl : clazz.getDeclaredClasses()) { + if (!Modifier.isStatic(cl.getModifiers()) != member) continue; + var nat = cl.getAnnotation(Native.class); + + if (nat != null) { + Object name = nat.value(); + if (((String)name).startsWith("@@")) name = env.symbol(((String)name).substring(2)); + else if (name.equals("")) name = cl.getName(); + + var getter = new NativeFunction("get " + name, (ctx, thisArg, args) -> cl); + + target.defineProperty(null, name, getter, null, true, false); + } + } + } + + /** + * Generates a prototype for the given class. + * The returned object will have appropriate wrappers for all instance members. + * All accessors and methods will expect the this argument to be a native wrapper of the given class type. + * @param clazz The class for which a prototype should be generated + */ + public static ObjectValue makeProto(Environment ctx, Class clazz) { + var res = new ObjectValue(); + + for (var overload : clazz.getDeclaredMethods()) { + var init = overload.getAnnotation(NativeInit.class); + if (init == null || init.value() != InitType.PROTOTYPE) continue; + try { overload.invoke(null, ctx, res); } + catch (ReflectiveOperationException e) { e.printStackTrace(); } + } + + applyMethods(ctx, true, res, clazz); + applyFields(ctx, true, res, clazz); + applyClasses(ctx, true, res, clazz); + + return res; + } + /** + * Generates a constructor for the given class. + * The returned function will have appropriate wrappers for all static members. + * When the function gets called, the underlying constructor will get called, unless the constructor is inaccessible. + * @param clazz The class for which a constructor should be generated + */ + public static FunctionValue makeConstructor(Environment ctx, Class clazz) { + FunctionValue func = new OverloadFunction(clazz.getName()); + + for (var overload : clazz.getDeclaredConstructors()) { + var nat = overload.getAnnotation(Native.class); + if (nat == null) continue; + ((OverloadFunction)func).add(Overload.fromConstructor(overload, nat.thisArg())); + } + for (var overload : clazz.getDeclaredMethods()) { + var constr = overload.getAnnotation(NativeConstructor.class); + if (constr == null) continue; + ((OverloadFunction)func).add(Overload.fromMethod(overload, constr.thisArg())); + } + for (var overload : clazz.getDeclaredMethods()) { + var init = overload.getAnnotation(NativeInit.class); + if (init == null || init.value() != InitType.CONSTRUCTOR) continue; + try { overload.invoke(null, ctx, func); } + catch (ReflectiveOperationException e) { e.printStackTrace(); } + } + + if (((OverloadFunction)func).overloads.size() == 0) { + func = new NativeFunction(clazz.getName(), (a, b, c) -> { throw EngineException.ofError("This constructor is not invokable."); }); + } + + applyMethods(ctx, false, func, clazz); + applyFields(ctx, false, func, clazz); + applyClasses(ctx, false, func, clazz); + + func.special = true; + + return func; + } + /** + * Generates a namespace for the given class. + * The returned function will have appropriate wrappers for all static members. + * This method behaves almost like {@link NativeWrapperProvider#makeConstructor}, but will return an object instead. + * @param clazz The class for which a constructor should be generated + */ + public static ObjectValue makeNamespace(Environment ctx, Class clazz) { + ObjectValue res = new ObjectValue(); + + for (var overload : clazz.getDeclaredMethods()) { + var init = overload.getAnnotation(NativeInit.class); + if (init == null || init.value() != InitType.NAMESPACE) continue; + try { overload.invoke(null, ctx, res); } + catch (ReflectiveOperationException e) { e.printStackTrace(); } + } + + applyMethods(ctx, false, res, clazz); + applyFields(ctx, false, res, clazz); + applyClasses(ctx, false, res, clazz); + + return res; + } + + private void initType(Class clazz, FunctionValue constr, ObjectValue proto) { + if (constr != null && proto != null) return; + // i vomit + if ( + clazz == Object.class || + clazz == Void.class || + clazz == Number.class || clazz == Double.class || clazz == Float.class || + clazz == Long.class || clazz == Integer.class || clazz == Short.class || + clazz == Character.class || clazz == Byte.class || clazz == Boolean.class || + clazz.isPrimitive() || + clazz.isArray() || + clazz.isAnonymousClass() || + clazz.isEnum() || + clazz.isInterface() || + clazz.isSynthetic() + ) return; + + if (constr == null) constr = makeConstructor(env, clazz); + if (proto == null) proto = makeProto(env, clazz); + + proto.values.put("constructor", constr); + constr.values.put("prototype", proto); + + prototypes.put(clazz, proto); + constructors.put(clazz, constr); + + var parent = clazz.getSuperclass(); + if (parent == null) return; + + var parentProto = getProto(parent); + var parentConstr = getConstr(parent); + + if (parentProto != null) proto.setPrototype(null, parentProto); + if (parentConstr != null) constr.setPrototype(null, parentConstr); + } + + public ObjectValue getProto(Class clazz) { + initType(clazz, constructors.get(clazz), prototypes.get(clazz)); + return prototypes.get(clazz); + } + public FunctionValue getConstr(Class clazz) { + initType(clazz, constructors.get(clazz), prototypes.get(clazz)); + return constructors.get(clazz); + } + + public void setProto(Class clazz, ObjectValue value) { + prototypes.put(clazz, value); + } + public void setConstr(Class clazz, FunctionValue value) { + constructors.put(clazz, value); + } + + public NativeWrapperProvider(Environment env) { + this.env = env; + } +} diff --git a/src/me/topchetoeu/jscript/interop/Overload.java b/src/me/topchetoeu/jscript/interop/Overload.java index 191f0e4..a4548e1 100644 --- a/src/me/topchetoeu/jscript/interop/Overload.java +++ b/src/me/topchetoeu/jscript/interop/Overload.java @@ -17,28 +17,29 @@ public class Overload { public final OverloadRunner runner; public final boolean variadic; + public final boolean passThis; public final Class thisArg; public final Class[] params; - public static Overload fromMethod(Method method) { + public static Overload fromMethod(Method method, boolean passThis) { return new Overload( (ctx, th, args) -> method.invoke(th, args), - method.isVarArgs(), + method.isVarArgs(), passThis, Modifier.isStatic(method.getModifiers()) ? null : method.getDeclaringClass(), method.getParameterTypes() ); } - public static Overload fromConstructor(Constructor method) { + public static Overload fromConstructor(Constructor method, boolean passThis) { return new Overload( (ctx, th, args) -> method.newInstance(args), - method.isVarArgs(), - Modifier.isStatic(method.getModifiers()) ? null : method.getDeclaringClass(), + method.isVarArgs(), passThis, + null, method.getParameterTypes() ); } public static Overload getterFromField(Field field) { return new Overload( - (ctx, th, args) -> field.get(th), false, + (ctx, th, args) -> field.get(th), false, false, Modifier.isStatic(field.getModifiers()) ? null : field.getDeclaringClass(), new Class[0] ); @@ -46,23 +47,24 @@ public class Overload { public static Overload setterFromField(Field field) { 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, false, 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 passThis) { return new Overload( - (ctx, th, args) -> runner.run(ctx, th, args), false, + (ctx, th, args) -> runner.run(ctx, th, args), false, passThis, thisArg, new Class[0] ); } - public Overload(OverloadRunner runner, boolean variadic, Class thisArg, Class args[]) { + public Overload(OverloadRunner runner, boolean variadic, boolean passThis, Class thisArg, Class args[]) { this.runner = runner; this.variadic = variadic; + this.passThis = passThis; this.thisArg = thisArg; this.params = args; } diff --git a/src/me/topchetoeu/jscript/interop/OverloadFunction.java b/src/me/topchetoeu/jscript/interop/OverloadFunction.java index 321b04d..5f54ceb 100644 --- a/src/me/topchetoeu/jscript/interop/OverloadFunction.java +++ b/src/me/topchetoeu/jscript/interop/OverloadFunction.java @@ -5,29 +5,37 @@ import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.List; +import me.topchetoeu.jscript.Location; 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]; + boolean consumesEngine = overload.params.length > 0 && overload.params[0] == Context.class; + int start = (consumesEngine ? 1 : 0) + (overload.passThis ? 1 : 0); + int end = overload.params.length - (overload.variadic ? 1 : 0); + 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]); + 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)); + } } if (overload.variadic) { @@ -36,15 +44,34 @@ public class OverloadFunction extends FunctionValue { Object varArg = Array.newInstance(type, n); for (var i = 0; i < n; i++) { - Array.set(varArg, i, Values.convert(ctx, args[i + end - start], type)); + 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; + var thisArgType = overload.passThis ? overload.params[consumesEngine ? 1 : 0] : overload.thisArg; + Object _this; - Object _this = overload.thisArg == null ? null : Values.convert(ctx, thisArg, overload.thisArg); + try { + _this = thisArgType == null ? null : Values.convert(ctx, thisArg, thisArgType); + } + catch (ConvertException e) { + if (overloads.size() > 1) continue loop; + else throw EngineException.ofType(String.format("This argument can't be converted from %s to %s", e.source, e.target)); + } + + if (consumesEngine) newArgs[0] = ctx; + if (overload.passThis) { + newArgs[consumesEngine ? 1 : 0] = _this; + _this = null; + } try { return Values.normalize(ctx, overload.runner.run(ctx, _this, newArgs)); @@ -56,15 +83,19 @@ public class OverloadFunction extends FunctionValue { continue; } catch (InvocationTargetException e) { + var loc = new Location(0, 0, ""); if (e.getTargetException() instanceof EngineException) { - throw ((EngineException)e.getTargetException()); + throw ((EngineException)e.getTargetException()).add(name, loc); + } + else if (e.getTargetException() instanceof NullPointerException) { + throw EngineException.ofType("Unexpected value of 'undefined'.").add(name, loc); } else { - throw EngineException.ofError(e.getTargetException().getMessage()); + throw EngineException.ofError(e.getTargetException().getMessage()).add(name, loc); } } catch (ReflectiveOperationException e) { - throw EngineException.ofError(e.getMessage()); + throw EngineException.ofError(e.getMessage()).add(name, new Location(0, 0, "")); } catch (Exception e) { throw e; @@ -82,4 +113,9 @@ public class OverloadFunction extends FunctionValue { public OverloadFunction(String name) { super(name, 0); } + + public static OverloadFunction of(String name, Overload overload) { + if (overload == null) return null; + else return new OverloadFunction(name).add(overload); + } } diff --git a/lib/lib.d.ts b/src/me/topchetoeu/jscript/js/lib.d.ts similarity index 99% rename from lib/lib.d.ts rename to src/me/topchetoeu/jscript/js/lib.d.ts index 4e83de2..df95570 100644 --- a/lib/lib.d.ts +++ b/src/me/topchetoeu/jscript/js/lib.d.ts @@ -244,6 +244,7 @@ interface ArrayConstructor { } interface Boolean { + toString(): string; valueOf(): boolean; } interface BooleanConstructor { @@ -349,7 +350,7 @@ interface Object { toString(): string; hasOwnProperty(key: any): boolean; } -interface ObjectConstructor extends Function { +interface ObjectConstructor { (arg: string): String; (arg: number): Number; (arg: boolean): Boolean; diff --git a/src/me/topchetoeu/jscript/parsing/Parsing.java b/src/me/topchetoeu/jscript/parsing/Parsing.java index cf95238..16d2f8c 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; @@ -516,12 +516,59 @@ public class Parsing { for (int i = 2; i < literal.length(); i++) { res *= 16; - res += fromHex(literal.charAt(i)); + int dig = fromHex(literal.charAt(i)); + res += dig; } return res; } + public static Double parseNumber(boolean octals, String value) { + if (value.startsWith("0x") || value.startsWith("0X")) { + if (value.length() == 2) return null; + return parseHex(value); + } + if (value.endsWith("e") || value.endsWith("E") || value.endsWith("-")) return null; + + int i = 0; + double res = 0, dotDivisor = 1; + boolean e = false, dot = false; + int exponent = 0; + + for (; i < value.length(); i++) { + char c = value.charAt(i); + if (c == '.') { dot = true; break; } + if (c == 'e') { e = true; break; } + if (!isDigit(c)) break; + + res = res * 10 + c - '0'; + } + + if (dot) for (i++; i < value.length(); i++) { + char c = value.charAt(i); + if (c == 'e') { e = true; break; } + if (!isDigit(c)) break; + + res += (c - '0') / (dotDivisor *= 10); + } + + if (e) for (i++; i < value.length(); i++) { + char c = value.charAt(i); + if (!isDigit(c)) break; + exponent = 10 * exponent + c - '0'; + } + + if (exponent < 0) for (int j = 0; j < -exponent; j++) res /= 10; + else for (int j = 0; j < exponent; j++) res *= 10; + + return res; + } + private static double parseNumber(Location loc, String value) { + var res = parseNumber(false, value); + if (res == null) throw new SyntaxException(loc, "Invalid number format."); + else return res; + } + private static List parseTokens(String filename, Collection tokens) { var res = new ArrayList(); @@ -529,28 +576,14 @@ public class Parsing { var loc = new Location(el.line, el.start, filename); switch (el.type) { case LITERAL: res.add(Token.identifier(el.line, el.start, el.value)); break; - case NUMBER: - if (el.value.startsWith("0x") || el.value.startsWith("0X")) { - if (el.value.endsWith("x") || el.value.endsWith("X")) { - throw new SyntaxException(loc, "Invalid number format."); - } - res.add(Token.number(el.line, el.start, parseHex(el.value))); break; - } - if ( - el.value.endsWith("e") || el.value.endsWith("E") || el.value.endsWith("-") - ) throw new SyntaxException(loc, "Invalid number format."); - else res.add(Token.number(el.line, el.start, Double.parseDouble(el.value))); break; + case NUMBER: res.add(Token.number(el.line, el.start, parseNumber(loc, el.value))); break; + case STRING: res.add(Token.string(el.line, el.start, parseString(loc, el.value))); break; + case REGEX: res.add(Token.regex(el.line, el.start, parseRegex(loc, el.value))); break; case OPERATOR: Operator op = Operator.parse(el.value); if (op == null) throw new SyntaxException(loc, String.format("Unrecognized operator '%s'.", el.value)); res.add(Token.operator(el.line, el.start, op)); break; - case STRING: - res.add(Token.string(el.line, el.start, parseString(loc, el.value))); - break; - case REGEX: - res.add(Token.regex(el.line, el.start, parseRegex(loc, el.value))); - break; } } @@ -652,7 +685,7 @@ public class Parsing { } } - public static ParseRes parseRegex(String filename, List tokens, int i) { + public static ParseRes parseRegex(String filename, List tokens, int i) { var loc = getLoc(filename, tokens, i); try { if (tokens.get(i).isRegex()) { @@ -660,11 +693,7 @@ public class Parsing { var index = val.lastIndexOf('/'); var first = val.substring(1, index); var second = val.substring(index + 1); - return ParseRes.res(new NewStatement(loc, - new VariableStatement(null, "RegExp"), - new ConstantStatement(loc, first), - new ConstantStatement(loc, second) - ), 1); + return ParseRes.res(new RegexStatement(loc, first, second), 1); } else return ParseRes.failed(); } @@ -1842,7 +1871,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(); @@ -1872,7 +1901,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/ArrayPolyfill.java b/src/me/topchetoeu/jscript/polyfills/ArrayPolyfill.java new file mode 100644 index 0000000..7f9edde --- /dev/null +++ b/src/me/topchetoeu/jscript/polyfills/ArrayPolyfill.java @@ -0,0 +1,357 @@ +package me.topchetoeu.jscript.polyfills; + +import java.util.Iterator; +import java.util.Stack; + +import me.topchetoeu.jscript.engine.Context; +import me.topchetoeu.jscript.engine.Environment; +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.Values; +import me.topchetoeu.jscript.interop.InitType; +import me.topchetoeu.jscript.interop.Native; +import me.topchetoeu.jscript.interop.NativeConstructor; +import me.topchetoeu.jscript.interop.NativeGetter; +import me.topchetoeu.jscript.interop.NativeInit; +import me.topchetoeu.jscript.interop.NativeSetter; + +public class ArrayPolyfill { + @NativeGetter(thisArg = true) public static int length(Context ctx, ArrayValue thisArg) throws InterruptedException { + return thisArg.size(); + } + @NativeSetter(thisArg = true) public static void length(Context ctx, ArrayValue thisArg, int len) throws InterruptedException { + thisArg.setSize(len); + } + + @Native(thisArg = true) public static ObjectValue values(Context ctx, ArrayValue thisArg) throws InterruptedException { + return Values.fromJavaIterable(ctx, thisArg); + } + @Native(thisArg = true) public static ObjectValue keys(Context ctx, ArrayValue thisArg) throws InterruptedException { + return Values.fromJavaIterable(ctx, () -> new Iterator() { + private int i = 0; + + @Override + public boolean hasNext() { + return i < thisArg.size(); + } + @Override + public Object next() { + if (!hasNext()) return null; + return i++; + } + }); + } + @Native(thisArg = true) public static ObjectValue entries(Context ctx, ArrayValue thisArg) throws InterruptedException { + return Values.fromJavaIterable(ctx, () -> new Iterator() { + private int i = 0; + + @Override + public boolean hasNext() { + return i < thisArg.size(); + } + @Override + public Object next() { + if (!hasNext()) return null; + return new ArrayValue(ctx, i, thisArg.get(i++)); + } + }); + } + + @Native(value = "@@Symbol.iterator", thisArg = true) + public static ObjectValue iterator(Context ctx, ArrayValue thisArg) throws InterruptedException { + return values(ctx, thisArg); + } + @Native(value = "@@Symbol.asyncIterator", thisArg = true) + public static ObjectValue asyncIterator(Context ctx, ArrayValue thisArg) throws InterruptedException { + return values(ctx, thisArg); + } + + @Native(thisArg = true) public static ArrayValue concat(Context ctx, ArrayValue thisArg, Object ...others) throws InterruptedException { + // TODO: Fully implement with non-array spreadable objects + var size = 0; + + for (int i = 0; i < others.length; i++) { + if (others[i] instanceof ArrayValue) size += ((ArrayValue)others[i]).size(); + else i++; + } + + var res = new ArrayValue(size); + + for (int i = 0, j = 0; i < others.length; i++) { + if (others[i] instanceof ArrayValue) { + int n = ((ArrayValue)others[i]).size(); + ((ArrayValue)others[i]).copyTo(ctx, res, 0, j, n); + j += n; + } + else { + res.set(ctx, j++, others[i]); + } + } + + return res; + } + + @Native(thisArg = true) public static void sort(Context ctx, ArrayValue arr, FunctionValue cmp) throws InterruptedException { + try { + arr.sort((a, b) -> { + try { + var res = Values.toNumber(ctx, cmp.call(ctx, null, a, b)); + if (res < 0) return -1; + if (res > 0) return 1; + return 0; + } + catch (InterruptedException e) { + throw new RuntimeException(e); + } + }); + } + catch (RuntimeException e) { + if (e.getCause() instanceof InterruptedException) throw (InterruptedException)e.getCause(); + else throw e; + } + } + + private static int normalizeI(int len, int i, boolean clamp) { + if (i < 0) i += len; + if (clamp) { + if (i < 0) i = 0; + if (i >= len) i = len; + } + return i; + } + + @Native(thisArg = true) public static ArrayValue fill(Context ctx, ArrayValue arr, Object val, int start, int end) throws InterruptedException { + start = normalizeI(arr.size(), start, true); + end = normalizeI(arr.size(), end, true); + + for (; start < end; start++) { + arr.set(ctx, start, val); + } + + return arr; + } + @Native(thisArg = true) public static ArrayValue fill(Context ctx, ArrayValue arr, Object val, int start) throws InterruptedException { + return fill(ctx, arr, val, start, arr.size()); + } + @Native(thisArg = true) public static ArrayValue fill(Context ctx, ArrayValue arr, Object val) throws InterruptedException { + return fill(ctx, arr, val, 0, arr.size()); + } + + @Native(thisArg = true) public static boolean every(Context ctx, ArrayValue arr, FunctionValue func, Object thisArg) throws InterruptedException { + for (var i = 0; i < arr.size(); i++) { + if (!Values.toBoolean(func.call(ctx, thisArg, arr.get(i), i, arr))) return false; + } + + return true; + } + @Native(thisArg = true) public static boolean some(Context ctx, ArrayValue arr, FunctionValue func, Object thisArg) throws InterruptedException { + for (var i = 0; i < arr.size(); i++) { + if (Values.toBoolean(func.call(ctx, thisArg, arr.get(i), i, arr))) return true; + } + + return false; + } + + @Native(thisArg = true) public static ArrayValue filter(Context ctx, ArrayValue arr, FunctionValue func, Object thisArg) throws InterruptedException { + var res = new ArrayValue(arr.size()); + + for (int i = 0, j = 0; i < arr.size(); i++) { + if (arr.has(i) && Values.toBoolean(func.call(ctx, thisArg, arr.get(i), i, arr))) res.set(ctx, j++, arr.get(i)); + } + return res; + } + @Native(thisArg = true) public static ArrayValue map(Context ctx, ArrayValue arr, FunctionValue func, Object thisArg) throws InterruptedException { + var res = new ArrayValue(arr.size()); + for (int i = 0, j = 0; i < arr.size(); i++) { + if (arr.has(i)) res.set(ctx, j++, func.call(ctx, thisArg, arr.get(i), i, arr)); + } + return res; + } + @Native(thisArg = true) public static void forEach(Context ctx, ArrayValue arr, FunctionValue func, Object thisArg) throws InterruptedException { + for (int i = 0; i < arr.size(); i++) { + if (arr.has(i)) func.call(ctx, thisArg, arr.get(i), i, arr); + } + } + + @Native(thisArg = true) public static ArrayValue flat(Context ctx, ArrayValue arr, int depth) throws InterruptedException { + var res = new ArrayValue(arr.size()); + var stack = new Stack(); + var depths = new Stack(); + + stack.push(arr); + depths.push(-1); + + while (!stack.empty()) { + var el = stack.pop(); + int d = depths.pop(); + + if (d <= depth && el instanceof ArrayValue) { + for (int i = ((ArrayValue)el).size() - 1; i >= 0; i--) { + stack.push(((ArrayValue)el).get(i)); + depths.push(d + 1); + } + } + else res.set(ctx, depth, arr); + } + + return res; + } + @Native(thisArg = true) public static ArrayValue flatMap(Context ctx, ArrayValue arr, FunctionValue cmp, Object thisArg) throws InterruptedException { + return flat(ctx, map(ctx, arr, cmp, thisArg), 1); + } + + @Native(thisArg = true) public static Object find(Context ctx, ArrayValue arr, FunctionValue cmp, Object thisArg) throws InterruptedException { + for (int i = 0; i < arr.size(); i++) { + if (arr.has(i) && Values.toBoolean(cmp.call(ctx, thisArg, arr.get(i), i, arr))) return arr.get(i); + } + + return null; + } + @Native(thisArg = true) public static Object findLast(Context ctx, ArrayValue arr, FunctionValue cmp, Object thisArg) throws InterruptedException { + for (var i = arr.size() - 1; i >= 0; i--) { + if (arr.has(i) && Values.toBoolean(cmp.call(ctx, thisArg, arr.get(i), i, arr))) return arr.get(i); + } + + return null; + } + + @Native(thisArg = true) public static int findIndex(Context ctx, ArrayValue arr, FunctionValue cmp, Object thisArg) throws InterruptedException { + for (int i = 0; i < arr.size(); i++) { + if (arr.has(i) && Values.toBoolean(cmp.call(ctx, thisArg, arr.get(i), i, arr))) return i; + } + + return -1; + } + @Native(thisArg = true) public static int findLastIndex(Context ctx, ArrayValue arr, FunctionValue cmp, Object thisArg) throws InterruptedException { + for (var i = arr.size() - 1; i >= 0; i--) { + if (arr.has(i) && Values.toBoolean(cmp.call(ctx, thisArg, arr.get(i), i, arr))) return i; + } + + return -1; + } + + @Native(thisArg = true) public static int indexOf(Context ctx, ArrayValue arr, Object val, int start) throws InterruptedException { + start = normalizeI(arr.size(), start, true); + + for (int i = 0; i < arr.size() && i < start; i++) { + if (Values.strictEquals(ctx, arr.get(i), val)) return i; + } + + return -1; + } + @Native(thisArg = true) public static int lastIndexOf(Context ctx, ArrayValue arr, Object val, int start) throws InterruptedException { + start = normalizeI(arr.size(), start, true); + + for (int i = arr.size(); i >= start; i--) { + if (Values.strictEquals(ctx, arr.get(i), val)) return i; + } + + return -1; + } + + @Native(thisArg = true) public static boolean includes(Context ctx, ArrayValue arr, Object el, int start) throws InterruptedException { + return indexOf(ctx, arr, el, start) >= 0; + } + + @Native(thisArg = true) public static Object pop(Context ctx, ArrayValue arr) throws InterruptedException { + if (arr.size() == 0) return null; + var val = arr.get(arr.size() - 1); + arr.shrink(1); + return val; + } + @Native(thisArg = true) public static int push(Context ctx, ArrayValue arr, Object ...values) throws InterruptedException { + arr.copyFrom(ctx, values, 0, arr.size(), values.length); + return arr.size(); + } + + @Native(thisArg = true) public static Object shift(Context ctx, ArrayValue arr) throws InterruptedException { + if (arr.size() == 0) return null; + var val = arr.get(0); + arr.move(1, 0, arr.size()); + arr.shrink(1); + return val; + } + @Native(thisArg = true) public static int unshift(Context ctx, ArrayValue arr, Object ...values) throws InterruptedException { + arr.move(0, values.length, arr.size()); + arr.copyFrom(ctx, values, 0, 0, values.length); + return arr.size(); + } + + @Native(thisArg = true) public static ArrayValue slice(Context ctx, ArrayValue arr, int start, int end) { + start = normalizeI(arr.size(), start, true); + end = normalizeI(arr.size(), end, true); + + var res = new ArrayValue(end - start); + arr.copyTo(ctx, res, start, 0, end - start); + return res; + } + @Native(thisArg = true) public static ArrayValue slice(Context ctx, ArrayValue arr, int start) { + return slice(ctx, arr, start, arr.size()); + } + + @Native(thisArg = true) public static ArrayValue splice(Context ctx, ArrayValue arr, int start, int deleteCount, Object ...items) throws InterruptedException { + start = normalizeI(arr.size(), start, true); + deleteCount = normalizeI(arr.size(), deleteCount, true); + if (start + deleteCount >= arr.size()) deleteCount = arr.size() - start; + + var size = arr.size() - deleteCount + items.length; + var res = new ArrayValue(deleteCount); + arr.copyTo(ctx, res, start, 0, deleteCount); + arr.move(start + deleteCount, start + items.length, arr.size() - start - deleteCount); + arr.copyFrom(ctx, items, 0, start, items.length); + arr.setSize(size); + + return res; + } + @Native(thisArg = true) public static ArrayValue splice(Context ctx, ArrayValue arr, int start) throws InterruptedException { + return splice(ctx, arr, start, arr.size() - start); + } + @Native(thisArg = true) public static String toString(Context ctx, ArrayValue arr) throws InterruptedException { + return join(ctx, arr, ","); + } + + @Native(thisArg = true) public static String join(Context ctx, ArrayValue arr, String sep) throws InterruptedException { + var res = new StringBuilder(); + var comma = true; + + for (int i = 0; i < arr.size(); i++) { + if (!arr.has(i)) continue; + if (comma) res.append(sep); + comma = false; + var el = arr.get(i); + if (el == null || el == Values.NULL) continue; + + res.append(Values.toString(ctx, el)); + } + + return res.toString(); + } + + @Native public static boolean isArray(Context ctx, Object val) { return val instanceof ArrayValue; } + @Native public static ArrayValue of(Context ctx, Object... args) { + var res = new ArrayValue(args.length); + res.copyFrom(ctx, args, 0, 0, args.length); + return res; + } + + @NativeConstructor public static ArrayValue constructor(Context ctx, Object... args) { + ArrayValue res; + + if (args.length == 1 && args[0] instanceof Number) { + int len = ((Number)args[0]).intValue(); + res = new ArrayValue(len); + res.setSize(len); + } + else { + res = new ArrayValue(args.length); + res.copyFrom(ctx, args, 0, 0, args.length); + } + + return res; + } + + @NativeInit(InitType.PROTOTYPE) public static void init(Environment env, ObjectValue target) { + target.defineProperty(null, env.symbol("Symbol.typeName"), "Array"); + } +} diff --git a/src/me/topchetoeu/jscript/polyfills/AsyncFunctionPolyfill.java b/src/me/topchetoeu/jscript/polyfills/AsyncFunctionPolyfill.java new file mode 100644 index 0000000..b568cf6 --- /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(ctx, frame); + + awaiting = false; + while (!awaiting) { + try { + res = frame.next(ctx, inducedValue, Runners.NO_RETURN, inducedError == Runners.NO_RETURN ? null : new EngineException(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..18950e3 --- /dev/null +++ b/src/me/topchetoeu/jscript/polyfills/AsyncGeneratorPolyfill.java @@ -0,0 +1,134 @@ +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 { + @Native("@@Symbol.typeName") public final String name = "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(ctx, frame); + state = 0; + + while (state == 0) { + try { + res = frame.next(ctx, inducedValue, inducedReturn, inducedError == Runners.NO_RETURN ? null : new EngineException(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/BooleanPolyfill.java b/src/me/topchetoeu/jscript/polyfills/BooleanPolyfill.java new file mode 100644 index 0000000..026dad2 --- /dev/null +++ b/src/me/topchetoeu/jscript/polyfills/BooleanPolyfill.java @@ -0,0 +1,36 @@ +package me.topchetoeu.jscript.polyfills; + +import me.topchetoeu.jscript.engine.Context; +import me.topchetoeu.jscript.engine.Environment; +import me.topchetoeu.jscript.engine.values.ObjectValue; +import me.topchetoeu.jscript.engine.values.Values; +import me.topchetoeu.jscript.interop.InitType; +import me.topchetoeu.jscript.interop.Native; +import me.topchetoeu.jscript.interop.NativeConstructor; +import me.topchetoeu.jscript.interop.NativeInit; + +public class BooleanPolyfill { + public static final BooleanPolyfill TRUE = new BooleanPolyfill(true); + public static final BooleanPolyfill FALSE = new BooleanPolyfill(false); + + public final boolean value; + + @NativeConstructor(thisArg = true) public static Object constructor(Context ctx, Object thisArg, Object val) { + val = Values.toBoolean(val); + if (thisArg instanceof ObjectValue) return (boolean)val ? TRUE : FALSE; + else return val; + } + @Native(thisArg = true) public static String toString(Context ctx, Object thisArg) { + return Values.toBoolean(thisArg) ? "true" : "false"; + } + @Native(thisArg = true) public static boolean valueOf(Context ctx, Object thisArg) { + return Values.toBoolean(thisArg); + } + + public BooleanPolyfill(boolean val) { + this.value = val; + } + @NativeInit(InitType.PROTOTYPE) public static void init(Environment env, ObjectValue target) { + target.defineProperty(null, env.symbol("Symbol.typeName"), "Boolean"); + } +} diff --git a/src/me/topchetoeu/jscript/polyfills/ErrorPolyfill.java b/src/me/topchetoeu/jscript/polyfills/ErrorPolyfill.java new file mode 100644 index 0000000..a466de9 --- /dev/null +++ b/src/me/topchetoeu/jscript/polyfills/ErrorPolyfill.java @@ -0,0 +1,67 @@ +package me.topchetoeu.jscript.polyfills; + +import me.topchetoeu.jscript.engine.Context; +import me.topchetoeu.jscript.engine.Environment; +import me.topchetoeu.jscript.engine.values.ArrayValue; +import me.topchetoeu.jscript.engine.values.ObjectValue; +import me.topchetoeu.jscript.engine.values.Values; +import me.topchetoeu.jscript.interop.InitType; +import me.topchetoeu.jscript.interop.Native; +import me.topchetoeu.jscript.interop.NativeConstructor; +import me.topchetoeu.jscript.interop.NativeInit; + +public class ErrorPolyfill { + private static String toString(Context ctx, Object cause, Object name, Object message, ArrayValue stack) throws InterruptedException { + if (name == null) name = ""; + else name = Values.toString(ctx, name).trim(); + if (message == null) message = ""; + else message = Values.toString(ctx, message).trim(); + StringBuilder res = new StringBuilder(); + + if (!name.equals("")) res.append(name); + if (!message.equals("") && !name.equals("")) res.append(": "); + if (!message.equals("")) res.append(message); + + if (stack != null) { + for (var el : stack) { + var str = Values.toString(ctx, el).trim(); + if (!str.equals("")) res.append("\n ").append(el); + } + } + + if (cause instanceof ObjectValue) res.append(toString(ctx, cause)); + + return res.toString(); + } + + @Native(thisArg = true) public static String toString(Context ctx, Object thisArg) throws InterruptedException { + if (thisArg instanceof ObjectValue) { + var stack = Values.getMember(ctx, thisArg, "stack"); + if (!(stack instanceof ArrayValue)) stack = null; + return toString(ctx, + Values.getMember(ctx, thisArg, "cause"), + Values.getMember(ctx, thisArg, "name"), + Values.getMember(ctx, thisArg, "message"), + (ArrayValue)stack + ); + } + else return "[Invalid error]"; + } + + @NativeConstructor(thisArg = true) public static ObjectValue constructor(Context ctx, Object thisArg, Object message) throws InterruptedException { + var target = new ObjectValue(); + if (thisArg instanceof ObjectValue) target = (ObjectValue)thisArg; + + target.defineProperty(ctx, "stack", new ArrayValue(ctx, ctx.message.stackTrace().toArray())); + target.defineProperty(ctx, "name", "Error"); + if (message == null) target.defineProperty(ctx, "message", ""); + else target.defineProperty(ctx, "message", Values.toString(ctx, message)); + + return target; + } + + @NativeInit(InitType.PROTOTYPE) public static void init(Environment env, ObjectValue target) { + target.defineProperty(null, env.symbol("Symbol.typeName"), "Error"); + target.defineProperty(null, "name", "Error"); + } +} diff --git a/src/me/topchetoeu/jscript/polyfills/FunctionPolyfill.java b/src/me/topchetoeu/jscript/polyfills/FunctionPolyfill.java new file mode 100644 index 0000000..6c5eb6a --- /dev/null +++ b/src/me/topchetoeu/jscript/polyfills/FunctionPolyfill.java @@ -0,0 +1,56 @@ +package me.topchetoeu.jscript.polyfills; + +import me.topchetoeu.jscript.engine.Context; +import me.topchetoeu.jscript.engine.Environment; +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.ObjectValue; +import me.topchetoeu.jscript.exceptions.EngineException; +import me.topchetoeu.jscript.interop.InitType; +import me.topchetoeu.jscript.interop.Native; +import me.topchetoeu.jscript.interop.NativeInit; + +public class FunctionPolyfill { + @Native(thisArg = true) public static Object apply(Context ctx, FunctionValue func, Object thisArg, ArrayValue args) throws InterruptedException { + return func.call(ctx, thisArg, args.toArray()); + } + @Native(thisArg = true) public static Object call(Context ctx, FunctionValue func, Object thisArg, Object... args) throws InterruptedException { + if (!(func instanceof FunctionValue)) throw EngineException.ofError("Expected this to be a function."); + + return func.call(ctx, thisArg, args); + } + @Native(thisArg = true) public static FunctionValue bind(FunctionValue func, Object thisArg, Object... args) { + if (!(func instanceof FunctionValue)) throw EngineException.ofError("Expected this to be a function."); + + return new NativeFunction(func.name + " (bound)", (callCtx, _0, callArgs) -> { + Object[] resArgs; + + if (args.length == 0) resArgs = callArgs; + else { + resArgs = new Object[args.length + callArgs.length]; + System.arraycopy(args, 0, resArgs, 0, args.length); + System.arraycopy(callArgs, 0, resArgs, args.length, callArgs.length); + } + + return func.call(callCtx, thisArg, resArgs); + }); + } + @Native(thisArg = true) public static String toString(Context ctx, Object func) { + 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); + } + + @NativeInit(InitType.PROTOTYPE) public static void init(Environment env, ObjectValue target) { + target.defineProperty(null, env.symbol("Symbol.typeName"), "Function"); + } +} diff --git a/src/me/topchetoeu/jscript/polyfills/GeneratorFunction.java b/src/me/topchetoeu/jscript/polyfills/GeneratorPolyfill.java similarity index 89% rename from src/me/topchetoeu/jscript/polyfills/GeneratorFunction.java rename to src/me/topchetoeu/jscript/polyfills/GeneratorPolyfill.java index 17b098f..db176ed 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 { @@ -18,6 +18,8 @@ public class GeneratorFunction extends FunctionValue { private boolean done = false; public CodeFrame frame; + @Native("@@Symbol.typeName") public final String name = "Generator"; + private ObjectValue next(Context ctx, Object inducedValue, Object inducedReturn, Object inducedError) throws InterruptedException { if (done) { if (inducedError != Runners.NO_RETURN) throw new EngineException(inducedError); @@ -28,12 +30,12 @@ public class GeneratorFunction extends FunctionValue { } Object res = null; - if (inducedValue != Runners.NO_RETURN) frame.push(ctx, inducedValue); - ctx.message.pushFrame(frame); + ctx.message.pushFrame(ctx, frame); yielding = false; + while (!yielding) { try { - res = frame.next(ctx, inducedReturn, inducedError); + res = frame.next(ctx, inducedValue, inducedReturn, inducedError == Runners.NO_RETURN ? null : new EngineException(inducedError)); inducedReturn = inducedError = Runners.NO_RETURN; if (res != Runners.NO_RETURN) { done = true; @@ -74,7 +76,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 +94,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..fd808ad 100644 --- a/src/me/topchetoeu/jscript/polyfills/Internals.java +++ b/src/me/topchetoeu/jscript/polyfills/Internals.java @@ -1,35 +1,32 @@ package me.topchetoeu.jscript.polyfills; +import java.util.HashMap; + import me.topchetoeu.jscript.engine.Context; -import me.topchetoeu.jscript.engine.FunctionContext; -import me.topchetoeu.jscript.engine.values.ArrayValue; -import me.topchetoeu.jscript.engine.values.CodeFunction; +import me.topchetoeu.jscript.engine.DataKey; +import me.topchetoeu.jscript.engine.Environment; +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.engine.values.Values; import me.topchetoeu.jscript.interop.Native; +import me.topchetoeu.jscript.interop.NativeWrapperProvider; public class Internals { - @Native public void markSpecial(FunctionValue ...funcs) { - for (var func : funcs) { - func.special = true; + private static final DataKey> THREADS = new DataKey<>(); + private static final DataKey I = new DataKey<>(); + + @Native public static FunctionValue bind(FunctionValue func, Object thisArg) throws InterruptedException { + return FunctionPolyfill.bind(func, thisArg); + } + @Native public static void log(Context ctx, Object ...args) throws InterruptedException { + for (var arg : args) { + Values.printValue(ctx, arg); } + System.out.println(); } - @Native public FunctionContext getEnv(Object func) { - if (func instanceof CodeFunction) return ((CodeFunction)func).environment; - else return null; - } - @Native public Object setEnv(Object func, FunctionContext 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 { - return func.call(ctx, thisArg, args.toArray()); - } - @Native public FunctionValue delay(Context ctx, double delay, FunctionValue callback) throws InterruptedException { - var thread = new Thread((Runnable)() -> { + + @Native public static int setTimeout(Context ctx, FunctionValue func, int delay, Object ...args) { + var thread = new Thread(() -> { var ms = (long)delay; var ns = (int)((delay - ms) * 10000000); @@ -38,111 +35,93 @@ public class Internals { } catch (InterruptedException e) { return; } - ctx.message.engine.pushMsg(false, ctx.message, callback, null); + ctx.message.engine.pushMsg(false, ctx.message, func, null, args); }); thread.start(); - return new NativeFunction((_ctx, thisArg, args) -> { - thread.interrupt(); - return null; - }); - } - @Native public void pushMessage(Context ctx, boolean micro, FunctionValue func, Object thisArg, Object[] args) { - ctx.message.engine.pushMsg(micro, ctx.message, func, thisArg, args); + int i = ctx.env.data.increase(I, 1, 0); + var threads = ctx.env.data.add(THREADS, new HashMap<>()); + threads.put(++i, thread); + return i; } + @Native public static int setInterval(Context ctx, FunctionValue func, int delay, Object ...args) { + var thread = new Thread(() -> { + var ms = (long)delay; + var ns = (int)((delay - ms) * 10000000); - @Native public int strlen(String str) { - return str.length(); - } - @Native("char") public int _char(String str) { - return str.charAt(0); - } - @Native public String stringFromChars(char[] str) { - return new String(str); - } - @Native public String stringFromStrings(String[] str) { - var res = new char[str.length]; - - for (var i = 0; i < str.length; i++) res[i] = str[i].charAt(0); - - return stringFromChars(res); - } - @Native public Symbol symbol(String str) { - return new Symbol(str); - } - @Native public String symbolToString(Symbol str) { - return str.value; - } - - @Native public static void log(Context ctx, Object ...args) throws InterruptedException { - for (var arg : args) { - Values.printValue(ctx, arg); - } - System.out.println(); - } - - @Native public boolean isArray(Object obj) { - return obj instanceof ArrayValue; - } - @Native public GeneratorFunction generator(FunctionValue obj) { - return new GeneratorFunction(obj); - } - - @Native public boolean defineField(Context ctx, ObjectValue obj, Object key, Object val, boolean writable, boolean enumerable, boolean configurable) { - return obj.defineProperty(ctx, key, val, writable, configurable, enumerable); - } - @Native public boolean defineProp(Context ctx, ObjectValue obj, Object key, FunctionValue getter, FunctionValue setter, boolean enumerable, boolean configurable) { - return obj.defineProperty(ctx, key, getter, setter, configurable, enumerable); - } - - @Native public ArrayValue keys(Context ctx, Object obj, boolean onlyString) throws InterruptedException { - var res = new ArrayValue(); - - var i = 0; - var list = Values.getMembers(ctx, obj, true, false); - - for (var el : list) res.set(ctx, i++, el); - - return res; - } - @Native public ArrayValue ownPropKeys(Context ctx, Object obj, boolean symbols) throws InterruptedException { - var res = new ArrayValue(); - - if (Values.isObject(obj)) { - var i = 0; - var list = Values.object(obj).keys(true); - - for (var el : list) res.set(ctx, i++, el); - } - - return res; - } - @Native public ObjectValue ownProp(Context ctx, ObjectValue val, Object key) throws InterruptedException { - return val.getMemberDescriptor(ctx, key); - } - @Native public void lock(ObjectValue val, String type) { - switch (type) { - case "ext": val.preventExtensions(); break; - case "seal": val.seal(); break; - case "freeze": val.freeze(); break; - } - } - @Native public boolean extensible(ObjectValue val) { - return val.extensible(); - } - - @Native public void sort(Context ctx, ArrayValue arr, FunctionValue cmp) { - arr.sort((a, b) -> { - try { - var res = Values.toNumber(ctx, cmp.call(ctx, null, a, b)); - if (res < 0) return -1; - if (res > 0) return 1; - return 0; - } - catch (InterruptedException e) { - Thread.currentThread().interrupt(); - return 0; + while (true) { + try { + Thread.sleep(ms, ns); + } + catch (InterruptedException e) { return; } + + ctx.message.engine.pushMsg(false, ctx.message, func, null, args); } }); + thread.start(); + + int i = ctx.env.data.increase(I, 1, 0); + var threads = ctx.env.data.add(THREADS, new HashMap<>()); + threads.put(++i, thread); + return i; + } + + @Native public static void clearTimeout(Context ctx, int i) { + var threads = ctx.env.data.add(THREADS, new HashMap<>()); + + var thread = threads.remove(i); + if (thread != null) thread.interrupt(); + } + @Native public static void clearInterval(Context ctx, int i) { + clearTimeout(ctx, i); + } + + @Native public static double parseInt(Context ctx, String val) throws InterruptedException { + return NumberPolyfill.parseInt(ctx, val); + } + @Native public static double parseFloat(Context ctx, String val) throws InterruptedException { + return NumberPolyfill.parseFloat(ctx, val); + } + + public void apply(Environment env) { + var wp = env.wrappersProvider; + var glob = env.global = new GlobalScope(NativeWrapperProvider.makeNamespace(env, Internals.class)); + + glob.define(null, "Object", false, wp.getConstr(ObjectPolyfill.class)); + glob.define(null, "Function", false, wp.getConstr(FunctionPolyfill.class)); + glob.define(null, "Array", false, wp.getConstr(ArrayPolyfill.class)); + + glob.define(null, "Boolean", false, wp.getConstr(BooleanPolyfill.class)); + glob.define(null, "Number", false, wp.getConstr(NumberPolyfill.class)); + glob.define(null, "String", false, wp.getConstr(StringPolyfill.class)); + glob.define(null, "Symbol", false, wp.getConstr(SymbolPolyfill.class)); + + glob.define(null, "Promise", false, wp.getConstr(PromisePolyfill.class)); + glob.define(null, "RegExp", false, wp.getConstr(RegExpPolyfill.class)); + glob.define(null, "Map", false, wp.getConstr(MapPolyfill.class)); + glob.define(null, "Set", false, wp.getConstr(SetPolyfill.class)); + + glob.define(null, "Error", false, wp.getConstr(ErrorPolyfill.class)); + glob.define(null, "SyntaxError", false, wp.getConstr(SyntaxErrorPolyfill.class)); + glob.define(null, "TypeError", false, wp.getConstr(TypeErrorPolyfill.class)); + glob.define(null, "RangeError", false, wp.getConstr(RangeErrorPolyfill.class)); + + env.setProto("object", wp.getProto(ObjectPolyfill.class)); + env.setProto("function", wp.getProto(FunctionPolyfill.class)); + env.setProto("array", wp.getProto(ArrayPolyfill.class)); + + env.setProto("bool", wp.getProto(BooleanPolyfill.class)); + env.setProto("number", wp.getProto(NumberPolyfill.class)); + env.setProto("string", wp.getProto(StringPolyfill.class)); + env.setProto("symbol", wp.getProto(SymbolPolyfill.class)); + + env.setProto("error", wp.getProto(ErrorPolyfill.class)); + env.setProto("syntaxErr", wp.getProto(SyntaxErrorPolyfill.class)); + env.setProto("typeErr", wp.getProto(TypeErrorPolyfill.class)); + env.setProto("rangeErr", wp.getProto(RangeErrorPolyfill.class)); + + wp.getProto(ObjectPolyfill.class).setPrototype(null, null); + + System.out.println("Loaded polyfills!"); } } diff --git a/src/me/topchetoeu/jscript/polyfills/JSON.java b/src/me/topchetoeu/jscript/polyfills/JSONPolyfill.java similarity index 95% rename from src/me/topchetoeu/jscript/polyfills/JSON.java rename to src/me/topchetoeu/jscript/polyfills/JSONPolyfill.java index 53873a5..9d70719 100644 --- a/src/me/topchetoeu/jscript/polyfills/JSON.java +++ b/src/me/topchetoeu/jscript/polyfills/JSONPolyfill.java @@ -1,85 +1,85 @@ -package me.topchetoeu.jscript.polyfills; - -import java.util.HashSet; -import java.util.stream.Collectors; - -import me.topchetoeu.jscript.engine.Context; -import me.topchetoeu.jscript.engine.values.ArrayValue; -import me.topchetoeu.jscript.engine.values.ObjectValue; -import me.topchetoeu.jscript.engine.values.Values; -import me.topchetoeu.jscript.exceptions.EngineException; -import me.topchetoeu.jscript.exceptions.SyntaxException; -import me.topchetoeu.jscript.interop.Native; -import me.topchetoeu.jscript.json.JSONElement; -import me.topchetoeu.jscript.json.JSONList; -import me.topchetoeu.jscript.json.JSONMap; - -public class JSON { - private static Object toJS(JSONElement val) { - if (val.isBoolean()) return val.bool(); - if (val.isString()) return val.string(); - if (val.isNumber()) return val.number(); - if (val.isList()) return ArrayValue.of(null, val.list().stream().map(JSON::toJS).collect(Collectors.toList())); - if (val.isMap()) { - var res = new ObjectValue(); - for (var el : val.map().entrySet()) { - res.defineProperty(null, el.getKey(), toJS(el.getValue())); - } - return res; - } - if (val.isNull()) return Values.NULL; - return null; - } - private static JSONElement toJSON(Context ctx, Object val, HashSet prev) throws InterruptedException { - if (val instanceof Boolean) return JSONElement.bool((boolean)val); - if (val instanceof Number) return JSONElement.number(((Number)val).doubleValue()); - if (val instanceof String) return JSONElement.string((String)val); - if (val == Values.NULL) return JSONElement.NULL; - if (val instanceof ObjectValue) { - if (prev.contains(val)) throw new EngineException("Circular dependency in JSON."); - prev.add(val); - - var res = new JSONMap(); - - for (var el : ((ObjectValue)val).keys(false)) { - var jsonEl = toJSON(ctx, ((ObjectValue)val).getMember(ctx, el), prev); - if (jsonEl == null) continue; - if (el instanceof String || el instanceof Number) res.put(el.toString(), jsonEl); - } - - prev.remove(val); - return JSONElement.of(res); - } - if (val instanceof ArrayValue) { - if (prev.contains(val)) throw new EngineException("Circular dependency in JSON."); - prev.add(val); - - var res = new JSONList(); - - for (var el : ((ArrayValue)val).toArray()) { - var jsonEl = toJSON(ctx, el, prev); - if (jsonEl == null) jsonEl = JSONElement.NULL; - res.add(jsonEl); - } - - prev.remove(val); - return JSONElement.of(res); - } - if (val == null) return null; - return null; - } - - @Native - public static Object parse(Context ctx, String val) throws InterruptedException { - try { - return toJS(me.topchetoeu.jscript.json.JSON.parse("", val)); - } - catch (SyntaxException e) { - throw EngineException.ofSyntax(e.msg); - } - } - @Native - public static String stringify(Context ctx, Object val) throws InterruptedException { - return me.topchetoeu.jscript.json.JSON.stringify(toJSON(ctx, val, new HashSet<>())); - } -} +package me.topchetoeu.jscript.polyfills; + +import java.util.HashSet; +import java.util.stream.Collectors; + +import me.topchetoeu.jscript.engine.Context; +import me.topchetoeu.jscript.engine.values.ArrayValue; +import me.topchetoeu.jscript.engine.values.ObjectValue; +import me.topchetoeu.jscript.engine.values.Values; +import me.topchetoeu.jscript.exceptions.EngineException; +import me.topchetoeu.jscript.exceptions.SyntaxException; +import me.topchetoeu.jscript.interop.Native; +import me.topchetoeu.jscript.json.JSONElement; +import me.topchetoeu.jscript.json.JSONList; +import me.topchetoeu.jscript.json.JSONMap; + +public class JSONPolyfill { + private static Object toJS(JSONElement val) { + if (val.isBoolean()) return val.bool(); + if (val.isString()) return val.string(); + if (val.isNumber()) return val.number(); + if (val.isList()) return ArrayValue.of(null, val.list().stream().map(JSONPolyfill::toJS).collect(Collectors.toList())); + if (val.isMap()) { + var res = new ObjectValue(); + for (var el : val.map().entrySet()) { + res.defineProperty(null, el.getKey(), toJS(el.getValue())); + } + return res; + } + if (val.isNull()) return Values.NULL; + return null; + } + private static JSONElement toJSON(Context ctx, Object val, HashSet prev) throws InterruptedException { + if (val instanceof Boolean) return JSONElement.bool((boolean)val); + if (val instanceof Number) return JSONElement.number(((Number)val).doubleValue()); + if (val instanceof String) return JSONElement.string((String)val); + if (val == Values.NULL) return JSONElement.NULL; + if (val instanceof ObjectValue) { + if (prev.contains(val)) throw new EngineException("Circular dependency in JSON."); + prev.add(val); + + var res = new JSONMap(); + + for (var el : ((ObjectValue)val).keys(false)) { + var jsonEl = toJSON(ctx, ((ObjectValue)val).getMember(ctx, el), prev); + if (jsonEl == null) continue; + if (el instanceof String || el instanceof Number) res.put(el.toString(), jsonEl); + } + + prev.remove(val); + return JSONElement.of(res); + } + if (val instanceof ArrayValue) { + if (prev.contains(val)) throw new EngineException("Circular dependency in JSON."); + prev.add(val); + + var res = new JSONList(); + + for (var el : ((ArrayValue)val).toArray()) { + var jsonEl = toJSON(ctx, el, prev); + if (jsonEl == null) jsonEl = JSONElement.NULL; + res.add(jsonEl); + } + + prev.remove(val); + return JSONElement.of(res); + } + if (val == null) return null; + return null; + } + + @Native + public static Object parse(Context ctx, String val) throws InterruptedException { + try { + return toJS(me.topchetoeu.jscript.json.JSON.parse("", val)); + } + catch (SyntaxException e) { + throw EngineException.ofSyntax(e.msg); + } + } + @Native + public static String stringify(Context ctx, Object val) throws InterruptedException { + return me.topchetoeu.jscript.json.JSON.stringify(toJSON(ctx, val, new HashSet<>())); + } +} diff --git a/src/me/topchetoeu/jscript/polyfills/MapPolyfill.java b/src/me/topchetoeu/jscript/polyfills/MapPolyfill.java new file mode 100644 index 0000000..1791c81 --- /dev/null +++ b/src/me/topchetoeu/jscript/polyfills/MapPolyfill.java @@ -0,0 +1,78 @@ +package me.topchetoeu.jscript.polyfills; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.stream.Collectors; + +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.Values; +import me.topchetoeu.jscript.interop.Native; +import me.topchetoeu.jscript.interop.NativeGetter; + +public class MapPolyfill { + private LinkedHashMap map = new LinkedHashMap<>(); + + @Native("@@Symbol.typeName") public final String name = "Map"; + @Native("@@Symbol.iterator") public ObjectValue iterator(Context ctx) throws InterruptedException { + return this.entries(ctx); + } + + @Native public void clear() { + map.clear(); + } + @Native public boolean delete(Object key) { + if (map.containsKey(key)) { + map.remove(key); + return true; + } + return false; + } + + @Native public ObjectValue entries(Context ctx) throws InterruptedException { + var res = map.entrySet().stream().map(v -> { + return new ArrayValue(ctx, v.getKey(), v.getValue()); + }).collect(Collectors.toList()); + return Values.fromJavaIterator(ctx, res.iterator()); + } + @Native public ObjectValue keys(Context ctx) throws InterruptedException { + var res = new ArrayList<>(map.keySet()); + return Values.fromJavaIterator(ctx, res.iterator()); + } + @Native public ObjectValue values(Context ctx) throws InterruptedException { + var res = new ArrayList<>(map.values()); + return Values.fromJavaIterator(ctx, res.iterator()); + } + + @Native public Object get(Object key) { + return map.get(key); + } + @Native public MapPolyfill set(Object key, Object val) { + map.put(key, val); + return this; + } + @Native public boolean has(Object key) { + return map.containsKey(key); + } + + @NativeGetter public int size() { + return map.size(); + } + + @NativeGetter public void forEach(Context ctx, FunctionValue func, Object thisArg) throws InterruptedException { + var keys = new ArrayList<>(map.keySet()); + + for (var el : keys) func.call(ctx, thisArg, el, map.get(el), this); + } + + @Native public MapPolyfill(Context ctx, Object iterable) throws InterruptedException { + for (var el : Values.toJavaIterable(ctx, iterable)) { + try { + set(Values.getMember(ctx, el, 0), Values.getMember(ctx, el, 1)); + } + catch (IllegalArgumentException e) { } + } + } +} diff --git a/src/me/topchetoeu/jscript/polyfills/Math.java b/src/me/topchetoeu/jscript/polyfills/Math.java index fe6f0d3..3be504a 100644 --- a/src/me/topchetoeu/jscript/polyfills/Math.java +++ b/src/me/topchetoeu/jscript/polyfills/Math.java @@ -1,6 +1,6 @@ package me.topchetoeu.jscript.polyfills; -import me.topchetoeu.jscript.engine.MessageContext; +import me.topchetoeu.jscript.engine.Message; import me.topchetoeu.jscript.interop.Native; public class Math { @@ -22,19 +22,19 @@ public class Math { public static final double LOG10E = java.lang.Math.log10(java.lang.Math.E); @Native - public static double asin(MessageContext ctx, double x) throws InterruptedException { + public static double asin(Message ctx, double x) throws InterruptedException { return java.lang.Math.asin(x); } @Native - public static double acos(MessageContext ctx, double x) throws InterruptedException { + public static double acos(Message ctx, double x) throws InterruptedException { return java.lang.Math.acos(x); } @Native - public static double atan(MessageContext ctx, double x) throws InterruptedException { + public static double atan(Message ctx, double x) throws InterruptedException { return java.lang.Math.atan(x); } @Native - public static double atan2(MessageContext ctx, double y, double x) throws InterruptedException { + public static double atan2(Message ctx, double y, double x) throws InterruptedException { double _y = y; double _x = x; if (_x == 0) { @@ -51,59 +51,59 @@ public class Math { } @Native - public static double asinh(MessageContext ctx, double x) throws InterruptedException { + public static double asinh(Message ctx, double x) throws InterruptedException { double _x = x; return java.lang.Math.log(_x + java.lang.Math.sqrt(_x * _x + 1)); } @Native - public static double acosh(MessageContext ctx, double x) throws InterruptedException { + public static double acosh(Message ctx, double x) throws InterruptedException { double _x = x; return java.lang.Math.log(_x + java.lang.Math.sqrt(_x * _x - 1)); } @Native - public static double atanh(MessageContext ctx, double x) throws InterruptedException { + public static double atanh(Message ctx, double x) throws InterruptedException { double _x = x; if (_x <= -1 || _x >= 1) return Double.NaN; return .5 * java.lang.Math.log((1 + _x) / (1 - _x)); } @Native - public static double sin(MessageContext ctx, double x) throws InterruptedException { + public static double sin(Message ctx, double x) throws InterruptedException { return java.lang.Math.sin(x); } @Native - public static double cos(MessageContext ctx, double x) throws InterruptedException { + public static double cos(Message ctx, double x) throws InterruptedException { return java.lang.Math.cos(x); } @Native - public static double tan(MessageContext ctx, double x) throws InterruptedException { + public static double tan(Message ctx, double x) throws InterruptedException { return java.lang.Math.tan(x); } @Native - public static double sinh(MessageContext ctx, double x) throws InterruptedException { + public static double sinh(Message ctx, double x) throws InterruptedException { return java.lang.Math.sinh(x); } @Native - public static double cosh(MessageContext ctx, double x) throws InterruptedException { + public static double cosh(Message ctx, double x) throws InterruptedException { return java.lang.Math.cosh(x); } @Native - public static double tanh(MessageContext ctx, double x) throws InterruptedException { + public static double tanh(Message ctx, double x) throws InterruptedException { return java.lang.Math.tanh(x); } @Native - public static double sqrt(MessageContext ctx, double x) throws InterruptedException { + public static double sqrt(Message ctx, double x) throws InterruptedException { return java.lang.Math.sqrt(x); } @Native - public static double cbrt(MessageContext ctx, double x) throws InterruptedException { + public static double cbrt(Message ctx, double x) throws InterruptedException { return java.lang.Math.cbrt(x); } @Native - public static double hypot(MessageContext ctx, double ...vals) throws InterruptedException { + public static double hypot(Message ctx, double ...vals) throws InterruptedException { var res = 0.; for (var el : vals) { var val = el; @@ -112,68 +112,68 @@ public class Math { return java.lang.Math.sqrt(res); } @Native - public static int imul(MessageContext ctx, double a, double b) throws InterruptedException { + public static int imul(Message ctx, double a, double b) throws InterruptedException { return (int)a * (int)b; } @Native - public static double exp(MessageContext ctx, double x) throws InterruptedException { + public static double exp(Message ctx, double x) throws InterruptedException { return java.lang.Math.exp(x); } @Native - public static double expm1(MessageContext ctx, double x) throws InterruptedException { + public static double expm1(Message ctx, double x) throws InterruptedException { return java.lang.Math.expm1(x); } @Native - public static double pow(MessageContext ctx, double x, double y) throws InterruptedException { + public static double pow(Message ctx, double x, double y) throws InterruptedException { return java.lang.Math.pow(x, y); } @Native - public static double log(MessageContext ctx, double x) throws InterruptedException { + public static double log(Message ctx, double x) throws InterruptedException { return java.lang.Math.log(x); } @Native - public static double log10(MessageContext ctx, double x) throws InterruptedException { + public static double log10(Message ctx, double x) throws InterruptedException { return java.lang.Math.log10(x); } @Native - public static double log1p(MessageContext ctx, double x) throws InterruptedException { + public static double log1p(Message ctx, double x) throws InterruptedException { return java.lang.Math.log1p(x); } @Native - public static double log2(MessageContext ctx, double x) throws InterruptedException { + public static double log2(Message ctx, double x) throws InterruptedException { return java.lang.Math.log(x) / LN2; } @Native - public static double ceil(MessageContext ctx, double x) throws InterruptedException { + public static double ceil(Message ctx, double x) throws InterruptedException { return java.lang.Math.ceil(x); } @Native - public static double floor(MessageContext ctx, double x) throws InterruptedException { + public static double floor(Message ctx, double x) throws InterruptedException { return java.lang.Math.floor(x); } @Native - public static double round(MessageContext ctx, double x) throws InterruptedException { + public static double round(Message ctx, double x) throws InterruptedException { return java.lang.Math.round(x); } @Native - public static float fround(MessageContext ctx, double x) throws InterruptedException { + public static float fround(Message ctx, double x) throws InterruptedException { return (float)x; } @Native - public static double trunc(MessageContext ctx, double x) throws InterruptedException { + public static double trunc(Message ctx, double x) throws InterruptedException { var _x = x; return java.lang.Math.floor(java.lang.Math.abs(_x)) * java.lang.Math.signum(_x); } @Native - public static double abs(MessageContext ctx, double x) throws InterruptedException { + public static double abs(Message ctx, double x) throws InterruptedException { return java.lang.Math.abs(x); } @Native - public static double max(MessageContext ctx, double ...vals) throws InterruptedException { + public static double max(Message ctx, double ...vals) throws InterruptedException { var res = Double.NEGATIVE_INFINITY; for (var el : vals) { @@ -184,7 +184,7 @@ public class Math { return res; } @Native - public static double min(MessageContext ctx, double ...vals) throws InterruptedException { + public static double min(Message ctx, double ...vals) throws InterruptedException { var res = Double.POSITIVE_INFINITY; for (var el : vals) { @@ -196,7 +196,7 @@ public class Math { } @Native - public static double sign(MessageContext ctx, double x) throws InterruptedException { + public static double sign(Message ctx, double x) throws InterruptedException { return java.lang.Math.signum(x); } @@ -205,7 +205,7 @@ public class Math { return java.lang.Math.random(); } @Native - public static int clz32(MessageContext ctx, double x) throws InterruptedException { + public static int clz32(Message ctx, double x) throws InterruptedException { return Integer.numberOfLeadingZeros((int)x); } } diff --git a/src/me/topchetoeu/jscript/polyfills/NumberPolyfill.java b/src/me/topchetoeu/jscript/polyfills/NumberPolyfill.java new file mode 100644 index 0000000..a9e30c6 --- /dev/null +++ b/src/me/topchetoeu/jscript/polyfills/NumberPolyfill.java @@ -0,0 +1,59 @@ +package me.topchetoeu.jscript.polyfills; + +import me.topchetoeu.jscript.engine.Context; +import me.topchetoeu.jscript.engine.Environment; +import me.topchetoeu.jscript.engine.values.ObjectValue; +import me.topchetoeu.jscript.engine.values.Values; +import me.topchetoeu.jscript.interop.InitType; +import me.topchetoeu.jscript.interop.Native; +import me.topchetoeu.jscript.interop.NativeConstructor; +import me.topchetoeu.jscript.interop.NativeInit; + +public class NumberPolyfill { + @Native public static final double EPSILON = java.lang.Math.ulp(1.0); + @Native public static final double MAX_SAFE_INTEGER = 9007199254740991.; + @Native public static final double MIN_SAFE_INTEGER = -MAX_SAFE_INTEGER; + // lmao big number go brrr + @Native public static final double MAX_VALUE = 179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368.; + @Native public static final double MIN_VALUE = -MAX_VALUE; + @Native public static final double NaN = 0. / 0; + @Native public static final double NEGATIVE_INFINITY = -1. / 0; + @Native public static final double POSITIVE_INFINITY = 1. / 0; + + public final double value; + + @Native public static boolean isFinite(Context ctx, double val) { return Double.isFinite(val); } + @Native public static boolean isInfinite(Context ctx, double val) { return Double.isInfinite(val); } + @Native public static boolean isNaN(Context ctx, double val) { return Double.isNaN(val); } + @Native public static boolean isSafeInteger(Context ctx, double val) { + return val > MIN_SAFE_INTEGER && val < MAX_SAFE_INTEGER; + } + + @Native public static double parseFloat(Context ctx, String val) throws InterruptedException { + return Values.toNumber(ctx, val); + } + @Native public static double parseInt(Context ctx, String val) throws InterruptedException { + return (long)Values.toNumber(ctx, val); + } + + @NativeConstructor(thisArg = true) public static Object constructor(Context ctx, Object thisArg, Object val) throws InterruptedException { + val = Values.toNumber(ctx, val); + if (thisArg instanceof ObjectValue) return new NumberPolyfill((double)val); + else return val; + } + @Native(thisArg = true) public static String toString(Context ctx, Object thisArg) throws InterruptedException { + return Values.toString(ctx, Values.toNumber(ctx, thisArg)); + } + @Native(thisArg = true) public static double valueOf(Context ctx, Object thisArg) throws InterruptedException { + if (thisArg instanceof NumberPolyfill) return ((NumberPolyfill)thisArg).value; + else return Values.toNumber(ctx, thisArg); + } + + public NumberPolyfill(double val) { + this.value = val; + } + + @NativeInit(InitType.PROTOTYPE) public static void init(Environment env, ObjectValue target) { + target.defineProperty(null, env.symbol("Symbol.typeName"), "Number"); + } +} diff --git a/src/me/topchetoeu/jscript/polyfills/ObjectPolyfill.java b/src/me/topchetoeu/jscript/polyfills/ObjectPolyfill.java new file mode 100644 index 0000000..1dca001 --- /dev/null +++ b/src/me/topchetoeu/jscript/polyfills/ObjectPolyfill.java @@ -0,0 +1,219 @@ +package me.topchetoeu.jscript.polyfills; + +import me.topchetoeu.jscript.engine.Context; +import me.topchetoeu.jscript.engine.Environment; +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.InitType; +import me.topchetoeu.jscript.interop.Native; +import me.topchetoeu.jscript.interop.NativeConstructor; +import me.topchetoeu.jscript.interop.NativeInit; + +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(thisArg = true) public static Object valueOf(Context ctx, Object thisArg) { + return thisArg; + } + @Native(thisArg = true) public static String toString(Context ctx, Object thisArg) 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(thisArg = true) public static boolean hasOwnProperty(Context ctx, Object thisArg, Object key) throws InterruptedException { + return ObjectPolyfill.hasOwn(ctx, thisArg, Values.convert(ctx, key, String.class)); + } + + @NativeConstructor(thisArg = true) public static Object constructor(Context ctx, Object thisArg, Object arg) throws InterruptedException { + if (arg == null || arg == Values.NULL) return new ObjectValue(); + else if (arg instanceof Boolean) return BooleanPolyfill.constructor(ctx, thisArg, arg); + else if (arg instanceof Number) return NumberPolyfill.constructor(ctx, thisArg, arg); + else if (arg instanceof String) return StringPolyfill.constructor(ctx, thisArg, arg); + // else if (arg instanceof Symbol) return SymbolPolyfill.constructor(ctx, thisArg, arg); + else return arg; + } + + @NativeInit(InitType.PROTOTYPE) public static void init(Environment env, ObjectValue target) { + target.defineProperty(null, env.symbol("Symbol.typeName"), "Object"); + } +} diff --git a/src/me/topchetoeu/jscript/polyfills/PromisePolyfill.java b/src/me/topchetoeu/jscript/polyfills/PromisePolyfill.java new file mode 100644 index 0000000..0235667 --- /dev/null +++ b/src/me/topchetoeu/jscript/polyfills/PromisePolyfill.java @@ -0,0 +1,348 @@ +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.Environment; +import me.topchetoeu.jscript.engine.Message; +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.InitType; +import me.topchetoeu.jscript.interop.Native; +import me.topchetoeu.jscript.interop.NativeInit; + +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(thisArg=true) public static Object then(Context ctx, Object thisArg, Object _onFulfill, Object _onReject) throws InterruptedException { + var onFulfill = _onFulfill instanceof FunctionValue ? ((FunctionValue)_onFulfill) : null; + var onReject = _onReject instanceof FunctionValue ? ((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", thisArg=true) public static Object _catch(Context ctx, Object thisArg, Object _onReject) throws InterruptedException { + return then(ctx, thisArg, null, _onReject); + } + /** + * Thread safe - you can call this from anywhere + * HOWEVER, it's strongly recommended to use this only in javascript + */ + @Native(value="finally", thisArg=true) public static Object _finally(Context ctx, Object thisArg, Object _handle) throws InterruptedException { + return then(ctx, thisArg, + new NativeFunction(null, (e, th, _args) -> { + if (_handle instanceof FunctionValue) ((FunctionValue)_handle).call(ctx); + return _args.length > 0 ? _args[0] : null; + }), + new NativeFunction(null, (e, th, _args) -> { + if (_handle instanceof FunctionValue) ((FunctionValue)_handle).call(ctx); + throw new EngineException(_args.length > 0 ? _args[0] : null); + }) + ); + } + + 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).setContext(ctx), "(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 Message(ctx.message.engine), fulfill, null, val); + else if (state == STATE_REJECTED) { + ctx.message.engine.pushMsg(true, new Message(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); + } + + @NativeInit(InitType.PROTOTYPE) public static void init(Environment env, ObjectValue target) { + target.defineProperty(null, env.symbol("Symbol.typeName"), "Promise"); + } +} diff --git a/src/me/topchetoeu/jscript/polyfills/RangeErrorPolyfill.java b/src/me/topchetoeu/jscript/polyfills/RangeErrorPolyfill.java new file mode 100644 index 0000000..99bfa82 --- /dev/null +++ b/src/me/topchetoeu/jscript/polyfills/RangeErrorPolyfill.java @@ -0,0 +1,20 @@ +package me.topchetoeu.jscript.polyfills; + +import me.topchetoeu.jscript.engine.Context; +import me.topchetoeu.jscript.engine.Environment; +import me.topchetoeu.jscript.engine.values.ObjectValue; +import me.topchetoeu.jscript.interop.InitType; +import me.topchetoeu.jscript.interop.NativeConstructor; +import me.topchetoeu.jscript.interop.NativeInit; + +public class RangeErrorPolyfill extends ErrorPolyfill { + @NativeConstructor(thisArg = true) public static ObjectValue constructor(Context ctx, Object thisArg, Object message) throws InterruptedException { + var target = ErrorPolyfill.constructor(ctx, thisArg, message); + target.defineProperty(ctx, "name", "RangeError"); + return target; + } + @NativeInit(InitType.PROTOTYPE) public static void init(Environment env, ObjectValue target) { + target.defineProperty(null, env.symbol("Symbol.typeName"), "RangeError"); + target.defineProperty(null, "name", "RangeError"); + } +} \ No newline at end of file diff --git a/src/me/topchetoeu/jscript/polyfills/RegExp.java b/src/me/topchetoeu/jscript/polyfills/RegExp.java deleted file mode 100644 index 804d0e7..0000000 --- a/src/me/topchetoeu/jscript/polyfills/RegExp.java +++ /dev/null @@ -1,187 +0,0 @@ -package me.topchetoeu.jscript.polyfills; - -import java.util.ArrayList; -import java.util.regex.Pattern; - -import me.topchetoeu.jscript.engine.Context; -import me.topchetoeu.jscript.engine.values.ArrayValue; -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.interop.Native; -import me.topchetoeu.jscript.interop.NativeGetter; -import me.topchetoeu.jscript.interop.NativeSetter; - -public class RegExp { - // I used Regex to analyze Regex - private static final Pattern NAMED_PATTERN = Pattern.compile("\\(\\?<([^=!].*?)>", Pattern.DOTALL); - private static final Pattern ESCAPE_PATTERN = Pattern.compile("[/\\-\\\\^$*+?.()|\\[\\]{}]"); - - private static String cleanupPattern(Context ctx, Object val) throws InterruptedException { - if (val == null) return "(?:)"; - if (val instanceof RegExp) return ((RegExp)val).source; - if (val instanceof NativeWrapper && ((NativeWrapper)val).wrapped instanceof RegExp) { - return ((RegExp)((NativeWrapper)val).wrapped).source; - } - var res = Values.toString(ctx, val); - if (res.equals("")) return "(?:)"; - return res; - } - private static String cleanupFlags(Context ctx, Object val) throws InterruptedException { - if (val == null) return ""; - return Values.toString(ctx, val); - } - - private static boolean checkEscaped(String s, int pos) { - int n = 0; - - while (true) { - if (pos <= 0) break; - if (s.charAt(pos) != '\\') break; - n++; - pos--; - } - - return (n % 2) != 0; - } - - @Native - public static RegExp escape(Context ctx, Object raw, Object flags) throws InterruptedException { - return escape(Values.toString(ctx, raw), cleanupFlags(ctx, flags)); - } - public static RegExp escape(String raw, String flags) { - return new RegExp(ESCAPE_PATTERN.matcher(raw).replaceAll("\\\\$0"), flags); - } - - private Pattern pattern; - private String[] namedGroups; - private int flags; - private int lastI = 0; - - @Native - public final String source; - @Native - public final boolean hasIndices; - @Native - public final boolean global; - @Native - public final boolean sticky; - - @NativeGetter("ignoreCase") - public boolean ignoreCase() { return (flags & Pattern.CASE_INSENSITIVE) != 0; } - @NativeGetter("multiline") - public boolean multiline() { return (flags & Pattern.MULTILINE) != 0; } - @NativeGetter("unicode") - public boolean unicode() { return (flags & Pattern.UNICODE_CHARACTER_CLASS) != 0; } - @NativeGetter("unicode") - public boolean dotAll() { return (flags & Pattern.DOTALL) != 0; } - - @NativeGetter("lastIndex") - public int lastIndex() { return lastI; } - @NativeSetter("lastIndex") - public void setLastIndex(Context ctx, Object i) throws InterruptedException { - lastI = (int)Values.toNumber(ctx, i); - } - public void setLastIndex(int i) { - lastI = i; - } - - @NativeGetter("flags") - public final String flags() { - String res = ""; - if (hasIndices) res += 'd'; - if (global) res += 'g'; - if (ignoreCase()) res += 'i'; - if (multiline()) res += 'm'; - if (dotAll()) res += 's'; - if (unicode()) res += 'u'; - if (sticky) res += 'y'; - return res; - } - - @Native - public Object exec(Context ctx, Object str) throws InterruptedException { - return exec(Values.toString(ctx, str)); - } - public Object exec(String str) { - var matcher = pattern.matcher(str); - if (lastI > str.length() || !matcher.find(lastI) || sticky && matcher.start() != lastI) { - lastI = 0; - return Values.NULL; - } - if (sticky || global) { - lastI = matcher.end(); - if (matcher.end() == matcher.start()) lastI++; - } - - var obj = new ArrayValue(); - var groups = new ObjectValue(); - - for (var el : namedGroups) { - try { - groups.defineProperty(null, el, matcher.group(el)); - } - catch (IllegalArgumentException e) { } - } - if (groups.values.size() == 0) groups = null; - - - for (int i = 0; i < matcher.groupCount() + 1; i++) { - obj.set(null, i, matcher.group(i)); - } - obj.defineProperty(null, "groups", groups); - obj.defineProperty(null, "index", matcher.start()); - obj.defineProperty(null, "input", str); - - if (hasIndices) { - var indices = new ArrayValue(); - for (int i = 0; i < matcher.groupCount() + 1; i++) { - indices.set(null, i, new ArrayValue(null, matcher.start(i), matcher.end(i))); - } - var groupIndices = new ObjectValue(); - for (var el : namedGroups) { - groupIndices.defineProperty(null, el, new ArrayValue(null, matcher.start(el), matcher.end(el))); - } - indices.defineProperty(null, "groups", groupIndices); - obj.defineProperty(null, "indices", indices); - } - - return obj; - } - - @Native - public RegExp(Context ctx, Object pattern, Object flags) throws InterruptedException { - this(cleanupPattern(ctx, pattern), cleanupFlags(ctx, flags)); - } - public RegExp(String pattern, String flags) { - if (pattern == null || pattern.equals("")) pattern = "(?:)"; - if (flags == null || flags.equals("")) flags = ""; - - this.flags = 0; - this.hasIndices = flags.contains("d"); - this.global = flags.contains("g"); - this.sticky = flags.contains("y"); - this.source = pattern; - - if (flags.contains("i")) this.flags |= Pattern.CASE_INSENSITIVE; - if (flags.contains("m")) this.flags |= Pattern.MULTILINE; - if (flags.contains("s")) this.flags |= Pattern.DOTALL; - if (flags.contains("u")) this.flags |= Pattern.UNICODE_CHARACTER_CLASS; - - this.pattern = Pattern.compile(pattern.replace("\\d", "[0-9]"), this.flags); - - var matcher = NAMED_PATTERN.matcher(source); - var groups = new ArrayList(); - - while (matcher.find()) { - if (!checkEscaped(source, matcher.start() - 1)) { - groups.add(matcher.group(1)); - } - } - - namedGroups = groups.toArray(String[]::new); - } - - public RegExp(String pattern) { this(pattern, null); } - public RegExp() { this(null, null); } -} diff --git a/src/me/topchetoeu/jscript/polyfills/RegExpPolyfill.java b/src/me/topchetoeu/jscript/polyfills/RegExpPolyfill.java new file mode 100644 index 0000000..1fb885c --- /dev/null +++ b/src/me/topchetoeu/jscript/polyfills/RegExpPolyfill.java @@ -0,0 +1,297 @@ +package me.topchetoeu.jscript.polyfills; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.regex.Pattern; + +import me.topchetoeu.jscript.engine.Context; +import me.topchetoeu.jscript.engine.values.ArrayValue; +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.interop.Native; +import me.topchetoeu.jscript.interop.NativeGetter; + +public class RegExpPolyfill { + // I used Regex to analyze Regex + private static final Pattern NAMED_PATTERN = Pattern.compile("\\(\\?<([^=!].*?)>", Pattern.DOTALL); + private static final Pattern ESCAPE_PATTERN = Pattern.compile("[/\\-\\\\^$*+?.()|\\[\\]{}]"); + + private static String cleanupPattern(Context ctx, Object val) throws InterruptedException { + if (val == null) return "(?:)"; + if (val instanceof RegExpPolyfill) return ((RegExpPolyfill)val).source; + if (val instanceof NativeWrapper && ((NativeWrapper)val).wrapped instanceof RegExpPolyfill) { + return ((RegExpPolyfill)((NativeWrapper)val).wrapped).source; + } + var res = Values.toString(ctx, val); + if (res.equals("")) return "(?:)"; + return res; + } + private static String cleanupFlags(Context ctx, Object val) throws InterruptedException { + if (val == null) return ""; + return Values.toString(ctx, val); + } + + private static boolean checkEscaped(String s, int pos) { + int n = 0; + + while (true) { + if (pos <= 0) break; + if (s.charAt(pos) != '\\') break; + n++; + pos--; + } + + return (n % 2) != 0; + } + + @Native + public static RegExpPolyfill escape(Context ctx, Object raw, Object flags) throws InterruptedException { + return escape(Values.toString(ctx, raw), cleanupFlags(ctx, flags)); + } + public static RegExpPolyfill escape(String raw, String flags) { + return new RegExpPolyfill(ESCAPE_PATTERN.matcher(raw).replaceAll("\\\\$0"), flags); + } + + private Pattern pattern; + private String[] namedGroups; + private int flags; + + @Native public int lastI = 0; + @Native public final String source; + @Native public final boolean hasIndices; + @Native public final boolean global; + @Native public final boolean sticky; + @Native("@@Symbol.typeName") public final String name = "RegExp"; + + @NativeGetter public boolean ignoreCase() { return (flags & Pattern.CASE_INSENSITIVE) != 0; } + @NativeGetter public boolean multiline() { return (flags & Pattern.MULTILINE) != 0; } + @NativeGetter public boolean unicode() { return (flags & Pattern.UNICODE_CHARACTER_CLASS) != 0; } + @NativeGetter public boolean dotAll() { return (flags & Pattern.DOTALL) != 0; } + + @NativeGetter("flags") public final String flags() { + String res = ""; + if (hasIndices) res += 'd'; + if (global) res += 'g'; + if (ignoreCase()) res += 'i'; + if (multiline()) res += 'm'; + if (dotAll()) res += 's'; + if (unicode()) res += 'u'; + if (sticky) res += 'y'; + return res; + } + + + @Native public Object exec(String str) { + var matcher = pattern.matcher(str); + if (lastI > str.length() || !matcher.find(lastI) || sticky && matcher.start() != lastI) { + lastI = 0; + return Values.NULL; + } + if (sticky || global) { + lastI = matcher.end(); + if (matcher.end() == matcher.start()) lastI++; + } + + var obj = new ArrayValue(); + var groups = new ObjectValue(); + + for (var el : namedGroups) { + try { + groups.defineProperty(null, el, matcher.group(el)); + } + catch (IllegalArgumentException e) { } + } + if (groups.values.size() == 0) groups = null; + + + for (int i = 0; i < matcher.groupCount() + 1; i++) { + obj.set(null, i, matcher.group(i)); + } + obj.defineProperty(null, "groups", groups); + obj.defineProperty(null, "index", matcher.start()); + obj.defineProperty(null, "input", str); + + if (hasIndices) { + var indices = new ArrayValue(); + for (int i = 0; i < matcher.groupCount() + 1; i++) { + indices.set(null, i, new ArrayValue(null, matcher.start(i), matcher.end(i))); + } + var groupIndices = new ObjectValue(); + for (var el : namedGroups) { + groupIndices.defineProperty(null, el, new ArrayValue(null, matcher.start(el), matcher.end(el))); + } + indices.defineProperty(null, "groups", groupIndices); + obj.defineProperty(null, "indices", indices); + } + + return obj; + } + + @Native public boolean test(String str) { + return this.exec(str) != Values.NULL; + } + @Native public String toString() { + return "/" + source + "/" + flags(); + } + + @Native("@@Symvol.match") public Object match(Context ctx, String target) throws InterruptedException { + if (this.global) { + var res = new ArrayValue(); + Object val; + while ((val = this.exec(target)) != Values.NULL) { + res.set(ctx, res.size(), Values.getMember(ctx, val, 0)); + } + lastI = 0; + return res; + } + else { + var res = this.exec(target); + if (!this.sticky) this.lastI = 0; + return res; + } + } + + @Native("@@Symvol.matchAll") public Object matchAll(Context ctx, String target) throws InterruptedException { + var pattern = new RegExpPolyfill(this.source, this.flags() + "g"); + + return Values.fromJavaIterator(ctx, new Iterator() { + private Object val = null; + private boolean updated = false; + + private void update() { + if (!updated) val = pattern.exec(target); + } + @Override public boolean hasNext() { + update(); + return val != Values.NULL; + } + @Override public Object next() { + update(); + updated = false; + return val; + } + }); + } + + @Native("@@Symvol.split") public ArrayValue split(Context ctx, String target, Object limit, boolean sensible) throws InterruptedException { + var pattern = new RegExpPolyfill(this.source, this.flags() + "g"); + Object match; + int lastEnd = 0; + var res = new ArrayValue(); + var lim = limit == null ? 0 : Values.toNumber(ctx, limit); + + while ((match = pattern.exec(target)) != Values.NULL) { + var added = new ArrayList(); + var arrMatch = (ArrayValue)match; + int index = (int)Values.toNumber(ctx, Values.getMember(ctx, match, "index")); + var matchVal = (String)arrMatch.get(0); + + if (index >= target.length()) break; + + if (matchVal.length() == 0 || index - lastEnd > 0) { + added.add(target.substring(lastEnd, pattern.lastI)); + if (pattern.lastI < target.length()) { + for (var i = 1; i < arrMatch.size(); i++) added.add((String)arrMatch.get(i)); + } + } + else { + for (var i = 1; i < arrMatch.size(); i++) added.add((String)arrMatch.get(i)); + } + + if (sensible) { + if (limit != null && res.size() + added.size() >= lim) break; + else for (var i = 0; i < added.size(); i++) res.set(ctx, res.size(), added.get(i)); + } + else { + for (var i = 0; i < added.size(); i++) { + if (limit != null && res.size() >= lim) return res; + else res.set(ctx, res.size(), added.get(i)); + } + } + lastEnd = pattern.lastI; + } + if (lastEnd < target.length()) { + res.set(ctx, res.size(), target.substring(lastEnd)); + } + return res; + } + // [Symbol.replace](target, replacement) { + // const pattern = new this.constructor(this, this.flags + "d") as RegExp; + // let match: RegExpResult | null; + // let lastEnd = 0; + // const res: string[] = []; + // // log(pattern.toString()); + // while ((match = pattern.exec(target)) !== null) { + // const indices = match.indices![0]; + // res.push(target.substring(lastEnd, indices[0])); + // if (replacement instanceof Function) { + // res.push(replacement(target.substring(indices[0], indices[1]), ...match.slice(1), indices[0], target)); + // } + // else { + // res.push(replacement); + // } + // lastEnd = indices[1]; + // if (!pattern.global) break; + // } + // if (lastEnd < target.length) { + // res.push(target.substring(lastEnd)); + // } + // return res.join(''); + // }, + // [Symbol.search](target, reverse, start) { + // const pattern: RegExp | undefined = new this.constructor(this, this.flags + "g") as RegExp; + // if (!reverse) { + // pattern.lastIndex = (start as any) | 0; + // const res = pattern.exec(target); + // if (res) return res.index; + // else return -1; + // } + // else { + // start ??= target.length; + // start |= 0; + // let res: RegExpResult | null = null; + // while (true) { + // const tmp = pattern.exec(target); + // if (tmp === null || tmp.index > start) break; + // res = tmp; + // } + // if (res && res.index <= start) return res.index; + // else return -1; + // } + // }, + @Native public RegExpPolyfill(Context ctx, Object pattern, Object flags) throws InterruptedException { + this(cleanupPattern(ctx, pattern), cleanupFlags(ctx, flags)); + } + public RegExpPolyfill(String pattern, String flags) { + if (pattern == null || pattern.equals("")) pattern = "(?:)"; + if (flags == null || flags.equals("")) flags = ""; + + this.flags = 0; + this.hasIndices = flags.contains("d"); + this.global = flags.contains("g"); + this.sticky = flags.contains("y"); + this.source = pattern; + + if (flags.contains("i")) this.flags |= Pattern.CASE_INSENSITIVE; + if (flags.contains("m")) this.flags |= Pattern.MULTILINE; + if (flags.contains("s")) this.flags |= Pattern.DOTALL; + if (flags.contains("u")) this.flags |= Pattern.UNICODE_CHARACTER_CLASS; + + this.pattern = Pattern.compile(pattern.replace("\\d", "[0-9]"), this.flags); + + var matcher = NAMED_PATTERN.matcher(source); + var groups = new ArrayList(); + + while (matcher.find()) { + if (!checkEscaped(source, matcher.start() - 1)) { + groups.add(matcher.group(1)); + } + } + + namedGroups = groups.toArray(String[]::new); + } + + public RegExpPolyfill(String pattern) { this(pattern, null); } + public RegExpPolyfill() { this(null, null); } +} diff --git a/src/me/topchetoeu/jscript/polyfills/SetPolyfill.java b/src/me/topchetoeu/jscript/polyfills/SetPolyfill.java new file mode 100644 index 0000000..c6bc7f3 --- /dev/null +++ b/src/me/topchetoeu/jscript/polyfills/SetPolyfill.java @@ -0,0 +1,63 @@ +package me.topchetoeu.jscript.polyfills; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.stream.Collectors; + +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.Values; +import me.topchetoeu.jscript.interop.Native; +import me.topchetoeu.jscript.interop.NativeGetter; + +public class SetPolyfill { + private LinkedHashSet set = new LinkedHashSet<>(); + + @Native("@@Symbol.typeName") public final String name = "Set"; + @Native("@@Symbol.iterator") public ObjectValue iterator(Context ctx) throws InterruptedException { + return this.values(ctx); + } + + @Native public ObjectValue entries(Context ctx) throws InterruptedException { + var res = set.stream().map(v -> new ArrayValue(ctx, v, v)).collect(Collectors.toList()); + return Values.fromJavaIterator(ctx, res.iterator()); + } + @Native public ObjectValue keys(Context ctx) throws InterruptedException { + var res = new ArrayList<>(set); + return Values.fromJavaIterator(ctx, res.iterator()); + } + @Native public ObjectValue values(Context ctx) throws InterruptedException { + var res = new ArrayList<>(set); + return Values.fromJavaIterator(ctx, res.iterator()); + } + + @Native public Object add(Object key) { + return set.add(key); + } + @Native public boolean delete(Object key) { + return set.remove(key); + } + @Native public boolean has(Object key) { + return set.contains(key); + } + + @Native public void clear() { + set.clear(); + } + + @NativeGetter public int size() { + return set.size(); + } + + @NativeGetter public void forEach(Context ctx, FunctionValue func, Object thisArg) throws InterruptedException { + var keys = new ArrayList<>(set); + + for (var el : keys) func.call(ctx, thisArg, el, el, this); + } + + @Native public SetPolyfill(Context ctx, Object iterable) throws InterruptedException { + for (var el : Values.toJavaIterable(ctx, iterable)) add(el); + } +} diff --git a/src/me/topchetoeu/jscript/polyfills/StringPolyfill.java b/src/me/topchetoeu/jscript/polyfills/StringPolyfill.java new file mode 100644 index 0000000..de56291 --- /dev/null +++ b/src/me/topchetoeu/jscript/polyfills/StringPolyfill.java @@ -0,0 +1,260 @@ +package me.topchetoeu.jscript.polyfills; + +import java.util.regex.Pattern; + +import me.topchetoeu.jscript.engine.Context; +import me.topchetoeu.jscript.engine.Environment; +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.Values; +import me.topchetoeu.jscript.exceptions.EngineException; +import me.topchetoeu.jscript.interop.InitType; +import me.topchetoeu.jscript.interop.Native; +import me.topchetoeu.jscript.interop.NativeConstructor; +import me.topchetoeu.jscript.interop.NativeGetter; +import me.topchetoeu.jscript.interop.NativeInit; + +// TODO: implement index wrapping properly +public class StringPolyfill { + public final String value; + + private static String passThis(Context ctx, String funcName, Object val) throws InterruptedException { + if (val instanceof StringPolyfill) return ((StringPolyfill)val).value; + else if (val instanceof String) return (String)val; + else throw EngineException.ofType(String.format("'%s' may only be called upon object and primitve strings.", funcName)); + } + private static int normalizeI(int i, int len, boolean clamp) { + if (i < 0) i += len; + if (clamp) { + if (i < 0) i = 0; + if (i >= len) i = len; + } + return i; + } + + @NativeGetter(thisArg = true) public static int length(Context ctx, Object thisArg) throws InterruptedException { + return passThis(ctx, "substring", thisArg).length(); + } + + @Native(thisArg = true) public static String substring(Context ctx, Object thisArg, int start, Object _end) throws InterruptedException { + var val = passThis(ctx, "substring", thisArg); + start = normalizeI(start, val.length(), true); + int end = normalizeI(_end == null ? val.length() : (int)Values.toNumber(ctx, _end), val.length(), true); + + return val.substring(start, end); + } + @Native(thisArg = true) public static String substr(Context ctx, Object thisArg, int start, Object _len) throws InterruptedException { + var val = passThis(ctx, "substr", thisArg); + int len = _len == null ? val.length() - start : (int)Values.toNumber(ctx, _len); + return substring(ctx, val, start, start + len); + } + + @Native(thisArg = true) public static String toLowerCase(Context ctx, Object thisArg) throws InterruptedException { + return passThis(ctx, "toLowerCase", thisArg).toLowerCase(); + } + @Native(thisArg = true) public static String toUpperCase(Context ctx, Object thisArg) throws InterruptedException { + return passThis(ctx, "toUpperCase", thisArg).toUpperCase(); + } + + @Native(thisArg = true) public static String charAt(Context ctx, Object thisArg, int i) throws InterruptedException { + return passThis(ctx, "charAt", thisArg).charAt(i) + ""; + } + @Native(thisArg = true) public static int charCodeAt(Context ctx, Object thisArg, int i) throws InterruptedException { + return passThis(ctx, "charCodeAt", thisArg).charAt(i); + } + + @Native(thisArg = true) public static boolean startsWith(Context ctx, Object thisArg, String term, int pos) throws InterruptedException { + return passThis(ctx, "startsWith", thisArg).startsWith(term, pos); + } + @Native(thisArg = true) public static boolean endsWith(Context ctx, Object thisArg, String term, int pos) throws InterruptedException { + var val = passThis(ctx, "endsWith", thisArg); + return val.lastIndexOf(term, pos) >= 0; + } + + @Native(thisArg = true) public static int indexOf(Context ctx, Object thisArg, Object term, int start) throws InterruptedException { + var val = passThis(ctx, "indexOf", thisArg); + + if (term != null && term != Values.NULL && !(term instanceof String)) { + var search = Values.getMember(ctx, term, ctx.env.symbol("Symbol.search")); + if (search instanceof FunctionValue) { + return (int)Values.toNumber(ctx, ((FunctionValue)search).call(ctx, term, val, false, start)); + } + } + + return val.indexOf(Values.toString(ctx, term), start); + } + @Native(thisArg = true) public static int lastIndexOf(Context ctx, Object thisArg, Object term, int pos) throws InterruptedException { + var val = passThis(ctx, "lastIndexOf", thisArg); + + if (term != null && term != Values.NULL && !(term instanceof String)) { + var search = Values.getMember(ctx, term, ctx.env.symbol("Symbol.search")); + if (search instanceof FunctionValue) { + return (int)Values.toNumber(ctx, ((FunctionValue)search).call(ctx, term, val, true, pos)); + } + } + + return val.lastIndexOf(Values.toString(ctx, term), pos); + } + + @Native(thisArg = true) public static boolean includes(Context ctx, Object thisArg, Object term, int pos) throws InterruptedException { + return lastIndexOf(ctx, passThis(ctx, "includes", thisArg), term, pos) >= 0; + } + + @Native(thisArg = true) public static String replace(Context ctx, Object thisArg, Object term, String replacement) throws InterruptedException { + var val = passThis(ctx, "replace", thisArg); + + if (term != null && term != Values.NULL && !(term instanceof String)) { + var replace = Values.getMember(ctx, term, ctx.env.symbol("Symbol.replace")); + if (replace instanceof FunctionValue) { + return Values.toString(ctx, ((FunctionValue)replace).call(ctx, term, val, replacement)); + } + } + + return val.replaceFirst(Pattern.quote(Values.toString(ctx, term)), replacement); + } + @Native(thisArg = true) public static String replaceAll(Context ctx, Object thisArg, Object term, String replacement) throws InterruptedException { + var val = passThis(ctx, "replaceAll", thisArg); + + if (term != null && term != Values.NULL && !(term instanceof String)) { + var replace = Values.getMember(ctx, term, ctx.env.symbol("Symbol.replace")); + if (replace instanceof FunctionValue) { + return Values.toString(ctx, ((FunctionValue)replace).call(ctx, term, val, replacement)); + } + } + + return val.replaceFirst(Pattern.quote(Values.toString(ctx, term)), replacement); + } + + @Native(thisArg = true) public static ArrayValue match(Context ctx, Object thisArg, Object term, String replacement) throws InterruptedException { + var val = passThis(ctx, "match", thisArg); + + FunctionValue match; + + try { + var _match = Values.getMember(ctx, term, ctx.env.symbol("Symbol.match")); + if (_match instanceof FunctionValue) match = (FunctionValue)_match; + else if (ctx.env.regexConstructor != null) { + var regex = Values.callNew(ctx, ctx.env.regexConstructor, Values.toString(ctx, term), ""); + _match = Values.getMember(ctx, regex, ctx.env.symbol("Symbol.match")); + if (_match instanceof FunctionValue) match = (FunctionValue)_match; + else throw EngineException.ofError("Regular expressions don't support matching."); + } + else throw EngineException.ofError("Regular expressions not supported."); + } + catch (IllegalArgumentException e) { return new ArrayValue(ctx, ""); } + + var res = match.call(ctx, term, val); + if (res instanceof ArrayValue) return (ArrayValue)res; + else return new ArrayValue(ctx, ""); + } + @Native(thisArg = true) public static Object matchAll(Context ctx, Object thisArg, Object term, String replacement) throws InterruptedException { + var val = passThis(ctx, "matchAll", thisArg); + + FunctionValue match = null; + + try { + var _match = Values.getMember(ctx, term, ctx.env.symbol("Symbol.matchAll")); + if (_match instanceof FunctionValue) match = (FunctionValue)_match; + } + catch (IllegalArgumentException e) { } + + if (match == null && ctx.env.regexConstructor != null) { + var regex = Values.callNew(ctx, ctx.env.regexConstructor, Values.toString(ctx, term), "g"); + var _match = Values.getMember(ctx, regex, ctx.env.symbol("Symbol.matchAll")); + if (_match instanceof FunctionValue) match = (FunctionValue)_match; + else throw EngineException.ofError("Regular expressions don't support matching."); + } + else throw EngineException.ofError("Regular expressions not supported."); + + return match.call(ctx, term, val); + } + + @Native(thisArg = true) public static ArrayValue split(Context ctx, Object thisArg, Object term, Object lim, boolean sensible) throws InterruptedException { + var val = passThis(ctx, "split", thisArg); + + if (lim != null) lim = Values.toNumber(ctx, lim); + + if (term != null && term != Values.NULL && !(term instanceof String)) { + var replace = Values.getMember(ctx, term, ctx.env.symbol("Symbol.replace")); + if (replace instanceof FunctionValue) { + var tmp = ((FunctionValue)replace).call(ctx, term, val, lim, sensible); + + if (tmp instanceof ArrayValue) { + var parts = new ArrayValue(((ArrayValue)tmp).size()); + for (int i = 0; i < parts.size(); i++) parts.set(ctx, i, Values.toString(ctx, ((ArrayValue)tmp).get(i))); + return parts; + } + } + } + + String[] parts; + var pattern = Pattern.quote(Values.toString(ctx, term)); + + if (lim == null) parts = val.split(pattern); + else if (sensible) parts = val.split(pattern, (int)(double)lim); + else { + var limit = (int)(double)lim; + parts = val.split(pattern, limit + 1); + ArrayValue res; + + if (parts.length > limit) res = new ArrayValue(limit); + else res = new ArrayValue(parts.length); + + for (var i = 0; i < parts.length && i < limit; i++) res.set(ctx, i, parts[i]); + + return res; + } + + var res = new ArrayValue(parts.length); + var i = 0; + + for (; i < parts.length; i++) { + if (lim != null && (double)lim <= i) break; + res.set(ctx, i, parts[i]); + } + + return res; + } + + @Native(thisArg = true) public static String slice(Context ctx, Object thisArg, int start, Object _end) throws InterruptedException { + return substring(ctx, passThis(ctx, "slice", thisArg), start, _end); + } + + @Native(thisArg = true) public static String concat(Context ctx, Object thisArg, Object... args) throws InterruptedException { + var res = new StringBuilder(passThis(ctx, "concat", thisArg)); + + for (var el : args) res.append(Values.toString(ctx, el)); + + return res.toString(); + } + @Native(thisArg = true) public static String trim(Context ctx, Object thisArg) throws InterruptedException { + return passThis(ctx, "trim", thisArg).trim(); + } + + @NativeConstructor(thisArg = true) public static Object constructor(Context ctx, Object thisArg, Object val) throws InterruptedException { + val = Values.toString(ctx, val); + if (thisArg instanceof ObjectValue) return new StringPolyfill((String)val); + else return val; + } + @Native(thisArg = true) public static String toString(Context ctx, Object thisArg) throws InterruptedException { + return passThis(ctx, "toString", thisArg); + } + @Native(thisArg = true) public static String valueOf(Context ctx, Object thisArg) throws InterruptedException { + return passThis(ctx, "valueOf", thisArg); + } + + @Native public static String fromCharCode(int ...val) { + char[] arr = new char[val.length]; + for (var i = 0; i < val.length; i++) arr[i] = (char)val[i]; + return new String(arr); + } + + public StringPolyfill(String val) { + this.value = val; + } + + @NativeInit(InitType.PROTOTYPE) public static void init(Environment env, ObjectValue target) { + target.defineProperty(null, env.symbol("Symbol.typeName"), "String"); + } +} diff --git a/src/me/topchetoeu/jscript/polyfills/SymbolPolyfill.java b/src/me/topchetoeu/jscript/polyfills/SymbolPolyfill.java new file mode 100644 index 0000000..6dfb33e --- /dev/null +++ b/src/me/topchetoeu/jscript/polyfills/SymbolPolyfill.java @@ -0,0 +1,69 @@ +package me.topchetoeu.jscript.polyfills; + +import java.util.HashMap; +import java.util.Map; + +import me.topchetoeu.jscript.engine.Context; +import me.topchetoeu.jscript.engine.Environment; +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.InitType; +import me.topchetoeu.jscript.interop.Native; +import me.topchetoeu.jscript.interop.NativeConstructor; +import me.topchetoeu.jscript.interop.NativeGetter; +import me.topchetoeu.jscript.interop.NativeInit; + +public class SymbolPolyfill { + private static final Map symbols = new HashMap<>(); + + @NativeGetter public static Symbol typeName(Context ctx) { return ctx.env.symbol("Symbol.typeName"); } + @NativeGetter public static Symbol replace(Context ctx) { return ctx.env.symbol("Symbol.replace"); } + @NativeGetter public static Symbol match(Context ctx) { return ctx.env.symbol("Symbol.match"); } + @NativeGetter public static Symbol matchAll(Context ctx) { return ctx.env.symbol("Symbol.matchAll"); } + @NativeGetter public static Symbol split(Context ctx) { return ctx.env.symbol("Symbol.split"); } + @NativeGetter public static Symbol search(Context ctx) { return ctx.env.symbol("Symbol.search"); } + @NativeGetter public static Symbol iterator(Context ctx) { return ctx.env.symbol("Symbol.iterator"); } + @NativeGetter public static Symbol asyncIterator(Context ctx) { return ctx.env.symbol("Symbol.asyncIterator"); } + + public final Symbol value; + + private static Symbol passThis(Context ctx, String funcName, Object val) throws InterruptedException { + if (val instanceof SymbolPolyfill) return ((SymbolPolyfill)val).value; + else if (val instanceof Symbol) return (Symbol)val; + else throw EngineException.ofType(String.format("'%s' may only be called upon object and primitve symbols.", funcName)); + } + + @NativeConstructor(thisArg = true) public static Object constructor(Context ctx, Object thisArg, Object val) throws InterruptedException { + if (thisArg instanceof ObjectValue) throw EngineException.ofType("Symbol constructor may not be called with new."); + if (val == null) return new Symbol(""); + else return new Symbol(Values.toString(ctx, val)); + } + @Native(thisArg = true) public static String toString(Context ctx, Object thisArg) throws InterruptedException { + return passThis(ctx, "toString", thisArg).value; + } + @Native(thisArg = true) public static Symbol valueOf(Context ctx, Object thisArg) throws InterruptedException { + return passThis(ctx, "valueOf", thisArg); + } + + @Native("for") public static Symbol _for(String key) { + if (symbols.containsKey(key)) return symbols.get(key); + else { + var sym = new Symbol(key); + symbols.put(key, sym); + return sym; + } + } + @Native public static String keyFor(Symbol sym) { + return sym.value; + } + + public SymbolPolyfill(Symbol val) { + this.value = val; + } + + @NativeInit(InitType.PROTOTYPE) public static void init(Environment env, ObjectValue target) { + target.defineProperty(null, env.symbol("Symbol.typeName"), "Symbol"); + } +} diff --git a/src/me/topchetoeu/jscript/polyfills/SyntaxErrorPolyfill.java b/src/me/topchetoeu/jscript/polyfills/SyntaxErrorPolyfill.java new file mode 100644 index 0000000..27c6ed1 --- /dev/null +++ b/src/me/topchetoeu/jscript/polyfills/SyntaxErrorPolyfill.java @@ -0,0 +1,20 @@ +package me.topchetoeu.jscript.polyfills; + +import me.topchetoeu.jscript.engine.Context; +import me.topchetoeu.jscript.engine.Environment; +import me.topchetoeu.jscript.engine.values.ObjectValue; +import me.topchetoeu.jscript.interop.InitType; +import me.topchetoeu.jscript.interop.NativeConstructor; +import me.topchetoeu.jscript.interop.NativeInit; + +public class SyntaxErrorPolyfill extends ErrorPolyfill { + @NativeConstructor(thisArg = true) public static ObjectValue constructor(Context ctx, Object thisArg, Object message) throws InterruptedException { + var target = ErrorPolyfill.constructor(ctx, thisArg, message); + target.defineProperty(ctx, "name", "SyntaxError"); + return target; + } + @NativeInit(InitType.PROTOTYPE) public static void init(Environment env, ObjectValue target) { + target.defineProperty(null, env.symbol("Symbol.typeName"), "SyntaxError"); + target.defineProperty(null, "name", "SyntaxError"); + } +} \ No newline at end of file diff --git a/src/me/topchetoeu/jscript/polyfills/TypeErrorPolyfill.java b/src/me/topchetoeu/jscript/polyfills/TypeErrorPolyfill.java new file mode 100644 index 0000000..5814f69 --- /dev/null +++ b/src/me/topchetoeu/jscript/polyfills/TypeErrorPolyfill.java @@ -0,0 +1,20 @@ +package me.topchetoeu.jscript.polyfills; + +import me.topchetoeu.jscript.engine.Context; +import me.topchetoeu.jscript.engine.Environment; +import me.topchetoeu.jscript.engine.values.ObjectValue; +import me.topchetoeu.jscript.interop.InitType; +import me.topchetoeu.jscript.interop.NativeConstructor; +import me.topchetoeu.jscript.interop.NativeInit; + +public class TypeErrorPolyfill extends ErrorPolyfill { + @NativeConstructor(thisArg = true) public static ObjectValue constructor(Context ctx, Object thisArg, Object message) throws InterruptedException { + var target = ErrorPolyfill.constructor(ctx, thisArg, message); + target.defineProperty(ctx, "name", "TypeError"); + return target; + } + @NativeInit(InitType.PROTOTYPE) public static void init(Environment env, ObjectValue target) { + target.defineProperty(null, env.symbol("Symbol.typeName"), "TypeError"); + target.defineProperty(null, "name", "TypeError"); + } +} \ No newline at end of file