You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
545 lines
12 KiB
545 lines
12 KiB
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"crypto/tls"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"os/user"
|
|
"path"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
var (
|
|
reWordStart *regexp.Regexp = regexp.MustCompile(`(?m)(?:^|\s):(?:$|\s)`)
|
|
reWordEnd *regexp.Regexp = regexp.MustCompile(`(?m)(?:^|\s);(?:$|\s)`)
|
|
reStringBoundary *regexp.Regexp = regexp.MustCompile(`(?:[^\\]|^)"`)
|
|
)
|
|
|
|
type stack struct {
|
|
stack []int
|
|
Pointer int
|
|
}
|
|
|
|
func NewStack(depth int) stack {
|
|
return stack{stack: make([]int, depth), Pointer: -1}
|
|
}
|
|
|
|
func (s *stack) Pop() int {
|
|
if s.Pointer < 0 {
|
|
panic("Stack underflow")
|
|
}
|
|
s.Pointer--
|
|
return s.stack[s.Pointer+1]
|
|
}
|
|
|
|
func (s *stack) Push(v int) {
|
|
if s.Pointer == stackDepth {
|
|
panic("Stack overflow")
|
|
}
|
|
s.Pointer++
|
|
s.stack[s.Pointer] = v
|
|
}
|
|
|
|
func (s *stack) Peep() int {
|
|
if s.Pointer < 0 || s.Pointer >= stackDepth {
|
|
panic("The stack is empty")
|
|
}
|
|
return s.stack[s.Pointer]
|
|
}
|
|
|
|
func nameExists(n string) bool {
|
|
if _, ok := names[n]; ok {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func bool2Int(f bool) int {
|
|
o := 0
|
|
if f {
|
|
o = ^o
|
|
}
|
|
return o
|
|
}
|
|
|
|
func recoverFromStackOverUnder() {
|
|
if r := recover(); r != nil {
|
|
data.Pointer, address.Pointer = -1, -1
|
|
handleError(fmt.Errorf("%v", r))
|
|
}
|
|
}
|
|
|
|
func unescapeString(s string) string {
|
|
s = strings.Replace(s, "\\0o", "\\0", -1)
|
|
var out strings.Builder
|
|
|
|
escapeNumBase := 10
|
|
|
|
var altNum bool
|
|
var otherNum strings.Builder
|
|
|
|
var slash bool
|
|
for _, c := range []rune(s) {
|
|
if slash && !altNum {
|
|
switch c {
|
|
case 't':
|
|
out.WriteRune('\t')
|
|
case 'n':
|
|
out.WriteRune('\n')
|
|
case 'r':
|
|
out.WriteRune('\r')
|
|
case 'v':
|
|
out.WriteRune('\v')
|
|
case 'e':
|
|
out.WriteRune(27)
|
|
case 'a':
|
|
out.WriteRune('\a')
|
|
case 'b':
|
|
out.WriteRune('\b')
|
|
case '\\':
|
|
out.WriteRune('\\')
|
|
case 'f':
|
|
out.WriteRune('\f')
|
|
case '_':
|
|
out.WriteRune(' ')
|
|
case '0':
|
|
escapeNumBase = 8
|
|
altNum = true
|
|
continue
|
|
case '1', '3', '4', '2', '5', '6', '7', '8', '9':
|
|
altNum = true
|
|
escapeNumBase = 10
|
|
otherNum.WriteRune(c)
|
|
continue
|
|
default:
|
|
out.WriteRune(c)
|
|
}
|
|
slash = false
|
|
} else if slash {
|
|
switch c {
|
|
case '0', '1', '3', '4', '2', '5', '6', '7', '8', '9':
|
|
otherNum.WriteRune(c)
|
|
case 'x':
|
|
if otherNum.String() == "" {
|
|
escapeNumBase = 16
|
|
continue
|
|
}
|
|
fallthrough
|
|
case 'A', 'B', 'C', 'D', 'E', 'F':
|
|
if escapeNumBase == 16 {
|
|
otherNum.WriteRune(c)
|
|
continue
|
|
}
|
|
fallthrough
|
|
default:
|
|
altNum = false
|
|
slash = false
|
|
|
|
if otherNum.Len() > 0 {
|
|
i, err := strconv.ParseInt(otherNum.String(), escapeNumBase, 64)
|
|
if err == nil {
|
|
out.WriteRune(rune(i))
|
|
} else {
|
|
out.WriteRune('?')
|
|
}
|
|
otherNum.Reset()
|
|
}
|
|
if c == '\\' {
|
|
slash = true
|
|
} else {
|
|
out.WriteRune(c)
|
|
}
|
|
}
|
|
} else if c == '\\' {
|
|
slash = true
|
|
} else {
|
|
out.WriteRune(c)
|
|
}
|
|
}
|
|
if otherNum.Len() > 0 {
|
|
i, err := strconv.ParseInt(otherNum.String(), escapeNumBase, 64)
|
|
if err == nil {
|
|
out.WriteRune(rune(i))
|
|
} else {
|
|
out.WriteRune('?')
|
|
}
|
|
}
|
|
return out.String()
|
|
}
|
|
|
|
func storeTempString(s string) {
|
|
memory.Set(50, len(s))
|
|
counter := 1
|
|
for _, c := range []rune(s) {
|
|
memory.Set(50+counter, int(c))
|
|
counter++
|
|
}
|
|
}
|
|
|
|
// If in string mode, adds to the tempstring
|
|
// and increments the tspointer as needed
|
|
// func addToStoredString(text string, pointer *int) {
|
|
// rslice := []rune(text)
|
|
// if *pointer > 52 && memory.Get(*pointer-1) != '\n' {
|
|
// memory.Set(*pointer, int(' '))
|
|
// *pointer++
|
|
// }
|
|
// for _, char := range rslice {
|
|
// memory.Set(*pointer, int(char))
|
|
// *pointer++
|
|
// }
|
|
// }
|
|
|
|
func storeNewString(name string) {
|
|
defer memory.Set(svarAssignmentFlag, 0)
|
|
_, err := strconv.Atoi(name)
|
|
if err == nil {
|
|
handleError(fmt.Errorf("Invalid svar name: %q", name))
|
|
return
|
|
}
|
|
if _, ok := names[name]; ok {
|
|
handleError(fmt.Errorf("Svar %q already exists", name))
|
|
return
|
|
}
|
|
names[name] = []token{token{t: strconv.Itoa(memory.NewSvarPointer()), line: -1, file: "runtime", tokType: intType}}
|
|
}
|
|
|
|
func nimfStringToGolang(address int) (string, error) {
|
|
length := memory.Get(address)
|
|
if length <= 0 {
|
|
return "", fmt.Errorf("Invalid string")
|
|
}
|
|
var text strings.Builder
|
|
for i := address + 1; i <= address+length; i++ {
|
|
text.WriteRune(rune(memory.Get(i)))
|
|
}
|
|
return text.String(), nil
|
|
|
|
}
|
|
|
|
func golangStringToNimf(text string) {
|
|
memory.Set(tempString, len(text))
|
|
memory.tspointer = 51
|
|
for _, c := range text {
|
|
if memory.tspointer > 29999 {
|
|
panic("Temporary string buffer overflow, string writing has been truncated")
|
|
}
|
|
memory.Set(memory.tspointer, int(c))
|
|
memory.tspointer++
|
|
}
|
|
}
|
|
|
|
func expandedAbsFilepath(p string) string {
|
|
if strings.HasPrefix(p, "~") {
|
|
if p == "~" || strings.HasPrefix(p, "~/") {
|
|
homedir, _ := os.UserHomeDir()
|
|
if len(p) <= 2 {
|
|
p = homedir
|
|
} else if len(p) > 2 {
|
|
p = filepath.Join(homedir, p[2:])
|
|
}
|
|
} else {
|
|
i := strings.IndexRune(p, '/')
|
|
var u string
|
|
var remainder string
|
|
if i < 0 {
|
|
u = p[1:]
|
|
remainder = ""
|
|
} else {
|
|
u = p[1:i]
|
|
remainder = p[i:]
|
|
}
|
|
usr, err := user.Lookup(u)
|
|
if err != nil {
|
|
p = filepath.Join("/home", u, remainder)
|
|
} else {
|
|
p = filepath.Join(usr.HomeDir, remainder)
|
|
}
|
|
}
|
|
} else if !strings.HasPrefix(p, "/") {
|
|
wd, _ := os.Getwd()
|
|
p = filepath.Join(wd, p)
|
|
}
|
|
|
|
path, _ := filepath.Abs(p)
|
|
return path
|
|
}
|
|
|
|
func cleanUpEarlyExit(remainingAction []token) {
|
|
thenSkip := 0
|
|
loopSkip := 0
|
|
for _, val := range remainingAction {
|
|
switch val.t.(string) {
|
|
case "if":
|
|
thenSkip += 1
|
|
case "do":
|
|
loopSkip += 1
|
|
case "then":
|
|
if thenSkip > 0 {
|
|
thenSkip -= 1
|
|
} else {
|
|
address.Pop()
|
|
memory.Set(conditionalDepthCounter, memory.Get(conditionalDepthCounter)-1)
|
|
priorRunState := address.Pop()
|
|
if memory.ValueEquals(conditionalDepthCounter, 0) {
|
|
memory.Set(conditionalBranchRunFlag, -1)
|
|
} else {
|
|
memory.Set(conditionalBranchRunFlag, priorRunState)
|
|
}
|
|
}
|
|
case "loop":
|
|
if loopSkip > 0 {
|
|
loopSkip -= 1
|
|
} else {
|
|
address.Pop()
|
|
memory.Set(loopSetFlag, 0) // Set the loop return flag
|
|
}
|
|
}
|
|
}
|
|
memory.Set(actionModeFlag, 0) // Turn off action mode
|
|
memory.Set(exitFlag, 0) // Turn the exit flag off
|
|
}
|
|
|
|
func cleanUpLocalMemory(start int) {
|
|
for i := start; i <= memory.lpointer; i++ {
|
|
memory.Set(memory.pointer+i, 0)
|
|
}
|
|
memory.lpointer = start
|
|
}
|
|
|
|
func readMoreInput(text string) bool {
|
|
wordStartCount := len(reWordStart.FindAllStringIndex(text, -1))
|
|
wordEndCount := len(reWordEnd.FindAllStringIndex(text, -1))
|
|
stringBoundaryCount := len(reStringBoundary.FindAllStringIndex(text, -1))
|
|
return (wordStartCount != wordEndCount || stringBoundaryCount%2 != 0)
|
|
}
|
|
|
|
func getReplInput() []token {
|
|
reader := bufio.NewReader(os.Stdin)
|
|
nl := ""
|
|
if memory.Get(outputPrintedFlag) == -1 {
|
|
nl = "\n"
|
|
memory.Set(outputPrintedFlag, 0)
|
|
}
|
|
fmt.Printf("%s\033[1;32m> \033[0m", nl)
|
|
text, err := reader.ReadString('\n')
|
|
if err != nil {
|
|
panic("Error reading line at repl")
|
|
}
|
|
for readMoreInput(text) {
|
|
fmt.Printf("%s\033[2;32m> \033[0m", nl)
|
|
ntext, err := reader.ReadString('\n')
|
|
if err != nil {
|
|
panic("Error reading line at repl")
|
|
}
|
|
text = text + ntext
|
|
}
|
|
return parse(text, true)
|
|
}
|
|
|
|
func handleParseError(e error, l int, f, tok string) {
|
|
errTemplate := "\033[91;1mError: \033[0m\033[1m%s\033[0m\n\nline : %d\nfile : %s\ntoken: %s\n\n"
|
|
fmt.Fprintf(os.Stderr, errTemplate, e.Error(), l, f, tok)
|
|
if interactive {
|
|
parseError = true
|
|
reset = true
|
|
data.Pointer, address.Pointer = -1, -1
|
|
memory.Set(3, 0) // string mode is off
|
|
memory.Set(4, 0) // string mode is off
|
|
memory.Set(8, 0) // branch depth is 0
|
|
memory.Set(9, 0) // loop depth is 0
|
|
memory.Set(11, 0) // variable setting is off
|
|
} else {
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func handleError(e error) {
|
|
line := -1
|
|
file := "error tracing file"
|
|
token := "error tracing token"
|
|
t, err := scanner.PeekLast()
|
|
if err != nil {
|
|
t, err = scanner.Read()
|
|
if err == nil {
|
|
scanner.Unread()
|
|
line = t.line
|
|
file = t.file
|
|
token = t.String()
|
|
}
|
|
} else {
|
|
line = t.line
|
|
file = t.file
|
|
token = t.String()
|
|
}
|
|
errTemplate := "\033[91;1mError: \033[0m\033[1m%s\033[0m\n\nline : %d\nfile : %s\ntoken: %s\n\n"
|
|
fmt.Fprintf(os.Stderr, errTemplate, e.Error(), line, file, token)
|
|
if interactive {
|
|
reset = true
|
|
data.Pointer, address.Pointer = -1, -1
|
|
memory.Set(3, 0) // string mode is off
|
|
memory.Set(4, 0) // string mode is off
|
|
memory.Set(8, 0) // branch depth is 0
|
|
memory.Set(9, 0) // loop depth is 0
|
|
memory.Set(11, 0) // variable setting is off
|
|
} else {
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func writeLib(name string, data []byte) {
|
|
if !strings.HasSuffix(name, ".nh") {
|
|
name = name + ".nh"
|
|
}
|
|
xdg_lib_path := os.Getenv("XDG_DATA_HOME")
|
|
if xdg_lib_path == "" {
|
|
xdg_lib_path = "~/.local/share/nimf/"
|
|
} else {
|
|
xdg_lib_path = filepath.Join(xdg_lib_path, "nimf")
|
|
}
|
|
_ = os.MkdirAll(xdg_lib_path, 0644)
|
|
modeFlag := os.O_WRONLY | os.O_CREATE | os.O_TRUNC
|
|
file, err := os.OpenFile(filepath.Join(xdg_lib_path, name), modeFlag, 0644)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Could not install file. Write failed. %s\n", err.Error())
|
|
os.Exit(1)
|
|
}
|
|
defer file.Close()
|
|
file.Write(data)
|
|
}
|
|
|
|
func getGemini(s string) []byte {
|
|
i := 1
|
|
conf := &tls.Config{
|
|
MinVersion: tls.VersionTLS12,
|
|
InsecureSkipVerify: true,
|
|
}
|
|
var data string
|
|
for i < 5 {
|
|
u, err := url.Parse(s)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Could not install file. Invalid URL. %s\n", err.Error())
|
|
os.Exit(1)
|
|
}
|
|
port := u.Port()
|
|
if port == "" {
|
|
port = "1965"
|
|
}
|
|
|
|
c, err := tls.Dial("tcp", u.Host+":"+port, conf)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Could not install file. Could not connect to server. %s\n", err.Error())
|
|
os.Exit(1)
|
|
}
|
|
|
|
defer c.Close()
|
|
send := u.String() + "\r\n"
|
|
_, err = c.Write([]byte(send))
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Could not install file. Could not write to server. %s\n", err.Error())
|
|
os.Exit(1)
|
|
}
|
|
result, err := ioutil.ReadAll(c)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Could not install file. Could not read from server. %s\n", err.Error())
|
|
os.Exit(1)
|
|
}
|
|
data = string(result)
|
|
endHeader := strings.Index(data, "\n")
|
|
|
|
if endHeader < 1 {
|
|
fmt.Fprintf(os.Stderr, "Could not install file. Invalid server response. %s\n", err.Error())
|
|
os.Exit(1)
|
|
}
|
|
|
|
if data[0] == '3' {
|
|
s = strings.TrimSpace(data[3:endHeader])
|
|
i++
|
|
continue
|
|
} else if data[0] != '2' {
|
|
fmt.Fprintf(os.Stderr, "Could not install file. Invalid server response. %s\n", err.Error())
|
|
os.Exit(1)
|
|
}
|
|
data = strings.TrimSpace(data[endHeader:])
|
|
break
|
|
}
|
|
return []byte(data)
|
|
}
|
|
|
|
func getGopher(s string) []byte {
|
|
u, err := url.Parse(s)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Could not install file. Invalid URL. %s\n", err.Error())
|
|
os.Exit(1)
|
|
}
|
|
port := u.Port()
|
|
if port == "" {
|
|
port = "70"
|
|
}
|
|
c, err := net.Dial("tcp", u.Host+":"+port)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Could not install file. Could not connect to server. %s\n", err.Error())
|
|
os.Exit(1)
|
|
}
|
|
defer c.Close()
|
|
c.Write([]byte(u.RawPath + "\r\n"))
|
|
data, err := io.ReadAll(c)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Could not install file. Download failed. %s\n", err.Error())
|
|
os.Exit(1)
|
|
}
|
|
return data
|
|
}
|
|
|
|
func getHTTP(s string) []byte {
|
|
resp, err := http.Get(s)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Could not install file. Download failed. %s\n", err.Error())
|
|
os.Exit(1)
|
|
}
|
|
defer resp.Body.Close()
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Could not install file. Download failed. %s\n", err.Error())
|
|
os.Exit(1)
|
|
}
|
|
return body
|
|
}
|
|
|
|
func getLocalFile(s string) []byte {
|
|
p := expandedAbsFilepath(s)
|
|
if _, err := os.Stat(p); errors.Is(err, os.ErrNotExist) {
|
|
fmt.Fprint(os.Stderr, "Could not install file. Path does not exist\n")
|
|
os.Exit(1)
|
|
}
|
|
data, err := os.ReadFile(p)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Could not install file. File read error: %s\n", err.Error())
|
|
os.Exit(1)
|
|
}
|
|
return data
|
|
}
|
|
|
|
func installModule(s string) {
|
|
lower := strings.ToLower(s)
|
|
if strings.HasPrefix(lower, "http://") || strings.HasPrefix(lower, "https://") {
|
|
writeLib(path.Base(s), getHTTP(s))
|
|
} else if strings.HasPrefix(lower, "gemini://") {
|
|
writeLib(path.Base(s), getGemini(s))
|
|
} else if strings.HasPrefix(lower, "gopher://") {
|
|
writeLib(path.Base(s), getGopher(s))
|
|
} else {
|
|
writeLib(path.Base(s), getLocalFile(s))
|
|
}
|
|
}
|