internal/rosa: evaluate packages late
All checks were successful
Test / Create distribution (push) Successful in 1m5s
Test / Sandbox (push) Successful in 2m52s
Test / ShareFS (push) Successful in 3m43s
Test / Sandbox (race detector) (push) Successful in 5m26s
Test / Hakurei (race detector) (push) Successful in 6m31s
Test / Hakurei (push) Successful in 2m47s
Test / Flake checks (push) Successful in 1m37s

This also enables concurrent evaluation.

Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
2026-05-19 01:26:21 +09:00
parent 8807cbc730
commit 4d60fa5632
3 changed files with 92 additions and 67 deletions

View File

@@ -84,15 +84,15 @@ func main() {
flagArch string flagArch string
flagCheck bool flagCheck bool
flagLTO bool flagLTO bool
flagET bool flagPT bool
flagCrossOverride int flagCrossOverride int
addr net.UnixAddr addr net.UnixAddr
) )
c := command.New(os.Stderr, log.Printf, "mbf", func([]string) error { c := command.New(os.Stderr, log.Printf, "mbf", func([]string) error {
if flagET { if flagPT {
log.Println("evaluated in", rosa.EvalTime()) log.Println("parsed in", rosa.ParseTime())
} }
msg.SwapVerbose(!flagQuiet) msg.SwapVerbose(!flagQuiet)
@@ -189,9 +189,9 @@ func main() {
"socket", command.StringFlag("$MBF_DAEMON_SOCKET"), "socket", command.StringFlag("$MBF_DAEMON_SOCKET"),
"Pathname of socket to bind to", "Pathname of socket to bind to",
).Flag( ).Flag(
&flagET, &flagPT,
"eval-time", command.BoolFlag(false), "parse-time", command.BoolFlag(false),
"Print duration of the initial azalea evaluation", "Print duration of the initial azalea parse",
) )
c.NewCommand( c.NewCommand(

View File

@@ -614,11 +614,11 @@ var native S
// Native returns the global [S]. // Native returns the global [S].
func Native() *S { return &native } func Native() *S { return &native }
// evalTime is the duration of the initial built-in evaluation. // parseTime is the duration of early parsing of built-in azalea expressions.
var evalTime time.Duration var parseTime time.Duration
// EvalTime returns the duration of the initial built-in evaluation. // ParseTime returns the time taken by early parsing of built-in azalea expressions.
func EvalTime() time.Duration { return evalTime } func ParseTime() time.Duration { return parseTime }
// nativeB is the backing directory of built-in azalea-based [Artifact] // nativeB is the backing directory of built-in azalea-based [Artifact]
// implementations. // implementations.
@@ -632,9 +632,9 @@ func init() {
panic(err) panic(err)
} }
t := time.Now() t := time.Now()
if err = native.EvaluateFS(sub); err != nil { if err = native.RegisterFS(sub); err != nil {
println(err.Error()) println(err.Error())
os.Exit(1) os.Exit(1)
} }
evalTime = time.Since(t) parseTime = time.Since(t)
} }

View File

@@ -189,9 +189,11 @@ type S struct {
archOnce sync.Once archOnce sync.Once
// Built-in functions. // Built-in functions.
s []azalea.Frame frame azalea.Frame
// For initialising s. // For initialising frame.
sOnce sync.Once frameOnce sync.Once
// Must only be accessed after a call to getFrame.
spool sync.Pool
// Options for [pkg.Artifact] created against [S]. // Options for [pkg.Artifact] created against [S].
opts int opts int
@@ -402,10 +404,15 @@ type deferredGit struct {
checksum string checksum string
} }
// getS must be called before accessing s. This value is not currently safe for // getFrame must be called before accessing s.
// concurrent use, but the underlying frame is immutable. func (s *S) getFrame() azalea.Frame {
func (s *S) getS() []azalea.Frame { s.frameOnce.Do(func() {
s.sOnce.Do(func() { s.spool.New = func() any {
v := make([]azalea.Frame, 1, 1<<4)
v[0] = s.getFrame()
return v
}
s.wantsArch() s.wantsArch()
k := func(name string) unique.Handle[azalea.Ident] { k := func(name string) unique.Handle[azalea.Ident] {
return unique.Make(azalea.Ident(name)) return unique.Make(azalea.Ident(name))
@@ -416,14 +423,13 @@ func (s *S) getS() []azalea.Frame {
identArch = k(s.arch) identArch = k(s.arch)
) )
s.s = make([]azalea.Frame, 1, 1<<4) s.frame.Val = map[unique.Handle[azalea.Ident]]any{
s.s[0].Val = map[unique.Handle[azalea.Ident]]any{
k("jobsE"): jobsE, k("jobsE"): jobsE,
k("jobsFlagE"): jobsFlagE, k("jobsFlagE"): jobsFlagE,
k("jobsLE"): jobsLE, k("jobsLE"): jobsLE,
k("jobsLFlagE"): jobsLFlagE, k("jobsLFlagE"): jobsLFlagE,
} }
s.s[0].Func = map[unique.Handle[azalea.Ident]]azalea.F{ s.frame.Func = map[unique.Handle[azalea.Ident]]azalea.F{
// intenral/pkg built-ins // intenral/pkg built-ins
@@ -551,9 +557,18 @@ func (s *S) getS() []azalea.Frame {
}}, }},
} }
}) })
return s.s return s.frame
} }
// getStack returns a new stack for azalea evaluation.
func (s *S) getStack() []azalea.Frame {
s.getFrame()
return s.spool.Get().([]azalea.Frame)
}
// putStack returns a stack to spool.
func (s *S) putStack(v []azalea.Frame) { s.spool.Put(v) }
// toHandles makes handles out of an [azalea.Array] of identifiers. // toHandles makes handles out of an [azalea.Array] of identifiers.
func toHandles(idents azalea.Array) (P, error) { func toHandles(idents azalea.Array) (P, error) {
handles := make(P, len(idents)) handles := make(P, len(idents))
@@ -576,11 +591,18 @@ func toHandles(idents azalea.Array) (P, error) {
return handles, nil return handles, nil
} }
// evalContext holds per-reader context. // evalContext holds per-artifact context.
type evalContext struct{ b fs.FS } type evalContext struct {
// Backing filesystem.
b fs.FS
// Pending azalea function.
expr *azalea.Func
// Current toolchain.
t Toolchain
}
// f implements [azalea.PF]. // pf implements [azalea.PF].
func (ctx *evalContext) f( func (ctx *evalContext) pf(
name azalea.Ident, name azalea.Ident,
args azalea.FArgs, args azalea.FArgs,
) (v any, set bool, err error) { ) (v any, set bool, err error) {
@@ -649,14 +671,13 @@ func (ctx *evalContext) f(
} }
meta.ID = int(anitya) meta.ID = int(anitya)
v = Artifact(func(t Toolchain) (*Metadata, pkg.Artifact) {
var source pkg.Artifact var source pkg.Artifact
switch p := sourceA.(type) { switch p := sourceA.(type) {
case pkg.Artifact: case pkg.Artifact:
source = p source = p
case deferredGit: case deferredGit:
source = t.newTagRemote(p.url, p.tag, p.checksum) source = ctx.t.newTagRemote(p.url, p.tag, p.checksum)
default: default:
panic(azalea.TypeError{ panic(azalea.TypeError{
@@ -665,15 +686,14 @@ func (ctx *evalContext) f(
}) })
} }
return &meta, t.NewPackage( v = cachedArtifact{&meta, ctx.t.NewPackage(
meta.Name, meta.Name,
meta.Version, meta.Version,
source, source,
&attr, &attr,
helper, helper,
inputsH..., inputsH...,
) )}
})
set = true set = true
return return
} }
@@ -684,9 +704,9 @@ var (
ErrToplevel = errors.New("top level must only contain package declarations") ErrToplevel = errors.New("top level must only contain package declarations")
) )
// Evaluate defines all package declarations from r. The backing filesystem is // RegisterAzalea registers all package declarations from r. The backing
// directly exposed to azalea pathnames. // filesystem is directly exposed to azalea pathnames.
func (s *S) Evaluate(r io.Reader, b fs.FS) error { func (s *S) RegisterAzalea(r io.Reader, b fs.FS) error {
var pending []*azalea.Func var pending []*azalea.Func
if expressions, err := azalea.Parse(r); err != nil { if expressions, err := azalea.Parse(r); err != nil {
return err return err
@@ -701,26 +721,31 @@ func (s *S) Evaluate(r io.Reader, b fs.FS) error {
} }
} }
ctx := evalContext{b}
for _, f := range pending { for _, f := range pending {
lf, set, err := azalea.Evaluate[Artifact](ctx.f, s.getS(), *f) if !s.Register(string(f.Ident), func(t Toolchain) (*Metadata, pkg.Artifact) {
v, set, err := azalea.Evaluate[cachedArtifact](
(&evalContext{b, f, t}).pf,
s.getStack(),
*f,
)
if err != nil { if err != nil {
return err panic(err)
} else if !set { } else if !set {
return errors.New("unexpected unset") panic(errors.New("unexpected unset"))
} }
if !s.Register(string(f.Ident), lf) { return v.meta, v.a
}) {
return RegisterError(H(string(f.Ident))) return RegisterError(H(string(f.Ident)))
} }
} }
return nil return nil
} }
// EvaluateFS evaluates azalea files discovered in fsys. A file is evaluated if // RegisterFS registers from azalea files discovered in fsys. A file is
// it is at the top level and its name has the suffix ".az", or it is in a // evaluated if it is at the top level and its name has the suffix ".az", or it
// top-level directory with the exact file name "package.az". The backing // is in a top-level directory with the exact file name "package.az". The
// filesystem is directly exposed to azalea pathnames. // backing filesystem is directly exposed to azalea pathnames.
func (s *S) EvaluateFS(fsys fs.FS) error { func (s *S) RegisterFS(fsys fs.FS) error {
dents, err := fs.ReadDir(fsys, ".") dents, err := fs.ReadDir(fsys, ".")
if err != nil { if err != nil {
return err return err
@@ -743,7 +768,7 @@ func (s *S) EvaluateFS(fsys fs.FS) error {
return err return err
} }
err = s.Evaluate(r, sub) err = s.RegisterAzalea(r, sub)
if _err := r.Close(); err == nil { if _err := r.Close(); err == nil {
err = _err err = _err
} }
@@ -763,7 +788,7 @@ func (s *S) EvaluateFS(fsys fs.FS) error {
return err return err
} }
err = s.Evaluate(r, fsys) err = s.RegisterAzalea(r, fsys)
if _err := r.Close(); err == nil { if _err := r.Close(); err == nil {
err = _err err = _err
} }