anglais/cli/main.go
2026-07-13 23:34:19 +02:00

329 lines
6.2 KiB
Go

package main
import (
"bufio"
"errors"
"log"
"os"
"path"
"path/filepath"
"strings"
"github.com/alecthomas/kong"
"neemek.com/anglais/core"
)
type Context struct {
Debug bool
}
type RunCmd struct {
IgnoreWarnings bool `name:"ignore-warnings" help:"Ignore warning messages"`
Bytecode bool `name:"bytecode" short:"c" help:"Run file as if it's bytecode"`
File string `arg:"" name:"file" help:"File to read program from" type:"existingfile"`
}
// DirectoryResolver resolves imports from several directories
type DirectoryResolver struct {
// directories to search through, in decreasing priority.
directories []string
}
func (r *DirectoryResolver) Resolve(from, path string) (*core.ImportResult, error) {
// relative imports
relpath := filepath.Join(filepath.Dir(from), path)
if f, err := os.ReadFile(relpath); err == nil {
return &core.ImportResult{
Source: string(f),
Path: relpath,
}, nil
}
for _, d := range r.directories {
pth := filepath.Join(d, path)
f, err := os.ReadFile(pth)
if err == nil {
return &core.ImportResult{
Source: string(f),
Path: pth,
}, nil
}
pth = filepath.Join(pth, "main.ang")
f, err = os.ReadFile(pth)
if err == nil {
return &core.ImportResult{
Source: string(f),
Path: pth,
}, nil
}
}
return nil, errors.New("couldn't resolve import")
}
func (r *DirectoryResolver) IsSame(a, b string) bool {
apath := filepath.Clean(a)
bpath := filepath.Clean(b)
return apath == bpath
}
func makeChunk(ctx *Context, fpath string, ignoreWarnings bool) (*core.Chunk, error) {
if ctx.Debug {
log.Println("Reading file")
}
f, err := os.ReadFile(fpath)
if err != nil {
return nil, err
}
src := string(f)
if ctx.Debug {
log.Println("Initializing lexer")
}
l := core.NewLexer(src)
if ctx.Debug {
log.Println("Lexing all tokens")
}
tokens, err := l.Tokenize()
if err != nil {
return nil, err
}
if len(tokens) <= 1 {
return nil, errors.New("empty file")
}
if ctx.Debug {
log.Printf("Lexed %d tokens", len(tokens))
}
p := core.NewParser(src, []string{fpath}, tokens)
if ctx.Debug {
log.Println("Initialized parser")
}
pathat, err := filepath.Abs(fpath)
if err != nil {
return nil, err
}
tree, err := p.Parse(pathat)
if ctx.Debug {
log.Printf("Parsed tree, meaning:\n%s", tree)
}
// if there were parsing errors, print them out
if err != nil {
print(err.(core.ParsingError).Format())
log.Fatal("Parsing had errors")
}
if ctx.Debug {
log.Println("Initialized compiler")
}
c := core.NewCompiler([]rune(src))
if ctx.Debug {
log.Println("Setting imports resolver")
}
lib := os.Getenv("PATH")
dirs := strings.Split(lib, ":")
dir, _ := path.Split(fpath)
c.SetImportsResolver(&DirectoryResolver{
append([]string{
dir,
}, dirs...),
})
if ctx.Debug {
log.Println("Compiling parse tree")
}
_, err = c.Compile(tree)
if err != nil {
var e core.FormatedError
if errors.As(err, &e) {
log.Fatal(e.Format())
}
log.Fatal(err)
}
// if there were non-critical warnings, report them
if len(c.Warnings) != 0 {
for _, warning := range c.Warnings {
log.Println(warning.Format() + "\n")
}
if !ignoreWarnings {
return nil, errors.New("compiler reported warning(s) (run anyways with --ignore-warnings)")
} else {
log.Println("compiler reported warning(s), but running anyways")
}
}
return c.Chunk, nil
}
func (cmd *RunCmd) Run(ctx *Context) error {
var chunk *core.Chunk
if !cmd.Bytecode {
c, err := makeChunk(ctx, cmd.File, cmd.IgnoreWarnings)
if err != nil {
return err
}
chunk = c
} else {
if ctx.Debug {
log.Println("Reading file")
}
f, err := os.ReadFile(cmd.File)
if err != nil {
return err
}
if ctx.Debug {
log.Println("Registering GOB types")
}
core.RegisterGOBTypes()
if ctx.Debug {
log.Println("Deserializing file")
}
chunk = core.DeserializeChunk(f)
}
if ctx.Debug {
log.Println("Printing chunk")
print(chunk.String())
log.Println("Initialized VM")
}
vm := core.NewVM(chunk, 256, 256)
if ctx.Debug {
log.Println("Executing bytecode")
log.Println("=v= output =v=")
}
// execute order 66
for vm.Next() {
}
return nil
}
type CompileCmd struct {
File string `arg:"" name:"file" help:"File to compile program from" type:"existingfile"`
Output string `arg:"" name:"output" help:"File path to output bytecode to" type:"path"`
IgnoreWarnings bool `name:"ignore-warnings" help:"Ignore warning messages"`
}
func (cmd *CompileCmd) Run(ctx *Context) error {
c, err := makeChunk(ctx, cmd.File, cmd.IgnoreWarnings)
if err != nil {
return err
}
if ctx.Debug {
log.Println("Registering GOB types")
}
core.RegisterGOBTypes()
if ctx.Debug {
log.Println("Serializing chunk")
}
serialized := c.Serialize()
if ctx.Debug {
log.Println("Writing file")
}
err = os.WriteFile(cmd.Output, serialized, 0666)
if err != nil {
return err
}
return nil
}
type ReplCmd struct {
}
func (cmd *ReplCmd) Run(ctx *Context) error {
c := core.NewCompiler([]rune{})
vm := core.NewVM(core.NewChunk([]core.Bytecode{}, []core.Value{}), 256, 256)
reader := bufio.NewReader(os.Stdin)
for {
print("> ")
src, err := reader.ReadString('\n')
if err != nil {
return err
}
l := core.NewLexer(src)
tokens, err := l.Tokenize()
if err != nil {
log.Println(err)
continue
}
p := core.NewParser(src, []string{}, tokens)
prog, err := p.Parse("REPL")
if err != nil {
var e core.FormatedError
if errors.As(err, &e) {
log.Print(e.Format())
}
continue
}
c.SetSource(src)
if _, err = c.Compile(prog); err != nil {
var e core.FormatedError
if errors.As(err, &e) {
log.Print(e.Format())
}
continue
}
vm.SetChunk(c.Chunk)
for vm.Next() {
}
println(vm.Stack.Pop().DebugString())
}
}
var cli struct {
Debug bool `short:"D" name:"debug" help:"Enable debug mode."`
Run RunCmd `cmd:"" name:"run" help:"Run program."`
Compile CompileCmd `cmd:"" name:"compile" help:"Compile program to bytecode."`
Repl ReplCmd `cmd:"" name:"repl" help:"Start a REPL loop."`
}
func main() {
ctx := kong.Parse(&cli)
// Call the Run() method of the selected parsed command.
err := ctx.Run(&Context{Debug: cli.Debug})
ctx.FatalIfErrorf(err)
}