Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,28 @@ STDLIB := $(STDLIB_ORIGINALS:scripts/rewrite-core/originals/%=%)
STDLIB_ORIGINALS := $(addprefix scripts/rewrite-core/originals/,$(STDLIB))
STDLIB_TARGETS := $(addprefix pkg/stdlib/glojure/,$(STDLIB:.clj=.glj))

OS-TYPE := $(shell bash -c 'echo $$OSTYPE')
OS-NAME := \
$(if $(findstring darwin,$(OS-TYPE))\
,macos,$(if $(findstring linux,$(OS-TYPE)),linux,))
ARCH-TYPE := $(shell bash -c 'echo $$MACHTYPE')
ARCH-NAME := \
$(if $(or $(findstring arm64,$(ARCH-TYPE)),\
$(findstring aarch64,$(ARCH-TYPE)))\
,arm64,$(if $(findstring x86_64,$(ARCH-TYPE)),int64,))

ifdef OS-NAME
ifdef ARCH-NAME
OS-ARCH := $(OS-NAME)-$(ARCH-NAME)
OA-linux-arm64 := linux_arm64
OA-linux-int64 := linux_amd64
OA-macos-arm64 := darwin_arm64
OA-macos-int64 := darwin_amd64
OA := $(OA-$(OS-ARCH))
GLJ := bin/$(OA)/glj
endif
endif

TEST_FILES := $(shell find ./test -name '*.glj' | sort)
TEST_TARGETS := $(addsuffix .test,$(TEST_FILES))

Expand All @@ -31,6 +53,13 @@ gocmd:
generate:
@go generate ./...

.PHONY: build
build: $(GLJ)

.PHONY: clean
clean:
$(RM) -r bin/

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 $@"
Expand All @@ -56,7 +85,7 @@ vet:
@go vet ./...

.PHONY: $(TEST_TARGETS)
$(TEST_TARGETS): gocmd
$(TEST_TARGETS): gocmd $(GLJ)
@$(GO_CMD) run ./cmd/glj/main.go $(basename $@)

.PHONY: test
Expand Down
35 changes: 32 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

![example workflow](https://github.com/glojurelang/glojure/actions/workflows/ci.yml/badge.svg)

[Try it in your browser!](https://glojurelang.github.io/glojure/) (fair warning: startup on the web is slow)
[Try it in your browser!](https://glojurelang.github.io/glojure/)
(fair warning: startup on the web is slow)

<img alt="Gopher image" src="./doc/logo.png" width="512" />

Expand Down Expand Up @@ -51,14 +52,28 @@ user=>

## Usage

Glojure can be used in two ways: as a standalone command-line tool (`glj`) or embedded within Go applications.
Glojure can be used in two ways: as a standalone command-line tool (`glj`) or
embedded within Go applications.

### Using the `glj` Command

The `glj` command provides a traditional Clojure development experience:

**Show the help:**
```
$ glj --help # or glj -h
```

**Show the version:**
```
$ glj --version
glojure v0.3.0
```

**Start a REPL (interactive session):**
```
user=> *glojure-version*
{:major 0, :minor 3, :incremental 0, :qualifier nil}
$ glj
user=> (+ 1 2 3)
6
Expand All @@ -67,6 +82,18 @@ Hello from Glojure!
nil
```

**Evaluate expressions:**
```
$ glj -e '(println "Hello, World!")'
Hello, World!
$ glj -e '(apply + (range 3 10))'
42
$ glj -e '
(defn factorial [n] (if (<= n 1) 1 (* n (factorial (dec n)))))
(factorial 5)'
120
```

**Run a Clojure script:**
```clojure
;; hello.glj
Expand Down Expand Up @@ -100,7 +127,8 @@ Server starting on :8080...

### Embedding Glojure in Go Applications

You can also embed Glojure as a scripting language within your Go applications. This is useful when you want to:
You can also embed Glojure as a scripting language within your Go applications.
This is useful when you want to:
- Add scriptable configuration to your Go application
- Allow users to extend your application with Clojure plugins
- Mix Go's performance with Clojure's expressiveness
Expand Down Expand Up @@ -185,6 +213,7 @@ runtime.ReadEval(`
- Writing standalone Clojure programs
- Interactive development with the REPL
- Running Clojure scripts
- Evaluating expressions directly from the command line
- Learning Clojure with Go interop

**Embed Glojure for:**
Expand Down
68 changes: 66 additions & 2 deletions pkg/gljmain/gljmain.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package gljmain

import (
"bufio"
"fmt"
"log"
"os"
"strings"

// bootstrap the runtime
_ "github.com/glojurelang/glojure/pkg/glj"
Expand All @@ -14,20 +16,82 @@ import (
"github.com/glojurelang/glojure/pkg/runtime"
)

func printHelp() {
fmt.Printf(`Glojure v%s

Usage: glj [options] [file]

Options:
-e <expr> Evaluate expression from command line
-h, --help Show this help message
--version Show version information

Examples:
glj # Start REPL
glj -e "(+ 1 2)" # Evaluate expression
glj script.glj # Run script file
glj --version # Show version
glj --help # Show this help

For more information, visit: https://github.com/glojurelang/glojure
`, runtime.VERSION)
}

func Main(args []string) {
runtime.AddLoadPath(os.DirFS("."))

if len(args) == 0 {
repl.Start()
} else if args[0] == "--version" {
fmt.Printf("glojure v%s\n", runtime.VERSION)
return
} else if args[0] == "--help" || args[0] == "-h" {
printHelp()
return
} else if args[0] == "-e" {
// Evaluate expression from command line
if len(args) < 2 {
log.Fatal("glj: -e requires an expression")
}
expr := args[1]
env := lang.GlobalEnv

// Set command line args (everything after -e and the expression)
core := lang.FindNamespace(lang.NewSymbol("glojure.core"))
core.FindInternedVar(lang.NewSymbol("*command-line-args*")).BindRoot(lang.Seq(args[2:]))

rdr := reader.New(strings.NewReader(expr), reader.WithGetCurrentNS(func() *lang.Namespace {
return env.CurrentNamespace()
}))
var lastResult interface{}
for {
val, err := rdr.ReadOne()
if err == reader.ErrEOF {
break
}
if err != nil {
log.Fatal(err)
}
result, err := env.Eval(val)
if err != nil {
log.Fatal(err)
}
lastResult = result
}
// Print only the final result unless it's nil
if !lang.IsNil(lastResult) {
fmt.Println(lang.PrintString(lastResult))
}
} else {
file, err := os.Open(os.Args[1])
// Execute file
file, err := os.Open(args[0])
if err != nil {
log.Fatal(err)
}
env := lang.GlobalEnv

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

rdr := reader.New(bufio.NewReader(file), reader.WithGetCurrentNS(func() *lang.Namespace {
return env.CurrentNamespace()
Expand Down
42 changes: 42 additions & 0 deletions pkg/runtime/envinit.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,48 @@ import (
"fmt"
"io"
"os"
"strconv"
"strings"

"github.com/glojurelang/glojure/pkg/lang"
"github.com/glojurelang/glojure/pkg/reader"
"github.com/glojurelang/glojure/pkg/stdlib"
)

// The current version of Glojure
const VERSION = "0.3.0"

// ParseVersion parses the VERSION string and returns a map with major, minor,
// incremental, and qualifier
func ParseVersion(version string) lang.IPersistentMap {
parts := strings.Split(version, ".")

major, _ := strconv.Atoi(parts[0])
minor, _ := strconv.Atoi(parts[1])

incremental := 0
qualifier := interface{}(nil)

if len(parts) > 2 {
// Check if the third part contains a qualifier (e.g., "0-alpha")
incrementalPart := parts[2]
if strings.Contains(incrementalPart, "-") {
qualifierParts := strings.SplitN(incrementalPart, "-", 2)
incremental, _ = strconv.Atoi(qualifierParts[0])
qualifier = qualifierParts[1]
} else {
incremental, _ = strconv.Atoi(incrementalPart)
}
}

return lang.NewMap(
lang.NewKeyword("major"), major,
lang.NewKeyword("minor"), minor,
lang.NewKeyword("incremental"), incremental,
lang.NewKeyword("qualifier"), qualifier,
)
}

type Program struct {
nodes []interface{}
}
Expand Down Expand Up @@ -113,6 +148,13 @@ func NewEnvironment(opts ...EvalOption) lang.Environment {
evalFile("glojure/core.glj")
}

// Set the glojure version
core := lang.FindNamespace(lang.NewSymbol("glojure.core"))
versionVar := core.FindInternedVar(lang.NewSymbol("*glojure-version*"))
if versionVar != nil {
versionVar.BindRoot(ParseVersion(VERSION))
}

return env
}

Expand Down
1 change: 1 addition & 0 deletions pkg/runtime/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ func newEnvironment(ctx context.Context, stdout, stderr io.Writer) *environment
"print-meta",
"print-dup",
"read-eval",
"glojure-version",
} {
coreNS.InternWithValue(lang.NewSymbol("*"+dyn+"*"), nil, true).SetDynamic()
}
Expand Down
77 changes: 77 additions & 0 deletions test/glojure/test_glojure/cli.glj
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
(ns glojure.test-glojure.cli
(:use glojure.test)
(:require [glojure.string :as str]))

(defmacro #^{:private true} test-that
"Provides a useful way for specifying the purpose of tests. If the first-level
forms are lists that make a call to a glojure.test function, it supplies the
purpose as the msg argument to those functions. Otherwise, the purpose just
acts like a comment and the forms are run unchanged."
[purpose & test-forms]
(let [tests (map
#(if (= (:ns (meta (resolve (first %))))
(the-ns 'glojure.test))
(concat % (list purpose))
%)
test-forms)]
`(do ~@tests)))

(defn run-cli-cmd [& args]
(let [bytes-to-string (fn [bytes]
(if (nil? bytes)
""
(apply str (map char (seq bytes)))))
cmd (apply os$exec.Command args)
[output err] (.CombinedOutput cmd)]
[(bytes-to-string output) (bytes-to-string err)]))
Comment on lines +19 to +26
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice!


(def glj
(let [[out err] (run-cli-cmd "find" "bin" "-name" "glj" "-executable")]
(if (and (seq out) (empty? err))
(first (str/split-lines out))
(throw (Exception. (str "Failed to find glj bin: " err))))))

(deftest e-flag-test
(test-that
"glj -e flag works correctly"
(let [[out err] (run-cli-cmd glj "-e" "(* 6 7)")]
(is (= out "42\n") "Command should output 42")
(is (empty? err) "Command should not return an error"))))

(deftest version-flag-test
(test-that
"glj --version flag works correctly"
(let [[out err] (run-cli-cmd glj "--version")]
(is (re-matches #"glojure v\d+\.\d+\.\d+\n" out)
"Command should output version")
(is (empty? err) "Command should not return an error"))))

(deftest glojure-version-test
(test-that
"*glojure-version* should be set correctly"
(let [[out err] (run-cli-cmd glj "-e" "*glojure-version*")]
(is (= out "{:major 0, :minor 3, :incremental 0, :qualifier nil}\n")
"Version should match expected format")
(is (empty? err) "Command should not return an error"))))

(deftest help-flag-test
(test-that
"glj --help flag works correctly"
(let [[out err] (run-cli-cmd glj "--help")]
(is (re-matches
#"(?s).*Glojure v0\.3\.0.*Usage: glj.*Options:.*-e.*-h.*--help.*--version.*Examples:.*"
out)
"Command should output help information")
(is (empty? err) "Command should not return an error"))))

(deftest short-help-flag-test
(test-that
"glj -h flag works correctly"
(let [[out err] (run-cli-cmd glj "-h")]
(is (re-matches
#"(?s).*Glojure v0\.3\.0.*Usage: glj.*Options:.*-e.*-h.*--help.*--version.*Examples:.*"
out)
"Command should output help information")
(is (empty? err) "Command should not return an error"))))

(run-tests)