JuliaCon Local Paris 2025

Argus.jl: Matching and transforming Julia syntax and writing linting rules
2025-10-03 , Robert Faure Amphitheater
Language: English

This presentation introduces Argus.jl, a JuliaSyntax-based package that proposes a new approach to syntax manipulation in Julia. It draws inspiration from the Racket libraries syntax/parse and resyntax.


Argus implements a framework for writing static analysis rules on top of a syntax matching mechanism. It is structured around several core concepts:

  • Syntax patterns
  • Pattern variables
  • Syntax classes
  • Syntax templates
  • Rules

Syntax patterns form the basis for syntax matching and closely resemble Julia code. For example, @pattern x = 2 matches an assignment where the left-hand side is a variable named x and the right-hand side is the literal 2. On the other hand, @pattern {x} = 2 matches any assignment or short-form function definition where the right-hand side is the literal 2; the expression on the left-hand side is bound to the pattern variable x.

A pattern variable is one of several special forms permitted within patterns. It can be seen as a "hole" that is filled by matching syntax. For example, when matching @pattern {x} = 2 against the expression f(a) = 2, x is bound to f(a). The result of a pattern match is either a set of bindings corresponding to the syntax matched by each pattern variable, or an error explaining why the matching failed.

A pattern variable can be constrained by a syntax class. In the example above, @pattern {x} = 2 is equivalent to @pattern {x:::expr} = 2, where expr is the syntax class that matches any expression. Syntax classes are defined through patterns and can reference other syntax classes. For example, a syntax class matching any assignment may be defined as such:

assign = @syntax_class "assignment" begin
    @pattern {lhs:::identifier} = {rhs}
end

Argus provides a set of pre-defined syntax classes, including expr, identifier and assign.

Syntax templates are expanded to produce Julia code. They contain variables that are replaced with information gathered during pattern matching.

A rule contains a description and a pattern. In the case of rules, matching recursively traverses a given unit of source code (e.g. a file) and collects the sub-expressions that match the rule's pattern. Pattern variables bound by these identifications are returned in corresponding binding sets.

Rules may be organised into rule groups. For example, it may be useful to group all rules related to Julia usage in a lang group:

lang_rules = RuleGroup("lang")

@define_rule_in_group lang_rules "compare-nothing" begin
    description = """
    Comparisons to `nothing` should use `===`, `!==` or `isnothing`.
    """

    pattern = @pattern begin
        ~or(
            nothing == {_},
            {_} == nothing,
            nothing != {_},
            {_} != nothing
        )
    end
end

This presentation is an opportunity to share Argus with the community and exchange feedback and ideas for further improvement.

GitHub: https://github.com/iuliadmtru/Argus.jl