// deno-lint-ignore-file no-explicit-any /** * nadesiko v3 */ // types import { Token, FuncList, FuncListItem, FuncArgs, NakoEvent, CompilerOptions, NakoComEventName, NakoDebugOption, ExportMap } from './nako_types.mjs' import { Ast, AstBlocks } from './nako_ast.mjs' // parser / lexer import { NakoParser } from './nako_parser3.mjs' import { NakoLexer } from './nako_lexer.mjs' import { NakoGen, generateJS, NakoGenOptions, NakoGenResult } from './nako_gen.mjs' import { NakoSyntaxError } from './nako_errors.mjs' import { NakoLogger } from './nako_logger.mjs' import { NakoGlobal } from './nako_global.mjs' // 取り込み文(require)の処理 / プラグイン管理 (#2360) import { Dependencies, LoaderTool, listRequireStatements, NakoRequireHost, NakoRequireLoader, NakoRequireScanner } from './nako_require.mjs' import { NakoPluginHost, NakoPluginManager } from './nako_plugin_manager.mjs' // 字句解析 / 実行 / イベント / 基本プラグイン (#2360) import { LexResult, NakoTokenizer } from './nako_tokenizer.mjs' import { NakoRunner, NakoRunnerHost, newCompilerOptions } from './nako_runner.mjs' import { NakoEventEmitter } from './nako_event.mjs' import { registerBasicPlugins } from './nako_basic_plugins.mjs' // version info import coreVersion from './nako_core_version.mjs' export interface NakoCompilerOption { useBasicPlugin: boolean; } // 分離したモジュールの型・関数は、外部から `nako3.mjs` 経由で参照されているため再エクスポートする (#2360) export type { LoaderTool, LoaderToolTask, Dependencies, DependenciesItem } from './nako_require.mjs' export { newCompilerOptions } from './nako_runner.mjs' type NakoVars = Map export interface NakoResetOption { needToClearPlugin: boolean } /** なでしこコンパイラ */ export class NakoCompiler { private nakoFuncList: FuncList private funclist: FuncList private moduleExport: ExportMap private parser: NakoParser /** プラグイン管理 (#2360) */ private pluginManager: NakoPluginManager /** 取り込み文の処理 (#2360) */ private requireLoader: NakoRequireLoader /** 字句解析 (#2360) */ private tokenizer: NakoTokenizer /** プログラムの実行 (#2360) */ private runner: NakoRunner /** イベント管理 (#2360) */ protected events: NakoEventEmitter private usedFuncs: Set private codeGenerateor: {[key: string]: any} protected logger: NakoLogger // global objects __varslist: NakoVars[] __locals: NakoVars __self: NakoCompiler __vars: NakoVars __v0: NakoVars __v1: NakoVars __module: Record> numFailures: number // エラーレポートの数を記録 public version: string public coreVersion: string public filename: string public debugOption: NakoDebugOption public reservedWords: string[] public josiList: string[] /** * @param {undefined | {'useBasicPlugin':true|false}} options */ constructor(options: NakoCompilerOption | undefined = undefined) { if (options === undefined) { options = { useBasicPlugin: true } } // 環境のリセット this.__varslist = [this.newVaiables(), this.newVaiables(), this.newVaiables()] // このオブジェクトは変更しないこと (this.gen.__varslist と共有する) this.__locals = this.newVaiables() // ローカル変数 this.__self = this this.__vars = this.__varslist[2] // alias of __varslist[2] this.__v1 = this.__varslist[1] // alias of __varslist[1] this.__v0 = this.__varslist[0] // alias of __varslist[0] // バージョンを設定 this.version = coreVersion.version this.coreVersion = coreVersion.version this.funclist = this.newVaiables() // プラグインで定義された関数 + ユーザーが定義した関数 this.moduleExport = this.newVaiables() this.nakoFuncList = this.newVaiables() // __v1に配置するJavaScriptのコードで定義された関数 this.codeGenerateor = {} // コードジェネレータ this.debugOption = { useDebug: false, waitTime: 0 } this.logger = new NakoLogger() this.filename = 'main.nako3' // 実行前に環境を変更するためのイベント (#2360) this.events = new NakoEventEmitter() // プラグイン管理を生成する (#2360) const pluginHost: NakoPluginHost = { getFuncList: () => this.funclist, // reset() で差し替わるため、その都度参照する getSysVars: () => this.__varslist[0], getLogger: () => this.logger } this.pluginManager = new NakoPluginManager(pluginHost) // requireなどで取り込んだモジュールの一覧(プラグイン管理と同じオブジェクトを共有する) this.__module = this.pluginManager.modules // 取り込み文の処理を生成する (#2360) const requireHost: NakoRequireHost = { rawtokenize: (code, line, filename, preCode) => this.rawtokenize(code, line, filename, preCode), addPluginFromFile: (fpath, po, persistent) => this.addPluginFromFile(fpath, po, persistent), getLogger: () => this.logger, // 名前空間(modList)を汚さないように、取り込み文の検出には別のコンパイラを使う createScanner: (): NakoRequireScanner => new NakoCompiler({ useBasicPlugin: true }), countFailure: () => { this.numFailures++ } } this.requireLoader = new NakoRequireLoader(requireHost) // 実行部を生成する (#2360) const runnerHost: NakoRunnerHost = { compileFromCode: (code, filename, opts) => this.compileFromCode(code, filename, opts), createNakoGlobal: (gen: NakoGen, guid: number) => new NakoGlobal(this, gen, guid), getLogger: () => this.logger, fireEvent: (eventName, event) => { this.events.fire(eventName, event) } } this.runner = new NakoRunner(runnerHost) this.usedFuncs = new Set() this.numFailures = 0 if (options.useBasicPlugin) { this.addBasicPlugins() } // 必要なオブジェクトを覚えておく this.parser = new NakoParser(this.logger) // 字句解析を生成する (#2360) this.tokenizer = new NakoTokenizer({ getLogger: () => this.logger, replaceRequireStatements: (tokens, includeGuard) => this.replaceRequireStatements(tokens, includeGuard), removeRequireStatements: (tokens) => this.removeRequireStatements(tokens) }) // 関数一覧を設定 this.lexer.setFuncList(this.funclist) this.lexer.setModuleExport(this.moduleExport) // link for plysin_system::予約語一覧取得/助詞一覧取得 this.reservedWords = JSON.parse(JSON.stringify(this.lexer.reservedWords)) // 外部公開用のデータなので複製して保持する this.josiList = JSON.parse(JSON.stringify(this.lexer.josiList)) } /** 字句解析器(実体は NakoTokenizer が所有する) */ private get lexer (): NakoLexer { return this.tokenizer.lexer } /** 実行前に環境を変更するためのイベント一覧 */ protected get eventList (): NakoEvent[] { return this.events.getEventList() } /** 生成した NakoGlobal のインスタンスを保持 */ get __globals (): NakoGlobal[] { return this.runner.globals } set __globals (values: NakoGlobal[]) { this.runner.globals = values } /** 現在のNakoGlobalオブジェクト */ get __globalObj (): NakoGlobal|null { return this.runner.currentGlobal } set __globalObj (value: NakoGlobal|null) { this.runner.currentGlobal = value } /** モジュール(名前空間)の一覧を取得する */ getModList(): string[] { return this.tokenizer.getModList() } getLogger(): NakoLogger { return this.logger } getNakoFuncList(): FuncList { return this.nakoFuncList } getNakoFunc(name: string): FuncListItem|undefined { return this.nakoFuncList.get(name) } getPluginfiles(): Record { return this.pluginManager.pluginfiles } /** * 取り込み文を置換するためのオブジェクト。 * 正規化されたファイル名がキーになり、取り込み文の引数に指定された正規化されていないファイル名はaliasに入れられる。 * JavaScriptファイルによるプラグインの場合、contentは空文字列。 * funclistはシンタックスハイライトの高速化のために事前に取り出した、ファイルが定義する関数名のリスト。 * (エディタのシンタックスハイライトから参照される) */ get dependencies (): Dependencies { return this.requireLoader.dependencies } /** * 基本的なプラグインを追加する * (プラグインの一覧は nako_basic_plugins.mts にある) */ addBasicPlugins() { registerBasicPlugins(this) } /** * loggerを新しいインスタンスで置き換える。 */ replaceLogger() { const logger = this.lexer.logger = this.parser.logger = this.logger = new NakoLogger() return logger } /** * ファイル内のrequire文の位置を列挙する。出力の配列はstartでソートされている。 * (実体は nako_require.mts の listRequireStatements) * @param {Token[]} tokens rawtokenizeの出力 */ static listRequireStatements (tokens: Token[]): Token[] { return listRequireStatements(tokens) } /** * プログラムが依存するファイルを再帰的に取得する。 * (実体は nako_require.mts の NakoRequireLoader.load) * * @param {string} code * @param {string} filename * @param {string} preCode * @param {LoaderTool} tools 実行環境 (ブラウザ or Node.js) によって外部ファイルの取得・実行方法は異なるため、引数でそれらを行う関数を受け取る。 * @returns {Promise | void} * @protected */ _loadDependencies (code: string, filename: string, preCode: string, tools: LoaderTool): Promise|void { return this.requireLoader.load(code, filename, preCode, tools) } /** * コードを単語に分割する * (実体は nako_tokenizer.mts の NakoTokenizer.rawtokenize) * @param code なでしこのプログラム * @param line なでしこのプログラムの行番号 * @param filename * @param preCode * @returns トークンのリスト */ rawtokenize(code: string, line: number, filename: string, preCode = ''): Token[] { return this.tokenizer.rawtokenize(code, line, filename, preCode) } /** * 単語の属性を構文解析に先立ち補正する * (実体は nako_tokenizer.mts の NakoTokenizer.converttoken) * @param {Token[]} tokens トークンのリスト * @param {boolean} isFirst 最初の呼び出しかどうか * @param {string} filename * @returns コード (なでしこ) */ converttoken(tokens: Token[], isFirst: boolean, filename: string): Token[] { return this.tokenizer.converttoken(tokens, isFirst, filename) } /** * 環境のリセット * {NakoResetOption|undefined} */ reset(options: NakoResetOption|undefined = undefined) { if (!options || options.needToClearPlugin) { // (メモ) #1245 // 通常、リセット処理では、プラグインの!クリアを呼ぶ。 // しかし、エディタではクリアイベントを呼ぶと、時計などのコンテンツが止まってしまう // そのため、例外的にオプションを指定すると、プラグインのクリアイベントを呼ばない try { this.clearPlugins() } catch { // pass } } /** * なでしこのローカル変数をスタックで管理 * __varslist[0] プラグイン領域 * __varslist[1] なでしこグローバル領域 * __varslist[2] 最初のローカル変数 ( == __vars } */ this.__varslist = [this.__varslist[0], this.newVaiables(), this.newVaiables()] this.__v0 = this.__varslist[0] // alias of __varslist[0] this.__v1 = this.__varslist[1] // alias of __varslist[1] this.__vars = this.__varslist[2] // alias of __varslist[2] this.__locals = this.newVaiables() // プラグイン命令以外を削除する。 this.funclist = this.pluginManager.createFuncListFromPlugins(this.__v0) this.lexer.setFuncList(this.funclist) this.moduleExport = new Map() this.lexer.setModuleExport(this.moduleExport) this.logger.clear() } /** * typeがcodeのトークンを単語に分割するための処理 * (実体は nako_tokenizer.mts の NakoTokenizer.lexCodeToken) * @param {string} code * @param {number} line * @param {string} filename * @param {number | null} startOffset * @returns * @private */ lexCodeToken(code: string, line: number, filename: string, startOffset: number|null): {commentTokens: Token[], tokens: Token[]} { return this.tokenizer.lexCodeToken(code, line, filename, startOffset) } /** * 再帰的にrequire文を置換する。 * .jsであれば削除し、.nako3であればそのファイルのトークン列で置換する。 * (実体は nako_require.mts の NakoRequireLoader.replaceRequireStatements) * @param {Token[]} tokens * @param {Set} [includeGuard] * @returns {Token[]} 削除された取り込み文のトークン */ replaceRequireStatements (tokens: Token[], includeGuard: Set = new Set()): Token[] { return this.requireLoader.replaceRequireStatements(tokens, includeGuard) } /** * replaceRequireStatementsのシンタックスハイライト用の実装。 * (実体は nako_require.mts の NakoRequireLoader.removeRequireStatements) * @param {Token[]} tokens * @returns {Token[]} 削除された取り込み文のトークン */ removeRequireStatements (tokens: Token[]): Token[] { return this.requireLoader.removeRequireStatements(tokens) } /** * 字句解析を行う * (実体は nako_tokenizer.mts の NakoTokenizer.lex) */ lex(code: string, filename = 'main.nako3', preCode = '', syntaxHighlighting = false): LexResult { return this.tokenizer.lex(code, filename, preCode, syntaxHighlighting) } /** * コードをパースしてASTにする */ parse(code: string, filename: string, preCode = ''): Ast { // 関数リストを字句解析と構文解析に登録 this.lexer.setFuncList(this.funclist) this.parser.setFuncList(this.funclist) // 関数リストを字句解析と構文解析に登録 this.lexer.setModuleExport(this.moduleExport) this.parser.setModuleExport(this.moduleExport) // 字句解析を行う const lexerOutput = this.lex(code, filename, preCode) // 構文木を作成 let ast: Ast try { this.parser.genMode = 'sync' // set default ast = this.parser.parse(lexerOutput.tokens, filename) } catch (err: any) { if (typeof err.startOffset !== 'number') { throw NakoSyntaxError.fromNode(err.message, lexerOutput.tokens[this.parser.getIndex()]) } throw err } // 使用したシステム関数の一覧を this.usedFuns に入れる(エディタなどで利用される) this.usedFuncs = this.parser.usedFuncs // 全ての関数呼び出し this.deleteUnNakoFuncs() // システム関数以外を削除 this.logger.trace('--- ast ---\n' + JSON.stringify(ast, null, 2)) return ast } getUsedFuncs(ast: Ast): Set { this.usedFuncs = new Set() this._getUsedFuncs(ast) return this.deleteUnNakoFuncs() } _getUsedFuncs(ast: Ast): void { if (!ast) { return } if ((ast.type === 'func' || ast.type === 'func_pointer') && ast.name) { this.usedFuncs.add(ast.name as string) } else if ((ast as AstBlocks).blocks) { // プロパティにblocksを含んでいる? for (const a of (ast as AstBlocks).blocks) { this._getUsedFuncs(a) } } } deleteUnNakoFuncs(): Set { for (const func of this.usedFuncs) { if (!this.pluginManager.hasCommand(func)) { this.usedFuncs.delete(func) } } return this.usedFuncs } /** * プログラムをコンパイルしてランタイム用のJavaScriptのコードを返す * @param code コード (なでしこ) * @param filename * @param isTest テストかどうか * @param preCode */ compile(code: string, filename: string, isTest = false, preCode = ''): string { const opt = newCompilerOptions() opt.testOnly = isTest opt.preCode = preCode const res = this.compileFromCode(code, filename, opt) return res.runtimeEnv } /** parse & generate */ compileFromCode(code: string, filename: string, options: CompilerOptions|undefined = undefined): NakoGenResult { if (filename === '') { filename = 'main.nako3' } if (options === undefined) { options = newCompilerOptions() } try { if (options.resetEnv) { this.reset() } if (options.resetAll) { this.clearPlugins() } // onBeforeParse this.events.fire('beforeParse', code) // parse const ast = this.parse(code, filename, options.preCode) // onBeforeGenerate this.events.fire('beforeGenerate', ast) // generate const genOptions = new NakoGenOptions(options.testOnly) const outCode = this.generateCode(ast, genOptions) // onAfterGenerate this.events.fire('afterGenerate', outCode) return outCode } catch (e: any) { this.logger.error(e) throw e } } /** * プログラムをコンパイルしてJavaScriptのコードオブジェクトを返す * @param ast * @param opt テストかどうか * @param mode 一般的に 'sync' を指定 */ generateCode(ast: Ast, opt: NakoGenOptions, mode = 'sync'): NakoGenResult { // Select Code Generator #637 // normal mode if (mode === 'sync') { return generateJS(this, ast, opt) } // 廃止の非同期モード #1164 if (mode === '非同期モード') { this.logger.error('『!非同期モード』は廃止されました。[詳細](https://github.com/kujirahand/nadesiko3/issues/1164)') } // その他のコードジェネレータ(PHPなど) const genObj: any|undefined = this.codeGenerateor[mode] if (!genObj) { throw new Error(`コードジェネレータの「${mode}」はサポートされていません。`) } return genObj.generate(this, ast, opt.isTest) } /** コードジェネレータを追加する */ addCodeGenerator(mode: string, obj: any) { this.codeGenerateor[mode] = obj } /** (非推奨) * @param code 非同期で実行する * @param fname * @param isReset * @param isTest テストかどうか。stringの場合は1つのテストのみ。 * @param [preCode] * @deprecated 代わりにrunAsyncメソッドを使ってください。(core #52) */ async _run(code: string, fname: string, isReset: boolean, isTest: boolean, preCode = ''): Promise { return this.runner.run(code, fname, isReset, isTest, preCode) } /** * 各プラグインをリセットする * (実体は nako_runner.mts の NakoRunner.clearPlugins) */ clearPlugins() { this.runner.clearPlugins() } /** * 環境を指定してJavaScriptのコードを実行する * (実体は nako_runner.mts の NakoRunner.evalJS) * @param code JavaScriptのコード * @param nakoGlobal 実行環境 */ private evalJS(code: string, nakoGlobal: NakoGlobal): void { this.runner.evalJS(code, nakoGlobal) } /** * (非推奨) 同期的になでしこのプログラムcodeを実行する * (実体は nako_runner.mts の NakoRunner.runSync) * @param code なでしこのプログラム * @param filename ファイル名 * @param options オプション * @returns 実行に利用したグローバルオブジェクト * @deprecated 代わりにrunAsyncメソッドを使ってください。(core #52) */ public runSync(code: string, filename: string, options: CompilerOptions|undefined = undefined): NakoGlobal { return this.runner.runSync(code, filename, options) } /** * 非同期になでしこのプログラムcodeを実行する * (実体は nako_runner.mts の NakoRunner.runAsync) * @param code なでしこのプログラム * @param filename ファイル名 * @param options オプション * @returns 実行に利用したグローバルオブジェクト */ public async runAsync(code: string, filename: string, options: CompilerOptions|undefined = undefined): Promise { return this.runner.runAsync(code, filename, options) } private getNakoGlobal(options: CompilerOptions, gen: NakoGen, filename: string): NakoGlobal { return this.runner.getNakoGlobal(options, gen, filename) } /** * イベントを登録する * @param eventName イベント名 * @param callback コールバック関数 */ addListener(eventName: NakoComEventName, callback: (event:any) => void) { this.events.on(eventName, callback) } /** * テストを実行する * @param code * @param fname * @param preCode * @param testOnly */ test(code: string, fname: string, preCode = '', testOnly = false) { return this.runner.test(code, fname, preCode, testOnly) } /** * なでしこのプログラムを実行(他に実行しているインスタンスはそのまま) * @param code * @param fname * @param [preCode] * @deprecated 代わりに runAsync を使ってください。 */ run(code: string, fname = 'main.nako3', preCode = ''): NakoGlobal { const options = newCompilerOptions() options.preCode = preCode return this.runSync(code, fname, options) } /** * JavaScriptのみで動くコードを取得する場合 * @param code * @param filename * @param opt? オプション */ compileStandalone(code: string, filename: string, options?: NakoGenOptions): string { if (options === undefined) { options = new NakoGenOptions() } const ast = this.parse(code, filename) return this.generateCode(ast, options).standalone } /** * プラグイン・オブジェクトを追加 * (実体は nako_plugin_manager.mts の NakoPluginManager.addPlugin) * @param po プラグイン・オブジェクト * @param persistent falseのとき、次以降の実行では使えない * @param fpath ファイルパス */ addPlugin (po: {[key: string]: any}, persistent = true, fpath = ''): void { this.pluginManager.addPlugin(po, persistent, fpath) } /** * プラグイン・オブジェクトを追加(ブラウザ向け) * @param objName オブジェクト名 (今後プラグイン名は、meta.value.pluginNameに指定する) * @param po 関数リスト * @param persistent falseのとき、次以降の実行では使えない */ addPluginObject (objName: string, po: {[key: string]: any}, persistent = true): void { this.pluginManager.addPluginObject(objName, po, persistent) } /** * プラグイン・ファイルを追加(Node.js向け) * @param objName オブジェクト名(ただし、v3.6.3以降のバージョンでは無効になる) * @param fpath ファイルパス * @param po 登録するオブジェクト * @param persistent falseのとき、次以降の実行では使えない * @deprecated 利用は非推奨 */ addPluginFile (_objName: string, fpath: string, po: {[key: string]: any}, persistent = true): void { this.addPluginFromFile(fpath, po, persistent) } /** * プラグイン・ファイルを追加(Node.js向け) * @param fpath ファイルパス * @param po 登録するオブジェクト * @param persistent falseのとき、次以降の実行では使えない */ addPluginFromFile (fpath: string, po: { [key: string]: any }, persistent = true): void { this.pluginManager.addPluginFromFile(fpath, po, persistent) } /** * 関数を追加する * @param {string} key 関数名 * @param {string[][]} josi 助詞 * @param {Function} fn 関数 * @param {boolean} returnNone 値を返す関数の場合はfalseを指定 * @param {boolean} asyncFn Promiseを返す関数かを指定 */ addFunc (key: string, josi: FuncArgs, fn: any, returnNone = true, asyncFn = false): void { this.pluginManager.addFunc(key, josi, fn, returnNone, asyncFn) } /** (非推奨) 互換性のため ... 関数を追加する * @deprecated 代わりにaddFuncを使ってください */ public setFunc (key: string, josi: FuncArgs, fn: any, returnNone = true, asyncFn = false): void { this.addFunc(key, josi, fn, returnNone, asyncFn) } /** * プラグイン関数を参照する * @param key プラグイン関数の関数名 * @returns プラグイン・オブジェクト */ getFunc (key: string): FuncListItem|undefined { return this.pluginManager.getFunc(key) } /** 同期的になでしこのプログラムcodeを実行する * (実体は nako_runner.mts の NakoRunner.runEx) * @deprecated 代わりにrunAsyncメソッドを使ってください。(core #52) */ private _runEx(code: string, filename: string, opts: CompilerOptions, preCode = '', nakoGlobal: NakoGlobal|undefined = undefined): NakoGlobal { return this.runner.runEx(code, filename, opts, preCode, nakoGlobal) } /** (非推奨) 同期的になでしこのプログラムcodeを実行する * @param code * @param fname * @param opts * @param [preCode] * @deprecated 代わりにrunAsyncメソッドを使ってください。(core #52) */ public runEx(code: string, fname: string, opts: CompilerOptions, preCode = '') { return this._runEx(code, fname, opts, preCode) } /** * (非推奨) なでしこのプログラムを実行(他に実行しているインスタンスもリセットする) * @param code * @param fname * @param [preCode] */ async runReset(code: string, fname = 'main.nako3', preCode = ''): Promise { return this.runner.runReset(code, fname, preCode) } /** * 新規のなでしこ変数管理オブジェクトを生成 * @returns 変数管理オブジェクト */ newVaiables(initVars?: Map): Map { return new Map(initVars) } }