UNPKG

nadesiko3

Version:
1,619 lines (1,526 loc) 95.4 kB
/* eslint-disable @typescript-eslint/no-explicit-any */ /** * nadesiko v3 parser */ import { opPriority, RenbunJosi, operatorList } from './nako_parser_const.mjs' import { NakoParserBase } from './nako_parser_base.mjs' import { NakoSyntaxError } from './nako_errors.mjs' import { NakoLexer } from './nako_lexer.mjs' import { FuncListItem, FuncListItemType, FuncArgs, NewEmptyToken, SourceMap } from './nako_types.mjs' import { NodeType, Ast, AstEol, AstBlocks, AstOperator, AstConst, AstLet, AstLetArray, AstIf, AstWhile, AstAtohantei, AstFor, AstForeach, AstSwitch, AstRepeatTimes, AstDefFunc, AstCallFunc, AstStrValue, AstDefVar, AstDefVarList } from './nako_ast.mjs' import { Token, TokenDefFunc, TokenCallFunc } from './nako_token.mjs' /** * 構文解析を行うクラス */ export class NakoParser extends NakoParserBase { /** * 構文解析を実行する */ parse (tokens: Token[], filename: string): Ast { this.reset() this.tokens = tokens this.modName = NakoLexer.filenameToModName(filename) this.modList.push(this.modName) // 解析処理 - 先頭から解析開始 const result = this.startParser() // 関数毎に非同期処理が必要かどうかを判定する this.isModifiedNodes = false this._checkAsyncFn(result) while (this.isModifiedNodes) { this.isModifiedNodes = false this._checkAsyncFn(result) } return result } /** パーサーの一番最初に呼び出す構文規則 */ startParser (): Ast { const b: Ast = this.ySentenceList() const c: Token|null = this.get() if (c && c.type !== 'eof') { this.logger.debug(`構文解析でエラー。${this.nodeToStr(c, { depth: 1 }, true)}の使い方が間違っています。`, c) throw NakoSyntaxError.fromNode(`構文解析でエラー。${this.nodeToStr(c, { depth: 1 }, false)}の使い方が間違っています。`, c) } return b } /** 何もしない * @returns {Ast} */ yNop (): Ast { return { type: 'nop', josi: '', ...this.peekSourceMap(), end: this.peekSourceMap() } } /** 複数文を返す */ ySentenceList(): AstBlocks { const blocks = [] let line = -1 const map = this.peekSourceMap() while (!this.isEOF()) { const n: Ast|null = this.ySentence() if (!n) { break } blocks.push(n) if (line < 0) { line = n.line } } if (blocks.length === 0) { const token = this.peek() || this.tokens[0] this.logger.debug('構文解析に失敗:' + this.nodeToStr(this.peek(), { depth: 1 }, true), token) throw NakoSyntaxError.fromNode('構文解析に失敗:' + this.nodeToStr(this.peek(), { depth: 1 }, false), token) } return { type: 'block', blocks: blocks, josi: '', ...map, end: this.peekSourceMap() } } /** 余剰スタックのレポートを作る */ makeStackBalanceReport (): string { const words: string[] = [] this.stack.forEach((t) => { let w = this.nodeToStr(t, { depth: 1 }, false) if (t.josi) { w += t.josi } words.push(w) }) const desc = words.join(',') // 最近使った関数の使い方レポートを作る #1093 let descFunc = '' const chA = 'A'.charCodeAt(0) for (const f of this.recentlyCalledFunc) { descFunc += ' - ' let no = 0 const josiA: FuncArgs | undefined = (f as FuncListItem).josi if (josiA) { for (const arg of josiA) { const ch = String.fromCharCode(chA + no) descFunc += ch if (arg.length === 1) { descFunc += arg[0] } else { descFunc += `(${arg.join('|')})` } no++ } } descFunc += f.name + '\n' } this.recentlyCalledFunc = [] return `未解決の単語があります: [${desc}]\n次の命令の可能性があります:\n${descFunc}` } yEOL(): AstEol | null { // 行末のチェック #1009 const eol = this.get() if (!eol) { return null } // 余剰スタックの確認 if (this.stack.length > 0) { const report = this.makeStackBalanceReport() throw NakoSyntaxError.fromNode(report, eol) } this.recentlyCalledFunc = [] return { type: 'eol', comment: eol.value, line: eol.line, column: eol.column, file: eol.file, } } /** @returns {Ast | null} */ ySentence (): Ast | null { const map: SourceMap = this.peekSourceMap() // 最初の語句が決まっている構文 if (this.check('eol')) { return this.yEOL() } if (this.check('もし')) { return this.yIF() } if (this.check('後判定')) { return this.yAtohantei() } if (this.check('エラー監視')) { return this.yTryExcept() } if (this.accept(['抜ける'])) { return { type: 'break', josi: '', ...map, end: this.peekSourceMap() } } if (this.accept(['続ける'])) { return { type: 'continue', josi: '', ...map, end: this.peekSourceMap() } } if (this.check('??')) { return this.yDebugPrint() } // 実行モードの指定 if (this.accept(['DNCLモード'])) { return this.yDNCLMode(1) } if (this.accept(['DNCL2モード'])) { return this.yDNCLMode(2) } if (this.accept(['not', 'string', 'モード設定'])) { return this.ySetGenMode(this.y[1].value) } if (this.accept(['not', 'モジュール公開既定値', 'eq', 'string'])) { return this.yExportDefault(this.y[3].value) } if (this.accept(['not', '厳チェック'])) { return this.ySetMode('厳しくチェック') } // (#1698) // (memo) 現状「取込」はプリプロセス段階(NakoCompiler.listRequireStatements)で処理される // if (this.accept(['require', 'string', '取込'])) { return this.yRequire() } // <廃止された構文> if (this.check('逐次実行')) { return this.yTikuji() } // 廃止 #1611 if (this.accept(['not', '非同期モード'])) { return this.yASyncMode() } // </廃止された構文> if (this.check2(['func', 'eq'])) { const word: Token = this.get() || NewEmptyToken() throw NakoSyntaxError.fromNode(`関数『${word.value}』に代入できません。`, word) } // 先読みして初めて確定する構文 if (this.accept([this.ySpeedMode])) { return this.y[0] } if (this.accept([this.yPerformanceMonitor])) { return this.y[0] } if (this.accept([this.yLet])) { return this.y[0] } if (this.accept([this.yDefTest])) { return this.y[0] } if (this.accept([this.yDefFunc])) { return this.y[0] } // 関数呼び出しの他、各種構文の実装 if (this.accept([this.yCall])) { const c1 = this.y[0] const nextToken = this.peek() if (nextToken && nextToken.type === 'ならば') { const map = this.peekSourceMap() const cond = c1 this.get() // skip ならば // もし文の条件として関数呼び出しがある場合 return this.yIfThen(cond, map) } else if (RenbunJosi.indexOf(c1.josi || '') >= 0) { // 連文をblockとして接続する(もし構文などのため) if (this.stack.length >= 1) { // スタックの余剰をチェック const report = this.makeStackBalanceReport() throw NakoSyntaxError.fromNode(report, c1) } const c2 = this.ySentence() if (c2 !== null) { return { type: 'block', blocks: [c1, c2], josi: c2.josi, ...map, end: this.peekSourceMap() } as AstBlocks } } return c1 } return null } /** [廃止] 非同期モード #11 @returns {Ast} */ yASyncMode (): Ast { this.logger.error('『非同期モード』構文は廃止されました(https://nadesi.com/v3/doc/go.php?1028)。', this.peek()) const map = this.peekSourceMap() return { type: 'eol', ...map, end: this.peekSourceMap() } } /** set DNCL mode */ yDNCLMode (ver: number): Ast { const map = this.peekSourceMap() if (ver === 1) { // 配列インデックスは1から this.arrayIndexFrom = 1 // 配列アクセスをJSと逆順で指定する this.flagReverseArrayIndex = true } else { // ver2はPythonに近いとのこと } // 配列代入時自動で初期化チェックする this.flagCheckArrayInit = true return { type: 'eol', ...map, end: this.peekSourceMap() } } /** @returns {Ast} */ ySetGenMode (mode: string): Ast { const map = this.peekSourceMap() this.genMode = mode return { type: 'eol', ...map, end: this.peekSourceMap() } } /** @returns {Ast} */ yExportDefault (mode: string): Ast { const map = this.peekSourceMap() this.isExportDefault = mode === '公開' this.moduleExport.set(this.modName, this.isExportDefault) return { type: 'eol', ...map, end: this.peekSourceMap() } } /** @returns {AstStrValue} */ ySetMode (mode: string): AstStrValue { const map = this.peekSourceMap() return { type: 'run_mode', value: mode, ...map, end: this.peekSourceMap() } } /** @returns {AstBlocks} */ yBlock(): AstBlocks { const map = this.peekSourceMap() const blocks = [] if (this.check('ここから')) { this.get() } while (!this.isEOF()) { if (this.checkTypes(['違えば', 'ここまで', 'エラー'])) { break } if (!this.accept([this.ySentence])) { break } blocks.push(this.y[0]) } return { type: 'block', blocks: blocks, josi: '', ...map, end: this.peekSourceMap() } } yDefFuncReadArgs (): Ast[]|null { if (!this.check('(')) { return null } const a: Ast[] = [] this.get() // skip '(' while (!this.isEOF()) { if (this.check(')')) { this.get() // skip '' break } const t = this.get() if (t) { a.push(t as any) } // Token to Ast if (this.check('comma')) { this.get() } } return a } yDefTest (): Ast|null { return this.yDefFuncCommon('def_test') } yDefFunc (): Ast|null { return this.yDefFuncCommon('def_func') } /** ユーザー関数の定義 * @returns {AstDefFunc | null} */ yDefFuncCommon(type: NodeType): AstDefFunc | null { if (!this.check(type)) { // yDefFuncから呼ばれれば def_func なのかをチェックする return null } const map = this.peekSourceMap() // 関数定義トークンを取得(このmetaに先読みした関数の型などが入っている) // (ref) NakoLexer.preDefineFunc const defToken: Token|null = this.get() // 'def_func' or 'def_test' if (!defToken) { return null } const def = defToken as TokenDefFunc let isExport: boolean = this.isExportDefault if (this.check('{')) { this.get() const funcAttribute: Token|null = this.get() if (this.check('}')) { this.get() } else { throw NakoSyntaxError.fromNode('関数の属性の指定が正しくありません。『{』と『}』で囲む必要があります。', def) } if (funcAttribute != null) { if (funcAttribute.value === '公開') { isExport = true } if (funcAttribute.value === '非公開') { isExport = false } if (funcAttribute.value === 'エクスポート') { isExport = true } } } let defArgs: Ast[] = [] if (this.check('(')) { defArgs = this.yDefFuncReadArgs() || [] } // // lexerでも解析しているが再度詳しく const funcName: Token|null = this.get() if (!funcName || funcName.type !== 'func') { this.logger.debug(this.nodeToStr(funcName, { depth: 0, typeName: '関数' }, true) + 'の宣言でエラー。', funcName) throw NakoSyntaxError.fromNode(this.nodeToStr(funcName, { depth: 0, typeName: '関数' }, false) + 'の宣言でエラー。', def) } if (this.check('(')) { // 関数引数の二重定義 if (defArgs.length > 0) { this.logger.debug(this.nodeToStr(funcName, { depth: 0, typeName: '関数' }, true) + 'の宣言で、引数定義は名前の前か後に一度だけ可能です。', funcName) throw NakoSyntaxError.fromNode(this.nodeToStr(funcName, { depth: 0, typeName: '関数' }, false) + 'の宣言で、引数定義は名前の前か後に一度だけ可能です。', funcName) } defArgs = this.yDefFuncReadArgs() || [] } if (this.check('とは')) { this.get() } let block: Ast = this.yNop() let multiline = false let asyncFn = false if (this.check('ここから')) { multiline = true } if (this.check('eol')) { multiline = true } try { this.funcLevel++ this.usedAsyncFn = false // ローカル変数を生成 const backupLocalvars = this.localvars this.localvars = new Map([['それ', { type: 'var', value: '' }]]) if (multiline) { this.saveStack() // 関数の引数をローカル変数として登録する for (const arg of defArgs) { if (!arg) { continue } if (!(arg as AstStrValue).value) { continue } const fnName: string = (arg as AstStrValue).value this.localvars.set(fnName, { 'type': 'var', 'value': '' }) } block = this.yBlock() if (this.check('ここまで')) { this.get() } else { throw NakoSyntaxError.fromNode('『ここまで』がありません。関数定義の末尾に必要です。', def) } this.loadStack() } else { this.saveStack() block = this.ySentence() || this.yNop() this.loadStack() } this.funcLevel-- asyncFn = this.usedAsyncFn this.localvars = backupLocalvars } catch (err: any) { this.logger.debug(this.nodeToStr(funcName, { depth: 0, typeName: '関数' }, true) + 'の定義で以下のエラーがありました。\n' + err.message, def) throw NakoSyntaxError.fromNode(this.nodeToStr(funcName, { depth: 0, typeName: '関数' }, false) + 'の定義で以下のエラーがありました。\n' + err.message, def) } const func = this.funclist.get(funcName.value) if (func && !func.asyncFn && asyncFn) { func.asyncFn = asyncFn } return { type, name: funcName.value, args: defArgs, blocks: [block], asyncFn, isExport, josi: '', meta: def.meta, ...map, end: this.peekSourceMap() } } /** 「もし」文の条件を取得 */ yIFCond (): Ast { const map = this.peekSourceMap() let a: Ast | null = this.yGetArg() if (!a) { throw NakoSyntaxError.fromNode( '「もし」文の条件式に間違いがあります。' + this.nodeToStr(this.peek(), { depth: 1 }, false), map) } // console.log('@@yIFCond=', a) // チェック : Aならば if (a.josi === 'ならば') { return a } if (a.josi === 'でなければ') { a = { type: 'not', operator: 'not', blocks:[a], josi: '', ...map, end: this.peekSourceMap() } as AstOperator return a } // チェック : AがBならば --- 「関数B(A)」のとき if ((a.josi !== '') && (this.check('func'))) { // もし文で関数呼び出しがある場合 this.stack.push(a) a = this.yCall() } else // チェック : AがBならば --- 「A = B」のとき if (a.josi === 'が') { const tmpI = this.index const b = this.yGetArg() if (!b) { throw NakoSyntaxError.fromNode( 'もし文の条件「AがBならば」でBがないか条件が複雑過ぎます。' + this.nodeToStr(this.peek(), { depth: 1 }, false), map) } if (this.check('ならば')) { const naraba = this.get() || { 'value': 'ならば' } b.josi = naraba.value } if (b && (b.josi === 'ならば' || b.josi === 'でなければ')) { return { type: 'op', operator: (b.josi === 'でなければ') ? 'noteq' : 'eq', blocks: [a, b], josi: '', ...map, end: this.peekSourceMap() } as AstOperator } this.index = tmpI } // もし文で追加の関数呼び出しがある場合 if (!this.check('ならば')) { this.stack.push(a) a = this.yCall() } // (ならば|でなければ)を確認 if (!this.check('ならば')) { const smap: Ast = a || this.yNop() this.logger.debug( 'もし文で『ならば』がないか、条件が複雑過ぎます。' + this.nodeToStr(this.peek(), { depth: 1 }, false) + 'の直前に『ならば』を書いてください。', smap) throw NakoSyntaxError.fromNode( 'もし文で『ならば』がないか、条件が複雑過ぎます。' + this.nodeToStr(this.peek(), { depth: 1 }, false) + 'の直前に『ならば』を書いてください。', smap) } const naraba = this.get() // 否定形のチェック if (naraba && naraba.value === 'でなければ') { a = { type: 'not', operator: 'not', blocks: [a], josi: '', ...map, end: this.peekSourceMap() } as AstOperator } if (!a) { throw NakoSyntaxError.fromNode( '「もし」文の条件式に間違いがあります。' + this.nodeToStr(this.peek(), { depth: 1 }, false), map) } return a } /** もし文 * @returns {AstIf | null} */ yIF (): AstIf | null { const map = this.peekSourceMap() // 「もし」があれば「もし」文である if (!this.check('もし')) { return null } const mosi:Token|null = this.get() // skip もし if (mosi == null) { return null } while (this.check('comma')) { this.get() } // skip comma // 「もし」文の条件を取得 let expr: Ast | null = null try { expr = this.yIFCond() } catch (err: any) { throw NakoSyntaxError.fromNode('『もし』文の条件で次のエラーがあります。\n' + err.message, mosi) } return this.yIfThen(expr, map) } /** 「もし」文の「もし」以降の判定 ... 「もし」がなくても条件分岐は動くようになっている * @returns {AstIf | null} */ yIfThen (expr: Ast, map: SourceMap): AstIf | null { // 「もし」文の 真偽のブロックを取得 let trueBlock: Ast = this.yNop() let falseBlock: Ast = this.yNop() let tanbun = false // True Block if (this.check('eol')) { trueBlock = this.yBlock() } else { const block: Ast|null = this.ySentence() if (block) { trueBlock = block } tanbun = true } // skip EOL while (this.check('eol')) { this.get() } // Flase Block if (this.check('違えば')) { this.get() // skip 違えば while (this.check('comma')) { this.get() } if (this.check('eol')) { falseBlock = this.yBlock() } else { const block: Ast|null = this.ySentence() if (block) { falseBlock = block } tanbun = true } } if (tanbun === false) { if (this.check('ここまで')) { this.get() } else { throw NakoSyntaxError.fromNode('『もし』文で『ここまで』がありません。', map) } } return { type: 'if', blocks: [expr, trueBlock, falseBlock], josi: '', ...map, end: this.peekSourceMap() } } ySpeedMode (): AstBlocks | null { const map: SourceMap = this.peekSourceMap() if (!this.check2(['string', '実行速度優先'])) { return null } const optionNode: Token|null = this.get() this.get() let val = '' if (optionNode && optionNode.value) { val = optionNode.value } else { return null } const options: {[key: string]: boolean} = { 行番号無し: false, 暗黙の型変換無し: false, 強制ピュア: false, それ無効: false } for (const name of val.split('/')) { // 全て有効化 if (name === '全て') { for (const k of Object.keys(options)) { options[k] = true } break } // 個別に有効化 if (Object.keys(options).includes(name)) { options[name] = true } else { // 互換性を考えて、警告に留める。 this.logger.warn(`実行速度優先文のオプション『${name}』は存在しません。`, optionNode) } } let multiline = false if (this.check('ここから')) { this.get() multiline = true } else if (this.check('eol')) { multiline = true } let block: Ast = this.yNop() if (multiline) { block = this.yBlock() if (this.check('ここまで')) { this.get() } } else { block = this.ySentence() || block } return { type: 'speed_mode', options, blocks: [block], josi: '', ...map } } yPerformanceMonitor (): AstBlocks | null { const map = this.peekSourceMap() if (!this.check2(['string', 'パフォーマンスモニタ適用'])) { return null } const optionNode = this.get() if (!optionNode) { return null } this.get() const options: {[key: string]: boolean} = { ユーザ関数: false, システム関数本体: false, システム関数: false } for (const name of optionNode.value.split('/')) { // 全て有効化 if (name === '全て') { for (const k of Object.keys(options)) { options[k] = true } break } // 個別に有効化 if (Object.keys(options).includes(name)) { options[name] = true } else { // 互換性を考えて、警告に留める。 this.logger.warn(`パフォーマンスモニタ適用文のオプション『${name}』は存在しません。`, optionNode) } } let multiline = false if (this.check('ここから')) { this.get() multiline = true } else if (this.check('eol')) { multiline = true } let block: Ast = this.yNop() if (multiline) { block = this.yBlock() if (this.check('ここまで')) { this.get() } } else { block = this.ySentence() || block } return { type: 'performance_monitor', options, blocks: [block], josi: '', ...map } } /** [廃止] #1611 「逐次実行」構文 @returns {Ast | null} */ yTikuji (): Ast|null { if (!this.check('逐次実行')) { return null } const tikuji = this.getCur() // skip this.logger.error('『逐次実行』構文は廃止されました(https://nadesi.com/v3/doc/go.php?944)。', tikuji) return { type: 'eol', ...this.peekSourceMap(), end: this.peekSourceMap() } } /** * 1つ目の値を与え、その後に続く計算式を取得し、優先規則に沿って並び替えして戻す * @param {Ast} firstValue */ yGetArgOperator (firstValue: Ast): Ast|null { const args:Ast[] = [firstValue] while (!this.isEOF()) { // 演算子がある? let op = this.peek() if (op && opPriority[op.type]) { op = this.getCur() args.push(op as any) // Token to Ast // 演算子後の値を取得 const v = this.yValue() if (v === null) { throw NakoSyntaxError.fromNode( `計算式で演算子『${op.value}』後に値がありません`, firstValue) } args.push(v) continue } break } if (args.length === 0) { return null } if (args.length === 1) { return args[0] } return this.infixToAST(args) } /** * 範囲(関数)を返す * @param kara * @returns {AstCallFunc | null} */ yRange (kara: Ast): AstCallFunc | null { // 範囲オブジェクト? if (!this.check('…')) { return null } const map = this.peekSourceMap() this.get() // skip '…' const made = this.yValue() if (!kara || !made) { throw NakoSyntaxError.fromNode('範囲オブジェクトの指定エラー。『A…B』の書式で指定してください。', map) } const meta = this.funclist.get('範囲') if (!meta) { throw new Error('関数『範囲』が見つかりません。plugin_systemをシステムに追加してください。') } return { type: 'func', name: '範囲', blocks: [kara, made], josi: made.josi, meta, asyncFn: false, ...map, end: this.peekSourceMap() } } /** * 表示(関数)を返す 「??」のエイリアスで利用 (#1745) */ yDebugPrint (): AstCallFunc | null { const map = this.peekSourceMap() const t = this.get() // skip '??' if (!t || t.value !== '??') { throw NakoSyntaxError.fromNode('『??』で指定してください。', map) } const arg: Ast|null = this.yGetArg() if (!arg) { throw NakoSyntaxError.fromNode('『??(計算式)』で指定してください。', map) } const meta = this.funclist.get('ハテナ関数実行') if (!meta) { throw new Error('関数『ハテナ関数実行』が見つかりません。plugin_systemをシステムに追加してください。') } return { type: 'func', name: 'ハテナ関数実行', blocks: [arg], josi: '', meta, asyncFn: false, ...map, end: this.peekSourceMap() } } yGetArg (): Ast|null { // 値を一つ読む const value1 = this.yValue() if (value1 === null) { return null } // 範囲オブジェクト? if (this.check('…')) { return this.yRange(value1) } // 計算式がある場合を考慮 return this.yGetArgOperator(value1) } infixToPolish (list: Ast[]): Ast[] { // 中間記法から逆ポーランドに変換 const priority = (t: Ast) => { if (opPriority[t.type]) { return opPriority[t.type] } return 10 } const stack: Ast[] = [] const polish: Ast[] = [] while (list.length > 0) { const t = list.shift() if (!t) { break } while (stack.length > 0) { // 優先順位を見て移動する const sTop = stack[stack.length - 1] if (priority(t) > priority(sTop)) { break } const tpop = stack.pop() if (!tpop) { this.logger.error('計算式に間違いがあります。', t) break } polish.push(tpop) } stack.push(t) } // 残った要素を積み替える while (stack.length > 0) { const t = stack.pop() if (t) { polish.push(t) } } return polish } /** @returns {Ast | null} */ infixToAST (list: Ast[]): Ast | null { if (list.length === 0) { return null } // 逆ポーランドを構文木に const josi = list[list.length - 1].josi const node = list[list.length - 1] const polish = this.infixToPolish(list) /** @type {Ast[]} */ const stack = [] for (const t of polish) { if (!opPriority[t.type]) { // 演算子ではない stack.push(t) continue } const b:Ast|undefined = stack.pop() const a:Ast|undefined = stack.pop() if (a === undefined || b === undefined) { this.logger.debug('--- 計算式(逆ポーランド) ---\n' + JSON.stringify(polish)) throw NakoSyntaxError.fromNode('計算式でエラー', node) } /** @type {AstOperator} */ const op: AstOperator = { type: 'op', operator: t.type, blocks: [a, b], josi, startOffset: a.startOffset, endOffset: a.endOffset, line: a.line, column: a.column, file: a.file } stack.push(op) } const ans = stack.pop() if (!ans) { return null } return ans } yGetArgParen (y: Ast[]): Ast[] { // C言語風呼び出しでカッコの中を取得 let isClose = false const si = this.stack.length while (!this.isEOF()) { if (this.check(')')) { isClose = true break } const v = this.yGetArg() if (v) { this.pushStack(v) if (this.check('comma')) { this.get() } continue } break } if (!isClose) { throw NakoSyntaxError.fromNode(`C風関数『${(y[0] as AstStrValue).value}』でカッコが閉じていません`, y[0]) } const a: Ast[] = [] while (si < this.stack.length) { const v = this.popStack() if (v) { a.unshift(v) } } return a } /** @returns {AstRepeatTimes | null} */ yRepeatTime(): AstRepeatTimes | null { const map = this.peekSourceMap() if (!this.check('回')) { return null } this.get() // skip '回' if (this.check('comma')) { this.get() } // skip comma if (this.check('繰返')) { this.get() } // skip 'N回、繰り返す' (#924) const num = this.popStack([]) || { type: 'word', value: 'それ', josi: '', ...map, end: this.peekSourceMap() } as Ast let multiline = false let block: Ast = this.yNop() if (this.check('comma')) { this.get() } if (this.check('ここから')) { this.get() multiline = true } else if (this.check('eol')) { multiline = true } if (multiline) { // multiline block = this.yBlock() if (this.check('ここまで')) { this.get() } else { throw NakoSyntaxError.fromNode('『ここまで』がありません。『回』...『ここまで』を対応させてください。', map) } } else { // singleline const b = this.ySentence() if (b) { block = b } } return { type: 'repeat_times', blocks: [num, block], josi: '', ...map, end: this.peekSourceMap() } } /** @returns {AstWhile | null} */ yWhile(): AstWhile | null { // 「*の間」文 const map = this.peekSourceMap() if (!this.check('間')) { return null } this.get() // skip '間' while (this.check('comma')) { this.get() } // skip ',' if (this.check('繰返')) { this.get() } // skip '繰り返す' #927 const expr = this.popStack() if (expr === null) { throw NakoSyntaxError.fromNode('『間』で条件がありません。', map) } if (this.check('comma')) { this.get() } if (!this.checkTypes(['ここから', 'eol'])) { throw NakoSyntaxError.fromNode('『間』の直後は改行が必要です', map) } const block = this.yBlock() if (this.check('ここまで')) { this.get() } else { throw NakoSyntaxError.fromNode('『ここまで』がありません。『間』...『ここまで』を対応させてください。', map) } return { type: 'while', blocks: [expr, block], josi: '', ...map, end: this.peekSourceMap() } } /** @returns {AstAtohantei | null} */ yAtohantei(): AstAtohantei |null { const map = this.peekSourceMap() if (this.check('後判定')) { this.get() } // skip 後判定 if (this.check('繰返')) { this.get() } // skip 繰り返す if (this.check('ここから')) { this.get() } const block = this.yBlock() if (this.check('ここまで')) { this.get() } if (this.check('comma')) { this.get() } let cond = this.yGetArg() // 条件 let bUntil = false const t = this.peek() if (t && t.value === 'なる' && (t.josi === 'まで' || t.josi === 'までの')) { this.get() // skip なるまで bUntil = true } if (this.check('間')) { this.get() } // skip 間 if (bUntil) { // 条件を反転する cond = { type: 'not', operator: 'not', blocks: [cond], josi: '', ...map, end: this.peekSourceMap() } as AstOperator } if (!cond) { cond = {type: 'number', value: 1, josi: '', ...map, end: this.peekSourceMap()} as AstConst } return { type: 'atohantei', blocks: [cond, block], josi: '', ...map, end: this.peekSourceMap() } } /** @returns {AstFor | null} */ yFor (): AstFor | null { const errorForArguments = '『繰り返す』文でAからBまでの指定がありません。' let flagDown = true // AからBまでの時、A>=Bを許容するかどうか let flagUp = true // AからBまでの時、A<=Bを許容するかどうか let loopDirection : null | 'up' | 'down' = null // ループの方向を一方向に限定する const map = this.peekSourceMap() if (this.check('繰返') || this.check('増繰返') || this.check('減繰返')) { // pass } else { return null } const kurikaesu: Token = this.getCur() // skip 繰り返す // スタックに(増や|減ら)してがある? const incdec = this.stack.pop() if (incdec) { if (incdec.type === 'word' && (incdec.value === '増' || incdec.value === '減')) { if (incdec.value === '増') { flagDown = false } else { flagUp = false } const w = incdec.value + kurikaesu.type if (w == '増繰返') { kurikaesu.type = '増繰返' } else if (w == '減繰返') { kurikaesu.type = '減繰返' } else { throw Error('[System Error] 増繰り返し | 減繰り返しのエラー。') } } else { // 普通の繰り返しの場合 this.stack.push(incdec) // 違ったので改めて追加 } } let vInc: Ast = this.yNop() if (kurikaesu.type === '増繰返' || kurikaesu.type === '減繰返') { vInc = this.popStack(['ずつ']) || this.yNop() if (kurikaesu.type === '増繰返') { flagDown = false } else { flagUp = false } loopDirection = kurikaesu.type === '増繰返' ? 'up' : 'down' } const vTo = this.popStack(['まで', 'を']) // 範囲オブジェクトの場合もあり const vFrom = this.popStack(['から']) || this.yNop() const vWord: Ast|null = this.popStack(['を', 'で']) let wordStr: string = '' if (vWord !== null) { // 変数 if (vWord.type !== 'word') { throw NakoSyntaxError.fromNode('『(変数名)をAからBまで繰り返す』で指定してください。', vWord) } wordStr = (vWord as AstStrValue).value } if (vFrom === null || vTo === null) { // 『AからBの範囲を繰り返す』構文のとき (#1704) if (vFrom == null && vTo && (vTo.type === 'func' && vTo.name === '範囲')) { // ok } else { throw NakoSyntaxError.fromNode(errorForArguments, kurikaesu) } } if (this.check('comma')) { this.get() } // skip comma let multiline = false if (this.check('ここから')) { multiline = true this.get() } else if (this.check('eol')) { multiline = true this.get() } let block: Ast = this.yNop() if (multiline) { block = this.yBlock() if (this.check('ここまで')) { this.get() } else { throw NakoSyntaxError.fromNode('『ここまで』がありません。『繰り返す』...『ここまで』を対応させてください。', map) } } else { const b = this.ySentence() if (b) { block = b } } if (!block) { block = this.yNop() } return { type: 'for', blocks: [vFrom, vTo, vInc, block], flagDown, flagUp, loopDirection, word: wordStr, josi: '', ...map, end: this.peekSourceMap() } } /** @returns {AstBlocks | null} */ yReturn(): AstBlocks | null { const map = this.peekSourceMap() if (!this.check('戻る')) { return null } this.get() // skip '戻る' const v = this.popStack(['で', 'を']) || this.yNop() if (this.stack.length > 0) { throw NakoSyntaxError.fromNode('『戻』文の直前に未解決の引数があります。『(式)を戻す』のように式をカッコで括ってください。', map) } return { type: 'return', blocks: [v], josi: '', ...map, end: this.peekSourceMap() } } /** @returns {AstForeach | null} */ yForEach(): AstForeach |null { const map = this.peekSourceMap() if (!this.check('反復')) { return null } this.get() // skip '反復' while (this.check('comma')) { this.get() } // skip ',' const target = this.popStack(['を']) || this.yNop() // target == null なら「それ」の値が使われる const name = this.popStack(['で']) let wordStr: string = '' if (name !== null) { if (name.type !== 'word') { throw NakoSyntaxError.fromNode('『(変数名)で(配列)を反復』で指定してください。', map) } wordStr = (name as AstStrValue).value } let block: Ast = this.yNop() let multiline = false if (this.check('ここから')) { multiline = true this.get() } else if (this.check('eol')) { multiline = true } if (multiline) { block = this.yBlock() if (this.check('ここまで')) { this.get() } else { throw NakoSyntaxError.fromNode('『ここまで』がありません。『反復』...『ここまで』を対応させてください。', map) } } else { const b = this.ySentence() if (b) { block = b } } return { type: 'foreach', word: wordStr, blocks: [target, block], josi: '', ...map, end: this.peekSourceMap() } } /** 条件分岐構文 * @returns {AstSwitch | null} */ ySwitch (): AstSwitch | null { const map = this.peekSourceMap() if (!this.check('条件分岐')) { return null } const joukenbunki = this.get() // skip '条件分岐' if (!joukenbunki) { return null } const eol = this.get() // skip 'eol' if (!eol) { return null } const expr = this.popStack(['で']) if (!expr) { throw NakoSyntaxError.fromNode('『(値)で条件分岐』のように記述してください。', joukenbunki) } if (eol.type !== 'eol') { throw NakoSyntaxError.fromNode('『条件分岐』の直後は改行してください。', joukenbunki) } // const blocks: Ast[] = [] blocks[0] = expr blocks[1] = this.yNop() // 後で default のAstを再設定するため // while (!this.isEOF()) { if (this.check('eol')) { this.get() continue } // ここまで? if (this.check('ここまで')) { this.get() // skip ここまで break } // 違えば? const condToken: Token|null = this.peek() if (condToken && condToken.type === '違えば') { this.get() // skip 違えば if (this.check('comma')) { this.get() } // skip ',' const defaultBlock = this.yBlock() if (this.check('ここまで')) { this.get() // skip ここまで (違えばとペア) } while (this.check('eol')) { this.get() } // skip eol if (this.check('ここまで')) { this.get() // skip ここまで (条件分岐:ここまで) } blocks[1] = defaultBlock break } // 通常の条件 const cond: Ast | null = this.yValue() if (!cond) { throw NakoSyntaxError.fromNode('『条件分岐』は『(条件)ならば〜ここまで』と記述してください。', joukenbunki) } const naraba = this.get() // skip ならば if (!naraba || naraba.type !== 'ならば') { throw NakoSyntaxError.fromNode('『条件分岐』で条件は**ならばと記述してください。', joukenbunki) } if (this.check('comma')) { this.get() } // skip ',' // 条件にあったときに実行すること const condBlock = this.yBlock() const kokomade = this.peek() if (kokomade && kokomade.type === 'ここまで') { this.get() // skip ここまで } blocks.push(cond) blocks.push(condBlock) } const ast: AstSwitch = { type: 'switch', blocks, case_count: blocks.length / 2 - 1, josi: '', ...map, end: this.peekSourceMap() } return ast } /** 無名関数 * @returns {AstDefFunc|null} */ yMumeiFunc (): AstDefFunc | null { // 無名関数の定義 const map = this.peekSourceMap() if (!this.check('def_func')) { return null } const defToken = this.get() if (!defToken) { return null } const def = defToken as TokenDefFunc let args: Ast[] = [] // 「,」を飛ばす if (this.check('comma')) { this.get() } // 関数の引数定義は省略できる if (this.check('(')) { args = this.yDefFuncReadArgs() || [] } // 「,」を飛ばす if (this.check('comma')) { this.get() } // ブロックを読む this.funcLevel++ this.saveStack() const backupAsyncFn = this.usedAsyncFn this.usedAsyncFn = false const block = this.yBlock() const isAsyncFn = this.usedAsyncFn // 末尾の「ここまで」をチェック - もしなければエラーにする #1045 if (!this.check('ここまで')) { throw NakoSyntaxError.fromNode('『ここまで』がありません。『には』構文か無名関数の末尾に『ここまで』が必要です。', map) } this.get() // skip ここまで this.loadStack() this.usedAsyncFn = backupAsyncFn this.funcLevel-- return { type: 'func_obj', name: '', args, blocks: [block], meta: def.meta, josi: '', isExport: false, // 無名関数は外部公開しない asyncFn: isAsyncFn, // asyncFnかどうか ...map, end: this.peekSourceMap() } } /** 代入構文 */ yDainyu (): AstBlocks | null { const map = this.peekSourceMap() const dainyu = this.get() // 代入 if (dainyu === null) { return null } const value = this.popStack(['を']) || {type: 'word', value: 'それ', josi: 'を', ...map} as AstStrValue const word: Ast|null = this.popStack(['へ', 'に']) if (!word || (word.type !== 'word' && word.type !== 'func' && word.type !== 'ref_array')) { throw NakoSyntaxError.fromNode('代入文で代入先の変数が見当たりません。『(変数名)に(値)を代入』のように使います。', dainyu) } // 配列への代入 if (word.type === 'ref_array') { const indexArray = word.index || [] const blocks = [value, ...indexArray] return { type: 'let_array', name: (word.name as AstStrValue).value, indexes: word.index, blocks, josi: '', checkInit: this.flagCheckArrayInit, ...map, end: this.peekSourceMap() } as AstLetArray } // 一般的な変数への代入 const word2 = this.getVarName(word) return { type: 'let', name: (word2 as AstStrValue).value, blocks: [value], josi: '', ...map, end: this.peekSourceMap() } as AstLet } /** 定める構文 */ ySadameru (): AstBlocks | null { const map = this.peekSourceMap() const sadameru = this.get() // 定める if (sadameru === null) { return null } // 引数(定数名)を取得 const word = this.popStack(['を']) || { type: 'word', value: 'それ', josi: 'を', ...map, end: this.peekSourceMap() } as AstStrValue if (!word || (word.type !== 'word' && word.type !== 'func' && word.type !== 'ref_array')) { throw NakoSyntaxError.fromNode('『定める』文で定数が見当たりません。『(定数名)を(値)に定める』のように使います。', sadameru) } // 引数(値)を取得 const value = this.popStack(['へ', 'に', 'と']) || this.yNop() // 公開設定 let isExport: boolean = this.isExportDefault if (this.check2(['{', 'word', '}'])) { this.get() const attrNode = this.get() if (attrNode === null) { throw NakoSyntaxError.fromNode('定める『' + (word as AstStrValue).value + '』の定義エラー', word) } const attr = attrNode.value if (attr === '公開') { isExport = true } else if (attr === '非公開') { isExport = false } else if (attr === 'エクスポート') { isExport = true } else { this.logger.warn(`不明な変数属性『${attr}』が指定されています。`) } this.get() } // 変数を生成する const nameToken = this.createVar(word as AstStrValue, true, isExport) return { type: 'def_local_var', name: (nameToken as AstStrValue).value, vartype: '定数', isExport, blocks: [value], josi: '', ...map, end: this.peekSourceMap() } as AstDefVar } yIncDec (): AstBlocks | null { const map = this.peekSourceMap() const action = this.get() // (増やす|減らす) if (action === null) { return null } // 『Nずつ増やして繰り返す』文か? if (this.check('繰返')) { this.pushStack({ type: 'word', value: action.value, josi: action.josi, ...map, end: this.peekSourceMap() }) return this.yFor() } // スタックから引数をポップ let value = this.popStack(['だけ', '']) if (!value) { value = { type: 'number', value: 1, josi: 'だけ', ...map, end: this.peekSourceMap() } as AstConst } const word = this.popStack(['を']) if (!word || (word.type !== 'word' && word.type !== 'ref_array')) { throw NakoSyntaxError.fromNode( `『${action.type}』文で定数が見当たりません。『(変数名)を(値)だけ${action.type}』のように使います。`, action) } // 減らすなら-1かける if (action.value === '減') { const minus_one = { type: 'number', value: -1, line: action.line } as AstConst value = { type: 'op', operator: '*', blocks: [value, minus_one], josi: '', ...map } as AstOperator } return { type: 'inc', name: word, blocks: [value], josi: action.josi, ...map, end: this.peekSourceMap() } } yCall (): Ast | null { if (this.isEOF()) { return null } // スタックに積んでいく while (!this.isEOF()) { if (this.check('ここから')) { this.get() } // 代入 if (this.check('代入')) { return this.yDainyu() } if (this.check('定める')) { return this.ySadameru() } // 制御構文 if (this.check('回')) { return this.yRepeatTime() } if (this.check('間')) { return this.yWhile() } if (this.check('繰返') || this.check('増繰返') || this.check('減繰返')) { return this.yFor() } if (this.check('反復')) { return this.yForEach() } if (this.check('条件分岐')) { return this.ySwitch() } if (this.check('戻る')) { return this.yReturn() } if (this.check('増') || this.check('減')) { return this.yIncDec() } // C言語風関数 if (this.check2([['func', 'word'], '('])) { // C言語風 const cur = this.peek() if (cur && cur.josi === '') { const t: Ast|null = this.yValue() // yValueにてC言語風呼び出しをパース if (t) { const josi = t.josi || '' if (t.type === 'func' && (t.josi === '' || RenbunJosi.indexOf(josi) >= 0)) { t.josi = '' return t // 関数なら値とする } this.pushStack(t) } if (this.check('comma')) { this.get() } continue } } // なでしこ式関数 if (this.check('func')) { const r = this.yCallFunc() if (r === null) { continue } // 「〜する間」の形ならスタックに積む。 if (this.check('間')) { this.pushStack(r) continue } // 関数呼び出しの直後に、四則演算があるか? if (!this.checkTypes(operatorList)) { return r // 関数呼び出しの後に演算子がないのでそのまま関数呼び出しを戻す } // 四則演算があった場合、計算してスタックに載せる const s = this.yGetArgOperator(r) this.pushStack(s) continue } // 値のとき → スタックに載せる const t = this.yGetArg() if (t) { this.pushStack(t) continue } break } // end of while // 助詞が余ってしまった場合 if (this.stack.length > 0) { if (this.isReadingCalc) { return this.popStack() } this.logger.debug('--- stack dump ---\n' + JSON.stringify(this.stack, null, 2) + '\npeek: ' + JSON.stringify(this.peek(), null, 2)) let msgDebug = `不完全な文です。${this.stack.map((n) => this.nodeToStr(n, { depth: 0 }, true)).join('、')}が解決していません。` let msg = `不完全な文です。${this.stack.map((n) => this.nodeToStr(n, { depth: 0 }, false)).join('、')}が解決していません。` // 各ノードについて、更に詳細な情報があるなら表示 for (const n of this.stack) { const d0 = this.nodeToStr(n, { depth: 0 }, false) const d1 = this.nodeToStr(n, { depth: 1 }, false) if (d0 !== d1) { msgDebug += `${this.nodeToStr(n, { depth: 0 }, true)}は${this.nodeToStr(n, { depth: 1 }, true)}として使われています。` msg += `${d0}は${d1}として使われています。` } } const first = this.stack[0] const last = this.stack[this.stack.length - 1] this.logger.debug(msgDebug, first) throw NakoSyntaxError.fromNode(msg, first, last) } return this.popStack([]) } /** @returns {Ast | null} */ yCallFunc (): Ast | null { const map = this.peekSourceMap() const callToken = this.get() if (!callToken) { return null } const t = callToken as TokenCallFunc const f = t.meta const funcName: string = t.value // (関数)には ... 構文 ... https://github.com/kujirahand/nadesiko3/issues/66 let funcObj = null if (t.josi === 'には') { try { funcObj = this.yMumeiFunc() } catch (err: any) { throw NakoSyntaxError.fromNode(`『${t.value}には...』で無名関数の定義で以下の間違いがあります。\n${err.message}`, t) } if (funcObj === null) { throw NakoSyntaxError.fromNode('『Fには』構文がありましたが、関数定義が見当たりません。', t) } } if (!f || typeof f.josi === 'undefined') { throw NakoSyntaxError.fromNode('関数の定義でエラー。', t) } // 最近使った関数を記録 this.recentlyCalledFunc.push({ name: funcName, ...f }) // 呼び出す関数が非同期呼び出しが必要(asyncFn)ならマーク if (f && f.asyncFn) { this.usedAsyncFn = true } // 関数の引数を取り出す処理 const args: any[] = [] let nullCount = 0 let valueCount = 0 for (let i = f.josi.length - 1; i >= 0; i--) { for (;;) { // スタックから任意の助詞を持つ値を一つ取り出す、助詞がなければ末尾から得る let popArg = this.popStack(f.josi[i]) if (popArg !== null) { valueCount++ } else if (i < f.josi.length - 1 || !f.isVariableJosi) { nullCount++ popArg = funcObj } else { break } // 参照渡しの場合、引数が関数の参照渡しに該当する場合、typeを『func_pointer』に変更 if (popArg !== null && f.funcPointers !== undefined && f.funcPointers[i] !== null) { if (popArg.type === 'func') { // 引数が関数の参照渡しに該当する場合 popArg.type = 'func_pointer' } else { const varname = (f.varnames) ? f.varnames[i] : `${i + 1}番目の引数` throw NakoSyntaxError.fromNode( `関数『${t.value}』の引数『${varname}』には関数オブジェクトが必要です。`, t) } } // 引数がnullであれば、自動的に『変数「それ」』で補完する if (popArg === null) { popArg = { type: 'word', value: 'それ', josi: '', ...map, end: map } as AstStrValue } args.unshift(popArg) // 先頭に追加 if (i < f.josi.length - 1 || !f.isVariableJosi) { break } } } // 引数が不足しているとき(つまり、引数にnullがあるとき)、自動的に『変数「それ」』で補完される。 // ただし、nullが1つだけなら、変数「それ」で補完されるが、2つ以上あるときは、エラーにする if (nullCount >= 2 && (valueCount > 0 || t.josi === '' || RenbunJosi.indexOf(t.josi) >= 0)) { throw NakoSyntaxError.fromNode(`関数『${t.value}』の引数が不足しています。`, t) } this.usedFuncs.add(t.value) // 関数呼び出しのAstを構築 const funcNode: AstCallFunc = { type: 'func', name: t.value, blocks: args, meta: f, josi: t.josi, asyncFn: f.asyncFn ? true : false, ...map, end: this.peekSourceMap() } // 「プラグイン名設定」ならば、そこでスコープを変更することを意味する (#1112) if (funcNode.name === 'プラグイン名設定') { if (args.length > 0 && args[0]) { let fname: string = '' + args[0].value if (fname === 'メイン') { fname = '' + args[0].file } this.namespaceStack.push(this.modName) this.isExportStack.push(this.isExportDefault) this.modName = NakoLexer.filenameToModName(fname) this.modList.push(this.modName) } } else if (funcNode.name === '名前空間ポップ') { // (#1409) const space = this.namespaceStack.pop() if (space) { this.modName = space } const isexport = this.isExportStack.pop() if (isexport != null) { this.isExportDefault = isexport } } // 言い切りならそこで一度切る if (t.josi === '') { return funcNode } // 「**して、**」の場合も一度切る if (RenbunJosi.indexOf(t.josi) >= 0) { funcNode.josi = 'して' return funcNode } // 続き funcNode.meta = f this.pushStack(funcNode) return null } /** @returns {Ast | null} */ yLet (): AstBlocks | null { const map = this.peekSourceMap() // 通常の変数 if (this.check2(['word', 'eq'])) { const word = this.peek() let threw = false try { if (this.accept(['word', 'eq', this.yCalc]) || this.accept(['word', 'eq', this.ySentence])) { if (this.y[2].type === 'eol') { throw new Error('値が空です。') } if (this.check('comma')) { this.get() } // skip comma (ex) name1=val1, name2=val2 const nameToken = this.getVarName(this.y[0]) const valueToken = this.y[2] return { type: 'let', name: (nameToken as AstStrValue).value, blocks: [valueToken], josi: '', ...map, end: this.peekSourceMap() } as AstLet } else { threw = true this.logger.debug(`${this.nodeToStr(word, { depth: 1 }, true)}への代入文で計算式に書き間違いがあります。`, word) throw NakoSyntaxError.fromNode(`${this.nodeToStr(word, { depth: 1 }, false)}への代入文で計算式に書き間違いがあります。`, map) } } catch (err: any) { if (threw) { throw err } this.logger.debug(`${this.nodeToStr(word, { depth: 1 }, true)}への代入文で計算式に以下の書き間違いがあります。\n${err.message}`, word) throw NakoSyntaxError.fromNode(`${this.nodeToStr(word, { depth: 1 }, false)}への代入文で計算式に以下の書き間違いがあります。\n${err.message}`, map) } } // オブジェクトプロパティ構文 代入文 (#1793) if (this.check2(['word', '$', '*', '$', '*', '$', '*', '$', '*', 'eq']) || this.check2(['word', '$', '*', '$', '*', '$', '*', 'eq']) || this.check2(['word', '$', '*', '$', '*', 'eq']) || this.check2(['word', '$', '*', 'eq'])) { const propList = [] const word = this.getVarName(this.get() as Token) for (;;) { const flag = this.peek() if (flag === null || flag.type !== '$') { break } this.get() // skip $ propList.push(this.get() as Ast) // property } this.get() // skip eq const valueToken = this.yCalc() // calc if (valueToken === null) { throw NakoSyntaxError.fromNode(`${this.nodeToStr(word, { depth: 1 }, false)}への代入文の計算式に書き間違いがあります。`, map) } return { type: 'let_prop', name: (word as AstStrValue).value, index: propList, blocks: [valueToken], josi: '', ...map, end: this.peekSourceMap() } as AstLet } // オブジェクトプロパティ構文 ここまで // let_array ? if (this.check2(['word', '@'])) { const la = this.yLetArrayAt(map) if (this.check('comma')) { this.get() } // skip comma (ex) name1=val1, name2=val2 if (la) { la.checkInit = this.flagCheckArrayInit return la } } if (this.check2(['word', '['])) { const lb = this.yLetArrayBracket(map) as AstLetArray if (this.check('comma')) { this.get() } // skip comma (ex) name1=val1, name2=val2 if (lb) { lb.checkInit = this.flagCheckArrayInit return lb } } // ローカル変数定義 if (this.accept(['word', 'とは'])) { const wordToken = this.y[0] if (!this.checkTypes(['変数', '定数'])) { throw NakoSyntaxError.fromNode('ローカル変数『' + wordToken.value + '』の定義エラー', wordToken) } const vtype = this.getCur() // 変数 or 定数 let isExport : boolean = this.isExportDefault if (this.check2(['{', 'word', '}'])) { this.get() const attrNode = this.get() if (attrNode === null) { throw Na