nadesiko3
Version:
Japanese Programming Language
1,011 lines (1,010 loc) • 46.3 kB
JavaScript
// parser / lexer
import { NakoParser } from './nako_parser3.mjs';
import { NakoLexer } from './nako_lexer.mjs';
import { NakoPrepare } from './nako_prepare.mjs';
import { generateJS, NakoGenOptions } from './nako_gen.mjs';
import { convertInlineIndent, convertIndentSyntax } from './nako_indent_inline.mjs';
import { convertDNCL } from './nako_from_dncl.mjs';
import { convertDNCL2 } from './nako_from_dncl2.mjs';
import { SourceMappingOfTokenization, SourceMappingOfIndentSyntax, OffsetToLineColumn, subtractSourceMapByPreCodeLength } from './nako_source_mapping.mjs';
import { NakoLexerError, NakoImportError, NakoSyntaxError, InternalLexerError } from './nako_errors.mjs';
import { NakoLogger } from './nako_logger.mjs';
import { NakoGlobal } from './nako_global.mjs';
// version info
import coreVersion from './nako_core_version.mjs';
// basic plugins
import PluginSystem from './plugin_system.mjs';
import PluginMath from './plugin_math.mjs';
import PluginCSV from './plugin_csv.mjs';
import PluginPromise from './plugin_promise.mjs';
import PluginTest from './plugin_test.mjs';
const cloneAsJSON = (x) => JSON.parse(JSON.stringify(x));
const PLUGIN_MIN_VERSION_INT = 600; // = minor * 100 + patch
/** コンパイラ実行オプションを生成 */
export function newCompilerOptions(initObj = {}) {
if (typeof initObj !== 'object') {
initObj = {};
}
initObj.testOnly = initObj.testOnly || false;
initObj.resetEnv = initObj.resetEnv || false;
initObj.resetAll = initObj.resetAll || false;
initObj.preCode = initObj.preCode || '';
initObj.nakoGlobal = initObj.nakoGlobal || null;
return initObj;
}
/** なでしこコンパイラ */
export class NakoCompiler {
/**
* @param {undefined | {'useBasicPlugin':true|false}} options
*/
constructor(options = 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.__globals = []; // 生成した NakoGlobal のインスタンスを保持
this.__globalObj = null;
this.__module = {}; // requireなどで取り込んだモジュールの一覧
this.pluginFunclist = {}; // プラグインで定義された関数
this.funclist = this.newVaiables(); // プラグインで定義された関数 + ユーザーが定義した関数
this.moduleExport = this.newVaiables();
this.pluginfiles = {}; // 取り込んだファイル一覧
this.commandlist = new Set(); // プラグインで定義された定数・変数・関数の名前
this.nakoFuncList = this.newVaiables(); // __v1に配置するJavaScriptのコードで定義された関数
this.eventList = []; // 実行前に環境を変更するためのイベント
this.codeGenerateor = {}; // コードジェネレータ
this.debugOption = { useDebug: false, waitTime: 0 };
this.logger = new NakoLogger();
this.filename = 'main.nako3';
/**
* 取り込み文を置換するためのオブジェクト。
* 正規化されたファイル名がキーになり、取り込み文の引数に指定された正規化されていないファイル名はaliasに入れられる。
* JavaScriptファイルによるプラグインの場合、contentは空文字列。
* funclistはシンタックスハイライトの高速化のために事前に取り出した、ファイルが定義する関数名のリスト。
*/
this.dependencies = {};
this.usedFuncs = new Set();
this.numFailures = 0;
if (options.useBasicPlugin) {
this.addBasicPlugins();
}
// 必要なオブジェクトを覚えておく
this.prepare = NakoPrepare.getInstance();
this.parser = new NakoParser(this.logger);
this.lexer = new NakoLexer(this.logger);
// 関数一覧を設定
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));
}
/** モジュール(名前空間)の一覧を取得する */
getModList() {
return this.lexer.modList;
}
getLogger() {
return this.logger;
}
getNakoFuncList() {
return this.nakoFuncList;
}
getNakoFunc(name) {
return this.nakoFuncList.get(name);
}
getPluginfiles() {
return this.pluginfiles;
}
/**
* 基本的なプラグインを追加する
*/
addBasicPlugins() {
this.addPlugin(PluginSystem);
this.addPlugin(PluginMath);
this.addPlugin(PluginPromise);
this.addPlugin(PluginTest);
this.addPlugin(PluginCSV);
}
/**
* loggerを新しいインスタンスで置き換える。
*/
replaceLogger() {
const logger = this.lexer.logger = this.parser.logger = this.logger = new NakoLogger();
return logger;
}
/**
* ファイル内のrequire文の位置を列挙する。出力の配列はstartでソートされている。
* @param {Token[]} tokens rawtokenizeの出力
*/
static listRequireStatements(tokens) {
const requireStatements = [];
for (let i = 0; i + 2 < tokens.length; i++) {
// not (string|string_ex) '取り込み'
if (!(tokens[i].type === 'not' &&
(tokens[i + 1].type === 'string' || tokens[i + 1].type === 'string_ex') &&
tokens[i + 2].value === '取込')) {
continue;
}
// 取り込むライブラリ
let filename = tokens[i + 1].value + '';
// 『取り込む』文で「拡張プラグイン:」機構を追加する #139
// (ex) !『貯蔵庫:ojyo-sama.nako3』を取り込む → https://n3s.nadesi.com/plain/ojyo-sama.nako3
if (filename.startsWith('貯蔵庫:')) {
filename = `https://n3s.nadesi.com/plain/${filename.substring(4)}`;
}
// (ex) !『拡張プラグイン:music.js@1.0.2』を取り込む → https://cdn.jsdelivr.net/npm/nadesiko3-music@1.0.2/nadesiko3-music.js
if (filename.startsWith('拡張プラグイン:')) {
const name = filename.split(':')[1];
const m = name.match(/^([a-zA-Z0-9_-]+)\.(js|mjs|nako3)(@[0-9.]+)?$/);
if (m) {
let basename = m[1];
const ext = m[2];
const version = m[3] || '@latest';
if (ext === 'js' || ext === 'mjs') {
// JSプラグイン
if (!basename.startsWith('nadesiko3-')) {
basename = `nadesiko3-${basename}`;
}
filename = `https://cdn.jsdelivr.net/npm/${basename}${version}/${basename}.${ext}`;
}
else {
// なでしこ3プラグイン
filename = `https://n3s.nadesi.com/plain/${basename}.${ext}`;
}
}
else {
throw new NakoImportError('『取込』の指定エラー。『拡張プラグイン:(ファイル名).(js|nako3)(@ver)』の書式で指定してください。', tokens[i].file, tokens[i].line);
}
}
// push
requireStatements.push({
...tokens[i],
start: i,
end: i + 3,
value: filename,
firstToken: tokens[i],
lastToken: tokens[i + 2]
});
i += 2;
}
return requireStatements;
}
/**
* プログラムが依存するファイルを再帰的に取得する。
* - 依存するファイルがJavaScriptファイルの場合、そのファイルを実行して評価結果をthis.addPluginFileに渡す。
* - 依存するファイルがなでしこ言語の場合、ファイルの中身を取得して変数に保存し、再帰する。
*
* @param {string} code
* @param {string} filename
* @param {string} preCode
* @param {LoaderTool} tools 実行環境 (ブラウザ or Node.js) によって外部ファイルの取得・実行方法は異なるため、引数でそれらを行う関数を受け取る。
* - resolvePath は指定した名前をもつファイルを検索し、正規化されたファイル名を返す関数。返されたファイル名はreadNako3かreadJsの引数になる。
* - readNako3は指定されたファイルの中身を返す関数。
* - readJsは指定したファイルをJavaScriptのプログラムとして実行し、`export default` でエクスポートされた値を返す関数。
* @returns {Promise<unknown> | void}
* @protected
*/
_loadDependencies(code, filename, preCode, tools) {
const dependencies = {};
const compiler = new NakoCompiler({ useBasicPlugin: true });
/**
* @param {any} item
* @param {any} tasks
*/
const loadJS = (item, tasks) => {
// jsならプラグインとして読み込む。(ESMでは必ず動的に読む)
const obj = tools.readJs(item.filePath, item.firstToken);
tasks.push(obj.task.then((res) => {
const pluginFuncs = res();
this.addPluginFile(item.value, item.filePath, pluginFuncs, false);
dependencies[item.filePath].funclist = pluginFuncs;
dependencies[item.filePath].moduleExport = {};
dependencies[item.filePath].addPluginFile = () => { this.addPluginFile(item.value, item.filePath, pluginFuncs, false); };
}));
};
const loadNako3 = (item, tasks) => {
// nako3ならファイルを読んでdependenciesに保存する。
const content = tools.readNako3(item.filePath, item.firstToken);
const registerFile = (code) => {
// シンタックスハイライトの高速化のために、事前にファイルが定義する関数名のリストを取り出しておく。
// preDefineFuncはトークン列に変更を加えるため、事前にクローンしておく。
// 「プラグイン名設定」を行う (#956)
const modName = NakoLexer.filenameToModName(item.filePath);
code = `『${modName}』に名前空間設定;『${modName}』にプラグイン名設定;` + code + ';名前空間ポップ;';
const tokens = this.rawtokenize(code, 0, item.filePath);
dependencies[item.filePath].tokens = tokens;
const funclist = new Map();
const moduleexport = new Map();
NakoLexer.preDefineFunc(cloneAsJSON(tokens), this.logger, funclist, moduleexport);
dependencies[item.filePath].funclist = funclist;
dependencies[item.filePath].moduleExport = moduleexport;
// 再帰
return loadRec(code, item.filePath, '');
};
// 取り込み構文における問題を減らすため、必ず非同期でプログラムを読み込む仕様とした #1219
tasks.push(content.task.then((res) => registerFile(res)));
};
const loadRec = (code, filename, preCode) => {
const tasks = [];
// 取り込みが必要な情報一覧を調べる(トークン分割して、取り込みタグを得る)
const tags = NakoCompiler.listRequireStatements(compiler.rawtokenize(code, 0, filename, preCode));
// パスを解決する
const tagsResolvePath = tags.map((v) => ({ ...v, ...tools.resolvePath(v.value, v.firstToken, filename) }));
// 取り込み開始
for (const item of tagsResolvePath) {
// 2回目以降の読み込み
// eslint-disable-next-line no-prototype-builtins
if (dependencies.hasOwnProperty(item.filePath)) {
dependencies[item.filePath].alias.add(item.value);
continue;
}
// 初回の読み込み
// eslint-disable-next-line @typescript-eslint/no-empty-function
dependencies[item.filePath] = { tokens: [], alias: new Set([item.value]), addPluginFile: () => { }, funclist: {}, moduleExport: {} };
if (item.type === 'js' || item.type === 'mjs') {
loadJS(item, tasks);
}
else if (item.type === 'nako3') {
loadNako3(item, tasks);
}
else {
throw new NakoImportError(`ファイル『${item.value}』を読み込めません。ファイルが存在しないか未対応の拡張子です。`, item.firstToken.file, item.firstToken.line);
}
}
if (tasks.length > 0) {
return Promise.all(tasks);
}
return undefined;
};
try {
const result = loadRec(code, filename, preCode);
// 非同期な場合のエラーハンドリング
if (result !== undefined) {
result.catch((err) => {
// 読み込みに失敗したら処理を中断する
this.logger.error(err.msg);
this.numFailures++;
});
}
// すべてが終わってからthis.dependenciesに代入する。そうしないと、「実行」ボタンを連打した場合など、
// loadDependencies() が並列実行されるときに正しく動作しない。
this.dependencies = dependencies;
return result;
}
catch (err) {
// 同期処理では素直に例外を投げる
this.logger.error('' + err);
throw err;
}
}
/**
* コードを単語に分割する
* @param code なでしこのプログラム
* @param line なでしこのプログラムの行番号
* @param filename
* @param preCode
* @returns トークンのリスト
*/
rawtokenize(code, line, filename, preCode = '') {
if (!code.startsWith(preCode)) {
throw new Error('codeの先頭にはpreCodeを含める必要があります。');
}
// 名前空間のモジュールリストに自身を追加
const modName = NakoLexer.filenameToModName(filename);
const modList = this.getModList();
if (modList.indexOf(modName) < 0) {
modList.unshift(modName);
}
// 全角半角の統一処理
const preprocessed = this.prepare.convert(code);
const tokenizationSourceMapping = new SourceMappingOfTokenization(code.length, preprocessed);
const indentationSyntaxSourceMapping = new SourceMappingOfIndentSyntax(code, [], []);
const offsetToLineColumn = new OffsetToLineColumn(code);
// トークン分割
let tokens;
try {
tokens = this.lexer.tokenize(preprocessed.map((v) => v.text).join(''), line, filename);
}
catch (err) {
if (!(err instanceof InternalLexerError)) {
throw err;
}
// エラー位置をソースコード上の位置に変換して返す
const dest = indentationSyntaxSourceMapping.map(tokenizationSourceMapping.map(err.preprocessedCodeStartOffset), tokenizationSourceMapping.map(err.preprocessedCodeEndOffset));
const line = dest.startOffset === null ? err.line : offsetToLineColumn.map(dest.startOffset, false).line;
const map = subtractSourceMapByPreCodeLength({ ...dest, line }, preCode);
throw new NakoLexerError(err.msg, map.startOffset, map.endOffset, map.line, filename);
}
// DNCL ver2 (core #41)
tokens = convertDNCL2(tokens);
// DNCL ver1 (#1140)
tokens = convertDNCL(tokens);
// インデント構文を変換 #596
tokens = convertIndentSyntax(tokens);
// インラインインデントを変換 #1215
tokens = convertInlineIndent(tokens);
// ソースコード上の位置に変換
tokens = tokens.map((token) => {
const dest = indentationSyntaxSourceMapping.map(tokenizationSourceMapping.map(token.preprocessedCodeOffset || 0), tokenizationSourceMapping.map((token.preprocessedCodeOffset || 0) + (token.preprocessedCodeLength || 0)));
let line = token.line;
let column = 0;
if (token.type === 'eol' && dest.endOffset !== null) {
// eolはnako_genで `line = ${eolToken.line};` に変換されるため、
// 行末のeolのlineは次の行の行数を表す必要がある。
const out = offsetToLineColumn.map(dest.endOffset, false);
line = out.line;
column = out.column;
}
else if (dest.startOffset !== null) {
const out = offsetToLineColumn.map(dest.startOffset, false);
line = out.line;
column = out.column;
}
return {
...token,
...subtractSourceMapByPreCodeLength({ line, column, startOffset: dest.startOffset, endOffset: dest.endOffset }, preCode),
rawJosi: token.josi
};
});
return tokens;
}
/**
* 単語の属性を構文解析に先立ち補正する
* @param {Token[]} tokens トークンのリスト
* @param {boolean} isFirst 最初の呼び出しかどうか
* @param {string} filename
* @returns コード (なでしこ)
*/
converttoken(tokens, isFirst, filename) {
const tok = this.lexer.replaceTokens(tokens, isFirst, filename);
return tok;
}
/**
* 環境のリセット
* {NakoResetOption|undefined}
*/
reset(options = undefined) {
if (!options || options.needToClearPlugin) {
// (メモ) #1245
// 通常、リセット処理では、プラグインの!クリアを呼ぶ。
// しかし、エディタではクリアイベントを呼ぶと、時計などのコンテンツが止まってしまう
// そのため、例外的にオプションを指定すると、プラグインのクリアイベントを呼ばない
this.clearPlugins();
}
/**
* なでしこのローカル変数をスタックで管理
* __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 = new Map();
for (const name of this.__v0.keys()) {
const original = this.pluginFunclist[name]; // record
if (!original) {
continue;
}
this.funclist.set(name, JSON.parse(JSON.stringify(original)));
}
this.lexer.setFuncList(this.funclist);
this.moduleExport = new Map();
this.lexer.setModuleExport(this.moduleExport);
this.logger.clear();
}
/**
* typeがcodeのトークンを単語に分割するための処理
* @param {string} code
* @param {number} line
* @param {string} filename
* @param {number | null} startOffset
* @returns
* @private
*/
lexCodeToken(code, line, filename, startOffset) {
// 単語に分割
let tokens = this.rawtokenize(code, line, filename, '');
// 文字列内位置からファイル内位置へ変換
if (startOffset === null) {
for (const token of tokens) {
token.startOffset = undefined;
token.endOffset = undefined;
}
}
else {
for (const token of tokens) {
if (token.startOffset !== undefined) {
token.startOffset += startOffset;
}
if (token.endOffset !== undefined) {
token.endOffset += startOffset;
}
}
}
// convertTokenで消されるコメントのトークンを残す
const commentTokens = tokens.filter((t) => t.type === 'line_comment' || t.type === 'range_comment')
.map((v) => ({ ...v })); // clone
tokens = this.converttoken(tokens, false, filename);
return { tokens, commentTokens };
}
/**
* 再帰的にrequire文を置換する。
* .jsであれば削除し、.nako3であればそのファイルのトークン列で置換する。
* @param {TokenWithSourceMap[]} tokens
* @param {Set<string>} [includeGuard]
* @returns {Token[]} 削除された取り込み文のトークン
*/
replaceRequireStatements(tokens, includeGuard = new Set()) {
/** @type {TokenWithSourceMap[]} */
const deletedTokens = [];
for (const r of NakoCompiler.listRequireStatements(tokens).reverse()) {
// C言語のinclude guardと同じ仕組みで無限ループを防ぐ。
if (includeGuard.has(r.value)) {
deletedTokens.push(...tokens.splice((r.start || 0), (r.end || 0) - (r.start || 0)));
continue;
}
const filePath = Object.keys(this.dependencies).find((key) => this.dependencies[key].alias.has(r.value));
if (filePath === undefined) {
if (!r.firstToken) {
throw new Error(`ファイル『${r.value}』が読み込まれていません。`);
}
throw new NakoLexerError(`ファイル『${r.value}』が読み込まれていません。`, r.firstToken.startOffset || 0, r.firstToken.endOffset || 0, r.firstToken.line, r.firstToken.file);
}
this.dependencies[filePath].addPluginFile();
const children = cloneAsJSON(this.dependencies[filePath].tokens);
includeGuard.add(r.value);
deletedTokens.push(...this.replaceRequireStatements(children, includeGuard));
deletedTokens.push(...tokens.splice(r.start || 0, (r.end || 0) - (r.start || 0), ...children));
}
return deletedTokens;
}
/**
* replaceRequireStatementsのシンタックスハイライト用の実装。
* @param {TokenWithSourceMap[]} tokens
* @returns {TokenWithSourceMap[]} 削除された取り込み文のトークン
*/
removeRequireStatements(tokens) {
/** @type {TokenWithSourceMap[]} */
const deletedTokens = [];
for (const r of NakoCompiler.listRequireStatements(tokens).reverse()) {
// プラグイン命令のシンタックスハイライトのために、addPluginFileを呼んで関数のリストをthis.dependencies[filePath].funclistに保存させる。
const filePath = Object.keys(this.dependencies).find((key) => this.dependencies[key].alias.has(r.value));
if (filePath !== undefined) {
this.dependencies[filePath].addPluginFile();
}
// 全ての取り込み文を削除する。そうしないとトークン化に時間がかかりすぎる。
deletedTokens.push(...tokens.splice(r.start || 0, (r.end || 0) - (r.start || 0)));
}
return deletedTokens;
}
/** 字句解析を行う */
lex(code, filename = 'main.nako3', preCode = '', syntaxHighlighting = false) {
// 単語に分割
let tokens = this.rawtokenize(code, 0, filename, preCode);
// require文を再帰的に置換する
const requireStatementTokens = syntaxHighlighting ? this.removeRequireStatements(tokens) : this.replaceRequireStatements(tokens, undefined);
for (const t of requireStatementTokens) {
if (t.type === 'word' || t.type === 'not') {
t.type = 'require';
}
}
if (requireStatementTokens.length >= 3) {
// modList を更新
for (let i = 0; i < requireStatementTokens.length; i += 3) {
let modName = requireStatementTokens[i + 1].value;
modName = NakoLexer.filenameToModName(modName);
if (this.lexer.modList.indexOf(modName) < 0) {
this.lexer.modList.push(modName);
}
}
}
// convertTokenで消されるコメントのトークンを残す
const commentTokens = tokens.filter((t) => t.type === 'line_comment' || t.type === 'range_comment')
.map((v) => ({ ...v })); // clone
tokens = this.converttoken(tokens, true, filename);
// 'string_ex'トークンから変換された'code'トークンを字句解析する
for (let i = 0; i < tokens.length; i++) {
if (tokens[i] && tokens[i].type === 'code') {
const children = this.lexCodeToken(tokens[i].value, tokens[i].line, filename, tokens[i].startOffset || 0);
commentTokens.push(...children.commentTokens);
tokens.splice(i, 1, ...children.tokens);
i--;
}
}
this.logger.trace('--- lex ---\n' + JSON.stringify(tokens, null, 2));
return { commentTokens, tokens, requireTokens: requireStatementTokens };
}
/**
* コードをパースしてASTにする
*/
parse(code, filename, preCode = '') {
// 関数リストを字句解析と構文解析に登録
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;
try {
this.parser.genMode = 'sync'; // set default
ast = this.parser.parse(lexerOutput.tokens, filename);
}
catch (err) {
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) {
this.usedFuncs = new Set();
this._getUsedFuncs(ast);
return this.deleteUnNakoFuncs();
}
_getUsedFuncs(ast) {
if (!ast) {
return;
}
if ((ast.type === 'func' || ast.type === 'func_pointer') && ast.name) {
this.usedFuncs.add(ast.name);
}
else if (ast.blocks) { // プロパティにblocksを含んでいる?
for (const a of ast.blocks) {
this._getUsedFuncs(a);
}
}
}
deleteUnNakoFuncs() {
for (const func of this.usedFuncs) {
if (!this.commandlist.has(func)) {
this.usedFuncs.delete(func);
}
}
return this.usedFuncs;
}
/**
* プログラムをコンパイルしてランタイム用のJavaScriptのコードを返す
* @param code コード (なでしこ)
* @param filename
* @param isTest テストかどうか
* @param preCode
*/
compile(code, filename, isTest = false, preCode = '') {
const opt = newCompilerOptions();
opt.testOnly = isTest;
opt.preCode = preCode;
const res = this.compileFromCode(code, filename, opt);
return res.runtimeEnv;
}
/** parse & generate */
compileFromCode(code, filename, options = undefined) {
if (filename === '') {
filename = 'main.nako3';
}
if (options === undefined) {
options = newCompilerOptions();
}
try {
if (options.resetEnv) {
this.reset();
}
if (options.resetAll) {
this.clearPlugins();
}
// onBeforeParse
this.eventList.filter(o => o.eventName === 'beforeParse').map(e => e.callback(code));
// parse
const ast = this.parse(code, filename, options.preCode);
// onBeforeGenerate
this.eventList.filter(o => o.eventName === 'beforeGenerate').map(e => e.callback(ast));
// generate
const outCode = this.generateCode(ast, new NakoGenOptions(options.testOnly));
// onAfterGenerate
this.eventList.filter(o => o.eventName === 'afterGenerate').map(e => e.callback(outCode));
return outCode;
}
catch (e) {
this.logger.error(e);
throw e;
}
}
/**
* プログラムをコンパイルしてJavaScriptのコードオブジェクトを返す
* @param ast
* @param opt テストかどうか
* @param mode 一般的に 'sync' を指定
*/
generateCode(ast, opt, mode = 'sync') {
// 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 = this.codeGenerateor[mode];
if (!genObj) {
throw new Error(`コードジェネレータの「${mode}」はサポートされていません。`);
}
return genObj.generate(this, ast, opt.isTest);
}
/** コードジェネレータを追加する */
addCodeGenerator(mode, obj) {
this.codeGenerateor[mode] = obj;
}
/** (非推奨)
* @param code 非同期で実行する
* @param fname
* @param isReset
* @param isTest テストかどうか。stringの場合は1つのテストのみ。
* @param [preCode]
* @deprecated 代わりにrunAsyncメソッドを使ってください。(core #52)
*/
async _run(code, fname, isReset, isTest, preCode = '') {
const opts = newCompilerOptions({
resetEnv: isReset,
resetAll: isReset,
testOnly: isTest,
preCode
});
return this._runEx(code, fname, opts);
}
/** 各プラグインをリセットする */
clearPlugins() {
// 他に実行している「なでしこ」があればクリアする
this.__globals.forEach((sys) => {
// core #56
sys.__setSysVar('__forceClose', true);
sys.reset();
});
this.__globals = []; // clear
}
/**
* 環境を指定してJavaScriptのコードを実行する
* @param code JavaScriptのコード
* @param nakoGlobal 実行環境
*/
evalJS(code, nakoGlobal) {
this.__globalObj = nakoGlobal; // 現在のnakoGlobalを記録
this.__globalObj.lastJSCode = code;
// 実行前に環境を初期化するイベントを実行(beforeRun)
this.eventList.filter(o => o.eventName === 'beforeRun').map(e => e.callback(nakoGlobal));
try {
// eslint-disable-next-line no-new-func
const f = new Function(nakoGlobal.lastJSCode);
f.apply(nakoGlobal);
}
catch (err) {
// なでしこコードのエラーは抑止してログにのみ記録
nakoGlobal.numFailures++;
this.getLogger().error(err);
throw err;
}
// 実行後に終了イベントを実行(finish)
this.eventList.filter(o => o.eventName === 'finish').map(e => e.callback(nakoGlobal));
}
/**
* (非推奨) 同期的になでしこのプログラムcodeを実行する
* @param code なでしこのプログラム
* @param filename ファイル名
* @param options オプション
* @returns 実行に利用したグローバルオブジェクト
* @deprecated 代わりにrunAsyncメソッドを使ってください。(core #52)
*/
runSync(code, filename, options = undefined) {
// コンパイル
options = newCompilerOptions(options);
const out = this.compileFromCode(code, filename, options);
// 実行前に環境を生成
const nakoGlobal = this.getNakoGlobal(options, out.gen, filename);
// 実行
this.evalJS(out.runtimeEnv, nakoGlobal);
return nakoGlobal;
}
/**
* 非同期になでしこのプログラムcodeを実行する
* @param code なでしこのプログラム
* @param filename ファイル名
* @param options オプション
* @returns 実行に利用したグローバルオブジェクト
*/
async runAsync(code, filename, options = undefined) {
// コンパイル
options = newCompilerOptions(options);
const compiledCode = this.compileFromCode(code, filename, options);
// 実行前に環境を生成
const nakoGlobal = this.getNakoGlobal(options, compiledCode.gen, filename);
// 実行
this.evalJS(compiledCode.runtimeEnv, nakoGlobal);
return nakoGlobal;
}
getNakoGlobal(options, gen, filename) {
// オプションを参照
let g = options.nakoGlobal;
if (!g) {
// 空ならば前回の値を参照(リセットするなら新規で作成する)
if (this.__globals.length > 0 && options.resetAll === false && options.resetEnv === false) {
g = this.__globals[this.__globals.length - 1];
}
else {
g = new NakoGlobal(this, gen, (this.__globals.length + 1));
}
// 名前空間を設定
g.__varslist[0].set('名前空間', NakoLexer.filenameToModName(filename));
}
if (this.__globals.indexOf(g) < 0) {
this.__globals.push(g);
}
return g;
}
/**
* イベントを登録する
* @param eventName イベント名
* @param callback コールバック関数
*/
addListener(eventName, callback) {
this.eventList.push({ eventName, callback });
}
/**
* テストを実行する
* @param code
* @param fname
* @param preCode
* @param testOnly
*/
test(code, fname, preCode = '', testOnly = false) {
const options = newCompilerOptions();
options.preCode = preCode;
options.testOnly = testOnly;
return this.runSync(code, fname, options);
}
/**
* なでしこのプログラムを実行(他に実行しているインスタンスはそのまま)
* @param code
* @param fname
* @param [preCode]
* @deprecated 代わりに runAsync を使ってください。
*/
run(code, fname = 'main.nako3', preCode = '') {
const options = newCompilerOptions();
options.preCode = preCode;
return this.runSync(code, fname, options);
}
/**
* JavaScriptのみで動くコードを取得する場合
* @param code
* @param filename
* @param opt? オプション
*/
compileStandalone(code, filename, options) {
if (options === undefined) {
options = new NakoGenOptions();
}
const ast = this.parse(code, filename);
return this.generateCode(ast, options).standalone;
}
/**
* プラグイン・オブジェクトを追加
* @param po プラグイン・オブジェクト
* @param persistent falseのとき、次以降の実行では使えない
* @param fpath ファイルパス
*/
addPlugin(po, persistent = true, fpath = '') {
// __v0を取得
const __v0 = this.__varslist[0];
// プラグインのメタ情報をチェック (#1034) (#1647)
let __pluginInfo = __v0.get('__pluginInfo');
if (!__pluginInfo) {
__pluginInfo = {};
__v0.set('__pluginInfo', __pluginInfo);
}
// バージョンチェック
let intVersion = 0;
let pluginName = 'unknown';
let metaValue = { pluginName: 'unknown', nakoVersionResult: true, nakoVersion: '0.0.0', path: '' };
if (po.meta) {
if (po.meta.value && typeof (po.meta) === 'object') {
const meta = po.meta;
metaValue = meta.value || { pluginName: 'unknown', nakoVersion: '0.0.0' };
pluginName = metaValue.pluginName || 'unknown';
// version check
const nakoVersion = (metaValue.nakoVersion || '0.0.0') + '.0.0';
const versions = nakoVersion.split('.').map((v) => parseInt(v));
intVersion = versions[1] * 100 + versions[2];
// fpath
metaValue.path = fpath;
}
}
// unknown の場合は、関数名からプラグイン名を自動生成する
if (pluginName === 'unknown') {
pluginName = Object.keys(po).join('-');
}
// プラグイン名の重複を確認
if (__pluginInfo[pluginName] !== undefined) {
// プラグイン名が重複した場合はプラグインとして登録しない
return;
}
// Windowsのパスやファイル名に使えない文字列があると、JSファイル書き出しでエラーになるので置換
const removeInvalidFilenameChars = (str) => {
return str.replace(/[^a-zA-z0-9\-_\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FAF\u3400-\u4DBF\uF900-\uFAFF]/g, '_');
};
pluginName = removeInvalidFilenameChars(pluginName);
// プラグイン情報を記録
__pluginInfo[pluginName] = metaValue;
// バージョンチェック
if (PLUGIN_MIN_VERSION_INT > intVersion) {
const keyStr = Object.keys(po).join(',');
if (pluginName === 'unknown') {
pluginName = keyStr.substring(0, 30) + '...';
}
if (pluginName !== '') {
const errMsg = `なでしこプラグイン『${pluginName}』は古い形式なので正しく動作しない可能性があります。` +
`(ランタイムの要求: ${PLUGIN_MIN_VERSION_INT}/プラグイン: ${intVersion})`;
console.warn(errMsg, 'see', 'https://github.com/kujirahand/nadesiko3/issues/1647');
this.logger.warn(errMsg);
metaValue.nakoVersionResult = false;
}
}
// 初期化とクリアを変換する
this.__module[pluginName] = po;
this.pluginfiles[pluginName] = '*';
// `初期化`と`クリア`をチェック
if (typeof (po['初期化']) === 'object') {
const def = po['初期化'];
delete po['初期化'];
const initKey = `!${pluginName}:初期化`;
po[initKey] = def;
}
// プラグインの値を、なでしこシステム変数(Map)にコピー
for (const key in po) {
const v = po[key];
this.funclist.set(key, v);
if (persistent) {
this.pluginFunclist[key] = JSON.parse(JSON.stringify(v));
}
if (v.type === 'func') {
__v0.set(key, v.fn);
if (v.asyncFn) { // asyncFn を正しく実行するために pure に変更する (core#142)
v.pure = true;
}
}
else if (v.type === 'const' || v.type === 'var') {
// メタ情報としての const | var は現在利用していない
// meta[key] = { readonly: v.type === 'const' }
__v0.set(key, v.value);
}
else {
console.error('[プラグイン追加エラー]', v);
throw new Error('プラグインの追加でエラー。');
}
// コマンドを登録するか?
if (key === '初期化' || key.substring(0, 1) === '!') { // 登録しない関数名
continue;
}
this.commandlist.add(key);
}
}
/**
* プラグイン・オブジェクトを追加(ブラウザ向け)
* @param objName オブジェクト名 (今後プラグイン名は、meta.value.pluginNameに指定する)
* @param po 関数リスト
* @param persistent falseのとき、次以降の実行では使えない
*/
addPluginObject(objName, po, persistent = true) {
// metaプロパティがなければ互換性のため適当に追加
if (po.meta === undefined) {
po.meta = { type: 'const', value: { pluginName: objName, nakoVersion: '0.0.0' } };
}
this.addPlugin(po, persistent);
}
/**
* プラグイン・ファイルを追加(Node.js向け)
* @param objName オブジェクト名(ただし、v3.6.3以降のバージョンでは無効になる)
* @param fpath ファイルパス
* @param po 登録するオブジェクト
* @param persistent falseのとき、次以降の実行では使えない
* @deprecated 利用は非推奨
*/
addPluginFile(_objName, fpath, po, persistent = true) {
this.addPluginFromFile(fpath, po, persistent);
}
/**
* プラグイン・ファイルを追加(Node.js向け)
* @param fpath ファイルパス
* @param po 登録するオブジェクト
* @param persistent falseのとき、次以降の実行では使えない
*/
addPluginFromFile(fpath, po, persistent = true) {
this.addPlugin(po, persistent, fpath);
}
/**
* 関数を追加する
* @param {string} key 関数名
* @param {string[][]} josi 助詞
* @param {Function} fn 関数
* @param {boolean} returnNone 値を返す関数の場合はfalseを指定
* @param {boolean} asyncFn Promiseを返す関数かを指定
*/
addFunc(key, josi, fn, returnNone = true, asyncFn = false) {
const funcObj = { josi, fn, type: 'func', return_none: returnNone, asyncFn, pure: true };
this.funclist.set(key, funcObj);
this.pluginFunclist[key] = cloneAsJSON(funcObj);
this.__varslist[0].set(key, fn);
}
/** (非推奨) 互換性のため ... 関数を追加する
* @deprecated 代わりにaddFuncを使ってください
*/
setFunc(key, josi, fn, returnNone = true, asyncFn = false) {
this.addFunc(key, josi, fn, returnNone, asyncFn);
}
/**
* プラグイン関数を参照する
* @param key プラグイン関数の関数名
* @returns プラグイン・オブジェクト
*/
getFunc(key) {
return this.funclist.get(key);
}
/** 同期的になでしこのプログラムcodeを実行する
* @deprecated 代わりにrunAsyncメソッドを使ってください。(core #52)
*/
_runEx(code, filename, opts, preCode = '', nakoGlobal = undefined) {
// コンパイル
opts.preCode = preCode;
if (nakoGlobal) {
opts.nakoGlobal = nakoGlobal;
}
return this.runSync(code, filename, opts);
}
/** (非推奨) 同期的になでしこのプログラムcodeを実行する
* @param code
* @param fname
* @param opts
* @param [preCode]
* @deprecated 代わりにrunAsyncメソッドを使ってください。(core #52)
*/
runEx(code, fname, opts, preCode = '') {
return this._runEx(code, fname, opts, preCode);
}
/**
* (非推奨) なでしこのプログラムを実行(他に実行しているインスタンスもリセットする)
* @param code
* @param fname
* @param [preCode]
*/
async runReset(code, fname = 'main.nako3', preCode = '') {
const opts = newCompilerOptions({ resetAll: true, resetEnv: true, preCode });
return this.runAsync(code, fname, opts);
}
/**
* 新規のなでしこ変数管理オブジェクトを生成
* @returns 変数管理オブジェクト
*/
newVaiables(initVars) {
return new Map(initVars);
}
}