Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support gljdeps.edn in go module roots, automating pkgmap and glj source embedding for glojure projects #43

Merged
merged 11 commits into from
Aug 24, 2023
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ gocmd:
generate:
@go generate ./...

pkg/gen/gljimports/gljimports_%.go: ./scripts/gen-gljimports.sh ./cmd/gen-import-interop/main.go \
pkg/gen/gljimports/gljimports_%.go: ./scripts/gen-gljimports.sh ./cmd/gen-import-interop/main.go ./internal/genpkg/genpkg.go \
$(wildcard ./pkg/lang/*.go) $(wildcard ./pkg/runtime/*.go)
@echo "Generating $@"
@./scripts/gen-gljimports.sh $@ $* $(GO_CMD)
Expand Down
9 changes: 3 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,14 +137,11 @@ Then, in your own program:
package main

import (
"your.package/gljimports"

"github.com/glojurelang/glojure/pkg/runtime"
// Add your packages' exports to the pkgmap.
_ "your.package/gljimports"
)

func init() {
gljimports.RegisterImports(pkgmap.Set)
}
// ...
```

## Comparisons to other Go ports of Clojure
Expand Down
212 changes: 3 additions & 209 deletions cmd/gen-import-interop/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,9 @@ package main

import (
"flag"
"fmt"
"go/constant"
"go/format"
"go/importer"
"go/token"
"go/types"
"math"
"os"
"sort"
"strconv"
"strings"

"github.com/glojurelang/glojure/internal/genpkg"
)

var defaultPackages = []string{
Expand Down Expand Up @@ -179,11 +171,6 @@ var packagesFlag = flag.String(
"comma separated list of packages to import",
)

type export struct {
name string
obj types.Object
}

func main() {
flag.Parse()

Expand All @@ -192,198 +179,5 @@ func main() {
packages = strings.Split(*packagesFlag, ",")
}

sortedPackageNames, packageExports := collectExportedObjects(packages)
printGeneratedCode(sortedPackageNames, packageExports)
}

func collectExportedObjects(packages []string) ([]string, map[string][]export) {
var packageNames []string
packageExports := map[string][]export{}

fileSet := token.NewFileSet()
sourceImporter := importer.ForCompiler(fileSet, "source", nil)

for _, packageName := range packages {
packageData, err := sourceImporter.Import(packageName)
if err != nil {
panic(err)
}

var exportedObjects []export
for _, exportedObjectName := range packageData.Scope().Names() {
object := packageData.Scope().Lookup(exportedObjectName)
if isEligibleForExport(object, packageName) {
exportedObjects = append(exportedObjects, export{
name: exportedObjectName,
obj: object,
})
}
}

if len(exportedObjects) > 0 {
packageExports[packageName] = exportedObjects
packageNames = append(packageNames, packageName)
}
}

sort.Strings(packageNames)

return packageNames, packageExports
}

func isEligibleForExport(object types.Object, packageName string) bool {
if !object.Exported() {
return false
}

if _, isIllegalType := object.(*types.Builtin); isIllegalType {
return false
}

return true
}

func printGeneratedCode(packageNames []string, packageExports map[string][]export) {
builder := createHeaderBuilder(packageNames)
createFunctionBuilder(builder, packageNames, packageExports)

formattedCode, err := format.Source([]byte(builder.String()))
if err != nil {
panic(err)
}

os.Stdout.Write(formattedCode)
os.Stdout.Sync()
}

func createHeaderBuilder(packageNames []string) *strings.Builder {
builder := &strings.Builder{}

builder.WriteString("// GENERATED FILE. DO NOT EDIT.\n")
builder.WriteString("package gljimports\n\n")
builder.WriteString("import (\n")

for _, packageName := range packageNames {
aliasName := strings.NewReplacer(".", "_", "/", "_", "-", "_").Replace(packageName)
builder.WriteString(fmt.Sprintf("\t%s \"%s\"\n", aliasName, packageName))
}

builder.WriteString(")\n\n")

return builder
}

func createFunctionBuilder(builder *strings.Builder, packageNames []string, packageExports map[string][]export) {
builder.WriteString("func RegisterImports(_register func(string, interface{})) {\n")

first := true
for _, packageName := range packageNames {
if !first {
builder.WriteString("\n")
}
first = false
addPackageSection(builder, packageName, packageExports[packageName])
}

builder.WriteString("}\n")
}

func addPackageSection(builder *strings.Builder, packageName string, packageExports []export) {
builder.WriteString(fmt.Sprintf("\t// package %s\n", packageName))
builder.WriteString("\t////////////////////////////////////////\n")

packageAlias := replaceSpecChars(packageName)

for _, exportedObject := range packageExports {
declareExportedObject(builder, exportedObject, packageName, packageAlias)
}
}

func replaceSpecChars(value string) string {
return strings.NewReplacer(".", "_", "/", "_", "-", "_").Replace(value)
}

func declareExportedObject(builder *strings.Builder, exportedObject export, packageName string, packageAlias string) {
globalName := fmt.Sprintf("%s.%s", packageName, exportedObject.name)
aliasName := fmt.Sprintf("%s.%s", packageAlias, exportedObject.name)

decl := getDeclaration(exportedObject.obj, globalName, aliasName)

for _, line := range strings.Split(decl, "\n") {
builder.WriteRune('\t')
builder.WriteString(line)
builder.WriteRune('\n')
}
}

func getDeclaration(object types.Object, globalName string, aliasName string) string {
switch concreteObject := object.(type) {
case *types.Const:
return getConstDeclaration(concreteObject, globalName, aliasName)
case *types.Func:
return fmt.Sprintf("_register(%q, %s)", globalName, aliasName)
case *types.Var:
return getVarDeclaration(concreteObject, globalName, aliasName)
case *types.TypeName:
return getTypeNameDeclaration(concreteObject, globalName, aliasName)
default:
panic(fmt.Errorf("unknown type %T (%v)", object, object))
}
}

func getConstDeclaration(object *types.Const, globalName string, aliasName string) string {
value := object.Val()

switch value.Kind() {
case constant.Bool, constant.String, constant.Complex:
return fmt.Sprintf("_register(%q, %s)", globalName, aliasName)
case constant.Int:
val := aliasName
if intType := getTypedInt(value.ExactString()); intType != "" {
val = fmt.Sprintf("%s(%s)", intType, val)
}
return fmt.Sprintf("_register(%q, %s)", globalName, val)
case constant.Float:
return fmt.Sprintf("_register(%q, float64(%s))", globalName, aliasName)
default:
panic(fmt.Errorf("unknown constant type: %s", value.Kind()))
}
}

func getVarDeclaration(object *types.Var, globalName string, aliasName string) string {
typeName := object.Type().String()

if typeName == "sync.Mutex" || typeName == "sync.RWMutex" {
return fmt.Sprintf("_register(%q, &%s)", globalName, aliasName)
}

return fmt.Sprintf("_register(%q, %s)", globalName, aliasName)
}

func getTypeNameDeclaration(object *types.TypeName, globalName string, aliasName string) string {
isGeneric := strings.HasSuffix(object.Type().String(), "]")

if isGeneric {
return ""
}

return fmt.Sprintf("_register(%q, reflect.TypeOf((*%s)(nil)).Elem())", globalName, aliasName)
}

// getTypedInt derives the type of the integer literal from its string
// representation. Use the smallest type >= int that can represent the value.
func getTypedInt(value string) string {
if v, err := strconv.ParseInt(value, 10, 0); err == nil && v >= math.MinInt && v <= math.MaxInt {
return ""
} else if v, err := strconv.ParseInt(value, 10, 32); err == nil && v >= -2147483648 && v <= 2147483647 {
return "int32"
} else if v, err := strconv.ParseUint(value, 10, 32); err == nil && v <= 4294967295 {
return "uint32"
} else if v, err := strconv.ParseInt(value, 10, 64); err == nil && v >= -9223372036854775808 && v <= 9223372036854775807 {
return "int64"
} else if _, err := strconv.ParseUint(value, 10, 64); err == nil {
return "uint64"
} else {
panic(fmt.Errorf("cannot determine type of integer literal %s", value))
}
genpkg.GenPkgs(packages)
}
53 changes: 21 additions & 32 deletions cmd/glj/main.go
Original file line number Diff line number Diff line change
@@ -1,51 +1,40 @@
package main

import (
"bufio"
"fmt"
"log"
"os"
"os/exec"
"syscall"

"github.com/glojurelang/glojure/pkg/lang"
"github.com/glojurelang/glojure/pkg/reader"
"github.com/glojurelang/glojure/pkg/repl"
"github.com/glojurelang/glojure/pkg/runtime"
"github.com/glojurelang/glojure/internal/deps"

// Bootstrap the runtime
_ "github.com/glojurelang/glojure/pkg/glj"
"github.com/glojurelang/glojure/pkg/gljmain"
)

func main() {
args := os.Args[1:]

runtime.AddLoadPath(os.DirFS("."))
dps, err := deps.Load()
if err != nil {
log.Fatal(err)
}
if dps != nil {
if err := dps.Gen(); err != nil {
panic(err)
}

if len(args) == 0 {
repl.Start()
} else {
file, err := os.Open(os.Args[1])
exe, err := exec.LookPath("go")
if err != nil {
log.Fatal(err)
panic(fmt.Errorf("failed to find `go` executable: %v", err))
}
env := lang.GlobalEnv

core := lang.FindNamespace(lang.NewSymbol("glojure.core"))
core.FindInternedVar(lang.NewSymbol("*command-line-args*")).BindRoot(lang.Seq(os.Args[2:]))

rdr := reader.New(bufio.NewReader(file), reader.WithGetCurrentNS(func() *lang.Namespace {
return env.CurrentNamespace()
}))
for {
val, err := rdr.ReadOne()
if err == reader.ErrEOF {
break
}
if err != nil {
log.Fatal(err)
}
_, err = env.Eval(val)
if err != nil {
log.Fatal(err)
}
argv := append([]string{"go", "run", "./glj/cmd/glj"}, os.Args[1:]...)
if err := syscall.Exec(exe, argv, os.Environ()); err != nil {
log.Fatalf("failed to run %v: %v", exe, err)
}
panic("a successful exec syscall should replace this process")
}

gljmain.Main(os.Args[1:])
}
27 changes: 27 additions & 0 deletions internal/deps/deps.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package deps

import "fmt"

// Deps is a struct that contains all the dependencies for the
// Glojure application.
type Deps struct {
goModName string
goModDir string

Pkgs []string
Deps map[string]map[string]string
}

func (d *Deps) Gen() error {
if err := d.Get(); err != nil {
return fmt.Errorf("failed to fetch dependencies: %w", err)
}
if err := d.Embed(); err != nil {
return fmt.Errorf("failed to generate embed for packages: %w", err)
}
if err := d.GLJ(); err != nil {
return fmt.Errorf("failed to generate glj main: %w", err)
}

return nil
}
Loading