UNPKG

nadesiko3

Version:
781 lines (780 loc) 33.4 kB
/* eslint-disable @typescript-eslint/no-explicit-any */ // なでしこの字句解析を行う // 既に全角半角を揃えたコードに対して字句解析を行う import { opPriority } from './nako_parser_const.mjs'; // 予約語句 // (memo)「回」「間」「繰返」「反復」「抜」「続」「戻」「代入」などは _replaceWord で word から変換 /** @types {Record<string, string>} */ import reservedWords from './nako_reserved_words.mjs'; import { isIndentChars } from './nako_indent_chars.mjs'; // 助詞の一覧 import { josiRE, removeJosiMap, tararebaMap, josiListExport } from './nako_josi_list.mjs'; // 字句解析ルールの一覧 import { rules, unitRE, cssUnitRE } from './nako_lex_rules.mjs'; import { NakoLexerError, InternalLexerError } from './nako_errors.mjs'; export class NakoLexer { /** * @param logger */ constructor(logger) { this.logger = logger; // 字句解析した際,確認された関数の一覧 this.funclist = new Map(); this.modList = []; // 字句解析した際,取り込むモジュール一覧 --- nako3::lex で更新される this.result = []; this.modName = 'main.nako3'; // モジュール名 this.moduleExport = new Map(); this.reservedWords = Array.from(reservedWords.keys()); // for plugin_system::予約語一覧取得 this.josiList = josiListExport; // for plugin_system::助詞一覧取得 } /** 関数一覧をセット */ setFuncList(listMap) { this.funclist = listMap; } /** モジュール公開既定値一覧をセット */ setModuleExport(exportObj) { this.moduleExport = exportObj; } /** * @param tokens * @param {boolean} isFirst * @param {string} filename */ replaceTokens(tokens, isFirst, filename) { this.result = tokens; this.modName = NakoLexer.filenameToModName(filename); // 関数の定義があれば funclist を更新 NakoLexer.preDefineFunc(tokens, this.logger, this.funclist, this.moduleExport); this._replaceWord(this.result); if (isFirst) { if (this.result.length > 0) { const eof = this.result[this.result.length - 1]; this.result.push({ type: 'eol', line: eof.line, column: 0, indent: -1, file: eof.file, josi: '', value: '---', startOffset: eof.startOffset, endOffset: eof.endOffset, rawJosi: '' }); // 改行 this.result.push({ type: 'eof', line: eof.line, indent: -1, column: 0, file: eof.file, josi: '', value: '', startOffset: eof.startOffset, endOffset: eof.endOffset, rawJosi: '' }); // ファイル末尾 } else { this.result.push({ type: 'eol', line: 0, column: 0, indent: -1, file: '', josi: '', value: '---', startOffset: 0, endOffset: 0, rawJosi: '' }); // 改行 this.result.push({ type: 'eof', line: 0, column: 0, indent: -1, file: '', josi: '', value: '', startOffset: 0, endOffset: 0, rawJosi: '' }); // ファイル末尾 } } return this.result; } /** * ファイル内で定義されている関数名を列挙する。結果はfunclistに書き込む。その他のトークンの置換処理も行う。 * シンタックスハイライトの処理から呼び出すためにstaticメソッドにしている。 */ static preDefineFunc(tokens, logger, funclist, moduleexport) { // 関数を先読みして定義 let i = 0; let isFuncPointer = false; const readArgs = () => { const args = []; const keys = {}; if (tokens[i].type !== '(') { return []; } i++; while (tokens[i]) { const t = tokens[i]; i++; if (t.type === ')') { break; } if (t.type === 'func') { isFuncPointer = true; } else if (t.type !== '|' && t.type !== 'comma') { if (isFuncPointer) { t.funcPointer = true; isFuncPointer = false; } args.push(t); if (!keys[t.value]) { keys[t.value] = []; } keys[t.value].push(t.josi); } } const varnames = []; const funcPointers = []; const result = []; const already = {}; for (const arg of args) { if (!already[arg.value]) { const josi = keys[arg.value]; result.push(josi); varnames.push(arg.value); if (arg.funcPointer) { funcPointers.push(arg.value); } else { funcPointers.push(null); } already[arg.value] = true; } } return [result, varnames, funcPointers]; }; // トークンを一つずつ確認 while (i < tokens.length) { // タイプの置換 const t = tokens[i]; if (t.type === 'not' && tokens.length - i > 3) { let prevToken = { type: 'eol' }; if (i >= 1) { prevToken = tokens[i - 1]; } if (prevToken.type === 'eol') { let nextToken = tokens[i + 1]; if (nextToken.type === 'word' && nextToken.value === 'モジュール公開既定値') { nextToken.type = 'モジュール公開既定値'; nextToken = tokens[i + 2]; if (nextToken.type === 'string' && nextToken.value === '非公開') { const modName = NakoLexer.filenameToModName(t.file); moduleexport.set(modName, false); i += 3; continue; } else if (nextToken.type === 'string' && nextToken.value === '公開') { const modName = NakoLexer.filenameToModName(t.file); moduleexport.set(modName, true); i += 3; continue; } } } } // 無名関数の定義:「xxには**」があった場合 ... 暗黙的な関数定義とする if ((t.type === 'word' && t.josi === 'には') || (t.type === 'word' && t.josi === 'は~')) { t.josi = 'には'; tokens.splice(i + 1, 0, { type: 'def_func', value: '関数', indent: t.indent, line: t.line, column: t.column, file: t.file, josi: '', startOffset: t.endOffset, endOffset: t.endOffset, rawJosi: '', tag: '無名関数' }); i++; continue; } // 永遠に繰り返す→永遠の間に置換 #1686 if (t.type === 'word' && t.value === '永遠' && t.josi === 'に') { const t2 = tokens[i + 1]; if (t2.value === '繰返') { t2.value = '間'; t2.josi = 'の'; } i++; continue; } // N回をN|回に置換 if (t.type === 'word' && t.josi === '' && t.value.length >= 2) { if (t.value.match(/回$/)) { t.value = t.value.substring(0, t.value.length - 1); // N回を挿入 if (!t.endOffset) { t.endOffset = 1; } const kai = { type: '回', value: '回', indent: t.indent, line: t.line, column: t.column, file: t.file, josi: '', startOffset: t.endOffset - 1, endOffset: t.endOffset, rawJosi: '' }; tokens.splice(i + 1, 0, kai); t.endOffset--; i++; } } // 予約語の置換 if (t.type === 'word') { const rtype = reservedWords.get(t.value); if (rtype) { t.type = rtype; } if (t.value === 'そう') { t.value = 'それ'; } } // 関数定義の確認 if (t.type !== 'def_test' && t.type !== 'def_func') { i++; continue; } // 無名関数か普通関数定義かを判定する (1つ前が改行かどうかで判定) let isMumei = true; let prevToken = { type: 'eol' }; if (i >= 1) { prevToken = tokens[i - 1]; } if (prevToken.type === 'eol') { isMumei = false; } // 関数名や引数を得る const defToken = t; i++; // skip "●" or "関数" let josi = []; let varnames = []; let funcPointers = []; let funcName = ''; let funcNameToken = null; let isExport = null; // 関数の属性指定 if (tokens[i] && tokens[i].type === '{') { i++; const attr = tokens[i] && tokens[i].type === 'word' ? tokens[i].value : ''; if (attr === '公開') { isExport = true; } else if (attr === '非公開') { isExport = false; } else if (attr === 'エクスポート') { isExport = true; } else { logger.warn(`不明な関数属性『${attr}』が指定されています。`); } i++; if (tokens[i] && tokens[i].type === '}') { i++; } } // 関数名の前に引数定義 if (tokens[i] && tokens[i].type === '(') { [josi, varnames, funcPointers] = readArgs(); } // 関数名を得る if (!isMumei && tokens[i] && tokens[i].type === 'word') { funcNameToken = tokens[i++]; funcName = funcNameToken.value; } // 関数名の後で引数定義 if (josi.length === 0 && tokens[i] && tokens[i].type === '(') { [josi, varnames, funcPointers] = readArgs(); } // 名前のある関数定義ならば関数テーブルに関数名を登録 // 無名関数は登録しないように気をつける if (funcName !== '' && funcNameToken) { const modName = NakoLexer.filenameToModName(t.file); funcName = modName + '__' + funcName; if (funclist.has(funcName)) { // 関数の二重定義を警告 // main__は省略 #1223 const dispName = funcName.replace(/^main__/, ''); logger.warn(`関数『${dispName}』は既に定義されています。`, defToken); } funcNameToken.value = funcName; funclist.set(funcName, { type: 'func', josi, fn: null, asyncFn: false, isExport, varnames, funcPointers }); } // 無名関数のために const metaValue = { 'type': 'func', josi, varnames, funcPointers }; defToken.meta = metaValue; } } /** 文字列を{と}の部分で分割する。中括弧が対応していない場合nullを返す。 */ splitStringEx(code) { /** @type {string[]} */ const list = []; // "A{B}C{D}E" -> ["A", "B}C", "D}E"] -> ["A", "B", "C", "D", "E"] // "A{B}C}D{E}F" -> ["A", "B}C}D", "E}F"] -> ["A", "B", "C}D", "E", "F"] const arr = code.split(/[{{]/); list.push(arr[0]); for (const s of arr.slice(1)) { const end = s.replace('}', '}').indexOf('}'); if (end === -1) { return null; } list.push(s.slice(0, end), s.slice(end + 1)); } return list; } _replaceWord(tokens) { let comment = []; let i = 0; let isFuncPointer = false; const namespaceStack = []; const getLastType = () => { if (i <= 0) { return 'eol'; } return tokens[i - 1].type; }; let modSelf = (tokens.length > 0) ? NakoLexer.filenameToModName(tokens[0].file) : 'main'; while (i < tokens.length) { const t = tokens[i]; // モジュール名の変更に対応 if ((t.type === 'word' || t.type === 'func') && t.value === '名前空間設定') { if (isFuncPointer) { throw new InternalLexerError('名前空間設定の関数参照を取得することはできません。', t.startOffset === undefined ? 0 : t.startOffset, t.endOffset === undefined ? 0 : t.endOffset, t.line, t.file); } namespaceStack.push(modSelf); modSelf = tokens[i - 1].value; } if ((t.type === 'word' || t.type === 'func') && t.value === '名前空間ポップ') { if (isFuncPointer) { throw new InternalLexerError('名前空間ポップの関数参照を取得することはできません。', t.startOffset === undefined ? 0 : t.startOffset, t.endOffset === undefined ? 0 : t.endOffset, t.line, t.file); } const space = namespaceStack.pop(); if (space) { modSelf = space; } } // 関数を強制的に置換( word => func ) if (t.type === 'word' && t.value !== 'それ') { // 関数を変換 const funcName = t.value; if (funcName.indexOf('__') < 0) { // 自身のモジュール名を検索 const gname1 = `${modSelf}__${funcName}`; const gfo1 = this.funclist.get(gname1); if (gfo1 && gfo1.type === 'func') { const tt = t; tt.type = isFuncPointer ? 'func_pointer' : 'func'; tt.meta = gfo1; tt.value = gname1; if (isFuncPointer) { isFuncPointer = false; tokens.splice(i - 1, 1); } continue; } // モジュール関数を置換 for (const mod of this.modList) { const gname = `${mod}__${funcName}`; const gfo = this.funclist.get(gname); const exportDefault = this.moduleExport.get(mod); if (gfo && gfo.type === 'func' && (gfo.isExport === true || (gfo.isExport !== false && exportDefault !== false))) { const tt = t; tt.type = isFuncPointer ? 'func_pointer' : 'func'; tt.meta = gfo; tt.value = gname; if (isFuncPointer) { isFuncPointer = false; tokens.splice(i - 1, 1); } break; } } } const fo = this.funclist.get(funcName); if (fo && fo.type === 'func') { const tt = t; tt.type = isFuncPointer ? 'func_pointer' : 'func'; tt.meta = fo; if (isFuncPointer) { isFuncPointer = false; tokens.splice(i - 1, 1); continue; } } } // 関数ポインタの前置詞を検出 if (isFuncPointer) { // 無効な関数参照の指定がある。 } isFuncPointer = false; if (t.type === 'func' && t.value === '{関数}') { i++; isFuncPointer = true; continue; } // 数字につくマイナス記号を判定 // (ng) 5 - 3 || word - 3 // (ok) (行頭)-3 || 1 * -3 || Aに -3を 足す if (t.type === '-' && tokens[i + 1]) { const tokenType = tokens[i + 1].type; if (tokenType === 'number' || tokenType === 'bigint') { // 一つ前の語句が、(行頭|演算子|助詞付きの語句)なら 負数である const ltype = getLastType(); if (ltype === 'eol' || opPriority[ltype] || tokens[i - 1].josi !== '') { tokens.splice(i, 1); // remove '-' if (tokenType === 'number') { tokens[i].value *= -1; } else { tokens[i].value = '-' + tokens[i].value; } } } } // 助詞の「は」を = に展開 if (t.josi === undefined) { t.josi = ''; } if (t.josi === 'は') { if (!t.rawJosi) { t.rawJosi = t.josi; } const startOffset = (t.endOffset === undefined) ? undefined : t.endOffset - t.rawJosi.length; tokens.splice(i + 1, 0, { type: 'eq', indent: t.indent, line: t.line, column: t.column, file: t.file, startOffset, endOffset: t.endOffset, josi: '', rawJosi: '', value: undefined }); i += 2; t.josi = t.rawJosi = ''; t.endOffset = startOffset; continue; } // 「とは」を一つの単語にする if (t.josi === 'とは') { if (!t.rawJosi) { t.rawJosi = t.josi; } const startOffset = t.endOffset === undefined ? undefined : t.endOffset - t.rawJosi.length; tokens.splice(i + 1, 0, { type: 'とは', indent: t.indent, line: t.line, column: t.column, file: t.file, startOffset, endOffset: t.endOffset, josi: '', rawJosi: '', value: undefined }); t.josi = t.rawJosi = ''; t.endOffset = startOffset; i += 2; continue; } // 助詞のならばをトークンとする if (tararebaMap[t.josi]) { const josi = (t.josi === 'でなければ' || t.josi === 'なければ') ? 'でなければ' : 'ならば'; if (!t.rawJosi) { t.rawJosi = josi; } const startOffset = t.endOffset === undefined ? undefined : t.endOffset - t.rawJosi.length; tokens.splice(i + 1, 0, { type: 'ならば', value: josi, indent: t.indent, line: t.line, column: t.column, file: t.file, startOffset, endOffset: t.endOffset, josi: '', rawJosi: '' }); t.josi = t.rawJosi = ''; t.endOffset = startOffset; i += 2; continue; } // '_' + 改行 を飛ばす (演算子直後に改行を入れたい場合に使う) if (t.type === '_eol') { tokens.splice(i, 1); continue; } // コメントを飛ばす if (t.type === 'line_comment' || t.type === 'range_comment') { comment.push(t.value); tokens.splice(i, 1); continue; } // 改行にコメントを埋め込む if (t.type === 'eol') { t.value = comment.join('/'); comment = []; } i++; } } /** * インデントの個数を数える * @returns 戻り値として[インデント数, 読み飛ばすべき文字数]を返す */ countIndent(src) { let indent = 0; for (let i = 0; i < src.length; i++) { const c = src.charAt(i); const n = isIndentChars(c); if (n === 0) { return [indent, i]; } indent += n; } return [indent, src.length]; } /** * ソースコードをトークンに分割する * @param src なでしこのソースコード * @param line 先頭行の行番号 * @param filename ファイル名 */ tokenize(src, line, filename) { const srcLength = src.length; const result = []; let columnCurrent; let lineCurrent; let column = 1; let isDefTest = false; let indent = 0; // 最初にインデントを数える const ia = this.countIndent(src); indent = ia[0]; // インデント数 src = src.substring(ia[1]); // 読み飛ばす文字数 column += ia[1]; while (src !== '') { let ok = false; // 各ルールについて for (const rule of rules) { // 正規表現でマッチ const m = rule.pattern.exec(src); if (!m) { continue; } let ruleName = rule.name; ok = true; // 空白ならスキップ if (rule.name === 'space') { column += m[0].length; src = src.substring(m[0].length); continue; } // マッチしたルールがコールバックを持つなら if (rule.cbParser) { // コールバックを呼ぶ let rp; if (isDefTest && rule.name === 'word') { rp = rule.cbParser(src, false); } else { try { rp = rule.cbParser(src); } catch (e) { throw new NakoLexerError(e.message, srcLength - src.length, srcLength - src.length + 1, line, filename); } } if (rule.name === 'string_ex') { // 展開あり文字列 → aaa{x}bbb{x}cccc const list = this.splitStringEx(rp.res); if (list === null) { throw new InternalLexerError('展開あり文字列で値の埋め込み{...}が対応していません。', srcLength - src.length, srcLength - rp.src.length, line, filename); } let offset = 0; for (let i = 0; i < list.length; i++) { const josi = (i === list.length - 1) ? rp.josi : ''; if (i % 2 === 0) { result.push({ type: 'string', value: list[i], file: filename, josi, indent, line, column, preprocessedCodeOffset: srcLength - src.length + offset, preprocessedCodeLength: list[i].length + 2 + josi.length }); // 先頭なら'"...{'、それ以外なら'}...{'、最後は何でも良い offset += list[i].length + 2; } else { result.push({ type: '&', value: '&', josi: '', indent, file: filename, line, column, preprocessedCodeOffset: srcLength - src.length + offset, preprocessedCodeLength: 0 }); result.push({ type: '(', value: '(', josi: '', indent, file: filename, line, column, preprocessedCodeOffset: srcLength - src.length + offset, preprocessedCodeLength: 0 }); result.push({ type: 'code', value: list[i], josi: '', indent, file: filename, line, column, preprocessedCodeOffset: srcLength - src.length + offset, preprocessedCodeLength: list[i].length }); result.push({ type: ')', value: ')', josi: '', indent, file: filename, line, column, preprocessedCodeOffset: srcLength - src.length + offset + list[i].length, preprocessedCodeLength: 0 }); result.push({ type: '&', value: '&', josi: '', indent, file: filename, line, column, preprocessedCodeOffset: srcLength - src.length + offset + list[i].length, preprocessedCodeLength: 0 }); offset += list[i].length; } } line += rp.numEOL; column += src.length - rp.src.length; src = rp.src; if (rp.numEOL > 0) { column = 1; } break; } columnCurrent = column; column += src.length - rp.src.length; result.push({ type: rule.name, value: rp.res, josi: rp.josi, indent, line, column: columnCurrent, file: filename, preprocessedCodeOffset: srcLength - src.length, preprocessedCodeLength: src.length - rp.src.length }); src = rp.src; line += rp.numEOL; if (rp.numEOL > 0) { column = 1; } break; } // ソースを進める前に位置を計算 const srcOffset = srcLength - src.length; // 値を変換する必要があるか? let value = m[0]; if (rule.cb) { value = rule.cb(value); } // ソースを進める columnCurrent = column; lineCurrent = line; column += m[0].length; src = src.substring(m[0].length); // 改行の時の処理 if ((rule.name === 'eol' && value === '\n') || rule.name === '_eol') { value = line++; column = 1; } // 数値なら単位を持つか? --- #994 if (rule.name === 'number') { // 単位があれば読み飛ばす const um = unitRE.exec(src); if (um) { src = src.substring(um[0].length); column += m[0].length; } // CSSの単位なら自動的に文字列として認識させる #1811 const cssUnit = cssUnitRE.exec(src); if (cssUnit) { ruleName = 'string'; src = src.substring(cssUnit[0].length); column += m[0].length; value += cssUnit[0]; } } let josi = ''; if (rule.readJosi) { // 正規表現で助詞があるか読み取る const j = josiRE.exec(src); if (j) { column += j[0].length; josi = j[0].replace(/^\s+/, ''); src = src.substring(j[0].length); // 助詞の直後にあるカンマを無視 #877 if (src.charAt(0) === ',') { src = src.substring(1); } // 「**である」なら削除 #939 #974 if (removeJosiMap[josi]) { josi = ''; } // 「もの」構文 (#1614) if (josi.substring(0, 2) === 'もの') { josi = josi.substring(2); } } } switch (ruleName) { case 'def_test': { isDefTest = true; break; } case 'eol': { // eolの処理はほかに↑と↓にある isDefTest = false; break; } default: { break; } } // ここまで‰(#682) を処理 if (ruleName === 'dec_lineno') { line--; continue; } result.push({ type: ruleName, value, indent, line: lineCurrent, column: columnCurrent, file: filename, josi, preprocessedCodeOffset: srcOffset, preprocessedCodeLength: (srcLength - src.length) - srcOffset }); // 改行のとき次の行のインデントを調べる。なお、改行の後は必ずcolumnが1になる。インデント構文のため、一行に2つ以上の文を含むときを考慮する。(core #66) if (ruleName === 'eol' && column === 1) { const ia = this.countIndent(src); indent = ia[0]; column += ia[1]; src = src.substring(ia[1]); // インデントを飛ばす } break; } if (!ok) { throw new InternalLexerError('未知の語句: ' + src.substring(0, 3) + '...', srcLength - src.length, srcLength - srcLength + 3, line, filename); } } return result; } // トークン配列をtype文字列に変換 static tokensToTypeStr(tokens, sep) { const a = tokens.map((v) => { return v.type; }); return a.join(sep); } /** * ファイル名からモジュール名へ変換 * @param {string} filename * @returns {string} */ static filenameToModName(filename) { if (!filename) { return 'main'; } // パスがあればパスを削除 filename = filename.replace(/[\\:]/g, '/'); // Windowsのpath記号を/に置換 if (filename.indexOf('/') >= 0) { const a = filename.split('/'); filename = a[a.length - 1]; } filename = filename.replace(/\.nako3?$/, ''); return filename; } }