EBNFベースで数式の文法を書いたバージョンです。
やってみた感じでは・・・ただただ内部が面倒くさくなったダケでした。
なぜか、「1+2+3」が式として認識されるので、まだ大きなミスが残ってそう。
- EBNF文法を解析するパーサ
- セルフテスト
- EBNF文法で書いたEBNFの文法のテキストを文法解析させJSONデータを得る。
- 全文解析できればOK
- 使用する正規表現自身で字句解析は無理っぽいので簡易なパターンが通ればOK
- EBNF文法で書いたEBNFの文法のテキストを文法解析させJSONデータを得る。
- EBNF文法で書いた数式の文法のテキストを文法解析させJSONデータを得て、一部識別子の宣言にmapで解析結果をトークン化しやすくしておく。
- 数式のテキストを文法解析させ数式のトークンリストを得る。
- セルフテスト
- トークン関係
- 数式のトークンリストはindexedDBのexprプロパティに格納する。
- 数式のトークンリストで計算し結果をindexedDBのvalueプロパティに格納する。
- 画面関係
- 数式のトークンリストをテキスト化したものを画面のテーブルのセルにexpr属性として貼り付ける。
- セル上にマウスポインタを置くと、ツールチップでセルの内容を確認できる。
特にセルフテストで正規表現をEBNF文法で、
regexp ::= "/" /.+/ "/" /[dgimsuvy]*/
と、こんな雰囲気で書くと間に隙間(空白)が入ってもOKになってしまうし、だからと云って正規表現でまとめると読みにくい上に、new RegExpが「オプションの指定が矛盾しています」と云ってくるので悩ましい。
今のところ、特定の識別子の定義で空白をスキップしたく無い場合は、no_space_skipを混ぜている。
const no_space_skip = function () {
this.fSpaceSkip = false;
return undefined;
}.bind(this);
・・・
// wq_string = '"' /(?!\\\\)[^"]*/ '"' ;
const wq_string = _map(
no_space_skip, '"', /(?!\\)[^"]*/, '"'
, (parsed) => {
parsed = parsed[1]
return { _wq_string: `${parsed}` };
});
・・・
sequence(...parsers) {
・・・
const func =
(target, position) => {/*sequnce*/
const bk = this.fSpaceSkip; ☚空白読飛状態をバックアップ
・・・
for(let i=0;i<parsers.length;i++) {
parsers[i](target, position); ☚この中でno_space_skipが空白読飛状態を変更
}
・・・
this.fSpaceSkip = bk; ☚空白読飛状態を復元
return
}
・・・
return func;
}
no_space_skipを内包するsequenceとその配下での空白読飛しを停止できるが、
今のところsequenceパーサのパラメータにno_space_skipを直挿しするしか方法が無い。
EBNFテキストのsequence定義文のデリミタ「、」に「+」を加え、「+」ならconcat風に見えるので「空白読み飛ばし停止」的な感じにしたい。あと、先頭にも[ + ]が使えた方がいいかもしれない。
ps.2024/4/5
Ctrl+SでJSON形式でテーブルのデータをそのままダウンロード。そのファイルをテーブルにドロップするとアップロード。但し、indexedDBの検索が不調で連動計算がうまくいかない。setTimeoutで適当に間を開けてもダメなので、トランザクションがちゃんと終わるのを待たないとダメかな?と思ったら、参照セルアドレスの保存方法を修正漏れだった。setTimeoutも不要だった。
また数式の参照先が空っぽで動きを追っていくと、パーサのコールバック内でメソッド内の変数を更新しても、アロー関数作成時点のメソッドの変数の残像(ディープコピー)の方を更新しているみたいで当のメソッド内変数が更新されない。当初のパーサコンビネータ方式ではメソッドを呼び出す度にコールバックを作り直していた(のでちょっと遅い)支障なかったが、今回は式クラスのコンストラクタで1度だけ生成するようにしたせいで相違が出た。
/**
* スプレッドシートの数式クラス
*/
class SpreadSheetExprParser {
/**
* コンストラクタ
* @param {SpreadSheetCustomElement} spreadSheet
*/
constructor(spreadSheet) {
const _exprParser = this;
_exprParser.className = _exprParser.name ?? _exprParser.constructor.name;
_exprParser._spreadSheet = spreadSheet;
this.parseEBNF(undefined); // self test
this.parse('=1+2+3'); // expression setup ☚ここで仮データで数式のパーサを初期設定。
}
/**
* 数式を構文解析する
* @param {string} text 数式
*/
parse(text) {
const ebnf = ` (* セルのデータ *)
cell ::= expr_cell | value_cell ;
・・・数式のEBNFテキスト・・・
`;
if (this.exptEbnfAnalize === undefined) {
this.exptEbnfAnalize = this.parseEBNF(ebnf); ☚ ここで、数式用EBNFテキストを読ませて解析させる。
・・・・以下、解析結果をDBに保存できるようにトークン化するコールバック一式を作成。
※コンストラクタから呼び出された時にローカルなアロー関数は参照する変数をディープコピーしてるっぽい。
}
・・・・
// 数式の纏めパーサ
const cell = this.exptEbnfAnalize.parser['cell']; ☚ ここで数式の文法のトップを調べ、
var result = cell(target, 0); ☚ ここで数式を読ませ解析させる。この時点で新たなローカル変数が作られ、
上記のローカルなアロー関数がアクセスするローカル変数とは別腹。
if (!result.success) {
・・・・
仕方が無いので解析結果からA1やA1範囲のトークンを探すことにした。
さて、EBNFで今更気が付いたけど要は”…”と/…/opt で表現できる文字列のみでテキストが出来ているので、一気に字句解析させてみようと思ったけど、
sequce( “{” , /.*/ , “}” ) を見ただけで、意味が無いことが判った。
どちらかと云えば、「空白文字の列」と「空白以外の文字の列」に分けて下ごしらえした方がマシ。
パーサのソースが巨大になってきたので、
- パーサ
- EBNF構文のパーサ
- 数式構文のパーサ
に分離した。
wikiのEBNFの内容が充実してきたので、EBNF部分を見直してみると、グループやオプションでも「*」が使える様な雰囲気なので、EBNFのテキストを変えてみたら、rule-listすら通らない!
ENFテキスト中の “{” … “}”と違い、{ … } はEBNFのrepeateパーサを直接呼び出しているのにやっと気づき、EBNFのrepeate、group、optionパーサの文法も修正したので暫くは持ちそう。
(*$ SEQUENCE SEPARATOR IS REQUIRED $*)
(* 文法 *)
syntax = rule list, /\s*/ ;
rule list = { ( rule | special effect | comment ) } + ;
rule = identifier, defining symbol, definitions, ";" ;
defining symbol = "=" | "::=" ; (* 終端記号 '=':EBNF, '::=':BNF *)
(* 定義 *)
definitions = definition, { "|", definition } ; (* 定義リスト *)
definition = choice ; (* 定義 *)
(* リスト *)
choice = sequence, { "|", sequence} ; (* アレかコレかソレかのどれか *)
sequence = exception, { ",", exception } ; (* アレとコレとソレの一式。","は空白改行と同義 *)
exception = words, { "-", words } ; (* アレが良いが、コレとソレを除く。 *)
(* 囲み *)
words = [ comments ], word, [ comments ] ;
special effect = (*$ NO SPACE SKIP $*) "(*$", /[^\\?]*/, "$*)" ; (* 特殊効果 *)
comments = ( special effect | comment ), repeat symbol;
word = ( sq string | wq string | wraper | special text | regexp | identifier ) ;
comment = (*$ NO SPACE SKIP $*) "(*", /(?![*][/])(.*)/, "*)" ; (* 注意書き *)
sq string = (*$ NO SPACE SKIP $*) "'", /(?!\\\\)[^']*/, "'" ; (* ' で括られたテキスト *)
wq string = (*$ NO SPACE SKIP $*) '"', /(?!\\\\)[^"]*/, '"' ; (* " で括られたテキスト *)
wraper = ( group | option | repeate ), repeat symbol ;
group = "(", choice, ")" ; (* choiceを明示的に括る表現 *)
option = "[", choice, "]" ; (* あれば尚良し *)
repeate = "{", sequence, "}" ; (* 0回以上繰り返す *: 0回以上、+:1回以上 *)
regexp = /.+/i ; (* 正規表現されたテキスト "/" + /.+/ "/" + { /[dgimsuvy]/ } *)
identifier = /[A-Za-z][A-Z_a-z0-9\\x20]*/; (* 識別子 *)
special text = (*$ NO SPACE SKIP $*) "?", /[^\\?]*/, "?" ; (* 特殊文字列 *)
repeat symbol= [ "*" | "+" | '{', [ number ], ',', [ number ], '}']; (* 繰返し記号 *)
number = /\d+/ ;
EBNFは定義記号が=なので=でも::=でも通る様に修正、未終端記号(識別子のことか?)に空白を挟めるみたいだが、そうなると、sequenceのセパレータを必須にしなければいけなくなるので、
? NO SPACE SKIP ? (sequence内とその配下で有効)の他に特殊文字列を追加し、
? SEQUENCE SEPARATOR IS (NOT) REQUIRED? 指定で、識別子に空白OK(NO)にすれば良いかな?
ps.2024/4/8
やっと数値を右揃えで表示。論理式は数式のみ。’xxxxxで文字列をEBNFに追記。
? SEQUENCE SEPARATOR IS (NOT) REQUIRED?の方は組み込んでみた。
実装の都合上、branchパーサを追加
/**
* 判定式の結果で処理を分岐するパーサを作成する
* @param {Function} expr 判定式
* @param {Function} trueParser 結果が真の場合に実行するパーサ
* @param {Function} falseParser 結果が偽の場合実行するパーサ
* @returns
*/
branch(expr, trueParser, falseParser) {
const methodName = 'branch';
trueParser = this.normalizeParses(trueParser);
falseParser = this.normalizeParses(falseParser);
this.chkFunction(methodName, "trueParser", trueParser);
this.chkFunction(methodName, "falseParser", falseParser);
const func =
/**
* 生成した処理を分岐するパーサ
* @param {string} target ソースコード
* @param {integer} position 読取り位置
* @return {ParseInfo} パースした結果
*/
(target, position) => {/*branch*/
if (expr()) {
return trueParser(target, position);
} else {
return falseParser(target, position);
}
}
return func;
}
const identifier = _map(
_branch(() => { return this.sequnceSeparator }, /[A-Za-z][-A-Z_a-z0-9\x20]*/, /[A-Za-z][-A-Z_a-z0-9]*/)
, (parsed) => {
parsed = parsed.trim();
parsed = parsed.replaceAll(/\x20{2,}/g, '\x20'); //空白が並んでいた場合は1つにまとめる
return { '#identifier': parsed };
});
const sequnce = _map(
wraper, _repeate(_branch(() => { return this.sequnceSeparator }, ",", _option(",")), wraper, 0)
, (parsed) => {
const methodName = "sequnceのfn";
・・・
この程度の処理でもmapのsequenceパラメータ部に分岐処理を詰め込む必要があったのでパーサにしないといけないが、このままでは「?特殊文字列?」の機能はEbnfParser限定になってしまう。後、?と?の間の空白は両端をTrimし連続する空白は1個にまとめている。
?テスト?
サンプル = ?=テスト?, "TEST", "NOW" | "TEST" ; ☚上で?テスト?を宣言すると?=テスト?がtrue,'テスト'を返すとか?
のような使い方ができると分岐も書けて便利かな?特殊文字をコメントっぽく扱っているので、ちゃんと文字列あるいは何かのリテラルとして扱える様にしないとダメかもしれない。EBNFの*演算子は正規表現っぽく使わないらしいのでこの辺を見直さないといけないかな?例外’ー’の意味が判らなかったが、集合の差らしい。つまり
集合の積 = A , B , C ; (* A 且つ、 B 且つ、 C である *)
集合の和 = A | B | C ; (* A または、B または、C である *)
集合の差 = A - B - C ; (* A である、B は除外、C も除外 *)
っぽい考えらしい。実際には EBNFでは集合の積より順序の方が意味が強い、また最後のは、(* A である、B は除外、でも C は含む *)とも読めるけど、面倒そうだから気のせいにして、実装した。
/**
* 例外(集合の差)パーサを作成する
* @param {Array of parser} parsers 例外(集合の差)するパーサの配列
* @return {Function} 生成した連結パーサ
*/
exception(...parsers) {
const methodName = 'exception';
// パーサに正規表現やテキストが混ざっていたらパーサに変換する
parsers = this.normalizeParses(parsers);
this.chkFunction(methodName, "parsers", parsers);
// パーサが単体の場合はそのまま返す
if (parsers.length === 1) {
this.chkFunction(methodName, "parsers[0]", parsers[0]);
return parsers[0];
}
// パーサが複数の場合は、全ての判定が真の場合に成功を返すパーサを渡す
const func =
/**
* 生成した例外(集合の差)パーサ
* @param {string} target ソースコード
* @param {integer} position 読取り位置
* @return {ParseInfo} パースした結果
*/
(target, position) => {/*sequnce*/
const bk = this.fSpaceSkip;
if (this.fSpaceSkip) { [target, position] = this.skipSpace(target, position); }
const oldPosition = position;
let result;
for (let i = 0; i < parsers.length; i++) {
const parser = parsers[i];
this.chkFunction(methodName, "parser", parser);
const parsed = parser(target, position);
if (parsed === undefined) {
continue;
}
if ((i === 0 && parsed.success) || (i !== 0 && !parsed.success)) {
if (i === 0) {
result = parsed.result;
}
} else {
parsed.position = oldPosition;
// 一つでも判定で偽となれば、このパーサ自体が失敗を返す
parsed.success = false;
this.fSpaceSkip = bk;
return parsed;
}
}
this.fSpaceSkip = bk;
return new ParseInfo(true, result, position);
};
return func;
}
sequenceと違うのは名前とtrue,falseの判定の仕方だけ
「-」を演算子に使うので非終端記号(識別子)に「-」は使えなくなったのが地味に痛い。ここにもあっちにも「-」が散乱してたので修正ががが。
セルの数式をトークン化した影響でJSONファイルをアップロードするとセルに数式が入らない、日付がテキストに化けていたのを修正。テキストファイルのアップロードも。
ps.2024/4/11
数式のセル参照がセル範囲形式の場合に先頭と最終セルを格納していたので中間のセルの値が変わっても反応していなかった。マクロとJDOCと正規表現のEBNF構文を模索中。
- 今も正規表現を正規表現でちゃんと表現していないので時々通らない事がある
- とりあえずEBNF化して解析できるようにしたい。
- javascriptっぽいマクロが書けるようにしたい
- 組み込み関数もマクロで書いた方が楽そうだから
- でも、全く型無し支援無しは辛いのでJDOCをマクロ構文に盛り込みたい
- JDOCもEBNF化したい
- このマクロのEBNFを1つのBNFで書けない事はないがコピペだらけになる
- 各々のEbnfParser派生クラスで作成したparserを参照できるようにしたい。
- 作成する順番に依存すると大変
- lazy的な参照の仕組みにしよう
- 作成する順番に依存すると大変
- 各々のEbnfParser派生クラスで作成したparserを参照できるようにしたい。
- このマクロのEBNFを1つのBNFで書けない事はないがコピペだらけになる
- JDOCもEBNF化したい
- でも、全く型無し支援無しは辛いのでJDOCをマクロ構文に盛り込みたい
- 組み込み関数もマクロで書いた方が楽そうだから
- EbnfPaser ,ExprParser, ( MacroParser, JdocParser }
- RegexpParserは最終結果をEbnfParserのparser.regexpのパターンに組込む
- &:EBNFのパターンを空白無しで処理する演算子に正規表現も組み込めそう
- “/” + /.*/ + “/” 的なものが予想通りな結果になったら嬉しい
- &:EBNFのパターンを空白無しで処理する演算子に正規表現も組み込めそう
- RegexpParserは最終結果をEbnfParserのparser.regexpのパターンに組込む
- パーサ作成部(makeParser)とパーサ実行部(parse)に分離
- 各々のコンストラクタで自分の構文を事前にパーサ作成させる
- makeParserとparserは各々継承させる
- 各々のコンストラクタで自分の構文を事前にパーサ作成させる
ps.2024/4/12
パーサ作成処理内でテストパターンを処理させるようにしたら、ほとんどの数式がテキスト扱い。数式のEBNFはあちこちに*を付けていたが目障りで何となく削除したせいだった。真の原因は肝心の{…}のパーサの処理がリピートしておらず実質choiceになっていて、ごく一部の2つのパラメータを持つ関数や1+2などは数式として扱っていたものの、=1とかはテキストになっていた。関数のパラメータが無い場合の扱いが曖昧だった。
(*$ SEQUENCE SEPARATOR IS NOT REQUIRED $*)
(* セルのデータ *)
cell = expr_cell | value_cell ;
(* 値 *)
value_cell = time_stamp | date | time | number | boolean | sqstring | text_etc ;
time_stamp = date ' ' time ;
date = yyyy '/' MM '/' dd | MM '/' dd ;
yyyy = /\\d{4}/ ;
MM = /\\d{1,2}/ ;
dd = /\\d{1,2}/ ;
time = HH ':' mm ':' ss | HH ':' mm ;
HH = /\\d{1,2}/ ;
mm = /\\d{1,2}/ ;
ss = /\\d{1,2}/ ;
number = /[-]?([0-9]+)([.][0-9]*)?/ ;
boolean = /true/i | /false/i ;
sqstring = /'.*/ ;
text_etc = /.+/ ;
(* 数式 *)
expr_cell = '=' expr ;
expr = logical_expr ;
logical_expr = add_sub_expr [ ( '=' | '>' | '<' | '>=' | '<=' | '<>' ) add_sub_expr ] ;
add_sub_expr = mul_div_expr { ( '+' | '-' | '&' ) mul_div_expr } ;
mul_div_expr = factor { ( '*' | '/' ) factor } ;
factor = value | parenthesis_expr ;
parenthesis_expr = '(' expr ')' ;
value = number | boolean | function_def | a1_range | a1 | dqstring ;
a1_range = a1 ':' a1 ;
a1 = /([A-Za-z]+[1-9][0-9]*)/ ;
dqstring = /"[^"]*"/ ;
function_def = symbol '(' parameters ')' ;
symbol = /([A-Za-z][A-Za-z0-9_]*)/ ;
parameters = [ expr ] { ',' expr } ;
それにしても、さっきまでparameters = expr { ‘,’ expr } ;で動いていたのはなぜなんだろう。
- 循環計算をescキー入力で中断させたい。
- 数式セルに計算回数(count)を追加し、どこかでセルの内容を手入力で書き換えたらリセットして再計算を開始、セル値が更新したら+1し、10,100,1000,10000になったら、中止確認メッセージ
- どこかでセルの内容を手入力で書き換えたら、再計算開始時刻をリセットし、10秒たっても再計算が完了しなかったら、「タイムオーバー」
ps.2024/4/13
繰返し演算子に正規表現の{n,m}を追加したところ、パーサが長く読みにくいので、呼出部(parser.js)は今まで通りにパラメータの調整やチェックの後、基本部(coreParser.js)で生成されたパーサに空白読飛し等をラップする。
・どうやってもログのクラス名(this.className)が派生クラス名になる。
ps.2024/4/14
EBNFのパーサを微調整。repeat symboleを囲み文字やコメントのみにした。継承クラス同士で同じメソッド名になるがどうしても自分のクラスのメソッドを呼びたいケースがあるのでローカルメソッド名(#xxxx)を併用。
xxxx(a,b,c) { // 継承先のクラスのメソッドを呼びたい場合に使用する
return this.#xxxx(a,b,c);
}
#xxxx(a,b,c) { // 自分のクラスのメソッドをどうしても呼びたい場合に使用する ※makeParser, parse, selfTest, etc.
本体
}
ps.2024/4/15
でも、全ソースで同じ内容の#メソッドを書くのは、後々面倒なので、
・・・各派生クラスのconstructorに以下をコピペして
this.selfTest(
this.#getSelfTestList(), this.#parser,
{
className: 'EbnfParser',
testLog: false, jsonLog: false, mapLog: false,
parse: this.#parse.bind(this)
}
);
と呼び出し、
・・・以下、基底クラスで、実行させる
/**
* 作成したパーサをテストする
* @param {Function} parser
* @param {string} testText
* @param {object} info
* @returns
*/
selfTest(testText, parser, info) {
const methodName = 'selfTest';
if (Array.isArray(testText)) {
const stack = testText.map((t, index, a) => {
if (info) {
console.log(`${info.className}.${methodName}: pattern[${index + 1}/${a.length}]`);
}
return this.selfTest(t, parser, info);
});
return stack;
} else {
if (info && info.testLog) {
this.map.DEBUG = info.mapLog;
console.log(`${info.className}.${methodName}: text:'${testText}'`);
}
const result = info.parse(testText, parser);
if (info.jsonLog) {
this.jsonStringifyLog(result);
}
return result;
}
}
と、ローカルなメソッドをパラメータに列挙してかいくぐることにした。※いつかシンタックスなエラーになりそうだけど。
ps.2024/4/16
EBNFテキストはセルフサンプル用なので色々変更しても、数式など他のEBNF文法に影響を与えないので、どうでもいいのだけれど・・・EBNFテキストで「repeate='{‘, sequence, ‘}’」と修正した。実際のEBNFパーサrepeateの実装に揃えた形。なぜsequenceになったのか?文法の繰り返しで多用する「'{‘ 演算子, 非終端記号 ‘}’」を書くたびにrepeate(sequence(a,b,c}}と毎度sequenceを書くのが面倒で、多用する方に実装方法を寄せました。
それよりも
記事中のsequnceに波線が付いていたけど・・・sequence not found.のエラーメッセージの影響で、EBNFのsequenceの綴りを訂正。いっぱい間違っていたので、修正漏れとか修正ミスとかでサッパリ動かず。
&演算子の結果がundefinedだった。式が未入力のセルを参照した場合は数値の0として扱う。