Added support for unix scripts
All checks were successful
/ test (push) Successful in 29s

This commit is contained in:
Eduard Urbach 2025-07-07 16:54:38 +02:00
parent f6eb30e460
commit 031edd2ffe
Signed by: akyoto
GPG key ID: 49226B848C78F6C8
9 changed files with 102 additions and 3 deletions

View file

@ -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

7
examples/script/script.q Executable file
View file

@ -0,0 +1,7 @@
#!/usr/bin/env q
import io
main() {
io.write("Hello\n")
}

View file

@ -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)
}
}

View file

@ -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)

View file

@ -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)
}

View file

@ -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 // {

View file

@ -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)

View file

@ -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)
}

19
src/token/hash.go Normal file
View file

@ -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
}