Improved project structure

This commit is contained in:
2023-10-20 17:07:44 +02:00
parent 886ea27d54
commit 61af142930
18 changed files with 33 additions and 15 deletions

110
src/build/Build.go Normal file
View File

@ -0,0 +1,110 @@
package build
import (
"bufio"
"bytes"
"os"
"path/filepath"
"strings"
"git.akyoto.dev/cli/q/src/directory"
"git.akyoto.dev/cli/q/src/elf"
"git.akyoto.dev/cli/q/src/errors"
"git.akyoto.dev/cli/q/src/log"
)
// Build describes a compiler build.
type Build struct {
Name string
Directory string
Code bytes.Buffer
Data bytes.Buffer
WriteExecutable bool
}
// New creates a new build.
func New(directory string) *Build {
return &Build{
Name: filepath.Base(directory),
Directory: directory,
WriteExecutable: true,
}
}
// Run parses the input files and generates an executable file.
func (build *Build) Run() error {
err := build.Compile()
if err != nil {
return err
}
if build.WriteExecutable {
return writeToDisk(build.Executable(), build.Code.Bytes(), build.Data.Bytes())
}
return nil
}
// Compile compiles all the functions.
func (build *Build) Compile() error {
stat, err := os.Stat(build.Directory)
if err != nil {
return err
}
if !stat.IsDir() {
return &errors.InvalidDirectory{Path: build.Directory}
}
directory.Walk(build.Directory, func(file string) {
if !strings.HasSuffix(file, ".q") {
return
}
log.Info.Println(file)
})
build.Code.Write([]byte{
0xb8, 0x01, 0x00, 0x00, 0x00, // mov eax, 1
0xbf, 0x01, 0x00, 0x00, 0x00, // mov edi, 1
0xbe, 0xa2, 0x00, 0x40, 0x00, // mov esi, 0x4000a2
0xba, 0x06, 0x00, 0x00, 0x00, // mov edx, 6
0x0f, 0x05, // syscall
0xb8, 0x3c, 0x00, 0x00, 0x00, // mov eax, 60
0xbf, 0x00, 0x00, 0x00, 0x00, // mov edi, 0
0x0f, 0x05, // syscall
})
build.Data.Write([]byte{'H', 'e', 'l', 'l', 'o', '\n'})
return nil
}
// Executable returns the path to the executable.
func (build *Build) Executable() string {
return filepath.Join(build.Directory, build.Name)
}
// writeToDisk writes the executable file to disk.
func writeToDisk(filePath string, code []byte, data []byte) error {
file, err := os.Create(filePath)
if err != nil {
return err
}
buffer := bufio.NewWriter(file)
executable := elf.New(code, data)
executable.Write(buffer)
buffer.Flush()
err = file.Close()
if err != nil {
return err
}
return os.Chmod(filePath, 0755)
}

37
src/cli/Build.go Normal file
View File

@ -0,0 +1,37 @@
package cli
import (
"git.akyoto.dev/cli/q/src/build"
"git.akyoto.dev/cli/q/src/log"
)
// Build builds an executable.
func Build(args []string) int {
directory := "."
if len(args) > 0 {
directory = args[0]
}
b := build.New(directory)
for i := 1; i < len(args); i++ {
switch args[i] {
case "--dry":
b.WriteExecutable = false
default:
log.Error.Printf("Unknown parameter: %s\n", args[i])
return 2
}
}
err := b.Run()
if err != nil {
log.Error.Println(err)
return 1
}
return 0
}

14
src/cli/Help.go Normal file
View File

@ -0,0 +1,14 @@
package cli
import (
"git.akyoto.dev/cli/q/src/log"
)
// Help shows the command line argument usage.
func Help(args []string) int {
log.Error.Println("Usage: q [command] [options]")
log.Error.Println("")
log.Error.Println(" build [directory]")
log.Error.Println(" system")
return 2
}

21
src/cli/Main.go Normal file
View File

@ -0,0 +1,21 @@
package cli
// Main is the entry point for the CLI frontend.
// It returns the exit code of the compiler.
// We never call os.Exit directly here because it's bad for testing.
func Main(args []string) int {
if len(args) == 0 {
return Help(nil)
}
switch args[0] {
case "build":
return Build(args[1:])
case "system":
return System(args[1:])
default:
return Help(args[1:])
}
}

37
src/cli/System.go Normal file
View File

@ -0,0 +1,37 @@
package cli
import (
"os"
"runtime"
"git.akyoto.dev/cli/q/src/log"
)
// System shows system information.
func System(args []string) int {
line := "%-19s%s\n"
log.Info.Printf(line, "Platform:", runtime.GOOS)
log.Info.Printf(line, "Architecture:", runtime.GOARCH)
log.Info.Printf(line, "Go:", runtime.Version())
// Directory
directory, err := os.Getwd()
if err == nil {
log.Info.Printf(line, "Directory:", directory)
} else {
log.Info.Printf(line, "Directory:", err.Error())
}
// Compiler
executable, err := os.Executable()
if err == nil {
log.Info.Printf(line, "Compiler:", executable)
} else {
log.Info.Printf(line, "Compiler:", err.Error())
}
return 0
}

47
src/cli/cli_test.go Normal file
View File

@ -0,0 +1,47 @@
package cli_test
import (
"io"
"os"
"testing"
"git.akyoto.dev/cli/q/src/cli"
"git.akyoto.dev/cli/q/src/log"
"git.akyoto.dev/go/assert"
)
func TestMain(m *testing.M) {
log.Info.SetOutput(io.Discard)
log.Error.SetOutput(io.Discard)
os.Exit(m.Run())
}
func TestCLI(t *testing.T) {
type cliTest struct {
arguments []string
expectedExitCode int
}
tests := []cliTest{
{[]string{}, 2},
{[]string{"invalid"}, 2},
{[]string{"system"}, 0},
{[]string{"build", "non-existing-directory"}, 1},
{[]string{"build", "examples/hello/hello.q"}, 1},
{[]string{"build", "examples/hello", "--invalid"}, 2},
}
for _, test := range tests {
exitCode := cli.Main(test.arguments)
t.Log(test.arguments)
assert.Equal(t, exitCode, test.expectedExitCode)
}
}
func BenchmarkBuild(b *testing.B) {
args := []string{"build", "examples/hello", "--dry"}
for i := 0; i < b.N; i++ {
cli.Main(args)
}
}

61
src/directory/Walk.go Normal file
View File

@ -0,0 +1,61 @@
package directory
import (
"syscall"
"unsafe"
)
const blockSize = 8 << 10
// Walk calls your callback function for every file name inside the directory.
// It doesn't distinguish between files and directories.
func Walk(directory string, callBack func(string)) {
fd, err := syscall.Open(directory, 0, 0)
if err != nil {
panic(err)
}
defer syscall.Close(fd)
buffer := make([]byte, blockSize)
for {
n, err := syscall.ReadDirent(fd, buffer)
if err != nil {
panic(err)
}
if n <= 0 {
break
}
readBuffer := buffer[:n]
for len(readBuffer) > 0 {
dirent := (*syscall.Dirent)(unsafe.Pointer(&readBuffer[0]))
readBuffer = readBuffer[dirent.Reclen:]
// Skip deleted files
if dirent.Ino == 0 {
continue
}
// Skip hidden files
if dirent.Name[0] == '.' {
continue
}
for i, c := range dirent.Name {
if c != 0 {
continue
}
bytePointer := (*byte)(unsafe.Pointer(&dirent.Name[0]))
name := unsafe.String(bytePointer, i)
callBack(name)
break
}
}
}
}

69
src/elf/ELF.go Normal file
View File

@ -0,0 +1,69 @@
package elf
import (
"encoding/binary"
"io"
)
const (
minAddress = 0x10000
baseAddress = 0x40 * minAddress
)
// ELF represents an ELF file.
type ELF struct {
Header
ProgramHeader
Code []byte
Data []byte
}
// New creates a new ELF binary.
func New(code []byte, data []byte) *ELF {
elf := &ELF{
Header: Header{
Magic: [4]byte{0x7F, 'E', 'L', 'F'},
Class: 2,
Endianness: LittleEndian,
Version: 1,
OSABI: 0,
ABIVersion: 0,
Type: TypeExecutable,
Architecture: ArchitectureAMD64,
FileVersion: 1,
EntryPointInMemory: baseAddress + 0x80,
ProgramHeaderOffset: HeaderSize,
SectionHeaderOffset: 0,
Flags: 0,
Size: HeaderSize,
ProgramHeaderEntrySize: ProgramHeaderSize,
ProgramHeaderEntryCount: 1,
SectionHeaderEntrySize: SectionHeaderSize,
SectionHeaderEntryCount: 0,
SectionNameStringTableIndex: 0,
},
ProgramHeader: ProgramHeader{
Type: ProgramTypeLOAD,
Flags: ProgramFlagsExecutable,
Offset: 0x80,
VirtualAddress: baseAddress + 0x80,
PhysicalAddress: baseAddress + 0x80,
SizeInFile: int64(len(code)),
SizeInMemory: int64(len(code)),
Align: Align,
},
Code: code,
Data: data,
}
return elf
}
// Write writes the ELF64 format to the given writer.
func (elf *ELF) Write(writer io.Writer) {
binary.Write(writer, binary.LittleEndian, &elf.Header)
binary.Write(writer, binary.LittleEndian, &elf.ProgramHeader)
writer.Write([]byte{0, 0, 0, 0, 0, 0, 0, 0})
writer.Write(elf.Code)
writer.Write(elf.Data)
}

34
src/elf/Header.go Normal file
View File

@ -0,0 +1,34 @@
package elf
const (
LittleEndian = 1
TypeExecutable = 2
ArchitectureAMD64 = 0x3E
HeaderSize = 64
CacheLineSize = 64
Align = CacheLineSize
)
// Header contains general information.
type Header struct {
Magic [4]byte
Class byte
Endianness byte
Version byte
OSABI byte
ABIVersion byte
_ [7]byte
Type int16
Architecture int16
FileVersion int32
EntryPointInMemory int64
ProgramHeaderOffset int64
SectionHeaderOffset int64
Flags int32
Size int16
ProgramHeaderEntrySize int16
ProgramHeaderEntryCount int16
SectionHeaderEntrySize int16
SectionHeaderEntryCount int16
SectionNameStringTableIndex int16
}

37
src/elf/ProgramHeader.go Normal file
View File

@ -0,0 +1,37 @@
package elf
// ProgramHeaderSize is equal to the size of a program header in bytes.
const ProgramHeaderSize = 56
// ProgramHeader points to the executable part of our program.
type ProgramHeader struct {
Type ProgramType
Flags ProgramFlags
Offset int64
VirtualAddress int64
PhysicalAddress int64
SizeInFile int64
SizeInMemory int64
Align int64
}
type ProgramType int32
const (
ProgramTypeNULL ProgramType = 0
ProgramTypeLOAD ProgramType = 1
ProgramTypeDYNAMIC ProgramType = 2
ProgramTypeINTERP ProgramType = 3
ProgramTypeNOTE ProgramType = 4
ProgramTypeSHLIB ProgramType = 5
ProgramTypePHDR ProgramType = 6
ProgramTypeTLS ProgramType = 7
)
type ProgramFlags int32
const (
ProgramFlagsExecutable ProgramFlags = 0x1
ProgramFlagsWritable ProgramFlags = 0x2
ProgramFlagsReadable ProgramFlags = 0x4
)

45
src/elf/SectionHeader.go Normal file
View File

@ -0,0 +1,45 @@
package elf
// SectionHeaderSize is equal to the size of a section header in bytes.
const SectionHeaderSize = 64
// SectionHeader points to the data sections of our program.
type SectionHeader struct {
NameIndex int32
Type SectionType
Flags SectionFlags
VirtualAddress int64
Offset int64
SizeInFile int64
Link int32
Info int32
Align int64
EntrySize int64
}
type SectionType int32
const (
SectionTypeNULL SectionType = 0
SectionTypePROGBITS SectionType = 1
SectionTypeSYMTAB SectionType = 2
SectionTypeSTRTAB SectionType = 3
SectionTypeRELA SectionType = 4
SectionTypeHASH SectionType = 5
SectionTypeDYNAMIC SectionType = 6
SectionTypeNOTE SectionType = 7
SectionTypeNOBITS SectionType = 8
SectionTypeREL SectionType = 9
SectionTypeSHLIB SectionType = 10
SectionTypeDYNSYM SectionType = 11
)
type SectionFlags int64
const (
SectionFlagsWritable SectionFlags = 1 << 0
SectionFlagsAllocate SectionFlags = 1 << 1
SectionFlagsExecutable SectionFlags = 1 << 2
SectionFlagsStrings SectionFlags = 1 << 5
SectionFlagsTLS SectionFlags = 1 << 10
)

26
src/elf/elf.md Normal file
View File

@ -0,0 +1,26 @@
# ELF
## Basic structure
1. ELF header (0x00 - 0x40)
2. Program header (0x40 - 0x78)
3. Padding (0x78 - 0x80)
4. Machine code (0x80)
## Entry point
The entry point is defined in the first 64 bytes (ELF header).
## Base address
The minimum base address is controlled by the `mmap` settings:
```shell
sysctl vm.mmap_min_addr
```
Usually, this value is 65536 (0x1000).
## Initialization in Linux
See `/lib/modules/$(uname -r)/build/arch/x86/include/asm/elf.h`.

15
src/errors/InvalidPath.go Normal file
View File

@ -0,0 +1,15 @@
package errors
import "fmt"
type InvalidDirectory struct {
Path string
}
func (err *InvalidDirectory) Error() string {
if err.Path == "" {
return "Invalid directory"
}
return fmt.Sprintf("Invalid directory '%s'", err.Path)
}

14
src/log/log.go Normal file
View File

@ -0,0 +1,14 @@
package log
import (
"log"
"os"
)
var (
// Info is used for general info messages.
Info = log.New(os.Stdout, "", 0)
// Error is used for error messages.
Error = log.New(os.Stderr, "", 0)
)