nadesiko3 3.3.48 → 3.3.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/core/.editorconfig +6 -0
  2. package/core/.eslintrc.cjs +33 -0
  3. package/core/.github/dependabot.yml +7 -0
  4. package/core/.github/workflows/nodejs.yml +37 -0
  5. package/core/.github/workflows/super-linter.yml +61 -0
  6. package/core/.github/workflows/textlint.yml +199 -0
  7. package/core/LICENSE +21 -0
  8. package/core/README.md +66 -0
  9. package/core/batch/build_nako_version.nako3 +42 -0
  10. package/core/command/snako.mjs +105 -0
  11. package/core/command/snako.mts +116 -0
  12. package/core/index.mjs +21 -0
  13. package/core/index.mts +21 -0
  14. package/core/package.json +47 -0
  15. package/core/sample/hello.nako3 +7 -0
  16. package/core/sample/hoge.mjs +4 -0
  17. package/core/sample/hoge.mts +6 -0
  18. package/core/src/nako3.mjs +858 -0
  19. package/core/src/nako3.mts +967 -0
  20. package/core/src/nako_colors.mjs +78 -0
  21. package/core/src/nako_colors.mts +86 -0
  22. package/core/src/nako_core_version.mjs +8 -0
  23. package/core/src/nako_core_version.mts +19 -0
  24. package/core/src/nako_csv.mjs +185 -0
  25. package/core/src/nako_csv.mts +188 -0
  26. package/core/src/nako_errors.mjs +173 -0
  27. package/core/src/nako_errors.mts +197 -0
  28. package/core/src/nako_from_dncl.mjs +255 -0
  29. package/core/src/nako_from_dncl.mts +250 -0
  30. package/core/src/nako_gen.mjs +1648 -0
  31. package/core/src/nako_gen.mts +1719 -0
  32. package/core/src/nako_gen_async.mjs +1659 -0
  33. package/core/src/nako_gen_async.mts +1732 -0
  34. package/core/src/nako_global.mjs +107 -0
  35. package/core/src/nako_global.mts +138 -0
  36. package/core/src/nako_indent.mjs +445 -0
  37. package/core/src/nako_indent.mts +492 -0
  38. package/core/src/nako_josi_list.mjs +38 -0
  39. package/core/src/nako_josi_list.mts +45 -0
  40. package/core/src/nako_lex_rules.mjs +253 -0
  41. package/core/src/nako_lex_rules.mts +260 -0
  42. package/core/src/nako_lexer.mjs +609 -0
  43. package/core/src/nako_lexer.mts +612 -0
  44. package/core/src/nako_logger.mjs +199 -0
  45. package/core/src/nako_logger.mts +232 -0
  46. package/core/src/nako_parser3.mjs +2439 -0
  47. package/core/src/nako_parser3.mts +2195 -0
  48. package/core/src/nako_parser_base.mjs +370 -0
  49. package/core/src/nako_parser_base.mts +370 -0
  50. package/core/src/nako_parser_const.mjs +37 -0
  51. package/core/src/nako_parser_const.mts +37 -0
  52. package/core/src/nako_prepare.mjs +304 -0
  53. package/core/src/nako_prepare.mts +315 -0
  54. package/core/src/nako_reserved_words.mjs +38 -0
  55. package/core/src/nako_reserved_words.mts +38 -0
  56. package/core/src/nako_source_mapping.mjs +207 -0
  57. package/core/src/nako_source_mapping.mts +262 -0
  58. package/core/src/nako_test.mjs +37 -0
  59. package/core/src/nako_types.mjs +25 -0
  60. package/core/src/nako_types.mts +151 -0
  61. package/core/src/plugin_csv.mjs +49 -0
  62. package/core/src/plugin_csv.mts +50 -0
  63. package/core/src/plugin_math.mjs +328 -0
  64. package/core/src/plugin_math.mts +326 -0
  65. package/core/src/plugin_promise.mjs +91 -0
  66. package/core/src/plugin_promise.mts +91 -0
  67. package/core/src/plugin_system.mjs +2832 -0
  68. package/core/src/plugin_system.mts +2690 -0
  69. package/core/src/plugin_test.mjs +34 -0
  70. package/core/src/plugin_test.mts +34 -0
  71. package/core/test/array_test.mjs +34 -0
  72. package/core/test/basic_test.mjs +344 -0
  73. package/core/test/calc_test.mjs +140 -0
  74. package/core/test/core_module_test.mjs +23 -0
  75. package/core/test/debug_test.mjs +16 -0
  76. package/core/test/dncl_test.mjs +94 -0
  77. package/core/test/error_message_test.mjs +210 -0
  78. package/core/test/error_test.mjs +16 -0
  79. package/core/test/flow_test.mjs +373 -0
  80. package/core/test/func_call.mjs +160 -0
  81. package/core/test/func_test.mjs +149 -0
  82. package/core/test/indent_test.mjs +364 -0
  83. package/core/test/lex_test.mjs +168 -0
  84. package/core/test/literal_test.mjs +73 -0
  85. package/core/test/nako_lexer_test.mjs +35 -0
  86. package/core/test/nako_logger_test.mjs +76 -0
  87. package/core/test/nako_logger_test.mts +78 -0
  88. package/core/test/plugin_csv_test.mjs +38 -0
  89. package/core/test/plugin_promise_test.mjs +18 -0
  90. package/core/test/plugin_system_test.mjs +630 -0
  91. package/core/test/prepare_test.mjs +96 -0
  92. package/core/test/re_test.mjs +22 -0
  93. package/core/test/side_effects_test.mjs +92 -0
  94. package/core/test/variable_scope_test.mjs +149 -0
  95. package/core/tsconfig.json +101 -0
  96. package/package.json +4 -2
  97. package/release/_hash.txt +12 -12
  98. package/release/_script-tags.txt +14 -14
  99. package/release/editor.js +1 -1
  100. package/release/stats.json +1 -1
  101. package/release/version.js +1 -1
  102. package/release/wnako3.js +1 -1
  103. package/src/nako_version.mjs +2 -2
  104. package/src/nako_version.mts +2 -2
  105. package/test/async/async_basic_test.mjs +3 -3
@@ -0,0 +1,858 @@
1
+ /**
2
+ * nadesiko v3
3
+ */
4
+ // types
5
+ import { CompilerOptions } from './nako_types.mjs';
6
+ // parser / lexer
7
+ import { NakoParser } from './nako_parser3.mjs';
8
+ import { NakoLexer } from './nako_lexer.mjs';
9
+ import { NakoPrepare } from './nako_prepare.mjs';
10
+ import { generateJS, NakoGenOptions } from './nako_gen.mjs';
11
+ import { NakoGenASync } from './nako_gen_async.mjs';
12
+ import NakoIndent from './nako_indent.mjs';
13
+ import { convertDNCL } from './nako_from_dncl.mjs';
14
+ import { SourceMappingOfTokenization, SourceMappingOfIndentSyntax, OffsetToLineColumn, subtractSourceMapByPreCodeLength } from './nako_source_mapping.mjs';
15
+ import { NakoLexerError, NakoImportError, NakoSyntaxError, InternalLexerError } from './nako_errors.mjs';
16
+ import { NakoLogger } from './nako_logger.mjs';
17
+ import { NakoGlobal } from './nako_global.mjs';
18
+ // version info
19
+ import coreVersion from './nako_core_version.mjs';
20
+ // basic plugins
21
+ import PluginSystem from './plugin_system.mjs';
22
+ import PluginMath from './plugin_math.mjs';
23
+ import PluginCSV from './plugin_csv.mjs';
24
+ import PluginPromise from './plugin_promise.mjs';
25
+ import PluginTest from './plugin_test.mjs';
26
+ const cloneAsJSON = (x) => JSON.parse(JSON.stringify(x));
27
+ /** なでしこコンパイラ */
28
+ export class NakoCompiler {
29
+ /**
30
+ * @param {undefined | {'useBasicPlugin':true|false}} options
31
+ */
32
+ constructor(options = undefined) {
33
+ if (options === undefined) {
34
+ options = { useBasicPlugin: true };
35
+ }
36
+ // 環境のリセット
37
+ /** @type {Record<string, any>[]} */
38
+ this.__varslist = [{}, {}, {}]; // このオブジェクトは変更しないこと (this.gen.__varslist と共有する)
39
+ this.__locals = {}; // ローカル変数
40
+ this.__self = this;
41
+ this.__vars = this.__varslist[2];
42
+ this.__v0 = this.__varslist[0];
43
+ this.__v1 = this.__varslist[1];
44
+ // バージョンを設定
45
+ this.version = coreVersion.version;
46
+ this.coreVersion = coreVersion.version;
47
+ /**
48
+ * @type {NakoGlobal[]}
49
+ */
50
+ this.__globals = []; // 生成した NakoGlobal のインスタンスを保持
51
+ /** @type {Record<string, Record<string, NakoFunction>>} */
52
+ this.__module = {}; // requireなどで取り込んだモジュールの一覧
53
+ this.pluginFunclist = {}; // プラグインで定義された関数
54
+ this.funclist = {}; // プラグインで定義された関数 + ユーザーが定義した関数
55
+ this.pluginfiles = {}; // 取り込んだファイル一覧
56
+ this.commandlist = new Set(); // プラグインで定義された定数・変数・関数の名前
57
+ this.nakoFuncList = {}; // __v1に配置するJavaScriptのコードで定義された関数
58
+ this.eventList = []; // 実行前に環境を変更するためのイベント
59
+ this.logger = new NakoLogger();
60
+ // 必要なオブジェクトを覚えておく
61
+ this.prepare = NakoPrepare.getInstance();
62
+ this.parser = new NakoParser(this.logger);
63
+ this.lexer = new NakoLexer(this.logger);
64
+ /**
65
+ * 取り込み文を置換するためのオブジェクト。
66
+ * 正規化されたファイル名がキーになり、取り込み文の引数に指定された正規化されていないファイル名はaliasに入れられる。
67
+ * JavaScriptファイルによるプラグインの場合、contentは空文字列。
68
+ * funclistはシンタックスハイライトの高速化のために事前に取り出した、ファイルが定義する関数名のリスト。
69
+ */
70
+ this.dependencies = {};
71
+ this.usedFuncs = new Set();
72
+ this.numFailures = 0;
73
+ if (options.useBasicPlugin) {
74
+ this.addBasicPlugins();
75
+ }
76
+ }
77
+ getLogger() {
78
+ return this.logger;
79
+ }
80
+ getNakoFuncList() {
81
+ return this.nakoFuncList;
82
+ }
83
+ getNakoFunc(name) {
84
+ return this.nakoFuncList[name];
85
+ }
86
+ getPluginfiles() {
87
+ return this.pluginfiles;
88
+ }
89
+ /**
90
+ * 基本的なプラグインを追加する
91
+ */
92
+ addBasicPlugins() {
93
+ this.addPluginObject('PluginSystem', PluginSystem);
94
+ this.addPluginObject('PluginMath', PluginMath);
95
+ this.addPluginObject('PluginPromise', PluginPromise);
96
+ this.addPluginObject('PluginAssert', PluginTest);
97
+ this.addPluginObject('PluginCSV', PluginCSV);
98
+ }
99
+ /**
100
+ * loggerを新しいインスタンスで置き換える。
101
+ */
102
+ replaceLogger() {
103
+ const logger = this.lexer.logger = this.parser.logger = this.logger = new NakoLogger();
104
+ return logger;
105
+ }
106
+ /**
107
+ * ファイル内のrequire文の位置を列挙する。出力の配列はstartでソートされている。
108
+ * @param {Token[]} tokens rawtokenizeの出力
109
+ */
110
+ static listRequireStatements(tokens) {
111
+ const requireStatements = [];
112
+ for (let i = 0; i + 2 < tokens.length; i++) {
113
+ // not (string|string_ex) '取り込み'
114
+ if (!(tokens[i].type === 'not' &&
115
+ (tokens[i + 1].type === 'string' || tokens[i + 1].type === 'string_ex') &&
116
+ tokens[i + 2].value === '取込')) {
117
+ continue;
118
+ }
119
+ requireStatements.push({
120
+ ...tokens[i],
121
+ start: i,
122
+ end: i + 3,
123
+ value: tokens[i + 1].value + '',
124
+ firstToken: tokens[i],
125
+ lastToken: tokens[i + 2]
126
+ });
127
+ i += 2;
128
+ }
129
+ return requireStatements;
130
+ }
131
+ /**
132
+ * プログラムが依存するファイルを再帰的に取得する。
133
+ * - 依存するファイルがJavaScriptファイルの場合、そのファイルを実行して評価結果をthis.addPluginFileに渡す。
134
+ * - 依存するファイルがなでしこ言語の場合、ファイルの中身を取得して変数に保存し、再帰する。
135
+ *
136
+ * @param {string} code
137
+ * @param {string} filename
138
+ * @param {string} preCode
139
+ * @param {LoaderTool} tools 実行環境 (ブラウザ or Node.js) によって外部ファイルの取得・実行方法は異なるため、引数でそれらを行う関数を受け取る。
140
+ * - resolvePath は指定した名前をもつファイルを検索し、正規化されたファイル名を返す関数。返されたファイル名はreadNako3かreadJsの引数になる。
141
+ * - readNako3は指定されたファイルの中身を返す関数。
142
+ * - readJsは指定したファイルをJavaScriptのプログラムとして実行し、`export default` でエクスポートされた値を返す関数。
143
+ * @returns {Promise<unknown> | void}
144
+ * @protected
145
+ */
146
+ _loadDependencies(code, filename, preCode, tools) {
147
+ const dependencies = {};
148
+ const compiler = new NakoCompiler({ useBasicPlugin: true });
149
+ /**
150
+ * @param {any} item
151
+ * @param {any} tasks
152
+ */
153
+ const loadJS = (item, tasks) => {
154
+ // jsならプラグインとして読み込む。(ESMでは必ず動的に読む)
155
+ const obj = tools.readJs(item.filePath, item.firstToken);
156
+ tasks.push(obj.task.then((res) => {
157
+ const pluginFuncs = res();
158
+ this.addPluginFile(item.value, item.filePath, pluginFuncs, false);
159
+ dependencies[item.filePath].funclist = pluginFuncs;
160
+ dependencies[item.filePath].addPluginFile = () => { this.addPluginFile(item.value, item.filePath, pluginFuncs, false); };
161
+ }));
162
+ };
163
+ const loadNako3 = (item, tasks) => {
164
+ // nako3ならファイルを読んでdependenciesに保存する。
165
+ const content = tools.readNako3(item.filePath, item.firstToken);
166
+ const registerFile = (code) => {
167
+ // シンタックスハイライトの高速化のために、事前にファイルが定義する関数名のリストを取り出しておく。
168
+ // preDefineFuncはトークン列に変更を加えるため、事前にクローンしておく。
169
+ // 「プラグイン名設定」を行う (#956)
170
+ const modName = NakoLexer.filenameToModName(item.filePath);
171
+ code = `『${modName}』にプラグイン名設定;` + code + ';『メイン』にプラグイン名設定;';
172
+ const tokens = this.rawtokenize(code, 0, item.filePath);
173
+ dependencies[item.filePath].tokens = tokens;
174
+ const funclist = {};
175
+ NakoLexer.preDefineFunc(cloneAsJSON(tokens), this.logger, funclist);
176
+ dependencies[item.filePath].funclist = funclist;
177
+ // 再帰
178
+ return loadRec(code, item.filePath, '');
179
+ };
180
+ // 取り込み構文における問題を減らすため、必ず非同期でプログラムを読み込む仕様とした #1219
181
+ tasks.push(content.task.then((res) => registerFile(res)));
182
+ };
183
+ const loadRec = (code, filename, preCode) => {
184
+ const tasks = [];
185
+ // 取り込みが必要な情報一覧を調べる(トークン分割して、取り込みタグを得る)
186
+ const tags = NakoCompiler.listRequireStatements(compiler.rawtokenize(code, 0, filename, preCode));
187
+ // パスを解決する
188
+ const tagsResolvePath = tags.map((v) => ({ ...v, ...tools.resolvePath(v.value, v.firstToken, filename) }));
189
+ // 取り込み開始
190
+ for (const item of tagsResolvePath) {
191
+ // 2回目以降の読み込み
192
+ // eslint-disable-next-line no-prototype-builtins
193
+ if (dependencies.hasOwnProperty(item.filePath)) {
194
+ dependencies[item.filePath].alias.add(item.value);
195
+ continue;
196
+ }
197
+ // 初回の読み込み
198
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
199
+ dependencies[item.filePath] = { tokens: [], alias: new Set([item.value]), addPluginFile: () => { }, funclist: {} };
200
+ if (item.type === 'js' || item.type === 'mjs') {
201
+ loadJS(item, tasks);
202
+ }
203
+ else if (item.type === 'nako3') {
204
+ loadNako3(item, tasks);
205
+ }
206
+ else {
207
+ throw new NakoImportError(`ファイル『${item.value}』を読み込めません。ファイルが存在しないか未対応の拡張子です。`, item.firstToken.file, item.firstToken.line);
208
+ }
209
+ }
210
+ if (tasks.length > 0) {
211
+ return Promise.all(tasks);
212
+ }
213
+ return undefined;
214
+ };
215
+ try {
216
+ const result = loadRec(code, filename, preCode);
217
+ // 非同期な場合のエラーハンドリング
218
+ if (result !== undefined) {
219
+ result.catch((err) => {
220
+ // 読み込みに失敗しても処理は続ける方針なので、失敗しても例外は投げない
221
+ // たぶん、その後の構文解析でエラーになるため
222
+ this.logger.warn(err.msg);
223
+ });
224
+ }
225
+ // すべてが終わってからthis.dependenciesに代入する。そうしないと、「実行」ボタンを連打した場合など、
226
+ // loadDependencies() が並列実行されるときに正しく動作しない。
227
+ this.dependencies = dependencies;
228
+ return result;
229
+ }
230
+ catch (err) {
231
+ // 同期処理では素直に例外を投げる
232
+ this.logger.warn('' + err);
233
+ throw err;
234
+ }
235
+ }
236
+ /**
237
+ * コードを単語に分割する
238
+ * @param {string} code なでしこのプログラム
239
+ * @param {number} line なでしこのプログラムの行番号
240
+ * @param {string} filename
241
+ * @param {string} [preCode]
242
+ * @returns {TokenWithSourceMap[]} トークンのリスト
243
+ */
244
+ rawtokenize(code, line, filename, preCode = '') {
245
+ if (!code.startsWith(preCode)) {
246
+ throw new Error('codeの先頭にはpreCodeを含める必要があります。');
247
+ }
248
+ // インデント構文 (#596)
249
+ const { code: code2, insertedLines, deletedLines } = NakoIndent.convert(code, filename);
250
+ // DNCL構文 (#1140)
251
+ const code3 = convertDNCL(code2, filename);
252
+ // 全角半角の統一処理
253
+ const preprocessed = this.prepare.convert(code3);
254
+ const tokenizationSourceMapping = new SourceMappingOfTokenization(code2.length, preprocessed);
255
+ const indentationSyntaxSourceMapping = new SourceMappingOfIndentSyntax(code2, insertedLines, deletedLines);
256
+ const offsetToLineColumn = new OffsetToLineColumn(code);
257
+ // トークン分割
258
+ /** @type {import('./nako_lexer.mjs').Token[]} */
259
+ let tokens;
260
+ try {
261
+ tokens = this.lexer.tokenize(preprocessed.map((v) => v.text).join(''), line, filename);
262
+ }
263
+ catch (err) {
264
+ if (!(err instanceof InternalLexerError)) {
265
+ throw err;
266
+ }
267
+ // エラー位置をソースコード上の位置に変換して返す
268
+ const dest = indentationSyntaxSourceMapping.map(tokenizationSourceMapping.map(err.preprocessedCodeStartOffset), tokenizationSourceMapping.map(err.preprocessedCodeEndOffset));
269
+ /** @type {number | undefined} */
270
+ const line = dest.startOffset === null ? err.line : offsetToLineColumn.map(dest.startOffset, false).line;
271
+ const map = subtractSourceMapByPreCodeLength({ ...dest, line }, preCode);
272
+ throw new NakoLexerError(err.msg, map.startOffset, map.endOffset, map.line, filename);
273
+ }
274
+ // ソースコード上の位置に変換
275
+ return tokens.map((token) => {
276
+ const dest = indentationSyntaxSourceMapping.map(tokenizationSourceMapping.map(token.preprocessedCodeOffset || 0), tokenizationSourceMapping.map((token.preprocessedCodeOffset || 0) + (token.preprocessedCodeLength || 0)));
277
+ let line = token.line;
278
+ let column = 0;
279
+ if (token.type === 'eol' && dest.endOffset !== null) {
280
+ // eolはparserで `line = ${eolToken.line};` に変換されるため、
281
+ // 行末のeolのlineは次の行の行数を表す必要がある。
282
+ const out = offsetToLineColumn.map(dest.endOffset, false);
283
+ line = out.line;
284
+ column = out.column;
285
+ }
286
+ else if (dest.startOffset !== null) {
287
+ const out = offsetToLineColumn.map(dest.startOffset, false);
288
+ line = out.line;
289
+ column = out.column;
290
+ }
291
+ return {
292
+ ...token,
293
+ ...subtractSourceMapByPreCodeLength({ line, column, startOffset: dest.startOffset, endOffset: dest.endOffset }, preCode),
294
+ rawJosi: token.josi
295
+ };
296
+ });
297
+ }
298
+ /**
299
+ * 単語の属性を構文解析に先立ち補正する
300
+ * @param {Token[]} tokens トークンのリスト
301
+ * @param {boolean} isFirst 最初の呼び出しかどうか
302
+ * @param {string} filename
303
+ * @returns コード (なでしこ)
304
+ */
305
+ converttoken(tokens, isFirst, filename) {
306
+ const tok = this.lexer.replaceTokens(tokens, isFirst, filename);
307
+ return tok;
308
+ }
309
+ /**
310
+ * 環境のリセット
311
+ */
312
+ reset() {
313
+ /**
314
+ * なでしこのローカル変数をスタックで管理
315
+ * __varslist[0] プラグイン領域
316
+ * __varslist[1] なでしこグローバル領域
317
+ * __varslist[2] 最初のローカル変数 ( == __vars }
318
+ * @type {Record<string, any>[]}
319
+ */
320
+ this.__varslist = [this.__varslist[0], {}, {}];
321
+ this.__v0 = this.__varslist[0];
322
+ this.__v1 = this.__varslist[1];
323
+ this.__vars = this.__varslist[2];
324
+ this.__locals = {};
325
+ // プラグイン命令以外を削除する。
326
+ this.funclist = {};
327
+ for (const name of Object.keys(this.__v0)) {
328
+ const original = this.pluginFunclist[name];
329
+ if (!original) {
330
+ continue;
331
+ }
332
+ this.funclist[name] = JSON.parse(JSON.stringify(original));
333
+ }
334
+ this.lexer.setFuncList(this.funclist);
335
+ this.clearPlugins();
336
+ this.logger.clear();
337
+ }
338
+ /**
339
+ * typeがcodeのトークンを単語に分割するための処理
340
+ * @param {string} code
341
+ * @param {number} line
342
+ * @param {string} filename
343
+ * @param {number | null} startOffset
344
+ * @returns
345
+ * @private
346
+ */
347
+ lexCodeToken(code, line, filename, startOffset) {
348
+ // 単語に分割
349
+ let tokens = this.rawtokenize(code, line, filename, '');
350
+ // 文字列内位置からファイル内位置へ変換
351
+ if (startOffset === null) {
352
+ for (const token of tokens) {
353
+ token.startOffset = undefined;
354
+ token.endOffset = undefined;
355
+ }
356
+ }
357
+ else {
358
+ for (const token of tokens) {
359
+ if (token.startOffset !== undefined) {
360
+ token.startOffset += startOffset;
361
+ }
362
+ if (token.endOffset !== undefined) {
363
+ token.endOffset += startOffset;
364
+ }
365
+ }
366
+ }
367
+ // convertTokenで消されるコメントのトークンを残す
368
+ const commentTokens = tokens.filter((t) => t.type === 'line_comment' || t.type === 'range_comment')
369
+ .map((v) => ({ ...v })); // clone
370
+ tokens = this.converttoken(tokens, false, filename);
371
+ return { tokens, commentTokens };
372
+ }
373
+ /**
374
+ * 再帰的にrequire文を置換する。
375
+ * .jsであれば削除し、.nako3であればそのファイルのトークン列で置換する。
376
+ * @param {TokenWithSourceMap[]} tokens
377
+ * @param {Set<string>} [includeGuard]
378
+ * @returns {Token[]} 削除された取り込み文のトークン
379
+ */
380
+ replaceRequireStatements(tokens, includeGuard = new Set()) {
381
+ /** @type {TokenWithSourceMap[]} */
382
+ const deletedTokens = [];
383
+ for (const r of NakoCompiler.listRequireStatements(tokens).reverse()) {
384
+ // C言語のinclude guardと同じ仕組みで無限ループを防ぐ。
385
+ if (includeGuard.has(r.value)) {
386
+ deletedTokens.push(...tokens.splice((r.start || 0), (r.end || 0) - (r.start || 0)));
387
+ continue;
388
+ }
389
+ const filePath = Object.keys(this.dependencies).find((key) => this.dependencies[key].alias.has(r.value));
390
+ if (filePath === undefined) {
391
+ if (!r.firstToken) {
392
+ throw new Error(`ファイル『${r.value}』が読み込まれていません。`);
393
+ }
394
+ throw new NakoLexerError(`ファイル『${r.value}』が読み込まれていません。`, r.firstToken.startOffset || 0, r.firstToken.endOffset || 0, r.firstToken.line, r.firstToken.file);
395
+ }
396
+ this.dependencies[filePath].addPluginFile();
397
+ const children = cloneAsJSON(this.dependencies[filePath].tokens);
398
+ includeGuard.add(r.value);
399
+ deletedTokens.push(...this.replaceRequireStatements(children, includeGuard));
400
+ deletedTokens.push(...tokens.splice(r.start || 0, (r.end || 0) - (r.start || 0), ...children));
401
+ }
402
+ return deletedTokens;
403
+ }
404
+ /**
405
+ * replaceRequireStatementsのシンタックスハイライト用の実装。
406
+ * @param {TokenWithSourceMap[]} tokens
407
+ * @returns {TokenWithSourceMap[]} 削除された取り込み文のトークン
408
+ */
409
+ removeRequireStatements(tokens) {
410
+ /** @type {TokenWithSourceMap[]} */
411
+ const deletedTokens = [];
412
+ for (const r of NakoCompiler.listRequireStatements(tokens).reverse()) {
413
+ // プラグイン命令のシンタックスハイライトのために、addPluginFileを呼んで関数のリストをthis.dependencies[filePath].funclistに保存させる。
414
+ const filePath = Object.keys(this.dependencies).find((key) => this.dependencies[key].alias.has(r.value));
415
+ if (filePath !== undefined) {
416
+ this.dependencies[filePath].addPluginFile();
417
+ }
418
+ // 全ての取り込み文を削除する。そうしないとトークン化に時間がかかりすぎる。
419
+ deletedTokens.push(...tokens.splice(r.start || 0, (r.end || 0) - (r.start || 0)));
420
+ }
421
+ return deletedTokens;
422
+ }
423
+ /**
424
+ * @param {string} code
425
+ * @param {string} filename
426
+ * @param {string} [preCode]
427
+ */
428
+ lex(code, filename = 'main.nako3', preCode = '', syntaxHighlighting = false) {
429
+ // 単語に分割
430
+ let tokens = this.rawtokenize(code, 0, filename, preCode);
431
+ // require文を再帰的に置換する
432
+ const requireStatementTokens = syntaxHighlighting ? this.removeRequireStatements(tokens) : this.replaceRequireStatements(tokens, undefined);
433
+ for (const t of requireStatementTokens) {
434
+ if (t.type === 'word' || t.type === 'not') {
435
+ t.type = 'require';
436
+ }
437
+ }
438
+ if (requireStatementTokens.length >= 3) {
439
+ // modList を更新
440
+ for (let i = 0; i < requireStatementTokens.length; i += 3) {
441
+ let modName = requireStatementTokens[i + 1].value;
442
+ modName = NakoLexer.filenameToModName(modName);
443
+ if (this.lexer.modList.indexOf(modName) < 0) {
444
+ this.lexer.modList.push(modName);
445
+ }
446
+ }
447
+ }
448
+ // convertTokenで消されるコメントのトークンを残す
449
+ /** @type {TokenWithSourceMap[]} */
450
+ const commentTokens = tokens.filter((t) => t.type === 'line_comment' || t.type === 'range_comment')
451
+ .map((v) => ({ ...v })); // clone
452
+ tokens = this.converttoken(tokens, true, filename);
453
+ for (let i = 0; i < tokens.length; i++) {
454
+ if (tokens[i] && tokens[i].type === 'code') {
455
+ const children = this.lexCodeToken(tokens[i].value, tokens[i].line, filename, tokens[i].startOffset || 0);
456
+ commentTokens.push(...children.commentTokens);
457
+ tokens.splice(i, 1, ...children.tokens);
458
+ i--;
459
+ }
460
+ }
461
+ this.logger.trace('--- lex ---\n' + JSON.stringify(tokens, null, 2));
462
+ return { commentTokens, tokens, requireTokens: requireStatementTokens };
463
+ }
464
+ /**
465
+ * コードをパースしてASTにする
466
+ * @param code なでしこのプログラム
467
+ * @param filename
468
+ * @param [preCode]
469
+ * @return Ast
470
+ */
471
+ parse(code, filename, preCode = '') {
472
+ // 関数を字句解析と構文解析に登録
473
+ this.lexer.setFuncList(this.funclist);
474
+ this.parser.setFuncList(this.funclist);
475
+ const lexerOutput = this.lex(code, filename, preCode);
476
+ // 構文木を作成
477
+ /** @type {Ast} */
478
+ let ast;
479
+ try {
480
+ this.parser.genMode = 'sync'; // set default
481
+ ast = this.parser.parse(lexerOutput.tokens, filename);
482
+ }
483
+ catch (err) {
484
+ if (typeof err.startOffset !== 'number') {
485
+ throw NakoSyntaxError.fromNode(err.message, lexerOutput.tokens[this.parser.getIndex()]);
486
+ }
487
+ throw err;
488
+ }
489
+ this.usedFuncs = this.getUsedFuncs(ast);
490
+ this.logger.trace('--- ast ---\n' + JSON.stringify(ast, null, 2));
491
+ return ast;
492
+ }
493
+ /**
494
+ * @param {Ast} ast
495
+ */
496
+ getUsedFuncs(ast) {
497
+ const queue = [ast];
498
+ this.usedFuncs = new Set();
499
+ while (queue.length > 0) {
500
+ const ast_ = queue.pop();
501
+ if (ast_ !== null && ast_ !== undefined && ast_.block !== null && ast_.block !== undefined) {
502
+ this.getUsedAndDefFuncs(queue, JSON.parse(JSON.stringify(ast_.block)));
503
+ }
504
+ }
505
+ return this.deleteUnNakoFuncs();
506
+ }
507
+ getUsedAndDefFuncs(astQueue, blockQueue) {
508
+ while (blockQueue.length > 0) {
509
+ const block = blockQueue.pop();
510
+ if (block !== null && block !== undefined) {
511
+ this.getUsedAndDefFunc(block, astQueue, blockQueue);
512
+ }
513
+ }
514
+ }
515
+ getUsedAndDefFunc(block, astQueue, blockQueue) {
516
+ if (['func', 'func_pointer'].includes(block.type) && block.name !== null && block.name !== undefined) {
517
+ this.usedFuncs.add(block.name);
518
+ }
519
+ astQueue.push([block, block.block]);
520
+ blockQueue.push.apply(blockQueue, [block.value].concat(block.args));
521
+ }
522
+ deleteUnNakoFuncs() {
523
+ for (const func of this.usedFuncs) {
524
+ if (!this.commandlist.has(func)) {
525
+ this.usedFuncs.delete(func);
526
+ }
527
+ }
528
+ return this.usedFuncs;
529
+ }
530
+ /**
531
+ * プログラムをコンパイルしてランタイム用のJavaScriptのコードを返す
532
+ * @param code コード (なでしこ)
533
+ * @param filename
534
+ * @param isTest テストかどうか
535
+ * @param preCode
536
+ */
537
+ compile(code, filename, isTest = false, preCode = '') {
538
+ const opt = new CompilerOptions();
539
+ opt.testOnly = isTest;
540
+ opt.preCode = preCode;
541
+ const res = this.compileFromCode(code, filename, opt);
542
+ return res.runtimeEnv;
543
+ }
544
+ /** parse & generate */
545
+ compileFromCode(code, filename, options) {
546
+ if (filename === '') {
547
+ filename = 'main.nako3';
548
+ }
549
+ try {
550
+ if (options.resetEnv) {
551
+ this.reset();
552
+ }
553
+ if (options.resetAll) {
554
+ this.clearPlugins();
555
+ }
556
+ // onBeforeParse
557
+ this.eventList.filter(o => o.eventName === 'beforeParse').map(e => e.callback(code));
558
+ const ast = this.parse(code, filename, options.preCode);
559
+ // onBeforeGenerate
560
+ this.eventList.filter(o => o.eventName === 'beforeGenerate').map(e => e.callback(ast));
561
+ // generate
562
+ const outCode = this.generateCode(ast, new NakoGenOptions(options.testOnly));
563
+ // onAfterGenerate
564
+ this.eventList.filter(o => o.eventName === 'afterGenerate').map(e => e.callback(outCode));
565
+ return outCode;
566
+ }
567
+ catch (e) {
568
+ this.logger.error(e);
569
+ throw e;
570
+ }
571
+ }
572
+ /**
573
+ * プログラムをコンパイルしてJavaScriptのコードオブジェクトを返す
574
+ * @param {AST} ast
575
+ * @param opt テストかどうか
576
+ * @return {Object}
577
+ */
578
+ generateCode(ast, opt) {
579
+ // Select Code Generator #637
580
+ switch (ast.genMode) {
581
+ // ノーマルモード
582
+ case 'sync':
583
+ return generateJS(this, ast, opt);
584
+ // 『!非同期モード』は非推奨
585
+ case '非同期モード':
586
+ this.logger.warn('『!非同期モード』の利用は非推奨です。[詳細](https://github.com/kujirahand/nadesiko3/issues/1164)');
587
+ return NakoGenASync.generate(this, ast, opt.isTest);
588
+ default:
589
+ throw new Error(`コードジェネレータの「${ast.genMode}」はサポートされていません。`);
590
+ }
591
+ }
592
+ /** (非推奨)
593
+ * @param code
594
+ * @param fname
595
+ * @param isReset
596
+ * @param isTest テストかどうか。stringの場合は1つのテストのみ。
597
+ * @param [preCode]
598
+ */
599
+ async _run(code, fname, isReset, isTest, preCode = '') {
600
+ const opts = new CompilerOptions({
601
+ resetEnv: isReset,
602
+ resetAll: isReset,
603
+ testOnly: isTest,
604
+ preCode
605
+ });
606
+ return this._runEx(code, fname, opts);
607
+ }
608
+ /** 各プラグインをリセットする */
609
+ clearPlugins() {
610
+ // 他に実行している「なでしこ」があればクリアする
611
+ this.__globals.forEach((sys) => {
612
+ sys.reset();
613
+ });
614
+ this.__globals = []; // clear
615
+ }
616
+ /**
617
+ * 環境を指定してJavaScriptのコードを実行する
618
+ * @param code JavaScriptのコード
619
+ * @param nakoGlobal 実行環境
620
+ */
621
+ evalJS(code, nakoGlobal) {
622
+ // 実行前に環境を初期化するイベントを実行(beforeRun)
623
+ this.eventList.filter(o => o.eventName === 'beforeRun').map(e => e.callback(nakoGlobal));
624
+ // eslint-disable-next-line no-new-func
625
+ const f = new Function(code);
626
+ f.apply(nakoGlobal);
627
+ // 実行後に終了イベントを実行(finish)
628
+ this.eventList.filter(o => o.eventName === 'finish').map(e => e.callback(nakoGlobal));
629
+ }
630
+ /**
631
+ * 同期的になでしこのプログラムcodeを実行する
632
+ * @param code なでしこのプログラム
633
+ * @param filename ファイル名
634
+ * @param options オプション
635
+ * @returns 実行に利用したグローバルオブジェクト
636
+ */
637
+ runSync(code, filename, options = new CompilerOptions()) {
638
+ // コンパイル
639
+ const out = this.compileFromCode(code, filename, options);
640
+ // 実行前に環境を生成
641
+ const nakoGlobal = this.getNakoGlobal(options, out.gen);
642
+ // 実行
643
+ this.evalJS(out.runtimeEnv, nakoGlobal);
644
+ return nakoGlobal;
645
+ }
646
+ /**
647
+ * 非同期になでしこのプログラムcodeを実行する
648
+ * @param code なでしこのプログラム
649
+ * @param filename ファイル名
650
+ * @param options オプション
651
+ * @returns 実行に利用したグローバルオブジェクト
652
+ */
653
+ async runAsync(code, filename, options = new CompilerOptions()) {
654
+ // コンパイル
655
+ const out = this.compileFromCode(code, filename, options);
656
+ // 実行前に環境を生成
657
+ const nakoGlobal = this.getNakoGlobal(options, out.gen);
658
+ // 実行
659
+ this.evalJS(out.runtimeEnv, nakoGlobal);
660
+ return nakoGlobal;
661
+ }
662
+ getNakoGlobal(options, gen) {
663
+ // オプションを参照
664
+ let g = options.nakoGlobal;
665
+ if (!g) {
666
+ // 空ならば前回の値を参照(リセットするなら新規で作成する)
667
+ if (this.__globals.length > 0 && options.resetAll === false && options.resetEnv === false) {
668
+ g = this.__globals[this.__globals.length - 1];
669
+ }
670
+ else {
671
+ g = new NakoGlobal(this, gen);
672
+ }
673
+ }
674
+ if (this.__globals.indexOf(g) < 0) {
675
+ this.__globals.push(g);
676
+ }
677
+ return g;
678
+ }
679
+ /**
680
+ * イベントを登録する
681
+ * @param eventName イベント名
682
+ * @param callback コールバック関数
683
+ */
684
+ addListener(eventName, callback) {
685
+ this.eventList.push({ eventName, callback });
686
+ }
687
+ /**
688
+ * テストを実行する
689
+ * @param code
690
+ * @param fname
691
+ * @param preCode
692
+ * @param testOnly
693
+ */
694
+ test(code, fname, preCode = '', testOnly = false) {
695
+ const options = new CompilerOptions();
696
+ options.preCode = preCode;
697
+ options.testOnly = testOnly;
698
+ return this.runSync(code, fname, options);
699
+ }
700
+ /**
701
+ * なでしこのプログラムを実行(他に実行しているインスタンスはそのまま)
702
+ * @param code
703
+ * @param fname
704
+ * @param [preCode]
705
+ */
706
+ run(code, fname = 'main.nako3', preCode = '') {
707
+ const options = new CompilerOptions();
708
+ options.preCode = preCode;
709
+ return this.runSync(code, fname, options);
710
+ }
711
+ /**
712
+ * JavaScriptのみで動くコードを取得する場合
713
+ * @param code
714
+ * @param filename
715
+ * @param opt
716
+ */
717
+ compileStandalone(code, filename, opt = null) {
718
+ if (opt === null) {
719
+ opt = new NakoGenOptions();
720
+ }
721
+ const ast = this.parse(code, filename);
722
+ return this.generateCode(ast, opt).standalone;
723
+ }
724
+ /**
725
+ * プラグイン・オブジェクトを追加
726
+ * @param po プラグイン・オブジェクト
727
+ * @param persistent falseのとき、次以降の実行では使えない
728
+ */
729
+ addPlugin(po, persistent = true) {
730
+ // 変数のメタ情報を確認
731
+ const __v0 = this.__varslist[0];
732
+ if (__v0.meta === undefined) {
733
+ __v0.meta = {};
734
+ }
735
+ // プラグインの値をオブジェクトにコピー
736
+ for (const key in po) {
737
+ const v = po[key];
738
+ this.funclist[key] = v;
739
+ if (persistent) {
740
+ this.pluginFunclist[key] = JSON.parse(JSON.stringify(v));
741
+ }
742
+ if (v.type === 'func') {
743
+ __v0[key] = v.fn;
744
+ }
745
+ else if (v.type === 'const' || v.type === 'var') {
746
+ __v0[key] = v.value;
747
+ __v0.meta[key] = {
748
+ readonly: (v.type === 'const')
749
+ };
750
+ }
751
+ else {
752
+ console.error('[プラグイン追加エラー]', v);
753
+ throw new Error('プラグインの追加でエラー。');
754
+ }
755
+ // コマンドを登録するか?
756
+ if (key === '初期化' || key.substring(0, 1) === '!') { // 登録しない関数名
757
+ continue;
758
+ }
759
+ this.commandlist.add(key);
760
+ }
761
+ }
762
+ /**
763
+ * プラグイン・オブジェクトを追加(ブラウザ向け)
764
+ * @param objName オブジェクト名
765
+ * @param po 関数リスト
766
+ * @param persistent falseのとき、次以降の実行では使えない
767
+ */
768
+ addPluginObject(objName, po, persistent = true) {
769
+ this.__module[objName] = po;
770
+ this.pluginfiles[objName] = '*';
771
+ // 初期化をチェック
772
+ if (typeof (po['初期化']) === 'object') {
773
+ const def = po['初期化'];
774
+ delete po['初期化'];
775
+ const initKey = `!${objName}:初期化`;
776
+ po[initKey] = def;
777
+ }
778
+ // メタ情報をチェック (#1034)
779
+ if (po.meta && po.meta.value && typeof (po.meta) === 'object') {
780
+ const meta = po.meta;
781
+ delete po.meta;
782
+ const pluginName = meta.value.pluginName || objName;
783
+ const metaKey = `__${pluginName}`.replace('-', '__');
784
+ po[metaKey] = meta;
785
+ }
786
+ this.addPlugin(po, persistent);
787
+ }
788
+ /**
789
+ * プラグイン・ファイルを追加(Node.js向け)
790
+ * @param objName オブジェクト名
791
+ * @param fpath ファイルパス
792
+ * @param po 登録するオブジェクト
793
+ * @param persistent falseのとき、次以降の実行では使えない
794
+ */
795
+ addPluginFile(objName, fpath, po, persistent = true) {
796
+ // Windowsのパスがあると、JSファイル書き出しでエラーになるので、置換する
797
+ if (objName.indexOf('\\') >= 0) {
798
+ objName = objName.replace(/\\/g, '/');
799
+ }
800
+ this.addPluginObject(objName, po, persistent);
801
+ if (this.pluginfiles[objName] === undefined) {
802
+ this.pluginfiles[objName] = fpath;
803
+ }
804
+ }
805
+ /**
806
+ * 関数を追加する
807
+ * @param {string} key 関数名
808
+ * @param {string[][]} josi 助詞
809
+ * @param {Function} fn 関数
810
+ * @param {boolean} returnNone 値を返す関数の場合はfalseを指定
811
+ * @param {boolean} asyncFn Promiseを返す関数かを指定
812
+ */
813
+ addFunc(key, josi, fn, returnNone = true, asyncFn = false) {
814
+ this.funclist[key] = { josi, fn, type: 'func', return_none: returnNone, asyncFn };
815
+ this.pluginFunclist[key] = cloneAsJSON(this.funclist[key]);
816
+ this.__varslist[0][key] = fn;
817
+ }
818
+ // (非推奨) 互換性のため ... 関数を追加する
819
+ setFunc(key, josi, fn, returnNone = true, asyncFn = false) {
820
+ this.addFunc(key, josi, fn, returnNone, asyncFn);
821
+ }
822
+ /**
823
+ * プラグイン関数を参照する
824
+ * @param key プラグイン関数の関数名
825
+ * @returns プラグイン・オブジェクト
826
+ */
827
+ getFunc(key) {
828
+ return this.funclist[key];
829
+ }
830
+ /** (非推奨) 同期的になでしこのプログラムcodeを実行する */
831
+ _runEx(code, filename, opts, preCode = '', nakoGlobal = undefined) {
832
+ // コンパイル
833
+ const options = new CompilerOptions(opts);
834
+ options.preCode = preCode;
835
+ if (nakoGlobal) {
836
+ options.nakoGlobal = nakoGlobal;
837
+ }
838
+ return this.runSync(code, filename, options);
839
+ }
840
+ /** (非推奨) 同期的に実行
841
+ * @param code
842
+ * @param fname
843
+ * @param opts
844
+ * @param [preCode]
845
+ */
846
+ runEx(code, fname, opts, preCode = '') {
847
+ return this._runEx(code, fname, opts, preCode);
848
+ }
849
+ /**
850
+ * (非推奨) なでしこのプログラムを実行(他に実行しているインスタンスもリセットする)
851
+ * @param code
852
+ * @param fname
853
+ * @param [preCode]
854
+ */
855
+ async runReset(code, fname = 'main.nako3', preCode = '') {
856
+ return this._runEx(code, fname, { resetAll: true, resetEnv: true }, preCode);
857
+ }
858
+ }