felise/main.go

246 lines
6.2 KiB
Go

/*
_
|, _ |. __ _
| (/,||_) (/,
Copyright (C) 2023 Brian Evans (aka sloum). All rights reserved.
This source code is available under the terms of the ffsl, or,
Floodgap Free Software License. A copy of the license has been
provided as the file 'LICENSE' in the same folder as this source
code file. If for some reason it is not present, you can find the
terms of version 1 of the FFSL at the following URL:
https://www.floodgap.com/software/ffsl/license.html
*/
package main
import (
_ "embed"
"flag"
"fmt"
"io"
"os"
"sort"
"strings"
ln "github.com/peterh/liner"
)
const (
version float64 = 0.46
logo string = ` _
|, _ |. __ _
| (/,||_) (/,`
)
var rawInput bool = false
var line *ln.State
var initialTerm ln.ModeApplier = nil
var linerTerm ln.ModeApplier = nil
var flagArgs []string
var completions = []string{
"var!", "set!", "scoped-var!", "scoped-set!", "proc", "proc!", "length",
"dup", "drop", "over", "swap", "cast", "type", "while", "return",
"dowhile", "if", "else", "and", "or", "stackdump", "clearstack",
"->", "<-", "=>", "->!", "=>!", "append", "append!", "list-get",
"list-set", "list-set!", "split", "join", "file-write", "file-remove",
"file-exists?", "file-read", "docstring!", "input", "re-match?",
"re-find", "re-find-all", "re-replace", "slice", "stackdepth", "net-get", "try", "throw",
"catch", "import", "rot", "each!", "filter!", "INT", "STRING", "FLOAT",
"LIST", "BOOL", "TYPE", "DICT", "words", "time", "char-conv", "end", "env-get",
"env-set", "exec", "<-exec", "<-exec->", "exec->", "cd", "pwd",
}
//go:embed lib/std.fe
var stdImport string
//go:embed lib/math.fe
var mathImport string
//go:embed lib/stack.fe
var stackImport string
//go:embed lib/paths.fe
var pathsImport string
//go:embed lib/strings.fe
var stringsImport string
func prompt(l *ln.State, cont bool) string {
var val string
var err error
if rawInput {
p := "\033[95m#\033[0m "
if cont {
p = "\033[94m+\033[0m "
}
fmt.Print(p)
bytesIn, err := io.ReadAll(os.Stdin)
if err != nil {
// TODO handle this better
panic(err)
}
val = string(bytesIn)
} else {
p := "> "
if cont {
p = "+ "
}
val, err = l.Prompt(p)
if err == ln.ErrPromptAborted {
return ""
}
l.AppendHistory(val)
}
return val
}
func VersionString() string {
return fmt.Sprintf("%.2f\n", version)
}
func loadStdLib(en *env) error {
err := lexParseInterpret(stdImport, en, "std")
if err != nil {
return err
}
en.imports["std"] = true
return nil
}
func repl() {
replmode := "\033[33mRunning in standard-repl mode:\n\tTo submit multiple lines of text, end a line that you want\n\tto continue with a \033[1m\\\033[0m\n\n"
if rawInput {
replmode = "\033[33mRunning in raw-repl mode:\n\tTo submit text for execution use \033[1m^D\033[0m\n\n"
}
fmt.Printf("%s\nfelise version %s(c) 2024 sloum, all rights reserved\nLicensed under the terms of the Floodgap Free Software License\n(`bye` or `exit` to quit)\n\n%s", logo, VersionString(), replmode)
if !rawInput {
initialTerm, _ = ln.TerminalMode()
line = ln.NewLiner()
linerTerm, _ = ln.TerminalMode()
defer line.Close()
line.SetCompleter(func(line string) (c []string) {
index := strings.LastIndexAny(line, " ")
for _, n := range completions {
if index < 0 && strings.HasPrefix(string(n), line) {
c = append(c, string(n))
} else if len(line) > index+1 && strings.HasPrefix(string(n), line[index+1:]) {
start := len(line) - index - 1
if start < 0 {
start = 0
}
c = append(c, line+string(n)[start:])
}
}
return
})
}
var text strings.Builder
var cont bool
baseEnv := NewEnv(nil)
err := loadStdLib(baseEnv)
if err != nil {
processInterpretError(err, "std")
}
for {
if linerTerm != nil {
linerTerm.ApplyMode()
}
in := prompt(line, cont)
if initialTerm != nil {
initialTerm.ApplyMode()
}
if s := strings.TrimSpace(in); s == "" {
continue
} else if s == "bye" || s == "exit" {
break
}
in = strings.TrimSpace(in)
if strings.HasSuffix(in, "\\") {
cont = true
text.WriteString(in[:len(in)-1])
text.WriteRune('\n')
} else {
cont = false
text.WriteRune(' ')
text.WriteString(in)
}
if !cont {
processInterpretError(lexParseInterpret(text.String(), baseEnv, ""), "")
text.Reset()
}
}
}
func lexParseInterpret(s string, en *env, file string) error {
lexedTokens, err := lex(s, file)
if err == nil {
r := NewTokenReader(lexedTokens)
var parsedTokens []token
parsedTokens, err = parse(r)
if err == nil {
err = interpret(parsedTokens, en)
}
}
sort.Strings(completions)
return err
}
func processInterpretError(err error, file string) {
if err != nil {
fmt.Fprintf(os.Stderr, "ERROR | %s\n", err.Error())
if file == "" {
globalStack.sp = 0
resetParseCounters()
} else {
os.Exit(1)
}
} else if file == "" {
fmt.Println("\n\033[0mOk.")
}
}
func main() {
verFlag := flag.Bool("v", false, "Print version information and exit")
flag.BoolVar(&debug, "debug", false, "Turns on debug mode")
flag.BoolVar(&rawInput, "r", false, "Use raw stdin input in repl. Will accept newline, ec. and requires ^D to submit input")
flag.Parse()
flagArgs = flag.Args()
if *verFlag {
fmt.Print(VersionString())
} else if len(flagArgs) > 0 {
p := ExpandedAbsFilepath(flagArgs[0])
s, err := readFile(p)
if err != nil {
fmt.Fprintf(os.Stderr, "felise could not read the given input file:\n `%s`\n", flagArgs[0])
os.Exit(1)
}
en := NewEnv(nil)
err = loadStdLib(en)
if err != nil {
processInterpretError(err, "std")
}
if len(flagArgs) > 1 {
t := make([]token, len(flagArgs)-1)
for i, s := range flagArgs[1:] {
t[i] = token{STRING, s, -1, "built-in"}
}
en.Add("sys-args", token{LIST, list{t}, -1, "built-in"})
} else {
t := make([]token, 0)
en.Add("sys-args", token{LIST, list{t}, -1, "built-in"})
}
processInterpretError(lexParseInterpret(s, en, flagArgs[0]), flagArgs[0])
} else {
repl()
}
}