Source: 08.js

/** This module contains the classes for all examples in chapter eight.
   
    @module Eight
    @author © 2024 Axel T. Schreiner <axel@schreiner-family.net>
    @version 2024-06-24
  */

import * as Seven from './07.js';

/** [Example 8/01](../?eg=08/01): adds support for global higher-order functions.
    @mixin */
const Machine01 = superclass => class extends superclass {
  /** `stack: ... addr -> ... old-pc | pc: addr`
      @instance
      @memberof module:Eight~Machine01 */
  CallValue (memory) {
    memory.pc = memory.splice(-1, 1, memory.pc)[0];
  }
  /** `stack: ... x-len n*val -> ... n*val x-len`
      @instance
      @memberof module:Eight~Machine01 */
  Rotate (n, len = 1) {
    return memory => memory.push(... memory.splice(- n - len, len));
  }
};

/** [Example 8/01](../?eg=08/01): adds actions and infrastructure to compile global higher-order functions.
    @mixin
*/
const Global01 = superclass => class extends superclass {
  /** Describes a type.
      @class @extends super.Symbol
      @instance
      @memberof module:Eight~Global01
      @property {?Array<String|Type>} parms - null for scalar, else list of parameter types.
      @property {String|Type} returns - null or result type.
      @property {Boolean} isFun - `true` if function type.
      @property {function} toString() - represents as text. */
  get Type () { return this.#Type ??= class extends super.Symbol {
      parms = [];   // list of parameter types, `null` for 'number'
      returns;                                // result type if any
      get isFun () { return this.parms !== null; }

      constructor (owner, name, parms, returns) {
        super(owner, name);
        this.parms = parms; this.returns = returns;
      }
      toString () {
        const name = t => typeof t == 'string' ? t : t.name;
        return `type ${this.name}` +
          (!this.isFun ? '' :
            `(${this.parms.map(name).join(', ')})` +
              (this.returns ? `: ${name(this.returns)}` : ''));
      }
    };
  }
  #Type;

  /** Type table, maps names to descriptions.
      @instance
      @memberof module:Eight~Global01 */
  get typeSymbols () { return this.#typeSymbols; }
  #typeSymbols = new Map();

  /** Predefined type descriptor for `number`.
      @constant {Type}
      @instance
      @memberof module:Eight~Global01 */
  get numberType () { return this.#numberType; }
  #numberType;

  /** Predefined type descriptor for `main (): number`.
      @constant {Type}
      @instance
      @memberof module:Eight~Global01 */
  get mainType () { return this.#mainType; }
  #mainType;

  /** Describes a function in {@linkcode module:Eight~Global01 Global01}.
      @class @extends super.Fun
      @instance
      @memberof module:Eight~Global01
      @property {Type} type - function's type.
      @property {number[]} loads - slots to insert `Push start`.
      @property {function} setParms() - [replace] set/check types.
      @property {function} load() - generates `Push(start)`.
      @property {function} storeOk() - [extend] check type.
      @property {function} end() - [extend] fixes `loads`
      @property {function} toString() - [extend] shows type, if any. */
  get Fun () { return this.#Fun ??= class extends super.Fun {
      type;                                      // function's type
      loads = [];                     // forward references to push
      
      setParms (name) {           // [replace] sets parameter types
        this.parms = this.locals.size;   // may be wrong, see below
        this.size += 2;         // leave room for old pc and old fp
        this.addr = this.size ++;          // leave slot for result
        try {
          const type = this.owner.typeSymbols.get(name);
          if (!type) throw `${name}: not a type`;
          if (!type.isFun) throw `${name}: not a function type`;
          if (this.type && this.type != type)
            throw `${name} ${this.name}: ` +
              `previously declared as ${this.type.name}`;
          if (type.parms.length != this.locals.size)
            throw `${name} ${this.name} arguments: expects ` +
              `${type.parms.length}, receives ${this.locals.size}`;
          this.type = type;
          let n = 0;              // Map.forEach does not provide n
          this.locals.forEach(parm => parm.type = type.parms[n ++]);
        } catch (e) {
          if (e instanceof Error) throw e;      // shouldn't happen
          this.owner.parser.error(e);            // report an error
        }
      }
      load () {                           // generates 'Push start'
        if (typeof this.start == 'number')
          this.owner.machine.gen('Push', this.start);
        else
          this.loads.push(this.owner.machine.code.push(null) - 1);
      }
      storeOk (type) {                      // [extend] checks type
        try {
          if (this.type.returns) {        // return value expected?
            if (!type)                          // no return value?
              throw `must return ${this.type.returns}`;
            else if (this.type.returns != type)      // wrong type?
              throw `expects ${this.type.returns}, not ${type}`;
          } else if (type)            // return value not expected?
            throw  `doesn't return a value`;
          return super.storeOk();               // inside function?
        } catch (e) {
          if (e instanceof Error) throw e;      // shouldn't happen
          this.owner.parser.error(`${this.name}: ${e}`);
          return false;
        }
      }
      end () {                           // [extend] resolves loads
        const push = this.owner.machine.ins('Push', this.start);
        this.loads.forEach(p => this.owner.machine.code[p] = push);
        this.loads.length = 0;
        super.end();
      }
      toString () {                  // [extend] shows type, if any
        return this.type ? `${this.type.name} ${super.toString()}` :
          super.toString();
      }
    };
  }
  #Fun;

  /** Describes a variable in {@linkcode module:Eight~Global01 Global01}.
      @class @extends super.Var
      @instance
      @memberof module:Eight~Global01
      @property {Type} type - variable's type.
      @property {function} storeOk() - [replace] check type.
      @property {function} call() - code call to value
      @property {function} toString() - [extend] show type, if any. */
  get Var () { return this.#Var ??= class extends super.Var {
      type;                                      // variable's type

      storeOk (type) {                      // [replace] check type
        if (this.type == type) return true;
        this.owner.parser.error(`${this.name}: ` +
            `expects ${this.type}, not ${type}`);
        return false;
      }
      
      call () { this.load(); this.owner.machine.gen('CallValue'); }
      
      toString () {                   // [extend] show type, if any
        return this.type ? `${this.type.name} ${super.toString()}` :
          super.toString();
      }
    };
  }
  #Var;

  constructor (parser, machine) {
    super(parser, machine ?? new (Machine01(Seven.Machine06))());
    this.typeSymbols.set('number',
      this.#numberType = new this.Type(this, 'number', null, null));
    this.typeSymbols.set('main',
      this.#mainType =
        new this.Type(this, 'main', [ ], this.numberType));
  }

  /** `prog: [ typedcls ] [ vars ] funs;`
      @instance
      @memberof module:Eight~Global01 */
  prog (t, v, f) { return this.parser.call(this, super.prog, t, f); }

  /** `typedcls: { 'type' typedcl [{ ',' typedcl }] ';' };` checks and translates the types
      @instance
      @memberof module:Eight~Global01 */
  typedcls (some) {
    this.typeSymbols.forEach(sym => {  // check and translate types
      if (sym.isFun) {                       // avoid non-functions
        const check = name => { // return type description for name
          const type = this.typeSymbols.get(name);
          if (type) return type;
          this.parser.error(`${name}: not a type`);
          return this.numberType;                          // patch
        };
        sym.parms = sym.parms.map(check);     // convert to symbols
        if (typeof sym.returns == 'string') 
          sym.returns = check(sym.returns);
      }
    });
  }

  /** `typedcl: Name '(' [ types ] ')' [ ':' typename ];` declares
      @instance
      @memberof module:Eight~Global01 */
  typedcl (name, lp, types, rp, returns) {
    if (this.typeSymbols.get(name))
      this.parser.error(`${name}: duplicate type`);
    else
      this.typeSymbols.set(name, new this.Type(this, name,
        types ? types[0] : [], returns ? returns[1] : null));
  }

  /** `types: typename [{ ',' typename }];` returns list
      @instance
      @memberof module:Eight~Global01 */
  types (typename, many) {
    return [ typename ].
      concat(many ? many[0].map(list => list[1]) : []);
  }

  /** `typename: Name | 'number';` returns name or 'number'
      @instance
      @memberof module:Eight~Global01 */
  typename (name) { return name; }

  // vars: 'var' varname [{ ',' varname }] ';';

  /** `varname: Name [ ':' type ];` declares the name.
      Can be used with one or two arguments, defaults to `number`.
      @instance
      @memberof module:Eight~Global01 */
  varname (...arg) {
    let [ name, type ] = arg;
    type = type ? type[1] : this.numberType;
    this._dcl(this._alloc(name), true).type = type;
  }

  /** `type: Name | 'number';` returns type symbol
      @instance
      @memberof module:Eight~Global01 */
  type (name) {
    const type = this.typeSymbols.get(name);
    if (type) return type;
    this.parser.error(`${name}: not a type`);
    return this.numberType;
  }

  // names: Name [{ ',' Name }];
  // funs: { fun };
  // fun: head parms [ block ] ';';
  // head: 'function' Name;

  /** `parms: '(' [ names ] ')' [ ':' Name ];` declares
      @instance
      @memberof module:Eight~Global01 */
  parms (lp, names, rp, name) {   // funtion's name is default type
    this.funct.setParms(name ? name[1] : this.funct.name);
  }

  // block: begin [ vars ] stmts 'end';
  // begin: 'begin';
  // stmts: stmt [{ ';' stmt }];
  // stmt: assign | print | return | block | loop | select;
  // assign: symbol action;
  // action: store | call;

  /** `store: '=' sum;` expects context, codes assignment
      @instance
      @memberof module:Eight~Global01 */
  store (_, sum) {
    if (this.context.symbol.storeOk(sum))
      this.context.symbol.store();
  }

  // call: { args };

  /** `args: '(' [ sums ] ')';` codes call, chains context
      @instance
      @memberof module:Eight~Global01 */
  args (lp, sums, rp) {
    const args = sums === null ? [ ] : sums[0];    // list of types
    const type = 'type' in this.context ?   // chained call if true
      this.context.type : this.context.symbol.type;
    try {
      if (!type) throw 'too many argument lists';
      if (!type.isFun) throw 'not a function';
      if (type.parms.length != args.length)
        throw `arguments: ${type.parms.length} expected, ` +
          `${args.length} specified`;
      const errors = [];
      type.parms.forEach(
        (parm, n) => { if (parm != args[n]) errors.push(
          `argument ${n+1} is ${args[n].toString()}, ` +
          `not ${parm.toString()}`
        ); });
      if (errors.length) throw errors.join('; ');    
      if ('type' in this.context) {                 // chained call
        this._lift(args);   // move function address past arguments
        this.machine.gen('CallValue');     // call address on stack
      } else this.context.symbol.call();  // call function/variable 
    } catch (e) {
      if (e instanceof Error) throw e;         // should not happen
      this.parser.error(`call to ${this.context.symbol.name}: ${e}`);
    }
    this.context.type = type ? type.returns : null;  // result type
  }
  
  /** Move function address past arguments to the top of the stack.
      @param {Type[]} args - list of argument types.
      @instance
      @memberof module:Eight~Global01 */
  _lift (args) {
    if (args.length) this.machine.gen('Rotate', args.length);
  }

  /** `print: 'print' sums;` checks types
      @instance
      @memberof module:Eight~Global01 */
  print (p, sums) {
    if (!sums.every(sum => sum == this.numberType))
      this.parser.error('can only print numbers');
    this.parser.call(this, super.print, p, sums.length);
  }

  /** `sums: sum [{ ',' sum }];` returns list of types
      @instance
      @memberof module:Eight~Global01 */
  sums (sum, many) {
    return [ sum ].
      concat(many ? many[0].map(list => list[1]) : []);
  }

  /** `return: 'return' [ sum ];`
      @instance
      @memberof module:Eight~Global01 */
  return (_, sum) {
    if (this.funct.storeOk(sum ? sum[0] : null))
      if (sum)
        (this.funct.store(), this.machine.gen('Pop'));
    this.funct.return();
  }

  // loop: While cmp Do [ vars ] stmts 'od';
  // While: 'while';
  // Do: 'do';
  // select: 'if' cmp then [ else ] 'fi';
  // then: Then [ [ vars ] stmts ];
  // else: Else [ vars ] stmts;
  // Then: 'then';
  // Else: 'else';

  /** `cmp: sum rel;` checks for number
      @instance
      @memberof module:Eight~Global01 */
  cmp (sum, _) {
    if (sum != this.numberType)
      this.parser.error(`cannot compare ${sum.toString()}`);
  }

  // rel: eq | ne | gt | ge | lt | le;

  /** `eq: '=' sum;` checks for number
      @instance
      @memberof module:Eight~Global01 */
  eq (_, sum) {
    if (sum != this.numberType)
      this.parser.error(`cannot apply '=' to ${sum.toString()}`);
    else this.parser.call(this, super.eq);
  } 

  /** `ne: '<>' sum;` checks for number
      @instance
      @memberof module:Eight~Global01 */
  ne (_, sum) {
    if (sum != this.numberType)
      this.parser.error(`cannot apply '<>' to ${sum.toString()}`);
    else this.parser.call(this, super.ne);
  } 

  /** `gt: '>' sum;` checks for number
      @instance
      @memberof module:Eight~Global01 */
  gt (_, sum) {
    if (sum != this.numberType)
      this.parser.error(`cannot apply '>' to ${sum.toString()}`);
    else this.parser.call(this, super.gt);
  } 

  /** `ge: '>=' sum;` checks for number
      @instance
      @memberof module:Eight~Global01 */
  ge (_, sum) {
    if (sum != this.numberType)
      this.parser.error(`cannot apply '>=' to ${sum.toString()}`);
    else this.parser.call(this, super.ge);
  } 

  /** `lt: '<' sum;` checks for number
      @instance
      @memberof module:Eight~Global01 */
  lt (_, sum) {
    if (sum != this.numberType)
      this.parser.error(`cannot apply '<' to ${sum.toString()}`);
    else this.parser.call(this, super.lt);
  } 

  /** `le: '<=' sum;` checks for number
      @instance
      @memberof module:Eight~Global01 */
  le (_, sum) {
    if (sum != this.numberType)
      this.parser.error(`cannot apply '<=' to ${sum.toString()}`);
    else this.parser.call(this, super.le);
  } 

  /** `sum: product [{ add | subtract }];` returns product
      @instance
      @memberof module:Eight~Global01 */
  sum (product, many) {
    if (many && product != this.numberType)
      this.parser.error(`cannot apply '+' or '-' ` +
        `to ${product.toString()}`);
    return product;  
  }

  /** `add: '+' product;` checks for number
      @instance
      @memberof module:Eight~Global01 */
  add (_, product) {
    if (product != this.numberType)
      this.parser.error(`cannot apply '+' to ${product.toString()}`);
    else this.parser.call(this, super.add);
  }

  /** `subtract: '-' product;` checks for number
      @instance
      @memberof module:Eight~Global01 */
  subtract (_, product) {
    if (product != this.numberType)
      this.parser.error(`cannot apply '-' to ${product.toString()}`);
    else this.parser.call(this, super.subtract);
  }

  /** `product: signed [{ multiply | divide }];` returns signed
      @instance
      @memberof module:Eight~Global01 */
  product (signed, many) {
    if (many && signed != this.numberType)
      this.parser.error(`cannot apply '*' or '/' ` +
        `to ${signed.toString()}`);
    return signed;  
  }

  /** `multiply: '*' signed;` checks for number
      @instance
      @memberof module:Eight~Global01 */
  multiply (_, signed) {
    if (signed != this.numberType)
      this.parser.error(`cannot apply '*' to ${signed.toString()}`);
    else this.parser.call(this, super.multiply);
  }

  /** `divide: '/' signed;` checks for number
      @instance
      @memberof module:Eight~Global01 */
  divide (_, signed) {
    if (signed != this.numberType)
      this.parser.error(`cannot apply '/' to ${product.toString()}`);
    else this.parser.call(this, super.divide);
  }

  /** `signed: [ '-' ] term;` checks for number, returns term 
      @instance
      @memberof module:Eight~Global01 */
  signed (minus, term) {
    if (minus && term != this.numberType)
      this.parser.error(`cannot apply '-' to ${term.toString()}`);
    else this.parser.call(this, super.signed, minus, term);
    return term;
  }

  /** `term: input | number | name | '(' sum ')';` returns type
      @instance
      @memberof module:Eight~Global01 */
  term (...val) { return val.length > 1 ? val[1] : val[0]; }

  /** `input: 'input' [ Number ];` returns `this.numberType
      @instance
      @memberof module:Eight~Global01 */
  input (i, number) {
    this.parser.call(this, super.input, i, number); return this.numberType;
  }

  /** `number: Number;` returns `this.numberType`
      @instance
      @memberof module:Eight~Global01 */
  number (number) { 
    this.parser.call(this, super.number, number); return this.numberType;
  }

  /** `name: symbol [{ args }];`
      @instance
      @memberof module:Eight~Global01 */
  name (sym, args) {
    const context = this.context; this.context = null;
    if (args) return context.type;
    sym.load();
    return sym.type;
  }

  // symbol: Name;
};

/** [Example 8/08](../?eg=08/08): adds support for nested functions as arguments.
    @mixin */
const Machine08 = superclass => class extends superclass {
  /** `stack: ... arguments dp old-pc  
      -> ... arguments old-pc old-fp old-dp result display locals`
      @param {number} args - size of argument values.
      @param {number} depth - number of display entries.
      @param {number} vars - size of local variables.
      @instance
      @memberof module:Eight~Machine08 */
  Entry (args, depth, vars) {
    return memory => {
      const fp = memory.length - args - 2,        // next memory.fp
        dp = memory.splice(-1, 1, memory.pop(),    // retain old-pc
               memory.fp, memory.dp, 0  // push fp, dp, result slot
          )[0];                         // extract incoming display
      memory.fp = fp;                           // new frame's base
      memory.dp = memory.length - 1;          // new display's base
                             // copy incoming display up to depth-1
      memory.push(... memory.slice(dp + 1, dp + depth),
        memory.fp,                              // append new frame
        ... Array(vars).fill(0));     // initialize local variables
    };
  }
  /** `stack: ... arguments old-pc old-fp old-dp result display locals
      -> ... result old-pc`
      @param {number} args - size of argument values.
      @instance
      @memberof module:Eight~Machine08 */
  Exit (args) {
    return memory => {
      const fp = memory.fp;                        // current frame
      memory.splice(fp, args,             // remove argument values
        memory[fp + args + 3]);                    // insert result      
                           // restore old fp dp, free rest of frame
      [ memory.fp, memory.dp ] = memory.splice(fp + 2, Infinity);
    };
  }
  /** `stack: ... -> ... dp`
      @instance
      @memberof module:Eight~Machine08 */
  PushDP (memory) {
    memory.push(memory.dp);
  }
};

/** [Example 8/08](../?eg=08/08): adds actions and infrastructure to compile nested functions as arguments.
    Requires {@linkcode module:Seven~Nest13 Nest13} and {@linkcode module:Eight~Global01 Global01}.
    @mixin */
const Pass08 = superclass => class extends superclass {
  /** Describes a variable in {@linkcode module:Eight~Pass08 Pass08}
      @class @extends super.Var
      @instance
      @memberof module:Eight~Pass08
      @property {function} load() - [replace] for function slots.
      @property {function} storeOk(type) - [extend] false for function value.
*/
  get Var () { return this.#Var ??= class extends super.Var {
      
      load () {       // [replace] load two slots for function type
        const load = addr => {
          if (!this.depth)                                // global
            this.owner.machine.gen('Load', addr);
          else if (this.depth+1 != this.owner.functs.length)
                                                          // nested
            this.owner.machine.gen('LoadDP', addr, this.depth);
          else this.owner.machine.gen('LoadFP', addr);     // local
        };
        load(this.addr);              // top:value or below:display
        if (this.type.isFun) load(this.addr + 1);  // + top:address
      }
      
      storeOk (type) {    // [extend] read-only function parameters
        if (this.type?.isFun) {
          this.owner.parser.error(`${this.name}: read only parameter`);
          return false;
        }
        return super.storeOk(type);      
      }
    };
  }
  #Var;

  /** Describes a function in {@linkcode module:Eight~Pass08 Pass08}.
      @class @extends super.Fun
      @instance
      @memberof module:Eight~Pass08
      @property {number} parms - [replace] memory slots for arguments.
      @property {function} setParms() - [replace] function values take 2 slots.
      @property {function} call() - [extend] `PushDP`, `Call(addr)`.
      @property {function} load() - [extend] `PushDP`, `Push(start)`.
      @property {function} exit() - [replace] `Entry`, `Exit`. */
  get Fun () { return this.#Fun ??= class extends super.Fun {

      setParms (name) {           // [replace] sets parameter types
        try {
          const type = this.owner.typeSymbols.get(name);
          if (!type) throw `${name}: not a type`;
          if (!type.isFun) throw `${name}: not a function type`;
          if (this.type && this.type != type)
            throw `${name} ${this.name}: ` +
              `previously declared as ${this.type.name}`;
          if (type.parms.length != this.locals.size)
            throw `${name} ${this.name} arguments: expects ` +
              `${type.parms.length}, receives ${this.locals.size}`;
          this.type = type;
          this.size = 0;          // parameter addresses start at 0
          let n = 0;              // Map.forEach does not provide n
          this.locals.forEach(parm => {
            parm.addr = this.size ++;      // set parameter address
            parm.type = type.parms[n ++];     // set parameter type
            if (parm.type.isFun) ++ this.size; // function argument
          });
          this.parms = this.size;                 // argument slots
          this.size += 3;        // room for old pc, old fp, old dp
          this.addr = this.size;               // address of result
          this.size += 1 + this.depth;  // room for result, display
        } catch (e) {
          if (e instanceof Error) throw e;      // shouldn't happen
          this.owner.parser.error(e);            // report an error
        }
      }
      
      call () {                       // [extend] generate 'PushDP'
        this.owner.machine.gen('PushDP'); super.call();
      }
      
      load () {                       // [extend] generate 'PushDP'
        this.owner.machine.gen('PushDP'); super.load();
      }
      
      exit () {                    // [replace] new 'Entry', 'Exit'
        this.owner.machine.code[this.start] =
          this.owner.machine.ins('Entry', this.parms,  // arguments
            this.depth,                  // display, variable slots
            this.frameSize - (this.parms + 4 + this.depth));
        this.owner.machine.gen('Exit', this.parms);
        const end = this.owner.machine.gen('Return');
        if (this.scope)                    // need to repair bypass
          this.owner.machine.code[this.scope.bypass] =
            this.owner.machine.ins('Branch', end);
      }    
    };
  }
  #Fun;

  constructor (parser, machine) {
    super(parser, machine ?? new (Machine08(Machine01(Seven.Machine13)))());
  }

  /** [Replace] Need `PushDP` for `main`.
      @param {Fun} main - describes `main()`.
      @instance
      @memberof module:Eight~Pass08 */
  _startup (main) {
    for (let p = 0; p < main.parms; ++ p)  // push arguments if any
      this.machine.gen('Push', 0);
    this.machine.gen('PushDP');             // push display pointer
    this.machine.gen('Call', main.start);     // call main function
    this.machine.gen('Print', 1);                  // print and pop
  }  

  // prog: [ typedcls ] [ vars ] funs;
  // typedcls: { 'type' typedcl [{ ',' typedcl }] ';' };
  // typedcl: Name '(' [ types ] ')' [ ':' 'number' ];
  // types: typename [{ ',' typename }];
  // typename: Name | 'number';
  // vars: 'var' varname [{ ',' varname }] ';';
  // varname: Name;
  // funs: { fun };
  // fun: head parms [ block ] ';';
  // head: 'function' Name;
  // parms: '(' [ names ] ')' [ ':' Name ];
  // names: Name [{ ',' Name }];
  // block: begin body 'end';
  // body: [ vars ] [ funs ] stmts;
  // begin: 'begin';
  // stmts: stmt [{ ';' stmt }];
  // stmt: assign | print | return | block | loop | select;
  // assign: symbol action;
  // action: store | call;
  // store: '=' sum;
  // call: args;
  // args: '(' [ sums ] ')';
  // print: 'print' sums;
  // sums: sum [{ ',' sum }];
  // return: 'return' [ sum ];  
  // loop: While cmp Do body 'od';
  // While: 'while';
  // Do: 'do';
  // select: 'if' cmp then [ else ] 'fi';
  // then: Then [ body ];
  // else: Else body;
  // Then: 'then';
  // Else: 'else';
  // cmp: sum rel;
  // rel: eq | ne | gt | ge | lt | le;
  // eq: '=' sum;
  // ne: '<>' sum;
  // gt: '>' sum;
  // ge: '>=' sum;
  // lt: '<' sum;
  // le: '<=' sum;
  // sum: product [{ add | subtract }];
  // add: '+' product;
  // subtract: '-' product;
  // product: signed [{ multiply | divide }];
  // multiply: '*' signed;
  // divide: '/' signed;
  // signed: [ '-' ] term;
  // term: input | number | name | '(' sum ')';
  // input: 'input' [ Number ];
  // number: Number;
  // name: symbol [ args ];
  // symbol: Name;
};

/** [Example 8/14](../?eg=08/14): adds support for garbage-collected frames,
    cannot be mixed with {@linkcode module:Eight~Machine08 Machine08}.
    @mixin */
const Machine14 = superclass => class extends superclass {
  /** Data memory for nested functions as arguments
      @class @extends super.Memory
      @instance
      @memberof module:Eight~Machine14
      @property {?Array} fp - current frame
      @property {number} dp - no longer used
      @property {number} id - current tag for frames (trace)
      @property {number} newId - new tag for frames (trace)
      @property {?Array} dirty - changed frame (trace)
      @property {function} toString() - [replace] interpret `dirty` frame */
  get Memory () {
    return this.#Memory ??= class extends super.Memory {
      get newId () { ++ this.#id; return this.id; }
      get id () {          // returns a letter or a sequence number
        return this.#id <= 26 ? String.fromCharCode(96 + this.#id) :
          this.#id <= 52 ? String.fromCharCode(64 + this.#id - 26) :
          String(this.#id - 52);
      }
      #id = 0;                                  // current uniqe id
      dirty = null;                        // frame to be displayed
                                                    // no frame yet
      constructor (...args) { super(...args); this.fp = null; }
      
      toString () {      // [replace] global memory and dirty frame
        const dump = slot =>
          slot === null ? 'null' :
          slot instanceof Array ?
            'id' in slot ? `${slot.id}:[]` : '[?]' :
          slot;
        let result = 'mem:[ ' + this.map(dump).join(' ') + ' ] ' +
                     `fp: ${dump(this.fp)}`;
        if (this.dirty) {
          result += ` ${this.dirty.id}:[ ` +
                      this.dirty.map(dump).join(' ') + ' ]';
          this.dirty = null;
        }
        return result;
      }
    };
  }
  #Memory;

  /** `stack: ... arguments fp old-pc  
      -> ... | frame: old-pc old-fp display result arguments locals`
      @param {number} args - size of argument values.
      @param {number} depth - number of display entries.
      @param {number} result - size of result value.
      @param {number} vars - size of local variables.
      @instance
      @memberof module:Eight~Machine14 */
  Entry (args, depth, result, vars) {
    return memory => {
      const frame = [ memory.pop(), memory.fp ];  // old-pc, old-fp
      frame.id = memory.newId;                   // label new frame
      if (depth > 1)     // push (part of) incoming display, if any
        frame.push(... memory.pop().slice(1 + 1, 1 + depth));
      else memory.pop();                               // pop frame
      frame.push(frame);                   // push new frame's base
      frame.push(... Array(result).fill(0));   // push result value
      if (args)                          // move arguments to frame
        frame.push(... memory.splice(- args, Infinity));
      if (vars)                           // create local variables
        frame.push(... Array(vars).fill(0));
      memory.dirty = memory.fp = frame;                   // new fp
    };
  }
  
  /** `stack: ... | frame: old-pc old-fp display result ...  
      -> ... result old-pc | fp: old-fp | frame unchanged`
      @param {number} depth - number of display entries.
      @param {number} result - size of result value.
      @instance
      @memberof module:Eight~Machine14 */
  Exit (depth, result) {
    return memory => {
      memory.push(                                   // push result
        ... memory.fp.slice(2 + depth, 2 + depth + result),
        memory.fp[0]);                               // push old pc
      memory.fp = memory.fp[1];               // set previous frame
    };
  }

  /** `stack: ... -> ... frame[depth][addr]`
      @instance
      @memberof module:Eight~Machine14 */
  LoadGC (addr, depth) {
    return memory => memory.push(memory.fp[1 + depth][addr]);
  }

  /** `stack: ... val -> ... val | frame[depth][addr]: val`
      @instance
      @memberof module:Eight~Machine14 */
  StoreGC (addr, depth) {
    return memory =>
      (memory.dirty = memory.fp[1 + depth])[addr] = memory.at(-1);
  }

  /** `stack: ... -> ... fp`
      @instance
      @memberof module:Eight~Machine14 */
  PushFP (memory) {
    memory.push(memory.fp);
  }
};

/** [Example 8/14](../?eg=08/14): adds actions and infrastructure to compile nested functions
    as first-order values.
    Requires {@linkcode module:Seven~Nest13 Nest13} and {@linkcode module:Eight~Global01 Global01},
    cannot be mixed with {@linkcode module:Eight~Pass08 Pass08}.
    @mixin */
const First14 = superclass => class extends superclass {
  /** Describes a variable in {@linkcode module:Eight~First14 First14}.
      @class @extends super.Var
      @instance
      @memberof module:Eight~First14
      @property {function} load() - [replace] local/global and function slots
      @property {function} store() - [replace] local/global and function slots */
  get Var () { return this.#Var ??= class extends super.Var {
      load () {               // [replace] garbage-collected frames
        const load = addr => {
          if (this.depth)                                  // local
            this.owner.machine.gen('LoadGC', addr, this.depth);
          else                                            // global
            this.owner.machine.gen('Load', addr);
        };
        load(this.addr);              // top:value or below:display
        if (this.type.isFun) load(this.addr + 1);  // + top:address
      }
            
      store () {              // [replace] garbage-collected frames
        const store = addr => {
          if (this.depth)                                  // local
            this.owner.machine.gen('StoreGC', addr, this.depth);
          else                                            // global
            this.owner.machine.gen('Store', addr);
        };
        if (this.type.isFun) {
          store(this.addr + 1);                      // top:address
          this.owner.machine.gen('Rotate', 1);
          store(this.addr);                        // below:display
          this.owner.machine.gen('Rotate', 1);
        } else store(this.addr);                       // top:value 
      }
    };
  }
  #Var;
  
  /** Describes a function in {@linkcode module:Eight~First14 First14}.
      @class @extends super.Fun
      @instance
      @memberof module:Eight~First14
      @property {number} parms - [replace] memory slots for arguments.
      @property {function} setParms() - [replace] function values take 2 slots
        similar to {@linkcode module:Eight~Pass08#Fun Pass08.Fun.setParms()}.
      @property {function} call() - [extend] `PushFP`, `Call(addr)`.
      @property {function} load() - [extend] `PushFP`, `Push(start)`.
      @property {function} store() - [replace] `StoreGC`.
      @property {function} exit() - [replace] `Entry`, `Exit`. */
  get Fun () { return this.#Fun ??= class extends super.Fun {
    
      setParms (name) {           // [replace] sets parameter types
        try {
          const type = this.owner.typeSymbols.get(name);
          if (!type) throw `${name}: not a type`;
          if (!type.isFun) throw `${name}: not a function type`;
          if (this.type && this.type != type)
            throw `${name} ${this.name}: ` +
              `previously declared as ${this.type.name}`;
          if (type.parms.length != this.locals.size)
            throw `${name} ${this.name} arguments: expects ` +
              `${type.parms.length}, receives ${this.locals.size}`;
          this.type = type;
          this.size = 2 + this.depth;      // old-pc old-fp display
          this.addr = this.size ++;                       // result
          if (this.type.returns && this.type.returns.isFun)
            ++ this.size;                        // function result
          this.parms = this.size;             // begin of arguments
          let n = 0;              // Map.forEach does not provide n
          this.locals.forEach(parm => {
            parm.addr = this.size ++;      // set parameter address
            parm.type = type.parms[n ++];     // set parameter type
            if (parm.type.isFun)
               ++ this.size;                   // function argument
          });
          this.parms = this.size - this.parms;    // argument slots
        } catch (e) {
          if (e instanceof Error) throw e;      // shouldn't happen
          this.owner.parser.error(e);            // report an error
        }
      }
    
      call () {                       // [extend] generate 'PushFP'
        this.owner.machine.gen('PushFP'); super.call();
      }
      
      load () {                       // [extend] generate 'PushFP'
        this.owner.machine.gen('PushFP'); super.load();
      }
      
      store () {              // [replace] garbage-collected frames
        const store = addr =>
          this.owner.machine.gen('StoreGC', addr, this.depth);
        if (this.type.returns && this.type.returns.isFun) {
          store(this.addr + 1);                      // top:address
          this.owner.machine.gen('Rotate', 1);
          store(this.addr);                        // below:display
          this.owner.machine.gen('Rotate', 1);
        } else store(this.addr);                       // top:value 
      }

      exit () {                    // [replace] new 'Entry', 'Exit'
        const result =
          this.type.returns && this.type.returns.isFun ? 2 : 1;
        this.owner.machine.code[this.start] =
          this.owner.machine.ins('Entry', this.parms,  // arguments
            this.depth, result,          // display, result, locals
            this.frameSize - (2 + this.depth + result + this.parms));
        this.owner.machine.gen('Exit', this.depth, result);
        const end = this.owner.machine.gen('Return');
        if (this.scope)                    // need to repair bypass
          this.owner.machine.code[this.scope.bypass] =
            this.owner.machine.ins('Branch', end);
      }    
    };
  }
  #Fun;

  constructor (parser, machine) {
    super(parser, machine ?? new (Machine14(Machine01(Seven.Machine13)))());
  }

  /** [Replace] Need `PushFP` for `main`.
      @param {Fun} main - describes `main()`.
      @instance
      @memberof module:Eight~First14 */
  _startup (main) {
    for (let p = 0; p < main.parms; ++ p)  // push arguments if any
      this.machine.gen('Push', 0);
    this.machine.gen('PushFP');               // push frame pointer
    this.machine.gen('Call', main.start);     // call main function
    if (!main.type.returns?.isFun)      // only print number result
      this.machine.gen('Print', 1);                // print and pop
  }  

  // prog: [ typedcls ] [ vars ] funs;
  // typedcls: { 'type' typedcl [{ ',' typedcl }] ';' };
  // typedcl: Name '(' [ types ] ')' [ ':' typename ];
  // types: typename [{ ',' typename }];
  // typename: Name | 'number';
  // vars: 'var' varname [{ ',' varname }] ';';
  
  /** `varname: Name [ ':' type ];` [extend] two slots for function value.
      @instance
      @memberof module:Eight~First14 */
  varname (name, type) {
    super.varname(name, type);       // create single slot variable
    if (type?.[1].isFun)             // add slot for function value
      if (this.funct) this.funct.size ++;                  // local
      else this.size ++;                                  // global
  }
  
  // type: Name | 'number';
  // funs: { fun };
  // fun: head parms [ block ] ';';
  // head: 'function' Name;
  // parms: '(' [ names ] ')' [ ':' Name ];
  // names: Name [{ ',' Name }];
  // block: begin body 'end';
  // body: [ vars ] [ funs ] stmts;
  // begin: 'begin';
  // stmts: stmt [{ ';' stmt }];
  // stmt: assign | print | return | block | loop | select;
  // assign: symbol action;
  // action: store | call;
  
  /** `store: '=' sum;` [extend] pops extra slot for function value.
      @instance
      @memberof module:Eight~First14 */
  store (_, sum) {
    super.store(_, sum);
    if (sum.isFun) this.machine.gen('Pop');
  }
  
  /** `call: { args };` pops extra slot for function value.
      @instance
      @memberof module:Eight~First14 */
  call (_) {
    if (this.context.type && this.context.type.isFun)
      this.machine.gen('Pop');
  }
  
  // args: '(' [ sums ] ')';

  /** [replace] need two slots for function argument and value.
      @param {Type[]} args - list of argument types.
      @instance
      @memberof module:Eight~First14 */
  _lift (args) {
    if (args.length)
      this.machine.gen('Rotate',
        args.reduce(
          (length, type) => length + (type.isFun ? 2 : 1), 0),
        2);
  }
  
  // print: 'print' sums;
  // sums: sum [{ ',' sum }];

  /** `return: 'return' [ sum ];` [extend] pops extra slot for function value.
      @instance
      @memberof module:Eight~First14 */
  return (_, sum) {
    if (this.funct.storeOk(sum ? sum[0] : null))
      if (sum) {
        this.funct.store();
        this.machine.gen('Pop');
        if (sum[0].isFun) this.machine.gen('Pop');
      }
    this.funct.return();
  }
  
  // loop: While cmp Do body 'od';
  // While: 'while';
  // Do: 'do';
  // select: 'if' cmp then [ else ] 'fi';
  // then: Then [ body ];
  // else: Else body;
  // Then: 'then';
  // Else: 'else';
  // cmp: sum rel;
  // rel: eq | ne | gt | ge | lt | le;
  // eq: '=' sum;
  // ne: '<>' sum;
  // gt: '>' sum;
  // ge: '>=' sum;
  // lt: '<' sum;
  // le: '<=' sum;
  // sum: product [{ add | subtract }];
  // add: '+' product;
  // subtract: '-' product;
  // product: signed [{ multiply | divide }];
  // multiply: '*' signed;
  // divide: '/' signed;
  // signed: [ '-' ] term;
  // term: input | number | name | '(' sum ')';
  // input: 'input' [ Number ];
  // number: Number;
  // name: symbol [{ args }];
  // symbol: Name;
};

export {
  Machine01,
  Global01,
  Machine08,
  Pass08,
  Machine14,
  First14
};