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

View File

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

View File

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