Go · 5088 bytes Raw Blame History
1 // SPDX-License-Identifier: AGPL-3.0-or-later
2
3 package expr
4
5 import "fmt"
6
7 // Expr is the AST node interface. Each concrete type captures one
8 // kind of expression we support.
9 type Expr interface {
10 exprNode()
11 }
12
13 // LitString is a quoted string literal in the expression source.
14 type LitString struct{ V string }
15
16 // LitBool is `true` or `false`.
17 type LitBool struct{ V bool }
18
19 // LitNull is the `null` keyword. Used in equality checks against
20 // possibly-missing context fields.
21 type LitNull struct{}
22
23 // Ref is a dotted reference like `secrets.MY_SECRET` or
24 // `shithub.event.pull_request.title`. Path is a non-empty slice of
25 // identifiers (the lexer enforces leading-letter idents).
26 type Ref struct{ Path []string }
27
28 // Call is a function invocation: `contains('foo', 'bar')`. Name is
29 // validated against the allowlist at eval time, not parse time, so
30 // we can produce a precise error pointing at the call site.
31 type Call struct {
32 Name string
33 Args []Expr
34 }
35
36 // Unary is `!x`.
37 type Unary struct {
38 Op string // "!"
39 X Expr
40 }
41
42 // Binary is `x op y`. Op ∈ {==, !=, &&, ||}. Concat would go here too
43 // if we supported it (we don't in v1 — we tell users to use shell-side
44 // concat in `run:`).
45 type Binary struct {
46 Op string
47 L Expr
48 R Expr
49 }
50
51 func (LitString) exprNode() {}
52 func (LitBool) exprNode() {}
53 func (LitNull) exprNode() {}
54 func (Ref) exprNode() {}
55 func (Call) exprNode() {}
56 func (Unary) exprNode() {}
57 func (Binary) exprNode() {}
58
59 // Parse turns a token stream into an AST. The grammar is small enough
60 // that we hand-write a recursive-descent parser with explicit
61 // precedence: || → && → equality → unary → primary.
62 func Parse(tokens []Token) (Expr, error) {
63 p := &parser{tokens: tokens}
64 e, err := p.parseOr()
65 if err != nil {
66 return nil, err
67 }
68 if p.peek().Kind != TokEOF {
69 t := p.peek()
70 return nil, fmt.Errorf("expr: unexpected %s after expression at offset %d", t.Kind, t.Pos)
71 }
72 return e, nil
73 }
74
75 type parser struct {
76 tokens []Token
77 pos int
78 }
79
80 func (p *parser) peek() Token { return p.tokens[p.pos] }
81 func (p *parser) advance() Token {
82 t := p.tokens[p.pos]
83 p.pos++
84 return t
85 }
86
87 func (p *parser) parseOr() (Expr, error) {
88 left, err := p.parseAnd()
89 if err != nil {
90 return nil, err
91 }
92 for p.peek().Kind == TokOr {
93 op := p.advance().Value
94 right, err := p.parseAnd()
95 if err != nil {
96 return nil, err
97 }
98 left = Binary{Op: op, L: left, R: right}
99 }
100 return left, nil
101 }
102
103 func (p *parser) parseAnd() (Expr, error) {
104 left, err := p.parseEquality()
105 if err != nil {
106 return nil, err
107 }
108 for p.peek().Kind == TokAnd {
109 op := p.advance().Value
110 right, err := p.parseEquality()
111 if err != nil {
112 return nil, err
113 }
114 left = Binary{Op: op, L: left, R: right}
115 }
116 return left, nil
117 }
118
119 func (p *parser) parseEquality() (Expr, error) {
120 left, err := p.parseUnary()
121 if err != nil {
122 return nil, err
123 }
124 for p.peek().Kind == TokEq || p.peek().Kind == TokNe {
125 op := p.advance().Value
126 right, err := p.parseUnary()
127 if err != nil {
128 return nil, err
129 }
130 left = Binary{Op: op, L: left, R: right}
131 }
132 return left, nil
133 }
134
135 func (p *parser) parseUnary() (Expr, error) {
136 if p.peek().Kind == TokNot {
137 p.advance()
138 x, err := p.parseUnary()
139 if err != nil {
140 return nil, err
141 }
142 return Unary{Op: "!", X: x}, nil
143 }
144 return p.parsePrimary()
145 }
146
147 func (p *parser) parsePrimary() (Expr, error) {
148 t := p.peek()
149 switch t.Kind {
150 case TokString:
151 p.advance()
152 return LitString{V: t.Value}, nil
153 case TokBool:
154 p.advance()
155 return LitBool{V: t.Value == "true"}, nil
156 case TokNull:
157 p.advance()
158 return LitNull{}, nil
159 case TokLParen:
160 p.advance()
161 e, err := p.parseOr()
162 if err != nil {
163 return nil, err
164 }
165 if p.peek().Kind != TokRParen {
166 return nil, fmt.Errorf("expr: expected ')' at offset %d", p.peek().Pos)
167 }
168 p.advance()
169 return e, nil
170 case TokIdent:
171 return p.parseRefOrCall()
172 }
173 return nil, fmt.Errorf("expr: unexpected %s at offset %d", t.Kind, t.Pos)
174 }
175
176 func (p *parser) parseRefOrCall() (Expr, error) {
177 first := p.advance()
178 // Function call: ident immediately followed by '('
179 if p.peek().Kind == TokLParen {
180 p.advance()
181 var args []Expr
182 if p.peek().Kind != TokRParen {
183 for {
184 a, err := p.parseOr()
185 if err != nil {
186 return nil, err
187 }
188 args = append(args, a)
189 if p.peek().Kind == TokComma {
190 p.advance()
191 continue
192 }
193 break
194 }
195 }
196 if p.peek().Kind != TokRParen {
197 return nil, fmt.Errorf("expr: expected ')' to close call at offset %d", p.peek().Pos)
198 }
199 p.advance()
200 return Call{Name: first.Value, Args: args}, nil
201 }
202 // Otherwise it's a dotted reference. Walk subsequent `.ident`
203 // segments. The base segment IS allowed to stand alone (e.g.
204 // `secrets`) but we'll catch that at eval as "namespace requires
205 // a member".
206 path := []string{first.Value}
207 for p.peek().Kind == TokDot {
208 p.advance()
209 next := p.peek()
210 if next.Kind != TokIdent {
211 return nil, fmt.Errorf("expr: expected identifier after '.' at offset %d", next.Pos)
212 }
213 p.advance()
214 path = append(path, next.Value)
215 }
216 return Ref{Path: path}, nil
217 }
218