Skip to content

Commit

Permalink
Support gljdeps.edn in go module roots, automating pkgmap and glj sou…
Browse files Browse the repository at this point in the history
…rce embedding for glojure projects (#43)

* Move pkgmap gen to internal package

Signed-off-by: James Hamlin <[email protected]>

* Generate pkgmap from deps.edn

Signed-off-by: James Hamlin <[email protected]>

* Move glj main logic to reusable package

Signed-off-by: James Hamlin <[email protected]>

* Bootstrap runtime from gljmain

Signed-off-by: James Hamlin <[email protected]>

* gljimports automatically adds exports to the pkgmap

Signed-off-by: James Hamlin <[email protected]>

* Generate embed FS from deps.edn pkgs and auto-add to load path

Signed-off-by: James Hamlin <[email protected]>

* gofmt generated deps.edn embed file

Signed-off-by: James Hamlin <[email protected]>

* Autogen local glj from deps.edn, support for (seq) for go maps

Signed-off-by: James Hamlin <[email protected]>

* Fix for go map seq

Signed-off-by: James Hamlin <[email protected]>

* Fixes for mapping over chunked seqs

Signed-off-by: James Hamlin <[email protected]>

* Deps file is gljdeps.edn, not deps.edn

Signed-off-by: James Hamlin <[email protected]>

---------

Signed-off-by: James Hamlin <[email protected]>
  • Loading branch information
jfhamlin authored Aug 24, 2023
1 parent 8bc1e30 commit e323a35
Show file tree
Hide file tree
Showing 35 changed files with 1,115 additions and 302 deletions.
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

0 comments on commit e323a35

Please sign in to comment.