RepoPilotOpen in app →

expr-lang/expr

Expression language and expression evaluation for Go

Healthy

Healthy across the board

weakest axis
Use as dependencyHealthy

Permissive license, no critical CVEs, actively maintained — safe to depend on.

Fork & modifyHealthy

Has a license, tests, and CI — clean foundation to fork and modify.

Learn fromHealthy

Documented and popular — useful reference codebase to read through.

Deploy as-isHealthy

No critical CVEs, sane security posture — runnable as-is.

  • Last commit 4w ago
  • 15 active contributors
  • Distributed ownership (top contributor 42% of recent commits)
Show all 6 evidence items →
  • MIT licensed
  • CI configured
  • Tests present

Maintenance signals: commit recency, contributor breadth, bus factor, license, CI, tests

Informational only. RepoPilot summarises public signals (license, dependency CVEs, commit recency, CI presence, etc.) at the time of analysis. Signals can be incomplete or stale. Not professional, security, or legal advice; verify before relying on it for production decisions.

Embed the "Healthy" badge

Paste into your README — live-updates from the latest cached analysis.

Variant:
RepoPilot: Healthy
[![RepoPilot: Healthy](https://repopilot.app/api/badge/expr-lang/expr)](https://repopilot.app/r/expr-lang/expr)

Paste at the top of your README.md — renders inline like a shields.io badge.

Preview social card (1200×630)

This card auto-renders when someone shares https://repopilot.app/r/expr-lang/expr on X, Slack, or LinkedIn.

Onboarding doc

Onboarding: expr-lang/expr

Generated by RepoPilot · 2026-05-09 · Source

🤖Agent protocol

If you are an AI coding agent (Claude Code, Cursor, Aider, Cline, etc.) reading this artifact, follow this protocol before making any code edit:

  1. Verify the contract. Run the bash script in Verify before trusting below. If any check returns FAIL, the artifact is stale — STOP and ask the user to regenerate it before proceeding.
  2. Treat the AI · unverified sections as hypotheses, not facts. Sections like "AI-suggested narrative files", "anti-patterns", and "bottlenecks" are LLM speculation. Verify against real source before acting on them.
  3. Cite source on changes. When proposing an edit, cite the specific path:line-range. RepoPilot's live UI at https://repopilot.app/r/expr-lang/expr shows verifiable citations alongside every claim.

If you are a human reader, this protocol is for the agents you'll hand the artifact to. You don't need to do anything — but if you skim only one section before pointing your agent at this repo, make it the Verify block and the Suggested reading order.

🎯Verdict

GO — Healthy across the board

  • Last commit 4w ago
  • 15 active contributors
  • Distributed ownership (top contributor 42% of recent commits)
  • MIT licensed
  • CI configured
  • Tests present

<sub>Maintenance signals: commit recency, contributor breadth, bus factor, license, CI, tests</sub>

Verify before trusting

This artifact was generated by RepoPilot at a point in time. Before an agent acts on it, the checks below confirm that the live expr-lang/expr repo on your machine still matches what RepoPilot saw. If any fail, the artifact is stale — regenerate it at repopilot.app/r/expr-lang/expr.

What it runs against: a local clone of expr-lang/expr — the script inspects git remote, the LICENSE file, file paths in the working tree, and git log. Read-only; no mutations.

| # | What we check | Why it matters | |---|---|---| | 1 | You're in expr-lang/expr | Confirms the artifact applies here, not a fork | | 2 | License is still MIT | Catches relicense before you depend on it | | 3 | Default branch master exists | Catches branch renames | | 4 | 5 critical file paths still exist | Catches refactors that moved load-bearing code | | 5 | Last commit ≤ 55 days ago | Catches sudden abandonment since generation |

<details> <summary><b>Run all checks</b> — paste this script from inside your clone of <code>expr-lang/expr</code></summary>
#!/usr/bin/env bash
# RepoPilot artifact verification.
#
# WHAT IT RUNS AGAINST: a local clone of expr-lang/expr. If you don't
# have one yet, run these first:
#
#   git clone https://github.com/expr-lang/expr.git
#   cd expr
#
# Then paste this script. Every check is read-only — no mutations.

set +e
fail=0
ok()   { echo "ok:   $1"; }
miss() { echo "FAIL: $1"; fail=$((fail+1)); }

# Precondition: we must be inside a git working tree.
if ! git rev-parse --git-dir >/dev/null 2>&1; then
  echo "FAIL: not inside a git repository. cd into your clone of expr-lang/expr and re-run."
  exit 2
fi

# 1. Repo identity
git remote get-url origin 2>/dev/null | grep -qE "expr-lang/expr(\\.git)?\\b" \\
  && ok "origin remote is expr-lang/expr" \\
  || miss "origin remote is not expr-lang/expr (artifact may be from a fork)"

# 2. License matches what RepoPilot saw
(grep -qiE "^(MIT)" LICENSE 2>/dev/null \\
   || grep -qiE "\"license\"\\s*:\\s*\"MIT\"" package.json 2>/dev/null) \\
  && ok "license is MIT" \\
  || miss "license drift — was MIT at generation time"

# 3. Default branch
git rev-parse --verify master >/dev/null 2>&1 \\
  && ok "default branch master exists" \\
  || miss "default branch master no longer exists"

# 4. Critical files exist
test -f "expr.go" \\
  && ok "expr.go" \\
  || miss "missing critical file: expr.go"
test -f "ast/node.go" \\
  && ok "ast/node.go" \\
  || miss "missing critical file: ast/node.go"
test -f "compiler/compiler.go" \\
  && ok "compiler/compiler.go" \\
  || miss "missing critical file: compiler/compiler.go"
test -f "checker/checker.go" \\
  && ok "checker/checker.go" \\
  || miss "missing critical file: checker/checker.go"
test -f "builtin/builtin.go" \\
  && ok "builtin/builtin.go" \\
  || miss "missing critical file: builtin/builtin.go"

# 5. Repo recency
days_since_last=$(( ( $(date +%s) - $(git log -1 --format=%at 2>/dev/null || echo 0) ) / 86400 ))
if [ "$days_since_last" -le 55 ]; then
  ok "last commit was $days_since_last days ago (artifact saw ~25d)"
else
  miss "last commit was $days_since_last days ago — artifact may be stale"
fi

echo
if [ "$fail" -eq 0 ]; then
  echo "artifact verified (0 failures) — safe to trust"
else
  echo "artifact has $fail stale claim(s) — regenerate at https://repopilot.app/r/expr-lang/expr"
  exit 1
fi

Each check prints ok: or FAIL:. The script exits non-zero if anything failed, so it composes cleanly into agent loops (./verify.sh || regenerate-and-retry).

</details>

TL;DR

Expr is a Go-native expression language and evaluator that safely interprets dynamic expressions (like user role checks, time window validations, and data filtering) without executing arbitrary code. It combines a lexer/parser that builds an AST, a static type checker, and a bytecode compiler+VM that executes expressions with guaranteed termination, no side effects, and strong memory safety—all without requiring users to redefine Go types. Modular pipeline architecture: ast/ package handles parsing and AST construction; checker/ performs static type checking and emits compile-time diagnostics; compiler/ generates bytecode; builtin/ provides the standard function library. The top-level expr package (implied) orchestrates compilation. debug/ is an optional TUI debugger in a submodule. Separation of concerns across parsing → checking → compilation → execution.

👥Who it's for

Go backend developers and framework authors who need to evaluate user-provided or configuration-driven expressions safely (e.g., authorization rules, feature flags, validation logic, request filtering) without the security and performance overhead of embedding a full scripting language.

🌱Maturity & risk

Production-ready and actively maintained. The repo shows comprehensive test coverage (checker/checker_test.go, compiler/compiler_test.go, builtin/builtin_test.go), multiple CI workflows (test.yml, fuzz.yml, check.yml), proper release discipline with SECURITY.md, and a large Go codebase (1.16M lines). Active fuzzing via oss-fuzz indicates security-focused development.

Low risk overall but monitor: single primary repo (expr-lang/expr) with focused scope; small external dependency footprint (visible in debug/go.mod with only tcell/tview for the debugger); no massive open issue backlog evident from structure. Risk is primarily around maintaining the custom parser/compiler/VM architecture—changes to core bytecode format or type system can be breaking. Go version constraint (1.18+ from debug/go.mod) is reasonable.

Active areas of work

Active maintenance visible: comprehensive test coverage and CI automation in .github/workflows, fuzzing integration (fuzz.yml), documentation generation (docgen/), and a debugger TUI being maintained. Recent work likely focuses on robustness (fuzzing), diagnostics (checker/info.go), and builtin function library expansion.

🚀Get running

git clone https://github.com/expr-lang/expr.git
cd expr
go test ./...
go run ./debug (if testing the TUI debugger; requires Go 1.18+)

Daily commands: This is a library, not a binary application. To test locally: go test ./.... To use in your own Go project: import "github.com/expr-lang/expr" and call expr.Compile(program) to get a VM, then vm.Run(env) to evaluate. The debug/ submodule is a TUI if you want interactive debugging: cd debug && go run ..

🗺️Map of the codebase

  • expr.go — Main entry point exposing Eval, Compile, and Parse functions that all users interact with
  • ast/node.go — Core AST node definitions that represent the parsed expression structure throughout the codebase
  • compiler/compiler.go — Converts AST to executable bytecode; critical path for expression execution performance
  • checker/checker.go — Type checking and validation engine that ensures expression safety before compilation
  • builtin/builtin.go — Built-in function definitions and standard library that users rely on for expression capabilities
  • conf/config.go — Configuration API that controls parser, checker, and compiler behavior
  • file/source.go — Source file abstraction handling location tracking and error reporting

🧩Components & responsibilities

  • Parser (Lexer, recursive descent or similar parsing technique) — Tokenize input string and construct Abstract Syntax Tree respecting Expr language grammar
    • Failure mode: Syntax error with location information; prevents progression to type checking
  • Type Checker (Type inference, nature system, visitor pattern) — Traverse AST and verify type correctness, resolve function signatures, catch semantic errors
    • Failure mode: Type error with context; prevents compilation of unsafe expressions
  • Compiler (Code generation, instruction selection, bytecode format) — Convert validated AST to bytecode instructions optimized for VM execution
    • Failure mode: Compilation error (rare if type

🛠️How to make changes

Add a new built-in function

  1. Define the function signature and implementation in the builtin/lib.go file, following the pattern of existing functions (builtin/lib.go)
  2. Register the function in the Builtins map in builtin/builtin.go with proper function type metadata (builtin/builtin.go)
  3. Add input validation in builtin/validation.go if needed for complex type checking (builtin/validation.go)
  4. Write tests in builtin/builtin_test.go to verify behavior with various input types (builtin/builtin_test.go)

Add a new operator or language construct

  1. Define new AST node types in ast/node.go for the construct (ast/node.go)
  2. Update the parser to recognize and build the new node (typically in a parser file not shown—check conf/config.go for parser hookpoints) (conf/config.go)
  3. Add type checking rules in checker/checker.go to validate the new construct (checker/checker.go)
  4. Implement bytecode emission in compiler/compiler.go to execute the construct (compiler/compiler.go)
  5. Add test cases in checker/checker_test.go and compiler/compiler_test.go (checker/checker_test.go)

Customize expression compilation behavior

  1. Create a Config struct using conf/config.go with desired options for parser, checker, and compiler (conf/config.go)
  2. Pass config to Compile or Eval via options pattern (see expr.go for usage) (expr.go)
  3. Check checker/checker.go to see what options affect type checking behavior (checker/checker.go)
  4. Check compiler/compiler.go to see what options affect code generation (compiler/compiler.go)

Add type inference rules

  1. Understand type representation in checker/nature/nature.go (checker/nature/nature.go)
  2. Add inference logic to checker/checker.go in the relevant visitor methods (checker/checker.go)
  3. Update checker/info.go if new type metadata needs to be tracked (checker/info.go)
  4. Test in checker/checker_test.go with expressions that exercise the new rules (checker/checker_test.go)

🔧Why these technologies

  • Go — Compiles to single binary, strong standard library, good performance for bytecode interpretation
  • AST-based approach — Enables separate parsing, type-checking, and compilation phases for safety and flexibility
  • Bytecode VM — Faster than tree-walking interpretation, allows pre-compilation and caching of programs
  • Static type checking — Catches errors at compile-time before execution, improves safety in dynamic configuration

⚖️Trade-offs already made

  • Separate type-checking phase before compilation

    • Why: Enables safe error reporting and type inference without executing code
    • Consequence: Adds overhead to compilation time, but prevents runtime type errors
  • Bytecode compilation vs. direct AST interpretation

    • Why: Bytecode provides better performance for expressions evaluated multiple times
    • Consequence: Increased memory usage and complexity, but significant speedup for cached programs
  • Built-in function registry vs. reflection-based dispatch

    • Why: Explicit registry enables type checking and validation before execution
    • Consequence: Requires manual registration of functions, but provides safety and predictability
  • Source location tracking throughout pipeline

    • Why: Provides precise error messages with file/line information
    • Consequence: Memory overhead storing positions, but critical for user experience

🚫Non-goals (don't propose these)

  • Real-time expression evaluation with microsecond latency (designed for configuration, not trading systems)
  • Turing-complete language (deliberately limited to prevent infinite loops in configuration)
  • Generic external module system (expressions are self-contained, no dynamic imports)
  • JavaScript/Python compatibility (Expr has its own syntax optimized for Go integration)

🪤Traps & gotchas

Reflection overhead: builtin/ uses Go reflection to invoke functions; performance-sensitive paths may see reflection costs. Type inference complexity: checker/nature/ handles type inference for untyped values; understand Go's type rules before modifying. No explicit bytecode version: bytecode format changes could break stored compiled expressions—no versioning scheme visible. Operator overloading limited: Go's type system doesn't allow operator overloading; only preset operators in checker work (no custom +, -, ==, etc.). AST node string representation: ast/print.go and ast/dump.go are code-generated or manual; keep them in sync when adding new node types.

🏗️Architecture

💡Concepts to learn

  • Static Type Checking — Expr catches type mismatches (e.g., string + int) at compile time, not runtime, preventing silent bugs in production expressions; checker/checker.go is the implementation.
  • Abstract Syntax Tree (AST) — The AST (ast/node.go) is Expr's intermediate representation; understanding its structure is essential for modifying parsing, type checking, or compilation logic.
  • Bytecode Compilation and Virtual Machine — Expr compiles expressions to bytecode (compiler/compiler.go) rather than interpreting the AST directly; this gives it speed and enables optimizations like constant folding.
  • Type Inference (Nature) — checker/nature/ infers types of untyped values (e.g., literals) without explicit annotations; crucial for Go's duck-typing integration.
  • Visitor Pattern for AST Traversal — ast/visitor.go uses the Visitor pattern to decouple tree structure from analysis logic; enables adding new passes (checking, compilation, optimization) without modifying node types.
  • Reflection in Go — builtin/lib.go uses Go's reflect package to dynamically invoke user-defined functions and inspect their signatures; understanding reflection is key to extending builtins.
  • Termination Analysis / Loop Prevention — Expr is designed to prevent infinite loops; the checker and compiler enforce this by disallowing explicit loops and recursion, ensuring bounded execution time.
  • antonmedv/expr — The original JavaScript/Node.js expression evaluator that inspired this Go port; similar DSL goals but different ecosystem.
  • google/cel-go — Google's Common Expression Language for Go; similar goal (safe expression evaluation) but more standards-based and slightly higher overhead.
  • hashicorp/hcl — HashiCorp's configuration language; used for Terraform and Consul; overlaps in safe expression evaluation and type checking for config-as-code.
  • go-echarts/go-echarts — Popular Go visualization library; not directly related but often used alongside Expr for dynamic data filtering/transformation in dashboards.
  • expr-lang/playground — Official interactive playground for Expr (if it exists); companion repo for testing expressions without local tooling.

🪄PR ideas

To work on one of these in Claude Code or Cursor, paste: Implement the "<title>" PR idea from CLAUDE.md, working through the checklist as the task list.

Add comprehensive fuzzing tests for the parser and compiler packages

The repo has a fuzz.yml workflow but no visible fuzz test files in parser/compiler directories. Given that expr-lang processes user-provided expressions, adding targeted fuzz tests for ast/compiler/checker packages would improve security and catch edge cases in expression parsing and compilation. This directly supports the existing OSS-Fuzz integration mentioned in the README.

  • [ ] Create compiler/fuzz_test.go with fuzzing for compiler.Compile()
  • [ ] Create checker/fuzz_test.go with fuzzing for checker.Check()
  • [ ] Create ast/fuzz_test.go with fuzzing for AST node traversal and visitor patterns
  • [ ] Reference the existing .github/workflows/fuzz.yml to ensure integration
  • [ ] Add corpus seeds in testdata/fuzz directories for each test

Add integration tests for the debug/debugger package

The debug/ subdirectory has a separate go.mod and imports tview (TUI library), but there are no visible *_test.go files. The debugger is a complex interactive component that deserves integration tests covering breakpoint handling, variable inspection, and stepping through expressions.

  • [ ] Create debug/debugger_test.go with unit tests for Debugger struct methods
  • [ ] Add debug/integration_test.go testing expression evaluation with debug hooks
  • [ ] Test breakpoint setting/clearing and variable inspection workflows
  • [ ] Reference conf/config.go to test debugger integration with expression configuration
  • [ ] Add example scenarios in testdata/debug/

Add benchmarks and performance tests for the checker/nature type inference system

checker/nature/nature.go handles type inference but there are no visible benchmarks for this critical path. With checker_bench_test.go existing for the checker, expanding benchmarks for the type inference engine would help identify performance regressions in complex type checking scenarios.

  • [ ] Create checker/nature/nature_bench_test.go with benchmarks for type inference on complex nested expressions
  • [ ] Add benchmarks for type validation against different environment schemas (from conf/env.go)
  • [ ] Create a benchmark suite that tests type checking performance with large expression ASTs
  • [ ] Compare performance before/after for PRs affecting the nature package
  • [ ] Document benchmark results in docs/performance.md or similar

🌿Good first issues

  • Add test coverage for edge cases in ast/find.go (the node-finding logic); run go test -v ./ast and look for uncovered branches, then add table-driven tests.
  • Expand builtin/utils.go with additional utility functions (e.g., a flatten function for nested arrays) and document them in docs/ with examples, following the pattern in builtin/builtin_test.go.
  • Write integration tests in a new file (e.g., integration_test.go at the root) that exercise full pipelines: parse → check → compile → run for realistic expression scenarios, filling gaps between unit tests.

Top contributors

Click to expand

📝Recent commits

Click to expand
  • 3a46b19 — fix(compiler): pop stale bool from stack in emitLoopBackwards (#954) (lawrence3699)
  • b90e77c — docs: add Kargo's usage of expr (#944) (jessesuen)
  • 851b241 — fix(vm): handle non-comparable groupBy keys (#940) (thevilledev)
  • 27acc2d — fix: reject unexported struct fields at runtime (#935) (thevilledev)
  • 3abfc80 — fix: wrap LoadLocation error in date() builtin (#931) (thevilledev)
  • 21f4f05 — Fix impossible unsigned integer comparisons in Abs() function (#919) (Copilot)
  • 3461fbb — perf(vm): optimize loop iteration with scope pool (#908) (thevilledev)
  • 94ec86d — fix(patcher): ctx into nested custom funcs and Env (#883) (thevilledev)
  • 40bda0b — Disable check for missing predicate during parseing (antonmedv)
  • 552eb1b — Disable check for missing predicate during parseing (antonmedv)

🔒Security observations

The expr-lang/expr codebase demonstrates good security practices with an official security policy, active maintenance, and OSS-Fuzz integration. No critical vulnerabilities were identified in the static analysis. Minor concerns include: development snapshot dependencies in the debug module, relative path replacements in go.mod, and the need for explicit documentation on input validation and resource limits for expression evaluation. The project's backwards compatibility approach and security policy for version support are positive indicators. Recommendations focus on dependency version pinning, documentation improvements, and explicit resource limit controls.

  • Medium · Relative Path Dependency in Debug Module — debug/go.mod. The debug/go.mod file uses a relative path replacement directive (replace github.com/expr-lang/expr => ../) which could lead to unexpected builds if the directory structure changes or during concurrent builds. This is a development-only concern but could affect build reproducibility. Fix: Consider using absolute module paths or ensuring the development environment strictly controls directory structure. Document the required layout for contributors.
  • Low · Outdated UI Dependencies in Debug Module — debug/go.mod. The debug module depends on tview v0.0.0-20230814110005 which appears to be a development snapshot. UI libraries can contain XSS or similar vulnerabilities. The dependency version format suggests a specific commit rather than a stable release. Fix: Pin dependencies to official releases instead of development snapshots. Use semantic versioning tags from github.com/rivo/tview.
  • Low · Missing Input Validation Documentation — expr.go, checker/. While the codebase includes a checker package for static analysis, there is no explicit mention of input sanitization or validation for expression evaluation in the visible security context. Expression languages can be vectors for code injection or DOS attacks if not properly validated. Fix: Ensure comprehensive documentation of input validation mechanisms. Verify that the expression parser has protections against: ReDoS attacks, stack overflow via deeply nested expressions, and resource exhaustion.
  • Low · No Rate Limiting or Resource Quotas Visible — compiler/, ast/. The expression evaluation engine does not show explicit rate limiting, timeout controls, or memory/execution quotas in the visible code structure. Malicious or inefficient expressions could consume excessive resources. Fix: Implement timeout mechanisms and resource limits for expression evaluation. Consider adding: execution time limits, maximum expression complexity checks, and memory allocation bounds.

LLM-derived; treat as a starting point, not a security audit.


Generated by RepoPilot. Verdict based on maintenance signals — see the live page for receipts. Re-run on a new commit to refresh.

Healthy signals · expr-lang/expr — RepoPilot