expr_parse.dart 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. /// 把 Symja 语法的表达式文本解析成节点树。
  2. ///
  3. /// 用途是**粘贴**:计算器的输入是结构化的,从剪贴板进来的却是一串文本,
  4. /// 得先转成树才能继续用按钮编辑。
  5. ///
  6. /// 设计原则:**只解析一个明确的子集,遇到不认识的东西就明确报错,绝不猜。**
  7. /// 猜错了用户看到的是算错的答案,比直接报错糟糕得多。
  8. ///
  9. /// 支持的语法:
  10. /// 数字(含小数与 `1e-3` 科学计数法)
  11. /// `+ - * / ^`、一元负号、并置乘法(`2x`、`2(x+1)`)
  12. /// `Name[...]`、`( … )`、`{ … }`
  13. /// `-> == :=`(当普通运算符节点)
  14. /// 标识符可以带 `_` 和 `$`(`x_`、`$ans`)
  15. ///
  16. /// 不支持(会报错):`@ /;; & 过长的上下文反引号` 等等。
  17. library;
  18. import 'expr_node.dart';
  19. /// 解析失败。带上出错位置,方便定位。
  20. class ExprParseException implements Exception {
  21. final String message;
  22. final int position;
  23. const ExprParseException(this.message, this.position);
  24. @override
  25. String toString() => position >= 0 ? '第 ${position + 1} 个字符:$message' : message;
  26. }
  27. /// 解析成功返回根槽;失败抛 [ExprParseException]。
  28. SeqNode parseSymjaExpr(String source) => _Parser(source).parse();
  29. class _Parser {
  30. final String src;
  31. int i = 0;
  32. _Parser(this.src);
  33. bool get _eof => i >= src.length;
  34. String get _cur => src[i];
  35. void _ws() {
  36. while (!_eof && const {' ', '\t', '\n', '\r'}.contains(_cur)) {
  37. i++;
  38. }
  39. }
  40. void _expect(String ch) {
  41. _ws();
  42. if (_eof) throw ExprParseException('表达式不完整,缺少「$ch」', i);
  43. if (_cur != ch) throw ExprParseException('期望「$ch」,实际是「$_cur」', i);
  44. i++;
  45. }
  46. SeqNode parse() {
  47. final seq = SeqNode(_expr());
  48. _ws();
  49. if (!_eof) {
  50. throw ExprParseException('多余的内容「${src.substring(i)}」', i);
  51. }
  52. return seq;
  53. }
  54. // ---------------------------------------------------------------- 加减层
  55. //
  56. // 树里没有"加法节点"——加减乘都是序列里的字面量运算符,优先级交给 Symja
  57. // 求值时处理。只有 / 和 ^ 做成结构(分式、幂),因为它们要竖排/上标显示。
  58. List<ExprNode> _expr() {
  59. final out = <ExprNode>[..._term()];
  60. while (true) {
  61. _ws();
  62. if (_eof) break;
  63. // 多字符运算符要先判,否则 `x->0` 会被当成 `x` 减 `>0`
  64. if (src.startsWith('->', i)) {
  65. i += 2;
  66. out.add(LitNode('->'));
  67. out.addAll(_term());
  68. } else if (src.startsWith('==', i)) {
  69. i += 2;
  70. out.add(LitNode('=='));
  71. out.addAll(_term());
  72. } else if (src.startsWith(':=', i)) {
  73. i += 2;
  74. out.add(LitNode(':='));
  75. out.addAll(_term());
  76. } else if (_cur == '+' || _cur == '-') {
  77. out.add(LitNode(_cur));
  78. i++;
  79. out.addAll(_term());
  80. } else {
  81. break;
  82. }
  83. }
  84. return out;
  85. }
  86. // ---------------------------------------------------------------- 乘除层
  87. List<ExprNode> _term() {
  88. var out = <ExprNode>[..._unary()];
  89. while (true) {
  90. _ws();
  91. if (_eof) break;
  92. if (_cur == '*') {
  93. i++;
  94. out = [...out, LitNode('*'), ..._unary()];
  95. } else if (_cur == '/') {
  96. i++;
  97. out = [
  98. FracNode(num: SeqNode(out), den: SeqNode(_unary())),
  99. ];
  100. } else if (_startsValue(_cur)) {
  101. // 并置即乘法:2x、2(x+1)。不插 `*` 节点 —— 序列化出来还是 `2x`,
  102. // Symja 认这个写法,而且用户看到的和打进去的一样。
  103. out = [...out, ..._unary()];
  104. } else {
  105. break;
  106. }
  107. }
  108. return out;
  109. }
  110. bool _startsValue(String c) =>
  111. _isDigit(c) || c == '.' || _isIdentStart(c) || c == '(' || c == '{';
  112. // ---------------------------------------------------------------- 一元 / 幂
  113. List<ExprNode> _unary() {
  114. _ws();
  115. if (_eof) throw ExprParseException('表达式不完整', i);
  116. if (_cur == '-') {
  117. i++;
  118. return [LitNode('-'), ..._unary()];
  119. }
  120. if (_cur == '+') {
  121. i++;
  122. return _unary();
  123. }
  124. return _power();
  125. }
  126. List<ExprNode> _power() {
  127. final base = _primary();
  128. _ws();
  129. if (!_eof && _cur == '^') {
  130. i++;
  131. final exp = _unary(); // 右结合
  132. return [PowNode(base: SeqNode(base), exp: SeqNode(exp))];
  133. }
  134. return base;
  135. }
  136. // ---------------------------------------------------------------- 原子
  137. List<ExprNode> _primary() {
  138. _ws();
  139. if (_eof) throw ExprParseException('表达式不完整', i);
  140. final c = _cur;
  141. if (c == '(') {
  142. i++;
  143. final inner = _expr();
  144. _expect(')');
  145. return [GroupNode(SeqNode(inner))];
  146. }
  147. if (c == '{') {
  148. i++;
  149. // 花括号里是**逗号分隔的列表**(`{n, 1, Infinity}`),不是单个表达式。
  150. // 逗号当成字面量节点留着,序列化时拼回去。
  151. final inner = <ExprNode>[];
  152. _ws();
  153. if (!_eof && _cur != '}') {
  154. while (true) {
  155. if (inner.isNotEmpty) inner.add(LitNode(', '));
  156. inner.addAll(_expr());
  157. _ws();
  158. if (!_eof && _cur == ',') {
  159. i++;
  160. continue;
  161. }
  162. break;
  163. }
  164. }
  165. _expect('}');
  166. return [BraceNode(SeqNode(inner))];
  167. }
  168. if (_isDigit(c) || c == '.') return [LitNode(_number())];
  169. if (_isIdentStart(c)) {
  170. final name = _ident();
  171. _ws();
  172. if (!_eof && _cur == '[') {
  173. i++;
  174. final args = _args();
  175. _expect(']');
  176. return [FuncNode(name, args)];
  177. }
  178. return [LitNode(name)];
  179. }
  180. throw ExprParseException('看不懂的字符「$c」', i);
  181. }
  182. List<SeqNode> _args() {
  183. final out = <SeqNode>[];
  184. _ws();
  185. if (!_eof && _cur == ']') return out; // Sin[] 这种空参数
  186. while (true) {
  187. final nodes = _expr();
  188. // 空槽序列化出来是 Null;解析时还原成空槽,这样"复制→粘贴"能闭环
  189. final isNull = nodes.length == 1 &&
  190. nodes.first is LitNode &&
  191. (nodes.first as LitNode).text == 'Null';
  192. out.add(isNull ? SeqNode() : SeqNode(nodes));
  193. _ws();
  194. if (!_eof && _cur == ',') {
  195. i++;
  196. continue;
  197. }
  198. break;
  199. }
  200. return out;
  201. }
  202. String _number() {
  203. final start = i;
  204. while (!_eof && (_isDigit(_cur) || _cur == '.')) {
  205. i++;
  206. }
  207. // 科学计数法
  208. if (!_eof && (_cur == 'e' || _cur == 'E')) {
  209. var k = i + 1;
  210. if (k < src.length && (src[k] == '+' || src[k] == '-')) k++;
  211. if (k < src.length && _isDigit(src[k])) {
  212. i = k;
  213. while (!_eof && _isDigit(_cur)) {
  214. i++;
  215. }
  216. }
  217. }
  218. return src.substring(start, i);
  219. }
  220. String _ident() {
  221. final start = i;
  222. while (!_eof && _isIdentPart(_cur)) {
  223. i++;
  224. }
  225. return src.substring(start, i);
  226. }
  227. }
  228. bool _isDigit(String c) {
  229. final u = c.codeUnitAt(0);
  230. return u >= 0x30 && u <= 0x39;
  231. }
  232. bool _isLetter(String c) {
  233. final u = c.codeUnitAt(0);
  234. return (u >= 0x41 && u <= 0x5A) || (u >= 0x61 && u <= 0x7A) || c == '√';
  235. }
  236. bool _isIdentStart(String c) => _isLetter(c) || c == '_' || c == r'$';
  237. bool _isIdentPart(String c) => _isLetter(c) || _isDigit(c) || c == '_' || c == r'$';