From 031edd2ffe249b0057676f6e65660a1c5b076afd Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 7 Jul 2025 16:54:38 +0200 Subject: [PATCH] Added support for unix scripts --- docs/readme.md | 32 ++++++++++++++++++++++++++++++++ examples/script/script.q | 7 +++++++ src/cli/Exec.go | 10 +++++++++- src/cli/Exec_test.go | 5 +++-- src/scanner/scanFile.go | 1 + src/token/Kind.go | 1 + src/token/Tokenize.go | 3 +++ src/token/Tokenize_test.go | 27 +++++++++++++++++++++++++++ src/token/hash.go | 19 +++++++++++++++++++ 9 files changed, 102 insertions(+), 3 deletions(-) create mode 100755 examples/script/script.q create mode 100644 src/token/hash.go diff --git a/docs/readme.md b/docs/readme.md index a0da0fb..a8e03ba 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -13,6 +13,7 @@ * High performance (`ssa` and `asm` optimizations) * Tiny executables ("Hello World" is ~500 bytes) * Fast compilation (5-10x faster than most) +* Unix scripting (JIT compilation) * No dependencies (no llvm, no libc) ## Installation @@ -25,10 +26,41 @@ go build ## Usage +Quick test: + ```shell q run examples/hello ``` +Build an executable: + +```shell +q build examples/hello +``` + +Cross-compile for another OS: + +```shell +q build examples/hello --os windows +``` + +### Unix scripts + +The compiler is actually so fast that it's possible to use `q` for scripting. Create a new file: + +```q +#!/usr/bin/env q + +import io + +main() { + io.write("Hello\n") +} +``` + +Add permissions via `chmod +x`. The file can be executed from anywhere now. +The machine code is run directly from memory if the OS supports it. + ## Tests ```shell diff --git a/examples/script/script.q b/examples/script/script.q new file mode 100755 index 0000000..20a64a7 --- /dev/null +++ b/examples/script/script.q @@ -0,0 +1,7 @@ +#!/usr/bin/env q + +import io + +main() { + io.write("Hello\n") +} \ No newline at end of file diff --git a/src/cli/Exec.go b/src/cli/Exec.go index 3230998..328743a 100644 --- a/src/cli/Exec.go +++ b/src/cli/Exec.go @@ -1,5 +1,7 @@ package cli +import "os" + // Exec runs the command included in the first argument and returns the exit code. func Exec(args []string) int { if len(args) == 0 { @@ -17,6 +19,12 @@ func Exec(args []string) int { return help() default: - return invalid() + _, err := os.Stat(args[0]) + + if err != nil { + invalid() + } + + return run(args) } } \ No newline at end of file diff --git a/src/cli/Exec_test.go b/src/cli/Exec_test.go index 9f44c60..866ca09 100644 --- a/src/cli/Exec_test.go +++ b/src/cli/Exec_test.go @@ -16,18 +16,19 @@ func TestExec(t *testing.T) { assert.Equal(t, cli.Exec([]string{"build", "../../examples/hello", "--dry", "--os", "mac", "--arch", "x86"}), 0) assert.Equal(t, cli.Exec([]string{"build", "../../examples/hello", "--dry", "--os", "windows", "--arch", "arm"}), 0) assert.Equal(t, cli.Exec([]string{"build", "../../examples/hello", "--dry", "--os", "windows", "--arch", "x86"}), 0) - assert.Equal(t, cli.Exec([]string{"run", "../../examples/hello"}), 0) assert.Equal(t, cli.Exec([]string{"help"}), 0) + assert.Equal(t, cli.Exec([]string{"run", "../../examples/hello"}), 0) + assert.Equal(t, cli.Exec([]string{"../../examples/script/script.q"}), 0) } func TestExecErrors(t *testing.T) { assert.Equal(t, cli.Exec([]string{"build"}), 1) assert.Equal(t, cli.Exec([]string{"run"}), 1) + assert.Equal(t, cli.Exec([]string{"_"}), 1) } func TestExecWrongParameters(t *testing.T) { assert.Equal(t, cli.Exec(nil), 2) - assert.Equal(t, cli.Exec([]string{"_"}), 2) assert.Equal(t, cli.Exec([]string{"build", "--invalid-parameter"}), 2) assert.Equal(t, cli.Exec([]string{"build", "../../examples/hello", "--invalid-parameter"}), 2) assert.Equal(t, cli.Exec([]string{"build", "../../examples/hello", "--os"}), 2) diff --git a/src/scanner/scanFile.go b/src/scanner/scanFile.go index d324ea7..042d684 100644 --- a/src/scanner/scanFile.go +++ b/src/scanner/scanFile.go @@ -41,6 +41,7 @@ func (s *scanner) scanFile(path string, pkg string) error { return nil case token.Invalid: return errors.New(&InvalidCharacter{Character: tokens[i].String(file.Bytes)}, file, tokens[i].Position) + case token.Script: default: return errors.New(&InvalidTopLevel{Instruction: tokens[i].String(file.Bytes)}, file, tokens[i].Position) } diff --git a/src/token/Kind.go b/src/token/Kind.go index b4a8dbd..0f296b1 100644 --- a/src/token/Kind.go +++ b/src/token/Kind.go @@ -12,6 +12,7 @@ const ( Rune // Rune is a single unicode code point. String // String is an uninterpreted series of characters in the source code. Comment // Comment is a comment. + Script // Script is a shebang line. GroupStart // ( GroupEnd // ) BlockStart // { diff --git a/src/token/Tokenize.go b/src/token/Tokenize.go index e9bd066..3e6b331 100644 --- a/src/token/Tokenize.go +++ b/src/token/Tokenize.go @@ -37,6 +37,9 @@ func Tokenize(buffer []byte) List { case '0': tokens, i = zero(tokens, buffer, i) continue + case '#': + tokens, i = hash(tokens, buffer, i) + continue default: if isIdentifierStart(buffer[i]) { tokens, i = identifier(tokens, buffer, i) diff --git a/src/token/Tokenize_test.go b/src/token/Tokenize_test.go index cf5414c..a6ea39f 100644 --- a/src/token/Tokenize_test.go +++ b/src/token/Tokenize_test.go @@ -480,6 +480,20 @@ func TestComment(t *testing.T) { } func TestInvalid(t *testing.T) { + tokens := token.Tokenize([]byte(`@@`)) + + expected := []token.Kind{ + token.Invalid, + token.Invalid, + token.EOF, + } + + for i, kind := range expected { + assert.Equal(t, tokens[i].Kind, kind) + } +} + +func TestInvalidScript(t *testing.T) { tokens := token.Tokenize([]byte(`##`)) expected := []token.Kind{ @@ -570,6 +584,19 @@ func TestRune(t *testing.T) { token.EOF, } + for i, kind := range expected { + assert.Equal(t, tokens[i].Kind, kind) + } +} + +func TestScript(t *testing.T) { + tokens := token.Tokenize([]byte("#!/usr/bin/env q")) + + expected := []token.Kind{ + token.Script, + token.EOF, + } + for i, kind := range expected { assert.Equal(t, tokens[i].Kind, kind) } diff --git a/src/token/hash.go b/src/token/hash.go new file mode 100644 index 0000000..4bea82e --- /dev/null +++ b/src/token/hash.go @@ -0,0 +1,19 @@ +package token + +// hash handles all tokens starting with '#'. +func hash(tokens List, buffer []byte, i Position) (List, Position) { + if i+1 < Position(len(buffer)) && buffer[i+1] == '!' { + position := i + + for i < Position(len(buffer)) && buffer[i] != '\n' { + i++ + } + + tokens = append(tokens, Token{Kind: Script, Position: position, Length: Length(i - position)}) + } else { + tokens = append(tokens, Token{Kind: Invalid, Position: i, Length: 1}) + i++ + } + + return tokens, i +} \ No newline at end of file