Source: engine.mjs

/**
 * @file Slawn の評価エンジンです
 * @author kokokou126 <kokokou126@gmail.com>
 */

// @ts-check

/**
 * Slawn オペレータ
 * @typedef {Object} Operator
 * @property {number} arg 引数の数
 * @property {Function} body オペレータの処理部分
 */

/**
 * Slawn 式
 * @typedef {Array<string|number>} Expression
 */

// Constants
// @ts-ignore
import nullOperator from './operators/nullOperator.mjs'
// @ts-ignore
import undefinedOperator from './operators/undefinedOperator.mjs'
// @ts-ignore
import NaNOperator from './operators/NaNOperator.mjs'
// @ts-ignore
import InfinityOperator from './operators/InfinityOperator.mjs'
// @ts-ignore
import ArrayOperator from './operators/ArrayOperator.mjs'
// @ts-ignore
import ObjectOperator from './operators/ObjectOperator.mjs'

// Boolean
// @ts-ignore
import trueOperator from './operators/trueOperator.mjs'
// @ts-ignore
import falseOperator from './operators/falseOperator.mjs'

/**
 * Slawn エンジン
 */
class Engine {
  /**
   * オペレータを定義します
   * @param {Object} operators
   */
  constructor (operators) {
    /**
     * self is this
     * @type {Object}
     */
    const self = this

    /**
     * 変数のための名前空間
     * @type {Map}
     */
    this.namespace = new Map()

    /**
     * 代入オペレータ
     * @operator
     * @type {Operator}
     */
    const setOperator = {
      arg: 2,
      /**
       * 名前空間にキーを指定して値を代入します
       * @param {any} p1 - 代入する値
       * @param {string} p2 - キー名
       * @returns {null}
       */
      body: (p1, p2) => {
        self.namespace.set(p2, p1)
        return null
      }
    }

    /**
     * 展開オペレータ
     * @operator
     * @type {Operator}
     */
    const getOperator = {
      arg: 1,
      /**
       * 名前空間から指定したキーの値を返します
       * @param {string} p1 - キー名
       * @returns {any}
       */
      body: p1 => self.namespace.get(p1)
    }

    /**
     * 削除オペレータ
     * @operator
     * @type {Operator}
     */
    const deleteOperator = {
      arg: 1,
      /**
       * 名前空間からキーと値のペアを削除します
       * @param {string} p1 - キー名
       * @returns {null}
       */
      body: p1 => {
        self.namespace.delete(p1)
        return null
      }
    }

    /**
     * 加算代入オペレータ
     * @operator
     * @type {Operator}
     */
    const addAssignOperator = {
      arg: 2,
      /**
       * 名前空間から指定したキーの値に値を加え、
       * その結果を代入します
       * @param {any} p1 - 加える値
       * @param {string} p2 - キー名
       * @returns {null}
       */
      body: (p1, p2) => {
        self.namespace.set(p2, self.namespace.get(p2) + p1)
        return null
      }
    }

    /**
     * 減算代入オペレータ
     * @operator
     * @type {Operator}
     */
    const subtractAssignOperator = {
      arg: 2,
      /**
       * 名前空間から指定したキーの値の数値を減らし、
       * その結果を代入します
       * @param {number} p1 - 減らす値
       * @param {string} p2 - キー名
       * @returns {null}
       */
      body: (p1, p2) => {
        self.namespace.set(p2, self.namespace.get(p2) - p1)
        return null
      }
    }

    /**
     * 乗算代入オペレータ
     * @operator
     * @type {Operator}
     */
    const multiplyAssignOperator = {
      arg: 2,
      /**
       * 名前空間から指定したキーの値の数値で乗算を行い、
       * その結果を代入します
       * @param {number} p1 - 掛ける値
       * @param {string} p2 - キー名
       * @returns {null}
       */
      body: (p1, p2) => {
        self.namespace.set(p2, self.namespace.get(p2) * p1)
        return null
      }
    }

    /**
     * 除算代入オペレータ
     * @operator
     * @type {Operator}
     */
    const divideAssignOperator = {
      arg: 2,
      /**
       * 名前空間から指定したキーの値の数値で除算を行い、
       * その結果を代入します
       * @param {number} p1 - 割る値
       * @param {string} p2 - キー名
       * @returns {null}
       */
      body: (p1, p2) => {
        self.namespace.set(p2, self.namespace.get(p2) / p1)
        return null
      }
    }

    /**
     * 剰余代入オペレータ
     * @operator
     * @type {Operator}
     */
    const modAssignOperator = {
      arg: 2,
      /**
       * 名前空間から指定したキーの値の数値で除算を行い、
       * 剰余を代入します
       * @param {number} p1 - 割る値
       * @param {string} p2 - キー名
       * @returns {null}
       */
      body: (p1, p2) => {
        self.namespace.set(p2, self.namespace.get(p2) % p1)
        return null
      }
    }

    /**
     * べき乗代入オペレータ
     * @operator
     * @type {Operator}
     */
    const powerAssignOperator = {
      arg: 2,
      /**
       * 名前空間から指定したキーの値の数値でべき乗を行い、
       * その結果を代入します
       * @param {number} p1 - 掛ける値
       * @param {string} p2 - キー名
       * @returns {null}
       */
      body: (p1, p2) => {
        self.namespace.set(p2, self.namespace.get(p2) ** p1)
        return null
      }
    }

    /**
     * 評価オペレータ
     * @operator
     * @type {Operator}
     */
    const evalOperator = {
      arg: 1,
      /**
       * 渡された Slawn 式を評価します
       * @param {Expression} p1 - Slawn 式
       * @returns {null}
       */
      body: p1 => {
        this.eval(p1)
        return null
      }
    }

    /**
     * if オペレータ
     * @operator
     * @type {Operator}
     */
    const ifOperator = {
      arg: 3,
      /**
       * 条件式が true の場合、then 節を評価します。
       * さもなくば、else 節を評価します。
       * @param {Expression} p1 - else 節
       * @param {Expression} p2 - then 節
       * @param {boolean} p3 - 条件式
       * @returns {null}
       */
      body: (p1, p2, p3) => {
        if (p3) {
          this.eval(p2)
        } else {
          this.eval(p1)
        }
        return null
      }
    }

    /**
     * unless オペレータ
     * @operator
     * @type {Operator}
     */
    const unlessOperator = {
      arg: 3,
      /**
       * 条件式が false の場合、then 節を評価します。
       * さもなくば、else 節を評価します。
       * @param {Expression} p1 - else 節
       * @param {Expression} p2 - then 節
       * @param {boolean} p3 - 条件式
       * @returns {null}
       */
      body: (p1, p2, p3) => {
        if (!p3) {
          this.eval(p2)
        } else {
          this.eval(p1)
        }
        return null
      }
    }

    /**
     * each オペレータ
     * @operator
     * @type {Operator}
     */
    const eachOperator = {
      arg: 3,
      /**
       * 配列に対して反復処理を行います。
       * 指定した変数に配列の値を代入します。
       * @param {Expression} p1 - Slawn 式
       * @param {Array} p2 - 繰り返す配列
       * @param {string} p3 - 変数名
       * @return {null}
       */
      body: (p1, p2, p3) => {
        for (let i = 0; i < p2.length; i++) {
          self.namespace.set(p3, p2[i])
          this.eval(p1)
        }
        self.namespace.delete(p3)
        return null
      }
    }

    /**
     * while オペレータ
     * @operator
     * @type {Operator}
     */
    const whileOperator = {
      arg: 2,
      /**
       * 条件式が true の間、Slawn 式を評価し続けます
       * @param {Expression} p1 - 評価し続ける Slawn 式
       * @param {Expression} p2 - 条件式
       */
      body: (p1, p2) => {
        while (1) {
          this.eval(p2)
          if (!this.stack[this.stack.length - 1]) {
            this.stack.pop()
            break
          }
          this.eval(p1)
        }
        return null
      }
    }

    /**
     * until オペレータ
     * @operator
     * @type {Operator}
     */
    const untilOperator = {
      arg: 2,
      /**
       * 条件式が false の間、Slawn 式を評価し続けます
       * @param {Expression} p1 - 評価し続ける Slawn 式
       * @param {Expression} p2 - 条件式
       */
      body: (p1, p2) => {
        while (1) {
          this.eval(p2)
          if (this.stack[this.stack.length - 1]) {
            this.stack.pop()
            break
          }
          this.eval(p1)
        }
        return null
      }
    }

    /**
     * times オペレータ
     * @operator
     * @type {Operator}
     */
    const timesOperator = {
      arg: 2,
      /**
       * 指定した回数、Slawn 式を評価します
       * @param {Expression} p1 - Slawn 式
       * @param {number} p2 - 繰り返す回数
       */
      body: (p1, p2) => {
        Array(p2).fill(0).forEach(() => {
          this.eval(p1)
        })
        return null
      }
    }

    /**
     * push オペレータ
     * @operator
     * @type {Operator}
     */
    const pushOperator = {
      arg: 2,
      /**
       * 配列を指定し、指定した値を末尾に追加します
       * @param {any} p1 - 追加する値
       * @param {string} p2 - 配列名
       */
      body: (p1, p2) => {
        self.namespace.set(p2, [...self.namespace.get(p2), p1])
        return null
      }
    }

    /**
     * オペレータを定義
     * @type {Object}
     */
    this.operators = {
      $true: trueOperator,
      $false: falseOperator,
      $null: nullOperator,
      $undefined: undefinedOperator,
      $NaN: NaNOperator,
      $Infinity: InfinityOperator,
      '[]': ArrayOperator,
      '{}': ObjectOperator,
      '=': setOperator,
      '+=': addAssignOperator,
      '-=': subtractAssignOperator,
      '*=': multiplyAssignOperator,
      '/=': divideAssignOperator,
      '%=': modAssignOperator,
      '**=': powerAssignOperator,
      $: getOperator,
      '!delete': deleteOperator,
      '!eval': evalOperator,
      '!if': ifOperator,
      '!unless': unlessOperator,
      '!each': eachOperator,
      '!while': whileOperator,
      '!until': untilOperator,
      '!times': timesOperator,
      '!push': pushOperator,
      ...operators
    }

    /**
     * Slawn エンジンのスタック
     * @type {Array}
     */
    this.stack = []

    // 名前空間からスタックを参照できるようにする
    this.namespace.set('stack', this.stack)
  }

  /**
   * 名前空間にキーを指定して値を代入します
   * @param {string} key
   * @param {any} value
   * @returns {void}
   */
  setVariable (key, value) {
    this.namespace.set(key, value)
  }

  /**
   * 名前空間から指定したキーから値を取得し返します
   * @param {string} key
   * @return {any}
   */
  getVariable (key) {
    return this.namespace.get(key)
  }

  /**
   * Slawn 式を評価します
   * @param {Expression} expression - Slawn 式
   * @returns {Object} - スタックを返します
   */
  eval (expression) {
    expression.forEach(v => {
      if (v === '$null') {
        this.stack.push(null)
      } else if (this.operators[v]) {
        const args = []
        Array(this.operators[v].arg).fill(0).forEach(x => {
          args.push(this.stack.pop())
        })
        const result = this.operators[v].body(...args)
        if (result === null) {
          return
        } else {
          this.stack.push(result)
        }
      } else {
        this.stack.push(v)
      }

      return this.stack
    })
  }
}

export default Engine