'use strict'; const NQPExceptionWithCtx = require('./nqp-exception-with-ctx.js'); const NQPException = require('./nqp-exception.js'); const NQPObject = require('./nqp-object.js'); const Null = require('./null.js'); const BOOT = require('./BOOT.js'); const NQPInt = require('./nqp-int.js'); const NQPStr = require('./nqp-str.js'); const stackTrace = require('stack-trace'); const NEXT = 4; const REDO = 8; const LAST = 16; const RETURN = 32; const TAKE = 128; const WARN = 256; const SUCCEED = 512; const PROCEED = 1024; const LABELED = 4096; const AWAIT = 8192; const EMIT = 16384; const DONE = 32768; const categoryIDs = { NEXT: NEXT, REDO: REDO, LAST: LAST, RETURN: RETURN, TAKE: TAKE, WARN: WARN, SUCCEED: SUCCEED, PROCEED: PROCEED, AWAIT: AWAIT, EMIT: EMIT, DONE: DONE, }; const categoryToName = {}; for (const name of Object.keys(categoryIDs)) { categoryToName[categoryIDs[name]] = name; } class ResumeException { constructor(exception) { this.exception = exception; } }; class Ctx extends NQPObject { constructor(callerCtx, outerCtx, callThis) { super(); this.$$caller = callerCtx; this.$$outer = outerCtx; this.$$callThis = callThis; } codeRef() { return this.$$callThis; } last() { return this.controlException(LAST); } lastLabeled(label) { return this.controlExceptionLabeled(label, LAST); } next() { return this.controlException(NEXT); } nextLabeled(label) { return this.controlExceptionLabeled(label, NEXT); } redo() { return this.controlException(REDO); } redoLabeled(label) { return this.controlExceptionLabeled(label, REDO); } controlException(category) { const exType = BOOT.Exception; const exception = exType.$$STable.REPR.allocate(exType.$$STable); exception.$$category = category; return this.propagateControlException(exception); } controlExceptionLabeled(label, category) { const exType = BOOT.Exception; const exception = exType.$$STable.REPR.allocate(exType.$$STable); exception.$$category = category | LABELED; exception.$$payload = label; return this.propagateControlException(exception); } /*async*/ propagateControlException(exception) { const handler = '$$' + categoryToName[exception.$$category & ~LABELED]; const labeled = exception.$$category & LABELED; let ctx = this; while (ctx) { if (ctx.$$controlHandlerOuter) { ctx = ctx.$$controlHandlerOuter; } if (ctx.$$CONTROL || (ctx[handler] && (!labeled || ctx.$$label === exception.$$payload))) { exception.caught = ctx; ctx.exception = exception; try { const wrapped = new Ctx(this, this, null); wrapped.$$currentException = exception; if (ctx[handler]) { ctx.$$unwind.ret = /*await*/ ctx[handler](wrapped); } else { ctx.$$unwind.ret = /*await*/ ctx.$$CONTROL(wrapped); } } catch (e) { if (e instanceof ResumeException && e.exception === exception) { return; } else { throw e; } } throw ctx.$$unwind; } ctx = ctx.$$caller; } const uncaught_control = this.$$getHLL().get('uncaught_control'); if (uncaught_control) { try { /*await*/ uncaught_control.$$call(this, null, exception.$$decont(this)); } catch (e) { if (e instanceof ResumeException && e.exception === exception) { return; } else { throw e; } } } else { // TODO maybe wrap this up? throw exception; } } /*async*/ propagateException(exception) { if (exception.$$category) { return this.propagateControlException(exception); } let ctx = this; while (ctx) { if (ctx.$$catchHandlerOuter) { ctx = ctx.$$catchHandlerOuter; } if (ctx.$$CATCH) { exception.caught = ctx; ctx.exception = exception; try { const wrapped = new Ctx(this, this, null); wrapped.$$currentException = exception; ctx.$$unwind.ret = /*await*/ ctx.$$CATCH(wrapped); } catch (e) { if (e instanceof ResumeException && e.exception === exception) { return; } else { throw e; } } throw ctx.$$unwind; } ctx = ctx.$$caller; } const uncaught_exception = this.$$getHLL().get('uncaught_exception'); if (uncaught_exception) { try { /*await*/ uncaught_exception.$$call(this, null, exception); } catch (e) { if (e instanceof ResumeException && e.exception === exception) { return; } else { throw e; } } } else { // TODO maybe wrap this up? throw exception; } } /*async*/ catchException(exception) { // we don't have access to the most correct ctx in case of this sort of exception const wrapped = new Ctx(this, this, null); wrapped.$$currentException = exception; return /*await*/ this.$$CATCH(wrapped); } rethrow(exception) { return this.propagateException(exception); } die(msg) { return this.propagateException(new NQPExceptionWithCtx(msg, this, stackTrace.get())); } resume(exception) { throw new ResumeException(exception); } throw(exception) { // Partial workaround for browser that don't use the V8 stack trace API (like Firefox) try { exception.$$stack = stackTrace.get(); } catch (e) { exception.$$stack = []; } exception.$$ctx = this; return this.propagateException(exception); } throwpayloadlexcaller(category, payload) { let ctx = this.$$skipHandlers().$$caller; const isThunkOrCompilerStub = code => code !== null && (code.staticCode.isThunk || code.isCompilerStub); while (ctx && isThunkOrCompilerStub(ctx.codeRef())) { ctx = ctx.$$caller; } return this.$$throwLexicalException(ctx, category, payload); } throwpayloadlex(category, payload) { return this.$$throwLexicalException(this, category, payload); } /*async*/ $$throwLexicalException(lookFrom, category, payload) { const exType = BOOT.Exception; const exception = exType.$$STable.REPR.allocate(exType.$$STable); exception.$$category = category; exception.$$payload = payload; const handler = '$$' + categoryToName[category]; let ctx = lookFrom; while (ctx) { if (ctx.$$controlHandlerOuter) { ctx = ctx.$$controlHandlerOuter; } // TODO - think about checking the label if (ctx[handler] || ctx.$$CONTROL) { exception.caught = ctx; ctx.exception = exception; try { const wrapped = new Ctx(this, this, null); wrapped.$$currentException = exception; if (ctx[handler]) { ctx.$$unwind.ret = /*await*/ ctx[handler](wrapped); } else { ctx.$$unwind.ret = /*await*/ ctx.$$CONTROL(wrapped); } } catch (e) { if (e instanceof ResumeException && e.exception === exception) { return; } else { throw e; } } let check = lookFrom; while (check) { if (check === ctx) { throw ctx.$$unwind; } check = check.$$caller; } /*await*/ this.$$getHLL().get('lexical_handler_not_found_error').$$call(this, null, new NQPInt(category), new NQPInt(1)); return; } ctx = ctx.$$outer; } /*await*/ this.$$getHLL().get('lexical_handler_not_found_error').$$call(this, null, new NQPInt(category), new NQPInt(0)); } lookupDynamic(name) { let ctx = this; while (ctx) { if (ctx.hasOwnProperty(name)) { return ctx[name]; } ctx = ctx.$$caller; } return Null; /* Looking up of a contextual is allowed to fail, nqp code usually fallbacks to looking up of global */ } lookupOrNull(name) { if (this.hasOwnProperty(name)) { return this[name]; } else { return Null; } } lookupDynamicFromCaller(name) { let ctx = this.$$caller; while (ctx) { if (ctx.hasOwnProperty(name)) { return ctx[name]; } ctx = ctx.$$caller; } return Null; /* Looking up of a contextual is allowed to fail, nqp code usually fallbacks to looking up of global */ } lookupWithCallers(name) { let currentCallerCtx = this; while (currentCallerCtx) { let currentCtx = currentCallerCtx; while (currentCtx) { if (currentCtx.hasOwnProperty(name)) { return currentCtx[name]; } currentCtx = currentCtx.$$outer; } currentCallerCtx = currentCallerCtx.$$caller; } return Null; } lookup(name) { let ctx = this; while (ctx) { if (ctx.hasOwnProperty(name)) { return ctx[name]; } ctx = ctx.$$outer; } /* Rakudo depends on returning null when we can't lookup a lexical */ return Null; } lookupFromOuter(name) { return this.$$outer.lookup(name); } $$exception() { let ctx = this; while (ctx) { if (ctx.$$currentException) { return ctx.$$currentException; } ctx = ctx.$$caller; } console.log('exception not found'); } $$lastexpayload() { return this.$$exception().$$payload; } $$getHLL() { let ctx = this; while (ctx) { if (ctx.$$hll) { return ctx.$$hll; } ctx = ctx.$$outer; } } $$atkey(name) { let ctx = this; while (ctx) { if (ctx.hasOwnProperty(name)) { return ctx[name]; } ctx = ctx.$$outer; } throw new NQPException(`Lexical with name '${name}' does not exist in this frame`); } $$bindkey(key, value) { this[key] = value; return value; } $$existskey(key) { return (this.hasOwnProperty(key) ? 1 : 0); } $$skipHandlers() { return this; } bind(name, value) { let ctx = this; while (ctx) { if (ctx.hasOwnProperty(name)) { ctx[name] = value; return ctx[name]; } ctx = ctx.$$outer; } throw `Can't bind: ${name}`; } bindDynamic(name, value) { let ctx = this; while (ctx) { if (ctx.hasOwnProperty(name)) { ctx[name] = value; return ctx[name]; } ctx = ctx.$$caller; } throw `Can't bind dynamic: ${name}`; } bindDynamicFromCaller(name, value) { let ctx = this.$$caller; while (ctx) { if (ctx.hasOwnProperty(name)) { ctx[name] = value; return ctx[name]; } ctx = ctx.$$caller; } throw `Can't bind dynamic: ${name}`; } $$iterator() { return new CtxIter(this); } $$elems() { return Object.keys(this).filter(key => key.substr(0, 2) != '$$').length; } $$toBool(ctx) { return 1; } }; class CtxIter extends NQPObject { constructor(ctx) { super(); this.$$ctx = ctx; this.$$keys = Object.keys(ctx).filter(key => key.substr(0, 2) != '$$'); this.$$target = this.$$keys.length - 1; this.$$idx = -1; } $$shift() { this.$$idx++; return this; } $$iterval() { return this.$$ctx[this.$$keys[this.$$idx]]; } $$iterkey_s() { return this.$$keys[this.$$idx]; } $$toBool(ctx) { return this.$$idx < this.$$target; } p6$Str(ctx, _NAMED, self) { return new NQPStr(this.$$iterkey_s()); } p6$key(ctx, _NAMED, self) { return new NQPStr(this.$$iterkey_s()); } p6$value(ctx, _NAMED, self) { return this.$$iterval(); } }; Ctx.prototype.$$atkey_i = Ctx.prototype.$$atkey_n = Ctx.prototype.$$atkey_s = Ctx.prototype.$$atkey; Ctx.prototype.$$bindkey_i = Ctx.prototype.$$bindkey_n = Ctx.prototype.$$bindkey_s = Ctx.prototype.$$bindkey; module.exports = Ctx;