internal/rosa/azalea: ast and parser

This syntax is not final, but acts as a stopgap solution and a proof of concept.

Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
2026-05-15 06:48:04 +09:00
parent c32c06b2e8
commit 916ba22bed
2 changed files with 526 additions and 0 deletions

View File

@@ -0,0 +1,334 @@
// 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
}
// scanAs advances the scanner for an expected token.
func scanAs(s *scanner.Scanner, expects rune) {
e := TokenError{expects, s.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 parseInt(s *scanner.Scanner) int64 {
return must1(strconv.ParseInt(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 parseString(s *scanner.Scanner) string {
return must1(strconv.Unquote(s.TokenText()))
}
// appendStringSpec parses from the next token until the end of the StringSpec.
// It always advances to the delimiter.
func appendStringSpec(
s *scanner.Scanner,
delim rune,
op bool,
eof *bool,
p StringSpec,
) (v StringSpec, err error) {
v = p
for {
if op {
switch tok := s.Scan(); tok {
case delim, scanner.EOF:
if eof != nil {
*eof = tok == scanner.EOF
}
return
case '+':
break
default:
return v, TokenError{delim, tok}
}
}
switch tok := s.Scan(); tok {
case scanner.String, scanner.RawString:
v = append(v, String{parseString(s), false})
case scanner.Ident:
v = append(v, String{s.TokenText(), true})
default:
if !op {
return v, TokenError{scanner.String, tok}
}
return v, ExprError(tok)
}
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 next expression. A nil return indicates
// [scanner.EOF].
func parseExpr(s *scanner.Scanner, delim rune) (any, bool) {
tok := s.Scan()
if tok == scanner.EOF {
return nil, false
}
switch tok {
case scanner.Int:
return parseInt(s), false
case scanner.String, scanner.RawString:
return must1(appendStringSpec(s, delim, true, nil, StringSpec{
{parseString(s), false},
})), true
case scanner.Ident:
var v Func
v.Ident = s.TokenText()
if v.Package = v.Ident == "package"; v.Package {
scanAs(s, scanner.Ident)
v.Ident = s.TokenText()
}
switch tok = s.Scan(); tok {
case '{':
for {
switch tok = s.Scan(); tok {
case '}':
return v, false
case scanner.Ident:
break
default:
panic(TokenError{scanner.Ident, tok})
}
var next bool
arg := Arg{K: s.TokenText()}
switch tok = s.Scan(); tok {
case '=':
break
case '*':
arg.R = true
scanAs(s, '=')
default:
panic(TokenError{'=', tok})
}
arg.V, next = parseExpr(s, ';')
v.Args = append(v.Args, arg)
if !next {
scanAs(s, ';')
}
}
case ';':
return StringSpec{{v.Ident, true}}, true
case '+':
var eof bool
return must1(appendStringSpec(s, ';', false, &eof, StringSpec{
{v.Ident, true},
})), !eof
case scanner.EOF:
return StringSpec{{v.Ident, true}}, false
default:
panic(TokenError{'{', tok})
}
case '{':
var v []KV
for {
switch tok = s.Scan(); tok {
case '}':
return v, false
case scanner.String:
p := KV{K: parseString(s)}
switch tok = s.Scan(); tok {
case ';':
break
case ':':
var next bool
p.V, next = parseExpr(s, ';')
if !next {
scanAs(s, ';')
}
break
default:
panic(ExprError(tok))
}
v = append(v, p)
default:
panic(ExprError(tok))
}
}
case '[':
var v []StringSpec
for {
p, err := appendStringSpec(s, ',', false, nil, nil)
switch err {
case nil:
v = append(v, p)
continue
case TokenError{',', ']'}:
v = append(v, p)
return v, false
case TokenError{scanner.String, ']'}:
return v, false
default:
panic(err)
}
}
default:
panic(ExprError(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 s scanner.Scanner
s.Init(r)
s.Mode = scanner.ScanIdents |
scanner.ScanInts |
scanner.ScanStrings |
scanner.ScanRawStrings |
scanner.ScanComments |
scanner.SkipComments
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)
}()
for expr, next := parseExpr(&s, ';'); expr != nil; expr, next = parseExpr(&s, ';') {
if next {
return e, ExprError(';')
}
e = append(e, expr)
}
if s.ErrorCount != 0 {
err = ScanError(s.ErrorCount)
}
return
}