package sieve import "fmt" // ── AST ─────────────────────────────────────────────────────────────────────── type Script struct { Statements []Statement } // Statement is either an Action or an IfStatement. type Statement interface{ isStatement() } type Action struct { Name string // "fileinto" | "discard" | "keep" | "stop" Arg string // folder name for fileinto, empty otherwise } func (Action) isStatement() {} type IfStatement struct { Test Test Then []Statement ElseIfs []ElseIf Else []Statement HasElse bool } func (IfStatement) isStatement() {} type ElseIf struct { Test Test Then []Statement } // Test is a condition — this pass supports only header tests, the // overwhelming majority of real-world filtering rules. type Test struct { Kind string // "header" | "true" MatchType string // "contains" | "is" Header string Value string } // ── Parser ──────────────────────────────────────────────────────────────────── type parser struct { lex *lexer cur token } func Parse(script string) (*Script, error) { p := &parser{lex: newLexer(script)} if err := p.advance(); err != nil { return nil, err } s := &Script{} for p.cur.kind != tokEOF { stmt, err := p.parseStatement() if err != nil { return nil, err } s.Statements = append(s.Statements, stmt) } return s, nil } func (p *parser) advance() error { t, err := p.lex.next() if err != nil { return err } p.cur = t return nil } func (p *parser) expect(kind tokenKind, desc string) (token, error) { if p.cur.kind != kind { return token{}, fmt.Errorf("expected %s, got %q", desc, p.cur.value) } t := p.cur if err := p.advance(); err != nil { return token{}, err } return t, nil } func (p *parser) parseStatement() (Statement, error) { if p.cur.kind != tokIdent { return nil, fmt.Errorf("expected statement, got %q", p.cur.value) } switch p.cur.value { case "if": return p.parseIf() case "fileinto": if err := p.advance(); err != nil { return nil, err } arg, err := p.expect(tokString, "folder name") if err != nil { return nil, err } if _, err := p.expect(tokSemicolon, ";"); err != nil { return nil, err } return Action{Name: "fileinto", Arg: arg.value}, nil case "discard", "keep", "stop": name := p.cur.value if err := p.advance(); err != nil { return nil, err } if _, err := p.expect(tokSemicolon, ";"); err != nil { return nil, err } return Action{Name: name}, nil default: return nil, fmt.Errorf("unsupported command %q", p.cur.value) } } func (p *parser) parseIf() (Statement, error) { if err := p.advance(); err != nil { // skip "if" return nil, err } test, err := p.parseTest() if err != nil { return nil, err } then, err := p.parseBlock() if err != nil { return nil, err } stmt := IfStatement{Test: test, Then: then} for p.cur.kind == tokIdent && p.cur.value == "elsif" { if err := p.advance(); err != nil { return nil, err } elifTest, err := p.parseTest() if err != nil { return nil, err } elifThen, err := p.parseBlock() if err != nil { return nil, err } stmt.ElseIfs = append(stmt.ElseIfs, ElseIf{Test: elifTest, Then: elifThen}) } if p.cur.kind == tokIdent && p.cur.value == "else" { if err := p.advance(); err != nil { return nil, err } elseBlock, err := p.parseBlock() if err != nil { return nil, err } stmt.Else = elseBlock stmt.HasElse = true } return stmt, nil } func (p *parser) parseTest() (Test, error) { if p.cur.kind != tokIdent { return Test{}, fmt.Errorf("expected test, got %q", p.cur.value) } if p.cur.value == "true" { if err := p.advance(); err != nil { return Test{}, err } return Test{Kind: "true"}, nil } if p.cur.value != "header" { return Test{}, fmt.Errorf("unsupported test %q (only 'header' and 'true' supported)", p.cur.value) } if err := p.advance(); err != nil { return Test{}, err } if p.cur.kind != tokTag { return Test{}, fmt.Errorf("expected match type (:contains or :is), got %q", p.cur.value) } matchType := p.cur.value[1:] // strip leading ':' if matchType != "contains" && matchType != "is" { return Test{}, fmt.Errorf("unsupported match type %q (only :contains and :is supported)", matchType) } if err := p.advance(); err != nil { return Test{}, err } headerTok, err := p.expect(tokString, "header name") if err != nil { return Test{}, err } valueTok, err := p.expect(tokString, "match value") if err != nil { return Test{}, err } return Test{Kind: "header", MatchType: matchType, Header: headerTok.value, Value: valueTok.value}, nil } func (p *parser) parseBlock() ([]Statement, error) { if _, err := p.expect(tokLBrace, "{"); err != nil { return nil, err } var stmts []Statement for p.cur.kind != tokRBrace { if p.cur.kind == tokEOF { return nil, fmt.Errorf("unterminated block, expected }") } stmt, err := p.parseStatement() if err != nil { return nil, err } stmts = append(stmts, stmt) } if _, err := p.expect(tokRBrace, "}"); err != nil { return nil, err } return stmts, nil }