| |

Dart 86 🎯 Writing Dynamic Code Evaluators and Parsers in Dart

⏱️ Estimated read: 19 min | |

Dart has no eval function. The language compiles ahead of time, and the runtime does not include a mechanism to parse and execute arbitrary Dart source from a string. The Stack Overflow revision citing a Dart team member is direct: “Dart will never have eval” . This is a deliberate design decision, not an omission. It means that any “dynamic code” in Dart is either a restricted expression language that you implement yourself, a third-party interpreter that parses a subset of Dart, or a code-generation step that produces valid Dart files at build time. The parser you write is the boundary between trusted application code and untrusted input, and the architecture of that parser determines whether the dynamic feature is a useful configuration mechanism or a remote code execution vulnerability.

The practical patterns fall into three categories. A math/expression evaluator parses a small grammar—numbers, operators, parentheses, variables—and evaluates it to a value. Packages like math_expressions use a Shunting Yard parser to handle operator precedence, and math_compute provides a customizable lexer/parser/evaluator with error tokens . A formula or condition parser extends the grammar with comparison operators, logical operators, and function calls, which is what digit_formula_parser does for config-driven form logic . A general-purpose interpreter parses a subset of Dart itself, with a permission system that blocks filesystem, network, and process access unless explicitly granted, which is the approach d4rt takes . The choice between these categories is a choice about how much grammar you are willing to own.

This chapter covers the lexer-parser-evaluator pipeline, the grammar design decisions that determine what the evaluator can express, the Pratt parsing technique for operator precedence, the security boundary that separates a safe expression evaluator from a dangerous one, and the cases where a third-party package is the correct choice over a hand-rolled parser. It assumes familiarity with Dart’s basic syntax and the concept of tokenization.

Key point: Dart has no eval. Every dynamic code feature is a parser you own. The grammar you define is the attack surface. A math evaluator that accepts only numbers and arithmetic operators is safe. A parser that accepts identifiers and resolves them to objects is a different security model entirely. Decide which one you are building before writing the first tokenizer.


Why dynamic evaluation requires a custom parser

The language has no runtime compiler. Dart compiles to kernel or machine code ahead of time. The dart:mirrors library exists for reflection in JIT mode, but it reflects over existing declarations; it does not parse source strings . The dart_vm_injector package can evaluate expressions inside a live JIT VM, but it requires the VM service to be enabled and it exposes the entire heap, including secrets. It is a debugging and forensics tool, not a feature you ship in an application .

Expression languages are smaller than Dart. A configuration formula like status == "active" && score > 80 needs identifiers, string and numeric literals, comparison operators, and logical operators. It does not need classes, generics, async, or imports. The smaller grammar is easier to parse, easier to secure, and easier to explain to the person writing the formula. The digit_formula_parser package demonstrates this: it supports arithmetic, comparisons, logical operators, parentheses, and a set of math functions, and nothing else .

Third-party interpreters trade ownership for capability. d4rt implements a Dart interpreter with a permission system that blocks dart:io and dart:isolate by default. The permissions are fine-grained: FilesystemPermission.writePath('/tmp') grants write access to one directory, not the entire filesystem . The trade-off is that the interpreter is a large dependency, it may not implement every Dart feature, and its security model is only as good as its permission enforcement. For a plugin system that needs to run user-supplied logic, the trade-off may be worth it. For a configuration formula, it is overkill.

The trade-off. A hand-rolled parser is more code to write and more code to test. A third-party interpreter is less code but a larger trust surface. An expression evaluator package is a middle ground: it owns the lexer, parser, and evaluator, but it restricts the grammar to what the package supports. The decision depends on whether the dynamic code needs to express arbitrary computation or a constrained set of rules.


a. The lexer-parser-evaluator pipeline

The pipeline has three stages. The lexer converts the input string into a list of tokens. Each token has a type (number, identifier, operator, parenthesis) and a value. The parser consumes the token list and produces an abstract syntax tree (AST) or a direct evaluation. The evaluator walks the AST and produces a value. The separation matters because it isolates the concerns: the lexer handles character-level errors, the parser handles grammar errors, and the evaluator handles runtime errors like division by zero or an unknown variable.

// A minimal lexer for arithmetic expressions.
enum TokenType { number, plus, minus, star, slash, lparen, rparen, eof }

class Token {
  final TokenType type;
  final String lexeme;
  final double? value; // Populated for numbers.

  Token(this.type, this.lexeme, [this.value]);
}

class Lexer {
  final String source;
  int _pos = 0;

  Lexer(this.source);

  List<Token> tokenize() {
    final tokens = <Token>[];
    while (_pos < source.length) {
      final ch = source[_pos];
      if (ch == ' ') {
        _pos++;
      } else if (ch == '+') {
        tokens.add(Token(TokenType.plus, '+'));
        _pos++;
      } else if (ch == '-') {
        tokens.add(Token(TokenType.minus, '-'));
        _pos++;
      } else if (ch == '*') {
        tokens.add(Token(TokenType.star, '*'));
        _pos++;
      } else if (ch == '/') {
        tokens.add(Token(TokenType.slash, '/'));
        _pos++;
      } else if (ch == '(') {
        tokens.add(Token(TokenType.lparen, '('));
        _pos++;
      } else if (ch == ')') {
        tokens.add(Token(TokenType.rparen, ')'));
        _pos++;
      } else if (_isDigit(ch)) {
        final start = _pos;
        while (_pos < source.length && (_isDigit(source[_pos]) || source[_pos] == '.')) {
          _pos++;
        }
        final text = source.substring(start, _pos);
        tokens.add(Token(TokenType.number, text, double.parse(text)));
      } else {
        throw FormatException('Unexpected character: $ch');
      }
    }
    tokens.add(Token(TokenType.eof, ''));
    return tokens;
  }

  bool _isDigit(String ch) => ch.codeUnitAt(0) >= 48 && ch.codeUnitAt(0) <= 57;
}

The parser has several strategies. A recursive descent parser writes one function per grammar rule, which is readable but can overflow the stack on deeply nested input. A Pratt parser (top-down operator precedence) handles binary operators with a binding-power table and is iterative, avoiding stack overflow on long operator chains . A Shunting Yard parser converts infix to postfix (Reverse Polish Notation) and evaluates the RPN with a stack, which is what math_expressions uses .

// A recursive descent parser for the arithmetic grammar.
class Parser {
  final List<Token> tokens;
  int _pos = 0;

  Parser(this.tokens);

  Token get _current => tokens[_pos];

  // expr := term (('+' | '-') term)*
  double parseExpr() {
    var value = _parseTerm();
    while (_current.type == TokenType.plus || _current.type == TokenType.minus) {
      final op = _current.type;
      _pos++;
      final right = _parseTerm();
      value = op == TokenType.plus ? value + right : value - right;
    }
    return value;
  }

  // term := factor (('*' | '/') factor)*
  double _parseTerm() {
    var value = _parseFactor();
    while (_current.type == TokenType.star || _current.type == TokenType.slash) {
      final op = _current.type;
      _pos++;
      final right = _parseFactor();
      if (op == TokenType.star) {
        value *= right;
      } else {
        if (right == 0) throw ArgumentError('Division by zero');
        value /= right;
      }
    }
    return value;
  }

  // factor := number | '(' expr ')'
  double _parseFactor() {
    if (_current.type == TokenType.number) {
      final value = _current.value!;
      _pos++;
      return value;
    }
    if (_current.type == TokenType.lparen) {
      _pos++;
      final value = parseExpr();
      if (_current.type != TokenType.rparen) {
        throw FormatException('Expected )');
      }
      _pos++;
      return value;
    }
    throw FormatException('Unexpected token: ${_current.lexeme}');
  }
}

// Usage:
void main() {
  final lexer = Lexer('2 + 3 * (4 - 1)');
  final parser = Parser(lexer.tokenize());
  print(parser.parseExpr()); // 11.0
}

The grammar above is left-associative and respects precedence: * and / bind tighter than + and -, and parentheses override. The recursive descent structure mirrors the grammar, which makes it readable. The limitation is that a long chain like 1 + 1 + 1 + ... creates a deep call stack. For arithmetic expressions, this is rarely a problem. For a general expression language with user-supplied input, the Pratt parser is the safer choice .


b. Grammar design: what the evaluator accepts

The grammar is the security boundary. A grammar that accepts only numbers and arithmetic operators cannot call a function, access a variable, or read a file. A grammar that accepts identifiers and resolves them to objects can do all three, depending on how the evaluator implements the lookup.

Variables add a symbol table. The evaluator receives a Map<String, double> and looks up the identifier when it encounters one. The math_expressions package provides a ContextModel with bindVariableName, and string_equation accepts a variable map in its constructor . The security question is what happens when the identifier is not in the map: throw an error, return zero, or return null. The correct answer is to throw, because a silent default hides typos and makes the formula’s behavior unpredictable.

Functions add a dispatch table. sqrt(16) requires the lexer to produce an identifier token, the parser to recognize the identifier( pattern, and the evaluator to look up sqrt in a function map. The digit_formula_parser supports sin, cos, sqrt, ceil, floor, round, abs, avg, min, and max . The security boundary is the function map: if the evaluator looks up functions in a global namespace that includes print, exit, or anything from dart:io, the formula can escape the arithmetic sandbox. The map should be a closed set defined by the application, not the global environment.

Strings and booleans expand the grammar to conditions. status == "active" requires string literals, comparison operators, and a value type that can hold strings, numbers, and booleans. The digit_formula_parser handles mixed-type comparisons with coercion, which is convenient but introduces ambiguity: "123" == 123 is true in that package . The application must decide whether coercion is acceptable. In a form validation rule, it often is. In a financial calculation, it is not.

Property access is the most dangerous addition. An expression like user.role == "admin" requires the evaluator to look up a property on an object. If the object is a plain map, the lookup is safe. If the object is a Dart object with getters, the evaluator can invoke arbitrary code. The d4rt interpreter handles this by requiring explicit permissions for dangerous operations, but an expression evaluator should avoid property access entirely unless the values are plain data .


c. The security boundary and the third-party decision

A dynamic evaluator is a trust boundary. The input is untrusted; the evaluator runs with the application’s privileges. The design decisions that determine the risk are the grammar, the evaluator’s environment, and the resource limits.

The grammar limits what can be expressed. An arithmetic grammar cannot call functions. A grammar with identifiers but no function calls cannot invoke anything. A grammar with function calls can invoke anything in the function map. The function map is the policy: define it as a closed set, and the grammar’s capability is bounded by that set.

The evaluator’s environment limits what the grammar can reach. A pure evaluator that operates only on its input tokens and a symbol table has no access to the filesystem, the network, or the process. A d4rt interpreter that loads dart:io has access to everything the permission system allows . The gap between these two is the gap between a formula evaluator and a plugin system.

Resource limits prevent denial of service. A recursive descent parser on deeply nested input can overflow the stack. An evaluator on a formula with a million multiplications can run for seconds. The rumil Pratt parser is iterative, which avoids the stack overflow . A production evaluator should also cap the input length, the token count, and the evaluation steps.

When to use a package. If the requirement is arithmetic and comparison expressions with variables and a few functions, math_expressions, string_equation, or digit_formula_parser already implement the pipeline . If the requirement is a custom grammar with specific operator precedence and error messages, math_compute provides a customizable lexer and parser . If the requirement is to run untrusted Dart code with a permission system, d4rt is the only practical option, and the permission system is the reason to choose it . Writing a parser from scratch is justified when the grammar is domain-specific and no existing package models it, or when the evaluator must be small enough to audit line by line.


Complete Example Session

// ============================================
// PART 1: TOKEN DEFINITION
// ============================================
enum TokenType { number, identifier, plus, minus, star, slash, lparen, rparen, eq, eof }

class Token {
  final TokenType type;
  final String lexeme;
  final double? number;

  Token(this.type, this.lexeme, [this.number]);
}

// ============================================
// PART 2: LEXER
// ============================================
class Lexer {
  final String source;
  int _pos = 0;

  Lexer(this.source);

  List<Token> tokenize() {
    final tokens = <Token>[];
    while (_pos < source.length) {
      final ch = source[_pos];
      if (ch == ' ') { _pos++; continue; }
      if (ch == '+') { tokens.add(Token(TokenType.plus, '+')); _pos++; continue; }
      if (ch == '-') { tokens.add(Token(TokenType.minus, '-')); _pos++; continue; }
      if (ch == '*') { tokens.add(Token(TokenType.star, '*')); _pos++; continue; }
      if (ch == '/') { tokens.add(Token(TokenType.slash, '/')); _pos++; continue; }
      if (ch == '(') { tokens.add(Token(TokenType.lparen, '(')); _pos++; continue; }
      if (ch == ')') { tokens.add(Token(TokenType.rparen, ')')); _pos++; continue; }
      if (ch == '=') { tokens.add(Token(TokenType.eq, '=')); _pos++; continue; }
      if (_isDigit(ch)) {
        final start = _pos;
        while (_pos < source.length && (_isDigit(source[_pos]) || source[_pos] == '.')) {
          _pos++;
        }
        final text = source.substring(start, _pos);
        tokens.add(Token(TokenType.number, text, double.parse(text)));
        continue;
      }
      if (_isAlpha(ch)) {
        final start = _pos;
        while (_pos < source.length && _isAlphaNumeric(source[_pos])) {
          _pos++;
        }
        tokens.add(Token(TokenType.identifier, source.substring(start, _pos)));
        continue;
      }
      throw FormatException('Unexpected character: $ch');
    }
    tokens.add(Token(TokenType.eof, ''));
    return tokens;
  }

  bool _isDigit(String c) => c.codeUnitAt(0) >= 48 && c.codeUnitAt(0) <= 57;
  bool _isAlpha(String c) =>
      (c.codeUnitAt(0) >= 65 && c.codeUnitAt(0) <= 90) ||
      (c.codeUnitAt(0) >= 97 && c.codeUnitAt(0) <= 122) ||
      c == '_';
  bool _isAlphaNumeric(String c) => _isAlpha(c) || _isDigit(c);
}

// ============================================
// PART 3: PARSER WITH PRECEDENCE
// ============================================
class Parser {
  final List<Token> tokens;
  final Map<String, double> variables;
  int _pos = 0;

  Parser(this.tokens, this.variables);

  Token get _current => tokens[_pos];

  double parse() {
    final value = _parseExpr();
    if (_current.type != TokenType.eof) {
      throw FormatException('Unexpected token: ${_current.lexeme}');
    }
    return value;
  }

  // expr := term (('+' | '-') term)*
  double _parseExpr() {
    var value = _parseTerm();
    while (_current.type == TokenType.plus || _current.type == TokenType.minus) {
      final op = _current.type;
      _pos++;
      final right = _parseTerm();
      value = op == TokenType.plus ? value + right : value - right;
    }
    return value;
  }

  // term := factor (('*' | '/') factor)*
  double _parseTerm() {
    var value = _parseFactor();
    while (_current.type == TokenType.star || _current.type == TokenType.slash) {
      final op = _current.type;
      _pos++;
      final right = _parseFactor();
      if (op == TokenType.star) {
        value *= right;
      } else {
        if (right == 0) throw ArgumentError('Division by zero');
        value /= right;
      }
    }
    return value;
  }

  // factor := number | identifier | '(' expr ')'
  double _parseFactor() {
    if (_current.type == TokenType.number) {
      final value = _current.number!;
      _pos++;
      return value;
    }
    if (_current.type == TokenType.identifier) {
      final name = _current.lexeme;
      _pos++;
      final value = variables[name];
      if (value == null) throw ArgumentError('Unknown variable: $name');
      return value;
    }
    if (_current.type == TokenType.lparen) {
      _pos++;
      final value = _parseExpr();
      if (_current.type != TokenType.rparen) throw FormatException('Expected )');
      _pos++;
      return value;
    }
    throw FormatException('Unexpected token: ${_current.lexeme}');
  }
}

// ============================================
// PART 4: EVALUATION HELPER
// ============================================
double evaluate(String expression, {Map<String, double> variables = const {}}) {
  final tokens = Lexer(expression).tokenize();
  return Parser(tokens, variables).parse();
}

// ============================================
// PART 5: USAGE
// ============================================
void main() {
  // Arithmetic with precedence.
  print(evaluate('2 + 3 * 4')); // 14.0
  print(evaluate('(2 + 3) * 4')); // 20.0

  // Variables.
  print(evaluate('price * quantity', variables: {'price': 10.0, 'quantity': 3.0})); // 30.0

  // Error cases.
  try {
    evaluate('1 / 0');
  } on ArgumentError catch (e) {
    print('Error: ${e.message}'); // Division by zero
  }

  try {
    evaluate('x + 1');
  } on ArgumentError catch (e) {
    print('Error: ${e.message}'); // Unknown variable: x
  }
}

// ============================================
// PART 6: THE GRAMMAR IS THE SECURITY BOUNDARY
// ============================================
// This evaluator accepts:
//   - Numbers (literals)
//   - Identifiers resolved from a Map
//   - +, -, *, /, parentheses
//
// It does NOT accept:
//   - Function calls
//   - Property access
//   - String literals
//   - Any reference to the Dart runtime
//
// The attack surface is the variable map and the
// division-by-zero check. Neither can reach
// the filesystem or the network.

// ============================================
// PART 7: WHEN TO USE A PACKAGE
// ============================================
// math_expressions: Shunting Yard, variable binding.
// math_compute: customizable lexer/parser/evaluator.
// string_equation: conditions with variable maps.
// digit_formula_parser: form validation formulas.
// d4rt: Dart interpreter with permission system.

// ============================================
// PART 8: PRATT PARSER FOR LONG CHAINS
// ============================================
// Recursive descent overflows on deep nesting.
// Pratt parsing is iterative and handles
// unbounded operator chains without stack growth.
// rumil provides pratt<int> with cFamilyPrecedence.

// ============================================
// PART 9: ERROR HANDLING
// ============================================
// Lexer: FormatException for unexpected characters.
// Parser: FormatException for grammar errors.
// Evaluator: ArgumentError for runtime errors
//   (division by zero, unknown variable).
// The caller catches and reports; the evaluator
// does not swallow errors.

// ============================================
// PART 10: THE COMPLETE FLOW
// ============================================
// 1. Input string.
// 2. Lexer → List<Token>.
// 3. Parser → AST or direct evaluation.
// 4. Evaluator → value or error.
// 5. Grammar defines capability.
// 6. Variable map defines context.
// 7. Package or custom: based on grammar shape.

Quick Reference

Pipeline Stages

StageInputOutputError Type
LexerStringList<Token>FormatException
ParserTokensAST or valueFormatException
EvaluatorASTValueArgumentError

Parser Strategies

StrategyStructureStack SafetyBest For
Recursive descentOne function per ruleDeep nesting overflowsSimple grammars
PrattBinding-power loopIterative, unboundedOperator precedence
Shunting YardInfix → RPN → stackStack-basedArithmetic expressions

Dart Packages

PackageTypeGrammarSecurity
math_expressionsExpression evaluatorArithmetic, variablesSandboxed
math_computeCustomizable evaluatorConfigurableSandboxed
string_equationCondition evaluatorComparisons, logicSandboxed
digit_formula_parserFormula parserComparisons, functionsSandboxed
d4rtDart interpreterDart subsetPermission system

Security Boundary

Grammar FeatureCapabilityRisk
Numbers, arithmeticComputeNone
Variables (Map lookup)Read configLow
Functions (closed set)ComputeLow if map is closed
Property accessInvoke gettersHigh
Dart importsFull runtimeCritical

Best Practices

✅ Do This:

// Define a closed grammar.                                   // ✅
// Numbers, operators, parentheses, variables from a Map.

// Throw on unknown variables.                                // ✅
if (value == null) throw ArgumentError('Unknown variable: $name');

// Use a Pratt parser for user-supplied long chains.          // ✅
// Iterative, no stack overflow.

// Cap input length and evaluation steps.                     // ✅
if (source.length > 10000) throw ArgumentError('Input too long');

// Use a package for standard grammars.                       // ✅
// math_expressions, string_equation, digit_formula_parser.

❌ Don’t Do This:

// Use dart:mirrors to evaluate strings.                      // ❌
// Not available in AOT; not a parser.

// Resolve identifiers to arbitrary objects.                  // ❌
// The evaluator can invoke getters and side effects.

// Expose the global function namespace.                      // ❌
// A formula could call print, exit, or dart:io.

// Use recursive descent on unbounded input.                  // ❌
// Stack overflow on deep nesting.

// Assume Dart has eval.                                      // ❌
// It does not, and it will not.

Common Pitfalls

PitfallWhy It HappensFix
Stack overflow on nested parensRecursive descentPratt parser or depth limit
Unknown variable returns zeroSilent defaultThrow ArgumentError
Formula calls a Dart functionGlobal function mapClosed set of functions
Division by zero unhandledNo check in evaluatorCheck and throw
Input length unboundedNo resource limitCap length and steps

Real-World Examples

1. Arithmetic with Precedence

evaluate('2 + 3 * 4'); // 14.0

2. Variable Substitution

evaluate('a + b', variables: {'a': 2.0, 'b': 3.0}); // 5.0

3. Unknown Variable Error

evaluate('x + 1'); // ArgumentError: Unknown variable: x

4. Division by Zero

evaluate('1 / 0'); // ArgumentError: Division by zero

5. Pratt Parser with rumil

final expr = pratt<int>(lexeme(number), cFamilyPrecedence<int>(...));
expr.run('1 + 2 * 3'); // Success(7)

6. Shunting Yard with math_expressions

final parser = ShuntingYardParser();
final exp = parser.parse('2 + 3 * 4');
final result = exp.evaluate(EvaluationType.REAL, ContextModel());

7. Condition with digit_formula_parser

FormulaParser('status == "active" && score > 80', {'status': 'active', 'score': 90}).parse;

8. D4rt Permission Grant

d4rt.grant(FilesystemPermission.writePath('/tmp'));
d4rt.eval('File("/tmp/x").writeAsStringSync("data");');

9. Custom Lexer Error

Lexer('2 @ 3').tokenize(); // FormatException: Unexpected character: @

10. Input Length Guard

if (input.length > 10000) throw ArgumentError('Input too long');

Visual: The Pipeline

┌──────────────────────────────────────────────┐
│  INPUT STRING                                │
│  "2 + 3 * (4 - 1)"                           │
│       │                                      │
│       ▼                                      │
│  LEXER                                       │
│  [2] [+] [3] [*] [(] [4] [-] [1] [)]        │
│       │                                      │
│       ▼                                      │
│  PARSER (recursive descent or Pratt)         │
│  AST: Add(2, Mul(3, Sub(4, 1)))              │
│       │                                      │
│       ▼                                      │
│  EVALUATOR                                   │
│  2 + 3 * 3 = 11                              │
│                                              │
│  Each stage has its own error type.          │
│  Each stage can reject input.                │
│                                              │
└──────────────────────────────────────────────┘

Grammar as Security Boundary

┌──────────────────────────────────────────────┐
│  GRAMMAR CAPABILITY                          │
│                                              │
│  Numbers + arithmetic:                       │
│    Cannot reach anything. Safe.              │
│                                              │
│  + Variables (Map):                          │
│    Reads config values. Low risk.            │
│                                              │
│  + Functions (closed set):                   │
│    Computes with known functions. Low risk.  │
│                                              │
│  + Property access:                          │
│    Invokes getters. High risk.               │
│                                              │
│  + Dart imports:                             │
│    Full runtime. Critical risk.              │
│                                              │
│  The grammar is the attack surface.          │
│                                              │
└──────────────────────────────────────────────┘

Recursive Descent vs Pratt

┌──────────────────────────────────────────────┐
│  RECURSIVE DESCENT                           │
│    parseExpr → parseTerm → parseFactor       │
│    Mirrors grammar. Readable.                │
│    Deep nesting overflows stack.             │
│                                              │
│  PRATT                                       │
│    Loop: atom, then while operator bp > min  │
│    Iterative. No stack growth.               │
│    Binding power table.                      │
│                                              │
│  For user-supplied input: Pratt.             │
│  For fixed internal grammar: recursive.      │
│                                              │
└──────────────────────────────────────────────┘

Package Decision

┌──────────────────────────────────────────────┐
│  WHICH PACKAGE?                              │
│                                              │
│  Arithmetic + variables?                     │
│    → math_expressions, string_equation       │
│                                              │
│  Conditions + functions?                     │
│    → digit_formula_parser                    │
│                                              │
│  Custom grammar + error tokens?              │
│    → math_compute                            │
│                                              │
│  Untrusted Dart code + permissions?          │
│    → d4rt                                    │
│                                              │
│  Nothing fits? → Write a parser.             │
│                                              │
└──────────────────────────────────────────────┘

Summary

ItemValue
Dart evalDoes not exist
PipelineLexer → Parser → Evaluator
Lexer outputList<Token>
Parser strategiesRecursive descent, Pratt, Shunting Yard
Evaluator inputAST or tokens
GrammarThe security boundary
VariablesMap<String, double> lookup
FunctionsClosed set in evaluator
Package for arithmeticmath_expressions, string_equation
Package for conditionsdigit_formula_parser
Package for Dart subsetd4rt (permission system)

Key takeaways:

  • Dart has no eval. The language compiles ahead of time, and the runtime does not parse source strings. Every dynamic code feature is a parser you own or a third-party interpreter you trust .
  • The pipeline is lexer, parser, evaluator. The lexer produces tokens, the parser produces an AST or evaluates directly, and the evaluator produces a value. Each stage has its own error type and its own rejection conditions.
  • The grammar is the attack surface. An arithmetic grammar cannot call functions. A grammar with variables reads from a map. A grammar with property access can invoke getters. The capability of the dynamic code is bounded by what the grammar can express.
  • Pratt parsing handles operator precedence without stack overflow. Recursive descent is readable for simple grammars but overflows on deeply nested input. Pratt parsing is iterative and uses a binding-power table .
  • Packages cover the common grammars. math_expressions and string_equation handle arithmetic and conditions. digit_formula_parser handles form validation formulas. d4rt runs a Dart subset with a permission system .
  • Resource limits are part of the design. Cap input length, token count, and evaluation steps. A parser that accepts unbounded input is a denial-of-service vector.

Remember: Dynamic evaluation in Dart is a parser you build or a package you trust. The language does not provide eval, and the packages that interpret Dart code do so with a permission system because the alternative is remote code execution. A math evaluator that accepts numbers, arithmetic operators, and a variable map is a small, auditable parser with a narrow attack surface. A formula parser that accepts comparisons and a closed set of functions is slightly larger but still bounded. A general Dart interpreter is a different category entirely, and the permission system is the only thing between a formula and the filesystem. Decide which category you are building before writing the first tokenizer. The grammar is the contract, and the grammar is the security boundary.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!