// Package azalea implements a proof-of-concept, domain-specific language for // Rosa OS software packaging. package azalea import ( "errors" "io" "strconv" "text/scanner" ) // idents are runes accepted in an identifier. var idents = [...]bool{ '0': true, '1': true, '2': true, '3': true, '4': true, '5': true, '6': true, '7': true, '8': true, '9': true, 'A': true, 'B': true, 'C': true, 'D': true, 'E': true, 'F': true, 'G': true, 'H': true, 'I': true, 'J': true, 'K': true, 'L': true, 'M': true, 'N': true, 'O': true, 'P': true, 'Q': true, 'R': true, 'S': true, 'T': true, 'U': true, 'V': true, 'W': true, 'X': true, 'Y': true, 'Z': true, 'a': true, 'b': true, 'c': true, 'd': true, 'e': true, 'f': true, 'g': true, 'h': true, 'i': true, 'j': true, 'k': true, 'l': true, 'm': true, 'n': true, 'o': true, 'p': true, 'q': true, 'r': true, 's': true, 't': true, 'u': true, 'v': true, 'w': true, 'x': true, 'y': true, 'z': true, '-': true, '_': true, } // TokenError describes an unexpected token. type TokenError [2]rune func (e TokenError) Error() string { return "expected " + scanner.TokenString(e[0]) + ", found " + scanner.TokenString(e[1]) } // ExprError is an unexpected token encountered while parsing an expression. type ExprError rune func (e ExprError) Error() string { return "unexpected token " + scanner.TokenString(rune(e)) } // must1 returns v, or panics if err is not nil. func must1[T any](v T, err error) T { if err != nil { panic(err) } return v } // parser retains the current token. type parser struct { s scanner.Scanner tok rune } // scan advances the underlying scanner to the next token, storing its result. func (p *parser) scan() rune { p.tok = p.s.Scan(); return p.tok } // expects panics with [TokenError] for an unexpected tok. func (p *parser) expects(expects rune) { if p.tok != expects { panic(TokenError{expects, p.tok}) } } // scanAs advances the scanner for an expected token. func (p *parser) scanAs(expects rune) { p.scan(); p.expects(expects) } // An Int is the value represented by an integer literal. type Int int64 func (v Int) GoString() string { return "azalea.Int(" + strconv.FormatInt(int64(v), 10) + ")" } // parseInt parses the current token as a base 10 representation of a 64-bit // signed integer. func (p *parser) parseInt() Int { v, err := strconv.ParseInt(p.s.TokenText(), 10, 64) return must1(Int(v), err) } // A String holds the unquoted content of a string literal. type String string func (v String) GoString() string { return "azalea.String(" + strconv.Quote(string(v)) + ")" } // parseString parses the current token as a string. func (p *parser) parseString() String { s, err := strconv.Unquote(p.s.TokenText()) return must1(String(s), err) } // An Ident holds the name of an identifier. type Ident string func (v Ident) GoString() string { return "azalea.Ident(" + strconv.Quote(string(v)) + ")" } // A Val are statements joined by the '+' operator. Only the [String] type // supports concatenation. type Val []any // parseVal parses until the end of the [Val]. func (p *parser) parseVal() (v Val) { v = append(v, p.parseExpr()) for p.tok == '+' { p.scan() v = append(v, p.parseExpr()) } return } // An Array holds statements in an array. type Array []Val // A KV holds a key/value pair. type KV struct { K String V Val } // An Arg represents an argument of [Func]. type Arg struct { K []Ident V Val R bool } // Func is a function call or package declaration. type Func struct { // Function or package identifier. Ident Ident // Whether this is a package declaration. Package bool // Key-value arguments. Args []Arg } // parseExpr parses the current expression. func (p *parser) parseExpr() any { switch p.tok { case scanner.Int: v := p.parseInt() p.scan() return v case scanner.String, scanner.RawString: v := p.parseString() p.scan() return v case scanner.Ident: var v Func v.Ident = Ident(p.s.TokenText()) if v.Package = v.Ident == "package"; v.Package { p.scanAs(scanner.Ident) v.Ident = Ident(p.s.TokenText()) } p.scan() switch p.tok { case '{': for { p.scan() switch p.tok { case '}': p.scan() return v case scanner.Ident: break default: panic(TokenError{scanner.Ident, p.tok}) } arg := Arg{K: []Ident{Ident(p.s.TokenText())}} delim := true arg: for { p.scan() switch p.tok { case ',': if delim { delim = false continue } panic(ExprError(p.tok)) case scanner.Ident: if delim { panic(TokenError{',', p.tok}) } delim = true arg.K = append(arg.K, Ident(p.s.TokenText())) default: break arg } } switch p.tok { case '=': break case '*': arg.R = true p.scanAs('=') default: panic(TokenError{'=', p.tok}) } p.scan() arg.V = p.parseVal() v.Args = append(v.Args, arg) p.expects(';') } default: return v.Ident } case '{': var v []KV for { p.scan() switch p.tok { case '}': p.scan() return v case scanner.String: pair := KV{K: p.parseString()} p.scan() switch p.tok { case ';': break case ':': p.scan() pair.V = p.parseVal() p.expects(';') break default: panic(ExprError(p.tok)) } v = append(v, pair) default: panic(ExprError(p.tok)) } } case '[': var ( v Array delim bool ) p.scan() for { switch p.tok { case ',': if delim { p.scan() delim = false continue } panic(ExprError(',')) case ']': p.scan() return v case scanner.EOF: panic(ExprError(scanner.EOF)) default: if delim { panic(TokenError{',', p.tok}) } delim = true break } v = append(v, p.parseVal()) } default: panic(ExprError(p.tok)) } } // ScanError is the error count parsing all expressions. type ScanError int func (ScanError) Error() string { return "aborting due to scanning errors" } // Parse parses expressions from r. func Parse(r io.Reader) (e []any, err error) { var p parser p.s.Init(r) p.s.Mode = scanner.ScanIdents | scanner.ScanInts | scanner.ScanStrings | scanner.ScanRawStrings | scanner.ScanComments | scanner.SkipComments p.s.IsIdentRune = func(ch rune, i int) bool { if i == 0 && ch >= '0' && ch <= '9' { return false } return ch > 0 && ch < rune(len(idents)) && idents[ch] } defer func() { v := recover() if v == nil { return } _err, ok := v.(error) if !ok { panic(v) } if err == nil { err = _err return } err = errors.Join(err, _err) }() p.scan() for p.tok != scanner.EOF { e = append(e, p.parseExpr()) } if p.s.ErrorCount != 0 { err = ScanError(p.s.ErrorCount) } return }