493 lines
9.5 KiB
Go
493 lines
9.5 KiB
Go
package workspace
|
|
|
|
import (
|
|
"errors"
|
|
"io/fs"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
|
|
"aego/script/abi"
|
|
"aego/script/ast"
|
|
"aego/script/bind/manifest"
|
|
"aego/script/bytecode"
|
|
"aego/script/checker"
|
|
"aego/script/diag"
|
|
"aego/script/parser"
|
|
"aego/script/source"
|
|
)
|
|
|
|
const Ext = ".aes"
|
|
|
|
type Config struct {
|
|
Root string
|
|
FS fs.FS
|
|
Module string
|
|
Scripts string
|
|
Bindings *abi.Table
|
|
Docs *manifest.Manifest
|
|
Debug bool
|
|
Tests bool
|
|
}
|
|
|
|
type Snapshot struct {
|
|
Gen uint64
|
|
Analysis *checker.Analysis
|
|
Sources *source.Manager
|
|
Files map[source.FileID]*ast.File
|
|
Packages map[source.FileID]string
|
|
Diagnostics []diag.Diagnostic
|
|
Bindings *abi.Table
|
|
Docs *manifest.Manifest
|
|
}
|
|
|
|
func (s *Snapshot) Errors() int {
|
|
n, _ := diag.Count(s.Diagnostics)
|
|
return n
|
|
}
|
|
|
|
type Generation struct {
|
|
ID uint64
|
|
Modules map[string]*bytecode.Module
|
|
Hashes map[string][32]byte
|
|
ABIHash [32]byte
|
|
Changed []string
|
|
Failed map[string][]diag.Diagnostic
|
|
Sources *source.Manager
|
|
}
|
|
|
|
type entry struct {
|
|
id source.FileID
|
|
path string
|
|
pkg string
|
|
test bool
|
|
disk []byte
|
|
onDisk bool
|
|
over []byte
|
|
hasOver bool
|
|
}
|
|
|
|
type built struct {
|
|
key [32]byte
|
|
iface [32]byte
|
|
mod *bytecode.Module
|
|
}
|
|
|
|
type Workspace struct {
|
|
mu sync.Mutex
|
|
bmu sync.Mutex
|
|
cfg Config
|
|
fs fs.FS
|
|
scripts string
|
|
files map[string]*entry
|
|
order []*entry
|
|
ver uint64
|
|
snap *Snapshot
|
|
empty *abi.Table
|
|
state map[string]built
|
|
committed *Generation
|
|
nextID uint64
|
|
}
|
|
|
|
func newGeneration() *Generation {
|
|
return &Generation{
|
|
Modules: map[string]*bytecode.Module{},
|
|
Hashes: map[string][32]byte{},
|
|
Failed: map[string][]diag.Diagnostic{},
|
|
Sources: source.NewManager(),
|
|
}
|
|
}
|
|
|
|
func Open(cfg Config) (*Workspace, error) {
|
|
if cfg.FS == nil {
|
|
root, err := filepath.Abs(cfg.Root)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cfg.Root = root
|
|
cfg.FS = os.DirFS(root)
|
|
}
|
|
scripts, err := scriptsDir(cfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if cfg.Module == "" {
|
|
cfg.Module = "game"
|
|
}
|
|
empty := abi.NewTable()
|
|
empty.Freeze()
|
|
w := &Workspace{
|
|
cfg: cfg,
|
|
fs: cfg.FS,
|
|
scripts: scripts,
|
|
files: map[string]*entry{},
|
|
empty: empty,
|
|
state: map[string]built{},
|
|
committed: newGeneration(),
|
|
}
|
|
if err := w.Rescan(); err != nil {
|
|
return nil, err
|
|
}
|
|
return w, nil
|
|
}
|
|
|
|
func scriptsDir(cfg Config) (string, error) {
|
|
s := cfg.Scripts
|
|
switch {
|
|
case s == "":
|
|
return "scripts", nil
|
|
case filepath.IsAbs(s):
|
|
if cfg.Root == "" {
|
|
return "", errors.New("an absolute scripts directory needs a workspace root")
|
|
}
|
|
r, err := filepath.Rel(cfg.Root, s)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
s = r
|
|
}
|
|
s = path.Clean(filepath.ToSlash(s))
|
|
if s == ".." || strings.HasPrefix(s, "../") || strings.HasPrefix(s, "/") {
|
|
return "", errors.New("scripts directory " + cfg.Scripts + " is outside the workspace")
|
|
}
|
|
return s, nil
|
|
}
|
|
|
|
func (w *Workspace) Module() string {
|
|
return w.cfg.Module
|
|
}
|
|
|
|
func (w *Workspace) Root() string {
|
|
return w.cfg.Root
|
|
}
|
|
|
|
// Version is the edit generation, matching the Gen of its snapshot.
|
|
func (w *Workspace) Version() uint64 {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
return w.ver
|
|
}
|
|
|
|
func (w *Workspace) rel(p string) string {
|
|
if filepath.IsAbs(p) && w.cfg.Root != "" {
|
|
if r, err := filepath.Rel(w.cfg.Root, p); err == nil {
|
|
p = r
|
|
}
|
|
}
|
|
return path.Clean(filepath.ToSlash(p))
|
|
}
|
|
|
|
func (w *Workspace) PackageOf(p string) (string, bool) {
|
|
r := w.rel(p)
|
|
if !strings.HasSuffix(r, Ext) {
|
|
return "", false
|
|
}
|
|
dir := path.Dir(r)
|
|
switch {
|
|
case dir == w.scripts:
|
|
return w.cfg.Module, true
|
|
case strings.HasPrefix(dir, w.scripts+"/"):
|
|
return w.cfg.Module + "/" + strings.TrimPrefix(dir, w.scripts+"/"), true
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
func (w *Workspace) entryFor(r string) *entry {
|
|
if e, ok := w.files[r]; ok {
|
|
return e
|
|
}
|
|
pkg, _ := w.PackageOf(r)
|
|
e := &entry{
|
|
id: source.FileID(len(w.order) + 1),
|
|
path: r,
|
|
pkg: pkg,
|
|
test: IsTest(r),
|
|
}
|
|
w.files[r] = e
|
|
w.order = append(w.order, e)
|
|
return e
|
|
}
|
|
|
|
func (w *Workspace) touch() {
|
|
w.ver++
|
|
w.snap = nil
|
|
}
|
|
|
|
func (w *Workspace) Rescan() error {
|
|
found := map[string][]byte{}
|
|
err := fs.WalkDir(w.fs, w.scripts, func(p string, d fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
if errors.Is(err, fs.ErrNotExist) && p == w.scripts {
|
|
return fs.SkipDir
|
|
}
|
|
return err
|
|
}
|
|
if d.IsDir() {
|
|
if p != w.scripts && strings.HasPrefix(d.Name(), ".") {
|
|
return fs.SkipDir
|
|
}
|
|
return nil
|
|
}
|
|
if path.Ext(p) != Ext {
|
|
return nil
|
|
}
|
|
src, err := fs.ReadFile(w.fs, p)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
found[p] = src
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
w.Sync(found)
|
|
return nil
|
|
}
|
|
|
|
func (w *Workspace) Sync(files map[string][]byte) {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
seen := make(map[string]bool, len(files))
|
|
for p, src := range files {
|
|
r := w.rel(p)
|
|
seen[r] = true
|
|
e := w.entryFor(r)
|
|
e.disk, e.onDisk = src, true
|
|
}
|
|
for _, e := range w.order {
|
|
if e.onDisk && !seen[e.path] {
|
|
e.disk, e.onDisk = nil, false
|
|
}
|
|
}
|
|
w.touch()
|
|
}
|
|
|
|
func (w *Workspace) Changed(paths []string) {
|
|
type read struct {
|
|
path string
|
|
src []byte
|
|
ok bool
|
|
}
|
|
var reads []read
|
|
for _, p := range paths {
|
|
r := w.rel(p)
|
|
if !strings.HasSuffix(r, Ext) {
|
|
continue
|
|
}
|
|
src, err := fs.ReadFile(w.fs, r)
|
|
reads = append(reads, read{path: r, src: src, ok: err == nil})
|
|
}
|
|
if len(reads) == 0 {
|
|
return
|
|
}
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
for _, rd := range reads {
|
|
e := w.entryFor(rd.path)
|
|
if rd.ok {
|
|
e.disk, e.onDisk = rd.src, true
|
|
} else {
|
|
e.disk, e.onDisk = nil, false
|
|
}
|
|
}
|
|
w.touch()
|
|
}
|
|
|
|
func (w *Workspace) Removed(paths []string) {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
for _, p := range paths {
|
|
if e, ok := w.files[w.rel(p)]; ok {
|
|
e.disk, e.onDisk = nil, false
|
|
}
|
|
}
|
|
w.touch()
|
|
}
|
|
|
|
func (w *Workspace) SetOverlay(p string, src []byte) {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
e := w.entryFor(w.rel(p))
|
|
e.over, e.hasOver = append([]byte(nil), src...), true
|
|
w.touch()
|
|
}
|
|
|
|
func (w *Workspace) ClearOverlay(p string) {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
if e, ok := w.files[w.rel(p)]; ok && e.hasOver {
|
|
e.over, e.hasOver = nil, false
|
|
w.touch()
|
|
}
|
|
}
|
|
|
|
func (w *Workspace) SetBindings(t *abi.Table) {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
w.cfg.Bindings = t
|
|
w.touch()
|
|
}
|
|
|
|
func (w *Workspace) SetDocs(m *manifest.Manifest) {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
w.cfg.Docs = m
|
|
w.touch()
|
|
}
|
|
|
|
func (w *Workspace) table() *abi.Table {
|
|
if w.cfg.Bindings != nil {
|
|
return w.cfg.Bindings
|
|
}
|
|
return w.empty
|
|
}
|
|
|
|
func (w *Workspace) Bindings() *abi.Table {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
return w.table()
|
|
}
|
|
|
|
func (w *Workspace) Docs() *manifest.Manifest {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
return w.cfg.Docs
|
|
}
|
|
|
|
func (w *Workspace) FileID(p string) (source.FileID, bool) {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
e, ok := w.files[w.rel(p)]
|
|
if !ok {
|
|
return source.NoFile, false
|
|
}
|
|
return e.id, true
|
|
}
|
|
|
|
func (w *Workspace) Path(id source.FileID) string {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
if id == source.NoFile || int(id) > len(w.order) {
|
|
return ""
|
|
}
|
|
return w.order[id-1].path
|
|
}
|
|
|
|
func (w *Workspace) Packages() []string {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
set := map[string]bool{}
|
|
for _, e := range w.order {
|
|
if e.pkg != "" && (e.onDisk || e.hasOver) {
|
|
set[e.pkg] = true
|
|
}
|
|
}
|
|
return sortedKeys(set)
|
|
}
|
|
|
|
func (w *Workspace) Sources() *source.Manager {
|
|
return w.Snapshot().Sources
|
|
}
|
|
|
|
func (w *Workspace) Committed() *Generation {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
return w.committed
|
|
}
|
|
|
|
type input struct {
|
|
sm *source.Manager
|
|
pkgs map[string][]source.FileID
|
|
srcs map[string]map[string][]byte
|
|
pkgOf map[source.FileID]string
|
|
}
|
|
|
|
func (w *Workspace) inputs(overlay, tests bool) *input {
|
|
in := &input{
|
|
sm: source.NewManager(),
|
|
pkgs: map[string][]source.FileID{},
|
|
srcs: map[string]map[string][]byte{},
|
|
pkgOf: map[source.FileID]string{},
|
|
}
|
|
for _, e := range w.order {
|
|
src, live := e.disk, e.onDisk
|
|
if overlay && e.hasOver {
|
|
src, live = e.over, true
|
|
}
|
|
if !live {
|
|
src = nil
|
|
}
|
|
id := in.sm.Add(e.path, src)
|
|
if !live || e.pkg == "" || (e.test && !tests) {
|
|
continue
|
|
}
|
|
in.pkgs[e.pkg] = append(in.pkgs[e.pkg], id)
|
|
in.pkgOf[id] = e.pkg
|
|
if in.srcs[e.pkg] == nil {
|
|
in.srcs[e.pkg] = map[string][]byte{}
|
|
}
|
|
in.srcs[e.pkg][e.path] = src
|
|
}
|
|
return in
|
|
}
|
|
|
|
func (in *input) analyze(tab *abi.Table, module string) (*checker.Analysis, map[source.FileID]*ast.File) {
|
|
files := make(map[string][]*ast.File, len(in.pkgs))
|
|
byID := make(map[source.FileID]*ast.File)
|
|
var ds []diag.Diagnostic
|
|
for pkg, list := range in.pkgs {
|
|
for _, id := range list {
|
|
f, d := parser.ParseFile(in.sm, id)
|
|
ds = append(ds, d...)
|
|
files[pkg] = append(files[pkg], f)
|
|
byID[id] = f
|
|
}
|
|
}
|
|
a := checker.Check(in.sm, files, tab, checker.Options{Module: module})
|
|
a.Diagnostics = append(ds, a.Diagnostics...)
|
|
diag.Sort(a.Diagnostics)
|
|
return a, byID
|
|
}
|
|
|
|
func (w *Workspace) Snapshot() *Snapshot {
|
|
w.mu.Lock()
|
|
if s := w.snap; s != nil && s.Gen == w.ver {
|
|
w.mu.Unlock()
|
|
return s
|
|
}
|
|
ver := w.ver
|
|
in := w.inputs(true, w.cfg.Tests)
|
|
tab, docs, module := w.table(), w.cfg.Docs, w.cfg.Module
|
|
w.mu.Unlock()
|
|
a, files := in.analyze(tab, module)
|
|
s := &Snapshot{
|
|
Gen: ver,
|
|
Analysis: a,
|
|
Sources: in.sm,
|
|
Files: files,
|
|
Packages: in.pkgOf,
|
|
Diagnostics: a.Diagnostics,
|
|
Bindings: tab,
|
|
Docs: docs,
|
|
}
|
|
w.mu.Lock()
|
|
if w.ver == ver {
|
|
w.snap = s
|
|
}
|
|
w.mu.Unlock()
|
|
return s
|
|
}
|
|
|
|
func sortedKeys[V any](m map[string]V) []string {
|
|
out := make([]string, 0, len(m))
|
|
for k := range m {
|
|
out = append(out, k)
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|