nadesiko3
Version:
Japanese Programming Language
708 lines (707 loc) • 31 kB
JavaScript
// deno-lint-ignore-file no-explicit-any
/**
* コマンドライン版のなでしこ3をモジュールとして定義
* 実際には cnako3.mjs から読み込まれる
*/
import fs from 'node:fs';
import { exec } from 'node:child_process';
import path from 'node:path';
import url from 'node:url';
import fse from 'fs-extra';
import fetch from 'node-fetch';
import { NakoCompiler, newCompilerOptions } from '../core/src/nako3.mjs';
import { NakoImportError } from '../core/src/nako_errors.mjs';
import { NakoGenOptions } from '../core/src/nako_gen.mjs';
import nakoVersion from './nako_version.mjs';
import PluginNode from './plugin_node.mjs';
import app from './commander_ja.mjs';
import { getEnv, isWindows, getCommandLineArgs, exit } from './deno_wrapper.mjs';
// __dirname のために
const __filename = url.fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
/** CNako3 */
export class CNako3 extends NakoCompiler {
constructor(opts = { nostd: false }) {
super({ useBasicPlugin: !opts.nostd });
this.debug = false;
this.filename = 'main.nako3';
this.version = nakoVersion.version;
if (!opts.nostd) {
this.addPluginFromFile('PluginNode', PluginNode, true);
}
// 必要な定数を設定
this.addListener('beforeRun', (g) => {
g.__varslist[0].set('ナデシコ種類', 'cnako3');
g.__varslist[0].set('ナデシコバージョン', this.version);
});
}
// CNAKO3で使えるコマンドを登録する
registerCommands() {
// コマンドライン引数を得る
const args = getCommandLineArgs();
// コマンド引数がないならば、ヘルプを表示(-hはcommandarにデフォルト用意されている)
if (args.length <= 2) {
args.push('-h');
}
const verInfo = `v${nakoVersion.version}`;
// commanderを使って引数を解析する
app
.title('日本語プログラミング言語「なでしこ」' + verInfo)
.version(verInfo, '-v, --version')
.usage('[オプション] 入力ファイル.nako3')
.option('-h, --help', 'コマンドの使い方を表示')
.option('-w, --warn', '警告を表示する')
.option('-d, --debug', 'デバッグモードの指定')
.option('-D, --trace', '詳細デバッグモードの指定')
.option('-c, --compile', 'コンパイルモードの指定')
.option('-t, --test', 'コンパイルモードの指定 (テスト用コードを出力)')
.option('-r, --run', 'コンパイルモードでも実行する')
.option('-e, --eval [src]', '直接プログラムを実行するワンライナーモード')
.option('-o, --output [filename]', '出力ファイル名の指定')
.option('-s, --silent', 'サイレントモードの指定')
.option('-l, --repl', '対話シェル(REPL)の実行')
.option('-b, --browsers', '対応機器/Webブラウザを表示する')
.option('-m, --man [command]', 'マニュアルを表示する')
.option('-p, --speed', 'スピード優先モードの指定')
.option('-A, --ast', '構文解析した結果をASTで出力する')
.option('-X, --lex', '字句解析した結果をJSONで出力する')
// .option('-h, --help', '使い方を表示する')
// .option('-v, --version', 'バージョンを表示する')
.parse(args);
return app;
}
/** コマンドライン引数を解析 */
checkArguments() {
const app = this.registerCommands();
let logLevel = 'error';
if (app.trace) {
logLevel = 'trace';
}
else if (app.debug) {
logLevel = 'debug';
}
else if (app.warn) {
logLevel = 'warn';
}
this.getLogger().addListener(logLevel, ({ nodeConsole }) => {
console.log(nodeConsole);
});
const args = {
compile: app.compile || false,
run: app.run || false,
source: app.eval || '',
man: app.man || '',
one_liner: app.eval || false,
debug: this.debug || false,
trace: app.trace,
warn: app.warn,
repl: app.repl || false,
test: app.test || false,
browsers: app.browsers || false,
speed: app.speed || false,
ast: app.ast || false,
lex: app.lex || false
};
args.mainfile = app.args[0];
args.output = app.output;
// todo: ESModule 対応の '.mjs' のコードを吐くように修正 #1217
const ext = '.mjs';
if (/\.(nako|nako3|txt|bak)$/.test(args.mainfile)) {
if (!args.output) {
if (args.test) {
args.output = args.mainfile.replace(/\.(nako|nako3)$/, '.spec' + ext);
}
else {
args.output = args.mainfile.replace(/\.(nako|nako3)$/, ext);
}
}
}
else {
if (!args.output) {
if (args.test) {
args.output = args.mainfile + '.spec' + ext;
}
else {
args.output = args.mainfile + ext;
}
}
args.mainfile += '.nako3';
}
return args;
}
// 実行する
async execCommand() {
// コマンドを解析
const opt = this.checkArguments();
// 使い方の表示か?
if (opt.man) {
this.cnakoMan(opt.man);
return;
}
// 対応ブラウザを表示する
if (opt.browsers) {
this.cnakoBrowsers();
return;
}
// REPLを実行する
if (opt.repl) {
this.cnakoRepl();
return;
}
// ワンライナーで実行する
if (opt.one_liner) {
this.cnakoOneLiner(opt);
return;
}
// メインプログラムを読み込む
this.filename = opt.mainfile;
const src = fs.readFileSync(opt.mainfile, 'utf-8');
if (opt.compile) {
await this.nakoCompile(opt, src, false);
return;
}
// 字句解析の結果をJSONで出力
if (opt.lex) {
const lex = this.lex(src, opt.mainfile);
console.log(this.outputJSON(lex, 0));
return;
}
// ASTを出力する
if (opt.ast) {
try {
await this.loadDependencies(src, opt.mainfile, '');
}
catch (err) {
if (this.numFailures > 0) {
this.logger.error(err);
exit(1);
}
}
this.outputAST(opt, src);
return;
}
// テストを実行する
if (opt.test) {
try {
await this.loadDependencies(src, opt.mainfile, '');
this.test(src, opt.mainfile);
return;
}
catch (e) {
if (this.numFailures > 0) {
this.logger.error(e);
exit(1);
}
}
}
// ファイルを読んで実行する
try {
// コンパイルと実行を行うメソッド
const g = await this.runAsync(src, opt.mainfile);
return g;
}
catch (e) {
// 文法エラーなどがあった場合
if (opt.debug || opt.trace) {
throw e;
}
}
}
/**
* コンパイルモードの場合
*/
async nakoCompile(opt, src, isTest) {
// 依存ライブラリなどを読み込む
await this.loadDependencies(src, this.filename, '');
// JSにコンパイル
const genOpt = new NakoGenOptions(isTest, ['plugin_node.mjs'], 'self.__setSysVar(\'ナデシコ種類\', \'cnako3\');');
const jscode = this.compileStandalone(src, this.filename, genOpt);
console.log(opt.output);
fs.writeFileSync(opt.output, jscode, 'utf-8');
// 実行に必要なファイルをコピー
const nakoRuntime = __dirname;
const outRuntime = path.join(path.dirname(opt.output), 'nako3runtime');
if (!fs.existsSync(outRuntime)) {
fs.mkdirSync(outRuntime);
}
// from ./src
for (const mod of ['nako_version.mjs', 'plugin_node.mjs', 'deno_wrapper.mjs']) {
fs.copyFileSync(path.join(nakoRuntime, mod), path.join(outRuntime, mod));
}
// from nadesiko3core/src
const srcDir = path.join(__dirname, '..', 'core', 'src');
const baseFiles = ['nako_errors.mjs', 'nako_core_version.mjs',
'plugin_system.mjs', 'plugin_math.mjs', 'plugin_promise.mjs', 'plugin_test.mjs', 'plugin_csv.mjs', 'nako_csv.mjs'];
for (const mod of baseFiles) {
fs.copyFileSync(path.join(srcDir, mod), path.join(outRuntime, mod));
}
// or 以下のコピーだと依存ファイルがコピーされない package.jsonを見てコピーする必要がある
const orgModule = path.join(__dirname, '..', 'node_modules');
const dirNodeModules = path.join(path.dirname(opt.output), 'node_modules');
const modlist = ['fs-extra', 'iconv-lite', 'opener', 'node-fetch', 'shell-quote'];
const copied = {};
// 再帰的に必要なモジュールをコピーする
const copyModule = (mod) => {
if (copied[mod]) {
return;
}
copied[mod] = true;
// ライブラリ自身をコピー
fse.copySync(path.join(orgModule, mod), path.join(dirNodeModules, mod));
// 依存ライブラリをコピー
const packageFile = path.join(orgModule, mod, 'package.json');
const jsonStr = fs.readFileSync(packageFile, 'utf-8');
const jsonData = JSON.parse(jsonStr);
// サブモジュールをコピー
for (const smod in jsonData.dependencies) {
copyModule(smod);
}
};
for (const mod of modlist) {
copyModule(mod);
}
if (opt.run) {
exec(`node ${opt.output}`, function (err, stdout, stderr) {
if (err) {
console.log('[ERROR]', stderr);
}
console.log(stdout);
});
}
}
// ワンライナーの場合
async cnakoOneLiner(opt) {
const org = opt.source;
try {
if (opt.source.indexOf('表示') < 0) {
opt.source = '' + opt.source + 'を表示。';
}
await this.runAsync(opt.source, 'main.nako3');
}
catch (e) {
// エラーになったら元のワンライナーで再挑戦
try {
if (opt.source !== org) {
await this.runAsync(org, 'main.nako3');
}
else {
throw e;
}
}
catch (e) {
if (this.debug) {
throw e;
}
else {
console.error(e.message);
}
}
}
}
/**
* JSONを出力
*/
outputJSON(ast, level) {
const makeIndent = (level) => {
let s = '';
for (let i = 0; i < level; i++) {
s += ' ';
}
return s;
};
const trim = (s) => { return s.replace(/(^\s+|\s+$)/g, ''); };
if (typeof (ast) === 'string') {
return makeIndent(level) + '"' + ast + '"';
}
if (typeof (ast) === 'number') {
return makeIndent(level) + ast;
}
if (ast instanceof Array) {
const s = makeIndent(level) + '[\n';
const sa = [];
ast.forEach((a) => {
sa.push(this.outputJSON(a, level + 1));
});
return s + sa.join(',\n') + '\n' + makeIndent(level) + ']';
}
if (ast instanceof Object) {
const s = makeIndent(level) + '{\n';
const sa = [];
for (const key in ast) {
const sv = trim(this.outputJSON((ast)[key], level + 1));
const so = makeIndent(level + 1) + '"' + key + '": ' + sv;
sa.push(so);
}
return s + sa.join(',\n') + '\n' + makeIndent(level) + '}';
}
return makeIndent(level) + ast;
}
/**
* ASTを出力
*/
outputAST(opt, src) {
const ast = this.parse(src, opt.mainfile);
console.log(this.outputJSON(ast, 0));
}
// REPL(対話実行環境)の場合
async cnakoRepl() {
const fname = path.join(__dirname, 'repl.nako3');
const src = fs.readFileSync(fname, 'utf-8');
await this.runAsync(src, 'main.nako3');
}
// マニュアルを表示する
cnakoMan(command) {
try {
const pathCommands = path.join(__dirname, '../release/command_cnako3.json');
const commands = JSON.parse(fs.readFileSync(pathCommands, 'utf-8'));
const data = commands[command];
for (const key in data) {
console.log(`${key}: ${data[key]}`);
}
}
catch (e) {
if (e.code === 'MODULE_NOT_FOUND') {
console.log('コマンド一覧がないため、マニュアルを表示できません。以下のコマンドでコマンド一覧を生成してください。\n$ npm run build');
}
else {
throw e;
}
}
}
// 対応機器/Webブラウザを表示する
cnakoBrowsers() {
const fileMD = path.resolve(__dirname, '../doc', 'browsers.md');
console.log(fs.readFileSync(fileMD, 'utf-8'));
}
// (js|nako3) loader
getLoaderTools() {
/** @type {string[]} */
const log = [];
const tools = {
resolvePath: (name, token, fromFile) => {
// 最初に拡張子があるかどうかをチェック
// JSプラグインか?
if (/\.(js|mjs)(\.txt)?$/.test(name)) {
const jspath = CNako3.findJSPluginFile(name, fromFile, __dirname, log);
if (jspath === '') {
throw new NakoImportError(`JSプラグイン『${name}』が見つかりません。コマンドラインで『npm install ${name}』を実行してみてください。以下のパスを検索しました。\n${log.join('\n')}`, token.file, token.line);
}
return { filePath: jspath, type: 'js' };
}
// なでしこプラグインか?
if (/\.(nako3|nako)(\.txt)?$/.test(name)) {
// ファイルかHTTPか
if (name.startsWith('http://') || name.startsWith('https://')) {
return { filePath: name, type: 'nako3' };
}
if (path.isAbsolute(name)) {
return { filePath: path.resolve(name), type: 'nako3' };
}
else {
// filename が undefined のとき token.file が undefined になる。
if (token.file === undefined) {
throw new Error('ファイル名を指定してください。');
}
const dir = path.dirname(fromFile);
return { filePath: path.resolve(path.join(dir, name)), type: 'nako3' };
}
}
// 拡張子がない、あるいは、(.js|.mjs|.nako3|.nako)以外はJSモジュールと見なす
const jspath2 = CNako3.findJSPluginFile(name, fromFile, __dirname, log);
if (jspath2 === '') {
throw new NakoImportError(`JSプラグイン『${name}』が見つかりません。コマンドラインで『npm install ${name}』を実行してみてください。以下のパスを検索しました。\n${log.join('\n')}`, token.file, token.line);
}
return { filePath: jspath2, type: 'js' };
},
readNako3: (name, token) => {
const loader = { task: null };
// ファイルかHTTPか
if (name.startsWith('http://') || name.startsWith('https://')) {
// Webのファイルを非同期で読み込む
loader.task = (async () => {
const res = await fetch(name);
if (!res.ok) {
throw new NakoImportError(`『${name}』からのダウンロードに失敗しました: ${res.status} ${res.statusText}`, token.file, token.line);
}
return await res.text();
})();
}
else {
// ファイルを非同期で読み込む
// ファイルチェックだけ先に実行
if (!fs.existsSync(name)) {
throw new NakoImportError(`ファイル ${name} が存在しません。`, token.file, token.line);
}
loader.task = (new Promise((resolve, reject) => {
fs.readFile(name, { encoding: 'utf-8' }, (err, data) => {
if (err) {
return reject(err);
}
resolve(data);
});
}));
}
// 非同期で読み込む
return loader;
},
readJs: (filePath, token) => {
const loader = { task: null };
if (isWindows()) {
if (filePath.substring(1, 3) === ':\\') {
filePath = 'file://' + filePath;
}
}
// + プラグインの読み込みタスクを生成する
// | プラグインがWeb(https?://...)に配置されている場合
if (filePath.startsWith('http://') || filePath.startsWith('https://')) {
// 動的 import が http 未対応のため、一度、Webのファイルを非同期で読み込んで/tmpに保存してから動的importを行う
loader.task = (new Promise((resolve, reject) => {
// 一時フォルダを得る
const tempDir = getEnv('TEMP');
const osTmpDir = isWindows() ? tempDir : '/tmp';
const osTmpDir2 = (osTmpDir) || path.join('./tmp');
const tmpDir = path.join(osTmpDir2, 'com.nadesi.v3.cnako');
const tmpFile = path.join(tmpDir, filePath.replace(/[^a-zA-Z0-9_.]/g, '_'));
if (!fs.existsSync(tmpDir)) {
fs.mkdirSync(tmpDir, { recursive: true });
}
// WEBからダウンロード
fetch(filePath)
.then((res) => {
return res.text();
})
.then((txt) => {
// ダウンロード
if (txt.indexOf('Failed to fetch') >= 0) {
const errFetch = new NakoImportError(`URL『${filePath}』のライブラリが存在しないか、指定のバージョンが間違っています。`, token.file, token.line);
reject(errFetch);
}
// 一時ファイルに保存
try {
fs.writeFileSync(tmpFile, txt, 'utf-8');
}
catch (err) {
const err2 = new NakoImportError(`URL『${filePath}』からダウンロードしたJSファイルがキャッシュに書き込めません。${String(err)}`, token.file, token.line);
reject(err2);
}
})
.then(() => {
// 一時ファイルから読み込む
import(tmpFile).then((mod) => {
// プラグインは export default で宣言
const obj = Object.assign({}, mod);
resolve(() => {
return obj.default;
});
}).catch((err) => {
const errS = '' + err;
if (errS.indexOf('SyntaxError:') >= 0) {
const err2 = new NakoImportError(`URL『${filePath}』からダウンロードしたJSファイルに文法エラーがあります。${err}`, token.file, token.line);
reject(err2);
}
else {
const err3 = new NakoImportError(`URL『${filePath}』からダウンロードしたはずのJSファイル読み込めません。${err}`, token.file, token.line);
reject(err3);
}
});
})
.catch((err) => {
const err2 = new NakoImportError(`URL『${filePath}』からJSファイルが読み込めません。${err}`, token.file, token.line);
reject(err2);
});
}));
return loader;
}
// | プラグインがファイル上に配置されている場合
loader.task = (new Promise((resolve, reject) => {
import(filePath).then((mod) => {
// プラグインは export default で宣言
const obj = Object.assign({}, mod);
resolve(() => { return obj.default; });
}).catch((err) => {
const err2 = new NakoImportError(`ファイル『${filePath}』が読み込めません。${err}`, token.file, token.line);
reject(err2);
});
}));
return loader;
}
};
return tools;
}
/** 『!「xxx」を取込』の処理 */
async loadDependencies(code, filename, preCode) {
const tools = this.getLoaderTools();
await super._loadDependencies(code, filename, preCode, tools);
}
/**
* 非同期でなでしこのコードを実行する
*/
async runAsync(code, fname, options = undefined) {
// オプション
const opt = newCompilerOptions(options);
// 取り込む文
await this.loadDependencies(code, fname, opt.preCode);
// 実行
return await super.runAsync(code, fname, options);
}
/**
* プラグインファイルの検索を行う
* @param pname プラグインの名前
* @param filename 取り込み元ファイル名
* @param srcDir このファイルが存在するディレクトリ
* @param log
* @return フルパス、失敗した時は、''を返す
*/
static findJSPluginFile(pname, filename, srcDir, log = []) {
log.length = 0;
const cachePath = {};
/** キャッシュ付きでファイルがあるか検索 */
const exists = (f) => {
// 同じパスを何度も検索することがないように
if (cachePath[f]) {
return cachePath[f];
}
try {
// ファイルがないと例外が出る
const stat = fs.statSync(f);
const b = !!(stat && stat.isFile());
cachePath[f] = b;
return b;
}
catch (_) {
return false;
}
};
/** 普通にファイルをチェック */
const fCheck = (pathTest, desc) => {
// 素直に指定されたパスをチェック
const bExists = exists(pathTest);
log.push(`- (${desc}) ${pathTest}, ${bExists}`);
return bExists;
};
/** 通常 + package.json のパスを調べる */
const fCheckEx = (pathTest, desc) => {
// 直接JSファイルが指定された?
if (/\.(js|mjs)$/.test(pathTest)) {
if (fCheck(pathTest, desc)) {
return pathTest;
}
}
// 指定パスのpackage.jsonを調べる
const json = path.join(pathTest, 'package.json');
if (fCheck(json, desc + '/package.json')) {
// package.jsonを見つけたので、メインファイルを調べて取り込む (CommonJSモジュール対策)
const jsonText = fs.readFileSync(json, 'utf-8');
const obj = JSON.parse(jsonText);
if (!obj.main) {
return '';
}
const mainFile = path.resolve(path.join(pathTest, obj.main));
return mainFile;
}
return '';
};
// URL指定か?
if (pname.substring(0, 8) === 'https://') {
return pname;
}
// 各パスを検索していく
const p1 = pname.substring(0, 1);
// フルパス指定か?
if (p1 === '/' || pname.substring(1, 3).toLowerCase() === ':\\' || pname.substring(0, 6) === 'file:/') {
const fileFullpath = fCheckEx(pname, 'フルパス');
if (fileFullpath) {
return fileFullpath;
}
return ''; // フルパスの場合別のフォルダは調べない
}
// 相対パスか?
if (p1 === '.' || pname.indexOf('/') >= 0) {
// 相対パス指定なので、なでしこのプログラムからの相対指定を調べる
const pathRelative = path.join(path.resolve(path.dirname(filename)), pname);
const fileRelative = fCheckEx(pathRelative, '相対パス');
if (fileRelative) {
return fileRelative;
}
return ''; // 相対パスの場合も別のフォルダは調べない
}
// plugin_xxx.mjs のようにファイル名のみが指定された場合のみ、いくつかのパスを調べる
// 母艦パス(元ファイルと同じフォルダ)か?
const testScriptPath = path.join(path.resolve(path.dirname(filename)), pname);
const fileScript = fCheckEx(testScriptPath, '母艦パス');
if (fileScript) {
return fileScript;
}
// ランタイムパス/src/<plugin>
if (pname.match(/^plugin_[a-z0-9_]+\.mjs/)) {
// cnako3mod.mjs は ランタイム/src に配置されていることが前提
const pathRoot = path.resolve(__dirname, '..');
const pathRuntimeSrc = path.join(pathRoot, 'src', pname);
const fileRuntimeSrc = fCheckEx(pathRuntimeSrc, 'CNAKO3パス');
if (fileRuntimeSrc) {
return fileRuntimeSrc;
}
// ランタイム/core/src/<plugin>
const pathCore = path.join(pathRoot, 'core', 'src', pname);
const fileCore = fCheckEx(pathCore, 'CNAKO3パス');
if (fileCore) {
return fileCore;
}
}
// 環境変数をチェック
// 環境変数 NAKO_LIB か?
const nakoLib = getEnv('NAKO_LIB');
if (nakoLib) {
const nakoLibFull = path.join(path.resolve(nakoLib), pname);
const nakoLibFull2 = fCheckEx(nakoLibFull, 'NAKO_LIB');
if (nakoLibFull2) {
return nakoLibFull2;
}
}
// ランタイムパス/node_modules/<plugin>
const pathRuntime = path.join(path.dirname(path.resolve(__dirname)));
const pathRuntimePname = path.join(pathRuntime, 'node_modules', pname);
const fileRuntime = fCheckEx(pathRuntimePname, 'runtime');
if (fileRuntime) {
return fileRuntime;
}
// ランタイムと同じ配置 | ランタイムパス/../<plugin>
const runtimeLib = path.join(pathRuntime, '..', pname);
const fileLib = fCheckEx(runtimeLib, 'runtimeLib');
if (fileLib) {
return fileLib;
}
// nadesiko3core | ランタイムパス/node_modules/nadesiko3core/src/<plugin>
const pathRuntimeSrc2 = path.join(pathRuntime, 'node_modules', 'nadesiko3core', 'src', pname); // cnako3mod.mjs は ランタイム/src に配置されていることが前提
const fileRuntimeSrc2 = fCheckEx(pathRuntimeSrc2, 'nadesiko3core');
if (fileRuntimeSrc2) {
return fileRuntimeSrc2;
}
// 環境変数 NAKO_HOMEか?
const nakoHome = getEnv('NAKO_HOME');
if (nakoHome) {
const nakoHomeFull = path.join(path.resolve(nakoHome), 'node_modules', pname);
const nakoHomeFull2 = fCheckEx(nakoHomeFull, 'NAKO_HOME');
if (nakoHomeFull2) {
return nakoHomeFull2;
}
// NAKO_HOME/src ?
const pathNakoHomeSrc = path.join(nakoHome, 'src', pname);
const fileNakoHomeSrc = fCheckEx(pathNakoHomeSrc, 'NAKO_HOME/src');
if (fileNakoHomeSrc) {
return fileNakoHomeSrc;
}
}
// 環境変数 NODE_PATH (global) 以下にあるか?
const nodePath = getEnv('NODE_PATH');
if (nodePath) {
const pathNode = path.join(path.resolve(nodePath), pname);
const fileNode = fCheckEx(pathNode, 'NODE_PATH');
if (fileNode) {
return fileNode;
}
}
// Nodeのパス検索には任せない(importで必ず失敗するので)
return '';
}
}