expr_parse.dart 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  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. // `%` 是 Symja 的"上一次结果",编辑框里统一显示成 ANS
  170. if (c == '%') {
  171. i++;
  172. return [const LitNode('%', display: 'ANS')];
  173. }
  174. if (_isIdentStart(c)) {
  175. final name = _ident();
  176. _ws();
  177. if (!_eof && _cur == '[') {
  178. i++;
  179. final args = _args();
  180. _expect(']');
  181. // ∫ / ∑ / ∏ / lim 的习惯写法认成大运算符,这样复制→粘贴能闭环
  182. final big = _tryBigOp(name, args);
  183. return [big ?? FuncNode(name, args)];
  184. }
  185. return [LitNode(name)];
  186. }
  187. throw ExprParseException('看不懂的字符「$c」', i);
  188. }
  189. /// 认得出来就返回大运算符节点,认不出返回 null(交给普通的 FuncNode)。
  190. static ExprNode? _tryBigOp(String name, List<SeqNode> args) {
  191. if (args.length != 2) return null;
  192. if (name == 'Limit') {
  193. // Limit[f, x -> v]
  194. final parts = _splitTopLevel(args[1], '->');
  195. if (parts == null) return null;
  196. return BigOpNode(
  197. kind: 'Limit',
  198. body: args[0],
  199. variable: parts[0],
  200. lower: parts[1],
  201. upper: SeqNode(),
  202. );
  203. }
  204. if (name != 'Integrate' && name != 'Sum' && name != 'Product') return null;
  205. // Integrate[f, x] —— 不定积分,没有上下限
  206. if (args[1].children.length != 1 || args[1].children.first is! BraceNode) {
  207. return BigOpNode(
  208. kind: name,
  209. body: args[0],
  210. variable: args[1],
  211. lower: SeqNode(),
  212. upper: SeqNode(),
  213. );
  214. }
  215. // Integrate[f, {x, a, b}]
  216. final inner = (args[1].children.first as BraceNode).inner;
  217. final parts = _splitTopLevel(inner, ',');
  218. if (parts == null || parts.length != 3) return null;
  219. return BigOpNode(
  220. kind: name,
  221. body: args[0],
  222. variable: parts[0],
  223. lower: parts[1],
  224. upper: parts[2],
  225. );
  226. }
  227. /// 按顶层出现的 [sep] 字面量把槽切开。没出现就返回 null。
  228. ///
  229. /// 逗号在嵌套结构里(比如 `{Sin[x, y], 1}` 的内层)不算 —— 这里的
  230. /// 子节点本来就是树,所以"顶层"天然成立。
  231. static List<SeqNode>? _splitTopLevel(SeqNode seq, String sep) {
  232. final hits = <int>[
  233. for (var k = 0; k < seq.children.length; k++)
  234. if (seq.children[k] is LitNode &&
  235. ((seq.children[k] as LitNode).text == sep ||
  236. (seq.children[k] as LitNode).text == '$sep '))
  237. k,
  238. ];
  239. if (hits.isEmpty) return null;
  240. if (hits.length == 1 && sep == '->') {
  241. final k = hits.first;
  242. return [
  243. SeqNode(seq.children.sublist(0, k)),
  244. SeqNode(seq.children.sublist(k + 1)),
  245. ];
  246. }
  247. if (sep != ',') return null;
  248. final out = <SeqNode>[];
  249. var start = 0;
  250. for (final k in hits) {
  251. out.add(SeqNode(seq.children.sublist(start, k)));
  252. start = k + 1;
  253. }
  254. out.add(SeqNode(seq.children.sublist(start)));
  255. return out;
  256. }
  257. List<SeqNode> _args() {
  258. final out = <SeqNode>[];
  259. _ws();
  260. if (!_eof && _cur == ']') return out; // Sin[] 这种空参数
  261. while (true) {
  262. final nodes = _expr();
  263. // 空槽序列化出来是 Null;解析时还原成空槽,这样"复制→粘贴"能闭环
  264. final isNull = nodes.length == 1 &&
  265. nodes.first is LitNode &&
  266. (nodes.first as LitNode).text == 'Null';
  267. out.add(isNull ? SeqNode() : SeqNode(nodes));
  268. _ws();
  269. if (!_eof && _cur == ',') {
  270. i++;
  271. continue;
  272. }
  273. break;
  274. }
  275. return out;
  276. }
  277. String _number() {
  278. final start = i;
  279. while (!_eof && (_isDigit(_cur) || _cur == '.')) {
  280. i++;
  281. }
  282. // 科学计数法
  283. if (!_eof && (_cur == 'e' || _cur == 'E')) {
  284. var k = i + 1;
  285. if (k < src.length && (src[k] == '+' || src[k] == '-')) k++;
  286. if (k < src.length && _isDigit(src[k])) {
  287. i = k;
  288. while (!_eof && _isDigit(_cur)) {
  289. i++;
  290. }
  291. }
  292. }
  293. return src.substring(start, i);
  294. }
  295. String _ident() {
  296. final start = i;
  297. while (!_eof && _isIdentPart(_cur)) {
  298. i++;
  299. }
  300. return src.substring(start, i);
  301. }
  302. }
  303. bool _isDigit(String c) {
  304. final u = c.codeUnitAt(0);
  305. return u >= 0x30 && u <= 0x39;
  306. }
  307. bool _isLetter(String c) {
  308. final u = c.codeUnitAt(0);
  309. return (u >= 0x41 && u <= 0x5A) || (u >= 0x61 && u <= 0x7A) || c == '√';
  310. }
  311. bool _isIdentStart(String c) => _isLetter(c) || c == '_' || c == r'$';
  312. bool _isIdentPart(String c) => _isLetter(c) || _isDigit(c) || c == '_' || c == r'$';