package loader
import (
"fmt"
"path/filepath"
"strings"
"github.com/marzeq/qk/attributes"
"github.com/marzeq/qk/parser"
)
type PartialModuleInfo struct {
Path string
Name string
Imports []string
Root *parser.RootNode
Links []attributes.Link
TrustedStandardLibrary bool
}
func CollectModuleInfo(root *parser.RootNode, trustedStandardLibrary bool) (*PartialModuleInfo, error) {
name := ""
imports := []string{}
seenModule := false
links := []attributes.Link{}
for i, node := range root.Body {
switch n := node.(type) {
case *parser.ModuleNode:
if i != 0 {
return nil, fmt.Errorf("module declaration must be first statement")
}
if seenModule {
return nil, fmt.Errorf("multiple module declarations")
}
seenModule = true
name = n.Name
for _, attr := range n.Attributes {
linkAttr, ok := attr.(attributes.ModuleAttributeLink)
if !ok {
continue
}
for _, link := range linkAttr.Links {
if (link.Kind == attributes.LinkPath || link.Kind == attributes.LinkSearchPath) && !filepath.IsAbs(link.Value) {
link.Value = filepath.Join(filepath.Dir(n.Loc.FilePath), link.Value)
}
if link.Kind == attributes.LinkPath || link.Kind == attributes.LinkSearchPath {
link.Value = filepath.Clean(link.Value)
}
links = append(links, link)
}
}
case *parser.ImportNode:
for i, imported := range n.Modules {
if i < len(n.ResolvedModules) && n.ResolvedModules[i] != "" {
imported = n.ResolvedModules[i]
}
imports = append(imports, imported)
}
}
}
if !seenModule || name == "" {
return nil, fmt.Errorf("module declaration is missing or empty")
}
if (name == "std" || strings.HasPrefix(name, "std.")) && !trustedStandardLibrary {
return nil, fmt.Errorf("module name %q is reserved for compiler-trusted standard-library sources", name)
}
if trustedStandardLibrary && name != "std" && !strings.HasPrefix(name, "std.") {
return nil, fmt.Errorf("trusted standard-library source declares module %q outside the reserved %q namespace", name, "std")
}
return &PartialModuleInfo{
Name: name,
Imports: imports,
Root: root,
Links: links,
TrustedStandardLibrary: trustedStandardLibrary,
}, nil
}
type ModuleInfo struct {
Path string
Name string
Imports []string
Root *parser.RootNode
Links []attributes.Link
TrustedStandardLibrary bool
}
func BuildModules(partials []*PartialModuleInfo) (map[string]*ModuleInfo, error) {
modules := map[string]*ModuleInfo{}
for _, p := range partials {
path := p.Path
if path == "" {
path = p.Name
}
if existing, ok := modules[path]; ok {
if existing.Name != p.Name {
return nil, fmt.Errorf("package %q contains both module %q and module %q", path, existing.Name, p.Name)
}
if existing.TrustedStandardLibrary != p.TrustedStandardLibrary {
return nil, fmt.Errorf("cannot mix trusted and untrusted sources in package %q", path)
}
mergeModuleRoot(existing.Root, p.Root)
existing.Imports = mergeImports(existing.Imports, p.Imports)
existing.Links = append(existing.Links, p.Links...)
} else {
modules[path] = &ModuleInfo{
Path: path,
Name: p.Name,
Imports: unique(p.Imports),
Root: cloneModuleRoot(p.Root),
Links: append([]attributes.Link(nil), p.Links...),
TrustedStandardLibrary: p.TrustedStandardLibrary,
}
}
}
return modules, nil
}
func cloneModuleRoot(root *parser.RootNode) *parser.RootNode {
return &parser.RootNode{
Body: append([]parser.Node(nil), root.Body...),
Loc: root.Loc,
}
}
func mergeModuleRoot(destination, source *parser.RootNode) {
for _, node := range source.Body {
if _, isModule := node.(*parser.ModuleNode); isModule {
continue
}
destination.Body = append(destination.Body, node)
}
}
func mergeImports(a, b []string) []string {
return unique(append(a, b...))
}
func unique(in []string) []string {
seen := map[string]struct{}{}
var out []string
for _, v := range in {
if _, ok := seen[v]; !ok {
seen[v] = struct{}{}
out = append(out, v)
}
}
return out
}