forked from rosa/hakurei
This syntax is not final, but acts as a stopgap solution and a proof of concept. Signed-off-by: Ophestra <cat@gensokyo.uk>
387 lines
7.2 KiB
Go
387 lines
7.2 KiB
Go
// 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 }
|
|
|
|
// scanAs advances the scanner for an expected token.
|
|
func (p *parser) scanAs(expects rune) {
|
|
e := TokenError{expects, p.scan()}
|
|
if e[0] != e[1] {
|
|
panic(e)
|
|
}
|
|
}
|
|
|
|
// parseInt parses the current token as a base 10 representation of a 64-bit
|
|
// signed integer.
|
|
func (p *parser) parseInt() int64 {
|
|
return must1(strconv.ParseInt(p.s.TokenText(), 10, 64))
|
|
}
|
|
|
|
// A String represents an identifier or string literal.
|
|
type String struct {
|
|
Value string
|
|
Ident bool
|
|
}
|
|
|
|
// A StringSpec describes a statement evaluating down to a string value.
|
|
type StringSpec []String
|
|
|
|
// parseString parses the current token as a string.
|
|
func (p *parser) parseString() string {
|
|
return must1(strconv.Unquote(p.s.TokenText()))
|
|
}
|
|
|
|
// appendStringSpec parses from the next token until the end of the StringSpec.
|
|
// It always advances past the final token.
|
|
func (p *parser) appendStringSpec(
|
|
op bool,
|
|
data StringSpec,
|
|
) (v StringSpec, ok bool) {
|
|
ok = true
|
|
v = data
|
|
for {
|
|
if op {
|
|
switch tok := p.scan(); tok {
|
|
case '+':
|
|
break
|
|
|
|
default:
|
|
return
|
|
}
|
|
}
|
|
|
|
switch tok := p.scan(); tok {
|
|
case scanner.String, scanner.RawString:
|
|
v = append(v, String{p.parseString(), false})
|
|
|
|
case scanner.Ident:
|
|
v = append(v, String{p.s.TokenText(), true})
|
|
|
|
default:
|
|
ok = op
|
|
return
|
|
}
|
|
op = true
|
|
}
|
|
}
|
|
|
|
// A KV holds a key/value pair.
|
|
type KV struct {
|
|
K string
|
|
V any
|
|
}
|
|
|
|
// An Arg represents an argument of [Func].
|
|
type Arg struct {
|
|
K []string
|
|
V any
|
|
R bool
|
|
}
|
|
|
|
// Func is a function call or package declaration.
|
|
type Func struct {
|
|
// Function or package identifier.
|
|
Ident string
|
|
// Whether this is a package declaration.
|
|
Package bool
|
|
// Key-value arguments.
|
|
Args []Arg
|
|
}
|
|
|
|
// parseExpr scans and parses the current expression. A nil return indicates
|
|
// [scanner.EOF].
|
|
func (p *parser) parseExpr() (any, bool) {
|
|
switch p.tok {
|
|
case scanner.EOF:
|
|
return nil, false
|
|
|
|
case scanner.Int:
|
|
return p.parseInt(), false
|
|
|
|
case scanner.String, scanner.RawString:
|
|
v, ok := p.appendStringSpec(true, StringSpec{
|
|
{p.parseString(), false},
|
|
})
|
|
if !ok {
|
|
panic(TokenError{scanner.String, p.tok})
|
|
}
|
|
return v, true
|
|
|
|
case scanner.Ident:
|
|
var v Func
|
|
v.Ident = p.s.TokenText()
|
|
if v.Package = v.Ident == "package"; v.Package {
|
|
p.scanAs(scanner.Ident)
|
|
v.Ident = p.s.TokenText()
|
|
}
|
|
|
|
p.scan()
|
|
switch p.tok {
|
|
case '{':
|
|
for {
|
|
p.scan()
|
|
switch p.tok {
|
|
case '}':
|
|
return v, false
|
|
|
|
case scanner.Ident:
|
|
break
|
|
|
|
default:
|
|
panic(TokenError{scanner.Ident, p.tok})
|
|
}
|
|
|
|
var next bool
|
|
arg := Arg{K: []string{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, 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, next = p.parseExpr()
|
|
v.Args = append(v.Args, arg)
|
|
if !next {
|
|
p.scanAs(';')
|
|
}
|
|
}
|
|
|
|
case ';':
|
|
return StringSpec{{v.Ident, true}}, true
|
|
|
|
case '+':
|
|
s, ok := p.appendStringSpec(false, StringSpec{
|
|
{v.Ident, true},
|
|
})
|
|
if !ok {
|
|
panic(TokenError{scanner.String, p.tok})
|
|
}
|
|
return s, p.tok != scanner.EOF
|
|
|
|
case scanner.EOF:
|
|
return StringSpec{{v.Ident, true}}, false
|
|
|
|
default:
|
|
return StringSpec{{v.Ident, true}}, true
|
|
}
|
|
|
|
case '{':
|
|
var v []KV
|
|
for {
|
|
p.scan()
|
|
switch p.tok {
|
|
case '}':
|
|
return v, false
|
|
|
|
case scanner.String:
|
|
pair := KV{K: p.parseString()}
|
|
p.scan()
|
|
switch p.tok {
|
|
case ';':
|
|
break
|
|
|
|
case ':':
|
|
var next bool
|
|
p.scan()
|
|
pair.V, next = p.parseExpr()
|
|
if !next {
|
|
p.scanAs(';')
|
|
}
|
|
break
|
|
|
|
default:
|
|
panic(ExprError(p.tok))
|
|
}
|
|
v = append(v, pair)
|
|
|
|
default:
|
|
panic(ExprError(p.tok))
|
|
}
|
|
}
|
|
|
|
case '[':
|
|
var (
|
|
v []any
|
|
e any
|
|
delim bool
|
|
next bool
|
|
)
|
|
for {
|
|
if !next {
|
|
p.scan()
|
|
}
|
|
switch p.tok {
|
|
case ',':
|
|
if delim {
|
|
delim = false
|
|
next = false
|
|
continue
|
|
}
|
|
panic(ExprError(','))
|
|
case ']':
|
|
return v, false
|
|
case scanner.EOF:
|
|
panic(ExprError(scanner.EOF))
|
|
default:
|
|
if delim {
|
|
panic(TokenError{',', p.tok})
|
|
}
|
|
delim = true
|
|
break
|
|
}
|
|
|
|
e, next = p.parseExpr()
|
|
v = append(v, e)
|
|
}
|
|
|
|
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 {
|
|
expr, next := p.parseExpr()
|
|
if expr == nil {
|
|
break
|
|
}
|
|
e = append(e, expr)
|
|
if !next {
|
|
p.scan()
|
|
}
|
|
}
|
|
|
|
if p.s.ErrorCount != 0 {
|
|
err = ScanError(p.s.ErrorCount)
|
|
}
|
|
return
|
|
}
|