felise/keywords.go

1749 lines
44 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 (
"bufio"
"fmt"
"io"
"math"
"net"
"net/http"
"net/url"
"os"
"os/exec"
"regexp"
"strconv"
"strings"
"syscall"
"time"
)
/*
TODO Add the following?
- cd, mkdir
- Update `file-remove` to also work on directories? Or add separate `rm` that can optionally be recursive?
- map
*/
func callKeyword(kw token, r *tokenReader, en *env) error {
switch kw.val.(string) {
case "+":
return libAdd(kw.line, kw.file)
case "-":
return libSubtract(kw.line, kw.file)
case "*":
return libMultiply(kw.line, kw.file)
case "/":
return libDivide(kw.line, kw.file)
case "var!":
return libVar(kw.line, r, en, kw.file)
case "scoped-var!":
return libVarPlus(kw.line, r, en, kw.file)
case "set!":
return libSet(kw.line, r, en, kw.file)
case "scoped-set!":
return libSetPlus(kw.line, r, en, kw.file)
case "dup":
return libDuplicate(kw.line, kw.file)
case "swap":
return libSwap(kw.line, kw.file)
case "over":
return libOver(kw.line, kw.file)
case "rot":
return libRot(kw.line, kw.file)
case "drop":
return libDrop(kw.line, kw.file)
case "cast":
return libConvertType(kw.line, kw.file)
case "and":
return libLogicalAnd(kw.line, kw.file)
case "or":
return libLogicalOr(kw.line, kw.file)
case "=":
return libEqual(kw.line, kw.file)
case ">":
return libGreaterThan(kw.line, kw.file)
case "<":
return libLessThan(kw.line, kw.file)
case "stackdump":
return libStackDump()
case "clearstack":
return libClearStack()
case "stackdepth":
return libStackDepth(kw.line, kw.file)
case "length":
return libLength(kw.line, kw.file)
case "append!", "=>!":
return libAppendSpecial(kw.line, r, en, kw.file)
case "append", "=>":
return libAppend(kw.line, kw.file)
case "list-get", "<-":
return libGetListItem(kw.line, r, en, kw.file)
case "list-set!", "->!":
return libSetListItemSpecial(kw.line, r, en, kw.file)
case "list-set", "->":
return libSetListItem(kw.line, kw.file)
case "file-append":
return libFileAppend(kw.line, kw.file, true)
case "file-exists?":
return libFileExists(kw.line, kw.file)
case "file-read":
return libFileRead(kw.line, kw.file)
case "file-write":
return libFileAppend(kw.line, kw.file, false)
case "docstring!":
return libDocstring(kw.line, r, en, kw.file)
case "input":
return libInput(kw.line, kw.file)
case "re-match?":
return libReMatch(kw.line, kw.file)
case "re-find":
return libReFind(kw.line, kw.file, false)
case "re-find-all":
return libReFind(kw.line, kw.file, true)
case "re-replace":
return libReReplace(kw.line, kw.file)
case "slice":
return libSlice(kw.line, kw.file)
case "type":
return libType(kw.line, kw.file)
case "net-get":
return libNetGet(kw.line, kw.file)
case "throw":
return libThrow(kw.line, kw.file)
case "import":
return libImport(kw.line, kw.file, en)
case "each!":
return libEach(kw.line, r, en, kw.file)
case "filter!":
return libFilter(kw.line, r, en, kw.file)
case "words":
return libWords(kw.line, kw.file)
case "time":
return libTime(kw.line, kw.file)
case "char-conv":
return libToChar(kw.line, kw.file)
case "env-get":
return libEnvGet(kw.line, kw.file)
case "env-set":
return libEnvSet(kw.line, kw.file)
case "exec":
return libSubprocess(kw.line, kw.file, false, false)
case "<-exec":
return libSubprocess(kw.line, kw.file, true, false)
case "exec->":
return libSubprocess(kw.line, kw.file, false, true)
case "<-exec->":
return libSubprocess(kw.line, kw.file, true, true)
case "cd":
return libChangeDir(kw.line, kw.file)
case "pwd":
return libPresentDir(kw.line, kw.file)
default:
return fmt.Errorf("Unknown keyword: `%s`", kw.line, kw.val.(string))
}
}
func libWords(line int, fp string) error {
return globalStack.Push(token{STRING, strings.Join(completions, " "), line, fp})
}
func libAdd(line int, fp string) error {
v2, err := globalStack.Pop()
if err != nil {
return err
}
v1, err := globalStack.Pop()
if err != nil {
return err
}
if v1.kind == STRING && v2.kind == STRING {
// String join
err = globalStack.Push(token{STRING, v1.val.(string) + v2.val.(string), line, fp})
} else if v1.kind == LIST && v2.kind == LIST {
// List join
l1 := v1.val.(list)
l2 := v2.val.(list)
nl := make([]token, len(l1.body)+len(l2.body))
for i, lt := range l1.body {
nl[i] = lt
}
for i, lt := range l2.body {
nl[i+len(l1.body)] = lt
}
err = globalStack.Push(token{LIST, list{nl}, line, fp})
} else if v1.kind == STRING && v2.kind == INT {
i := v2.val.(int)
if i < 0 {
return fmt.Errorf("Adding a rune to a string is only valid with positive integers, got %d", i)
}
s := v1.val.(string)
s += string(rune(i))
err = globalStack.Push(token{STRING, s, line, fp})
} else if v1.kind == INT && v2.kind == INT {
err = globalStack.Push(token{INT, v1.val.(int) + v2.val.(int), line, fp})
} else if v1.kind == FLOAT && v2.kind == FLOAT {
err = globalStack.Push(token{FLOAT, v1.val.(float64) + v2.val.(float64), line, fp})
} else if v1.kind == FLOAT && v2.kind == INT {
err = globalStack.Push(token{FLOAT, v1.val.(float64) + float64(v2.val.(int)), line, fp})
} else if v1.kind == INT && v2.kind == FLOAT {
err = globalStack.Push(token{FLOAT, v2.val.(float64) + float64(v1.val.(int)), line, fp})
} else if v1.kind == STRING {
err = globalStack.Push(token{STRING, v1.val.(string) + toString(v2, false), line, fp})
} else {
fmt.Println("STACK-DUMP: ", globalStack)
return fmt.Errorf("Cannot `+` %s and %s", kindToString(v1.kind), kindToString(v2.kind))
}
if err != nil {
return err
}
return nil
}
func libSubtract(line int, fp string) error {
v2, err := globalStack.Pop()
if err != nil {
return err
}
v1, err := globalStack.Pop()
if err != nil {
return err
}
if v1.kind == STRING {
s := strings.Replace(v1.val.(string), toString(v2, false), "", -1)
err = globalStack.Push(token{STRING, s, line, fp})
} else if v1.kind == INT && v2.kind == INT {
err = globalStack.Push(token{INT, v1.val.(int) - v2.val.(int), line, fp})
} else if v1.kind == FLOAT && v2.kind == FLOAT {
err = globalStack.Push(token{FLOAT, v1.val.(float64) - v2.val.(float64), line, fp})
} else if v1.kind == FLOAT && v2.kind == INT {
err = globalStack.Push(token{FLOAT, v1.val.(float64) - float64(v2.val.(int)), line, fp})
} else if v1.kind == INT && v2.kind == FLOAT {
err = globalStack.Push(token{FLOAT, float64(v1.val.(int)) - v2.val.(float64), line, fp})
} else if v1.kind == DICT && v2.kind == STRING {
// NOTE This works on the memory location, so acts as -!,
// which isn't exactly what we want I dont think? We'd
// want a separate way to do that, right? Compare to
// how this works with a list
delete(v1.val.(dict).body, v2.val.(string))
err = globalStack.Push(v1)
} else if v1.kind == LIST {
l := v1.val.(list).body
nl := make([]token, 0, len(l))
for i := range l {
if l[i].kind != v2.kind {
nl = append(nl, l[i])
} else if v2.kind == LIST {
l1 := l[i].val.(list)
l2 := v2.val.(list)
if l1.String() != l2.String() {
nl = append(nl, l[i])
}
} else if v2 != l[i] {
nl = append(nl, l[i])
}
}
err = globalStack.Push(token{LIST, list{nl}, line, fp})
} else {
return fmt.Errorf("Cannot `-` %s and %s", kindToString(v1.kind), kindToString(v2.kind))
}
if err != nil {
return err
}
return nil
}
func libMultiply(line int, fp string) error {
v2, err := globalStack.Pop()
if err != nil {
return err
}
v1, err := globalStack.Pop()
if err != nil {
return err
}
if v1.kind == STRING && v2.kind == INT {
if v2.val.(int) < 0 {
err = globalStack.Push(token{STRING, "", line, fp})
} else {
s := strings.Repeat(v1.val.(string), v2.val.(int))
err = globalStack.Push(token{STRING, s, line, fp})
}
} else if v1.kind == STRING && v2.kind == FLOAT {
if v2.val.(float64) < 0.0 {
err = globalStack.Push(token{STRING, "", line, fp})
} else {
s := strings.Repeat(v1.val.(string), int(v2.val.(float64)))
err = globalStack.Push(token{STRING, s, line, fp})
}
} else if v1.kind == LIST && v2.kind == STRING {
l := v1.val.(list)
s := l.Join(v2.val.(string))
err = globalStack.Push(token{STRING, s, line, fp})
} else if v1.kind == INT && v2.kind == INT {
err = globalStack.Push(token{INT, v1.val.(int) * v2.val.(int), line, fp})
} else if v1.kind == FLOAT && v2.kind == FLOAT {
err = globalStack.Push(token{FLOAT, v1.val.(float64) * v2.val.(float64), line, fp})
} else if v1.kind == FLOAT && v2.kind == INT {
err = globalStack.Push(token{FLOAT, v1.val.(float64) * float64(v2.val.(int)), line, fp})
} else if v1.kind == INT && v2.kind == FLOAT {
err = globalStack.Push(token{FLOAT, v2.val.(float64) * float64(v1.val.(int)), line, fp})
} else {
return fmt.Errorf("Cannot `*` %s and %s", kindToString(v1.kind), kindToString(v2.kind))
}
if err != nil {
return err
}
return nil
}
func libDivide(line int, fp string) error {
v2, err := globalStack.Pop()
if err != nil {
return err
}
v1, err := globalStack.Pop()
if err != nil {
return err
}
if v1.kind == STRING && v2.kind == STRING {
l := strings.SplitN(v1.val.(string), v2.val.(string), -1)
ts := make([]token, len(l))
for i := range l {
ts[i] = token{STRING, l[i], line, fp}
}
err = globalStack.Push(token{LIST, list{ts}, line, fp})
} else if v1.kind == INT && v2.kind == INT {
if v2.val.(int) == 0 {
return fmt.Errorf("Division by zero")
}
err = globalStack.Push(token{INT, v1.val.(int) / v2.val.(int), line, fp})
} else if v1.kind == FLOAT && v2.kind == FLOAT {
if v2.val.(float64) == 0.0 {
return fmt.Errorf("Division by zero")
}
err = globalStack.Push(token{FLOAT, v1.val.(float64) / v2.val.(float64), line, fp})
} else if v1.kind == FLOAT && v2.kind == INT {
if v2.val.(int) == 0 {
return fmt.Errorf("Division by zero")
}
err = globalStack.Push(token{FLOAT, v1.val.(float64) / float64(v2.val.(int)), line, fp})
} else if v1.kind == INT && v2.kind == FLOAT {
if v2.val.(float64) == 0.0 {
return fmt.Errorf("Division by zero")
}
err = globalStack.Push(token{FLOAT, float64(v1.val.(int)) / v2.val.(float64), line, fp})
} else {
return fmt.Errorf("Cannot `/` %s and %s", kindToString(v1.kind), kindToString(v2.kind))
}
if err != nil {
return err
}
return nil
}
func libAppend(line int, fp string) error {
valueToken, err := globalStack.Pop()
if err != nil {
return err
}
targetToken, err := globalStack.Pop()
if err != nil {
return err
}
switch targetToken.kind {
case LIST:
l := targetToken.val.(list)
err = l.Append(valueToken)
if err != nil {
return err
}
err = globalStack.Push(token{LIST, l, line, fp})
if err != nil {
return err
}
case STRING:
s := targetToken.val.(string)
s = s + toString(valueToken, false)
err = globalStack.Push(token{STRING, s, line, fp})
if err != nil {
return err
}
default:
return fmt.Errorf("Cannot `append` %s to %s", kindToString(valueToken.kind), kindToString(targetToken.kind))
}
return nil
}
func libAppendSpecial(line int, r *tokenReader, en *env, fp string) error {
valueToken, err := globalStack.Pop()
if err != nil {
return err
}
t, err := r.Read()
if err != nil {
return fmt.Errorf("Unexpectedly reached EOF")
}
var fromVariable bool
var targetToken token = t
var targetTokenenv *env
if t.kind == SYMBOL {
targetTokenenv, err = en.Find(t.val.(string))
if err != nil {
return err
}
targetToken = targetTokenenv.vars[t.val.(string)]
fromVariable = true
}
switch targetToken.kind {
case LIST:
l := targetToken.val.(list)
err = l.Append(valueToken)
if err != nil {
return err
}
if fromVariable {
targetTokenenv.vars[t.val.(string)] = token{LIST, l, line, fp}
} else {
err := globalStack.Push(token{LIST, l, targetToken.line, targetToken.file})
if err != nil {
return err
}
}
case STRING:
s := targetToken.val.(string) + toString(valueToken, false)
if fromVariable {
targetTokenenv.vars[t.val.(string)] = token{STRING, s, targetToken.line, targetToken.file}
} else {
targetToken.val = s
err := globalStack.Push(token{STRING, s, targetToken.line, targetToken.file})
if err != nil {
return err
}
}
default:
return fmt.Errorf("`=>` cannot append to %s", kindToString(targetToken.kind))
}
return nil
}
func libSetListItem(line int, fp string) error {
valueToken, err := globalStack.Pop()
if err != nil {
return err
}
indexToken, err := globalStack.Pop()
if err != nil {
return err
}
listToken, err := globalStack.Pop()
if err != nil {
return err
}
if listToken.kind == LIST && indexToken.kind != INT {
return fmt.Errorf("`list-set`/`->` expected a value, INT index, and LIST but found %s instead of INT", kindToString(indexToken.kind))
} else if listToken.kind == DICT && indexToken.kind != STRING {
return fmt.Errorf("`list-set`/`->` expected a value, STRING key, and DICT but found %s instead of STRING", kindToString(indexToken.kind))
} else if listToken.kind != LIST && listToken.kind != DICT {
return fmt.Errorf("`list-set`/`->` expected a value, an index or key, and a LIST or DICT but found %s instead of LIST or DICT", kindToString(indexToken.kind))
}
if listToken.kind == LIST {
l := listToken.val.(list)
err = l.Set(indexToken.val.(int), valueToken)
if err != nil {
return err
}
err = globalStack.Push(token{LIST, l, line, fp})
} else {
d := listToken.val.(dict)
err = d.Update(indexToken, valueToken)
if err != nil {
return err
}
err = globalStack.Push(token{DICT, d, line, fp})
}
return err
}
func libSetListItemSpecial(line int, r *tokenReader, en *env, fp string) error {
valueToken, err := globalStack.Pop()
if err != nil {
return err
}
indexToken, err := globalStack.Pop()
if err != nil {
return err
}
// Get var reference
t, err := r.Read()
if err != nil {
return err
}
if t.kind != SYMBOL {
return fmt.Errorf("`list-set!`/`->!` expected to read in a SYMBOL but found %s", kindToString(t.kind))
}
targetTokenEnv, err := en.Find(t.val.(string))
if err != nil {
return err
}
targetToken := targetTokenEnv.vars[t.val.(string)]
if targetToken.kind == LIST && indexToken.kind != INT {
return fmt.Errorf("`list-set!`/`->!` expected a value, INT index, and LIST but found %s instead of INT", kindToString(indexToken.kind))
} else if targetToken.kind == DICT && indexToken.kind != STRING {
return fmt.Errorf("`list-set!`/`->!` expected a value, STRING key, and DICT but found %s instead of STRING", kindToString(indexToken.kind))
} else if targetToken.kind != LIST && targetToken.kind != DICT {
return fmt.Errorf("`list-set!`/`->!` cannot set a value in type %s", kindToString(targetToken.kind))
}
if targetToken.kind == LIST {
l := targetToken.val.(list)
err = l.Set(indexToken.val.(int), valueToken)
} else {
d := targetToken.val.(dict)
err = d.Update(indexToken, valueToken)
}
if err != nil {
return err
}
return nil
}
func libGetListItem(line int, r *tokenReader, en *env, fp string) error {
indexToken, err := globalStack.Pop()
if err != nil {
return err
}
listToken, err := globalStack.Pop()
if err != nil {
return err
}
if listToken.kind == LIST && indexToken.kind != INT {
return fmt.Errorf("`<-`/`list-get` expected a LIST underneath an INT on the stack, LIST found but found %s instead of INT", kindToString(indexToken.kind))
} else if listToken.kind == DICT && indexToken.kind != STRING {
return fmt.Errorf("`<-`/`list-get` expected a DICT underneath a STRING on the stack, DICT found but found %s instead of STRING", kindToString(indexToken.kind))
}
var val token
if listToken.kind == LIST {
l := listToken.val.(list)
val, err = l.Get(indexToken.val.(int))
if err != nil {
return err
}
} else {
d := listToken.val.(dict)
val, err = d.Get(indexToken.val.(string))
if err != nil {
return err
}
}
err = globalStack.Push(val)
if err != nil {
return err
}
return nil
}
func libVar(line int, r *tokenReader, en *env, fp string) error {
typeToken, err := globalStack.Pop()
if err != nil {
return err
}
if typeToken.kind != TYPE {
return fmt.Errorf("`var!` expects TYPE on the top of the stack")
}
newToken := token{}
newToken.kind = typeToken.val.(int)
newToken.line = line
newToken.file = fp
t, err := r.Read()
if err != nil {
return fmt.Errorf("Unexpectedly reached EOF")
}
if t.kind != SYMBOL {
return fmt.Errorf("A non-symbol value was given as an identifier to `var!`")
}
newToken.val = typeInit(newToken.kind)
en.Add(t.val.(string), newToken)
return nil
}
func libVarPlus(line int, r *tokenReader, en *env, fp string) error {
scope, err := globalStack.Pop()
if err != nil {
return err
}
if scope.kind != INT {
return fmt.Errorf("`scoped-var!` expected an INT declaring how many scopes to jump up, found %s", kindToString(scope.kind))
}
for i := scope.val.(int); i > 0; i-- {
if en.parent != nil {
en = en.parent
} else {
break
}
}
typeToken, err := globalStack.Pop()
if err != nil {
return err
}
if typeToken.kind != TYPE {
return fmt.Errorf("`scoped-var!` expects a TYPE literal on the the stack after an INT, found %s", kindToString(typeToken.kind))
}
newToken := token{}
newToken.kind = typeToken.val.(int)
newToken.line = line
newToken.file = fp
t, err := r.Read()
if err != nil {
return fmt.Errorf("Unexpectedly reached EOF")
}
if t.kind != SYMBOL {
return fmt.Errorf("A non-symbol value was given as an identifier to `scoped-var!`")
}
newToken.val = typeInit(newToken.kind)
en.Add(t.val.(string), newToken)
return nil
}
func libSet(line int, r *tokenReader, en *env, f string) error {
t, err := r.Read()
if err != nil {
return fmt.Errorf("Unexpectedly reached EOF")
}
if t.kind != SYMBOL {
return fmt.Errorf("A non-symbol value was given as an identifier to `set!`")
}
e, err := en.Find(t.val.(string))
if err != nil {
return err
}
v1, err := globalStack.Pop()
if err != nil {
return err
}
current := e.vars[t.val.(string)]
if v1.kind != current.kind {
return fmt.Errorf("Cannot `set!` %s to %s", kindToString(v1.kind), kindToString(current.kind))
}
e.vars[t.val.(string)] = v1
return nil
}
// ANY INT scoped-set! SYMBOL
// INT represents the number of scopes to jump up
func libSetPlus(line int, r *tokenReader, en *env, fp string) error {
t, err := r.Read()
if err != nil {
return fmt.Errorf("Unexpectedly reached EOF")
}
if t.kind != SYMBOL {
return fmt.Errorf("A non-symbol value was given as an identifier to `scoped-set!`")
}
scope, err := globalStack.Pop()
if err != nil {
return err
}
if scope.kind != INT {
return fmt.Errorf("`scoped-set!` expected an INT declaring how many scopes to jump up")
}
for i := scope.val.(int); i > 0; i-- {
if en.parent != nil {
en = en.parent
} else {
break
}
}
e, err := en.Find(t.val.(string))
if err != nil {
return err
}
v1, err := globalStack.Pop()
if err != nil {
return err
}
current := e.vars[t.val.(string)]
if v1.kind != current.kind {
return fmt.Errorf("Cannot assign %s to %s var", kindToString(v1.kind), kindToString(current.kind))
}
e.vars[t.val.(string)] = v1
return nil
}
func libDuplicate(line int, fp string) error {
err := globalStack.Push(globalStack.Peek())
if err != nil {
return err
}
return nil
}
func libSwap(line int, fp string) error {
v2, err := globalStack.Pop()
if err != nil {
return err
}
v1, err := globalStack.Pop()
if err != nil {
return err
}
err = globalStack.Push(v2)
if err != nil {
return err
}
err = globalStack.Push(v1)
if err != nil {
return err
}
return nil
}
func libOver(line int, fp string) error {
v2, err := globalStack.Pop()
if err != nil {
return err
}
v1, err := globalStack.Pop()
if err != nil {
return err
}
err = globalStack.Push(v1)
if err != nil {
return err
}
err = globalStack.Push(v2)
if err != nil {
return err
}
err = globalStack.Push(v1)
if err != nil {
return err
}
return nil
}
func libRot(line int, fp string) error {
v3, err := globalStack.Pop()
if err != nil {
return err
}
v2, err := globalStack.Pop()
if err != nil {
return err
}
v1, err := globalStack.Pop()
if err != nil {
return err
}
err = globalStack.Push(v2)
if err != nil {
return err
}
err = globalStack.Push(v3)
if err != nil {
return err
}
err = globalStack.Push(v1)
if err != nil {
return err
}
return nil
}
func libDrop(line int, fp string) error {
_, err := globalStack.Pop()
if err != nil {
return err
}
return nil
}
func libLogicalAnd(line int, fp string) error {
v2, err := globalStack.Pop()
if err != nil {
return err
}
v1, err := globalStack.Pop()
if err != nil {
return err
}
if toBoolRaw(v1) && toBoolRaw(v2) {
err = globalStack.Push(token{BOOL, true, line, fp})
if err != nil {
return err
}
} else {
err = globalStack.Push(token{BOOL, false, line, fp})
if err != nil {
return err
}
}
return nil
}
func libLogicalOr(line int, fp string) error {
v2, err := globalStack.Pop()
if err != nil {
return err
}
v1, err := globalStack.Pop()
if err != nil {
return err
}
if toBoolRaw(v1) || toBoolRaw(v2) {
err = globalStack.Push(token{BOOL, true, line, fp})
if err != nil {
return err
}
} else {
err = globalStack.Push(token{BOOL, false, line, fp})
if err != nil {
return err
}
}
return nil
}
// `=` works at a value level and does not refer to
// the equality of memory adresses. As such, two lists
// with the same contents are equal even if they do
// not occupy the same memory/refer to the same object
func libEqual(line int, fp string) error {
v2, err := globalStack.Pop()
if err != nil {
return err
}
v1, err := globalStack.Pop()
if err != nil {
return err
}
if v1.kind != v2.kind {
err = globalStack.Push(token{BOOL, false, line, fp})
} else {
var equal bool
switch v1.kind {
case BOOL:
equal = v1.val.(bool) == v2.val.(bool)
case INT, TYPE:
equal = v1.val.(int) == v2.val.(int)
case STRING:
equal = v1.val.(string) == v2.val.(string)
case FLOAT:
equal = v1.val.(float64) == v2.val.(float64)
case LIST:
l1 := v1.val.(list)
l2 := v2.val.(list)
equal = (l1.String() == l2.String())
}
err = globalStack.Push(token{BOOL, equal, line, fp})
}
if err != nil {
return err
}
return nil
}
func libGreaterThan(line int, fp string) error {
v2, err := globalStack.Pop()
if err != nil {
return err
}
v1, err := globalStack.Pop()
if err != nil {
return err
}
if v1.kind == v2.kind {
switch v1.kind {
case STRING:
err = globalStack.Push(token{BOOL, v1.val.(string) > v2.val.(string), line, fp})
case INT:
err = globalStack.Push(token{BOOL, v1.val.(int) > v2.val.(int), line, fp})
case FLOAT:
err = globalStack.Push(token{BOOL, v1.val.(float64) > v2.val.(float64), line, fp})
default:
return fmt.Errorf("%s is not a comparable type", kindToString(v1.kind))
}
} else if v1.kind == INT && v2.kind == FLOAT {
err = globalStack.Push(token{BOOL, float64(v1.val.(int)) > v2.val.(float64), line, fp})
} else if v1.kind == FLOAT && v2.kind == INT {
err = globalStack.Push(token{BOOL, v1.val.(float64) > float64(v2.val.(int)), line, fp})
} else {
return fmt.Errorf("%s and %s cannot be compared", kindToString(v1.kind), kindToString(v2.kind))
}
if err != nil {
return err
}
return nil
}
func libLessThan(line int, fp string) error {
v2, err := globalStack.Pop()
if err != nil {
return err
}
v1, err := globalStack.Pop()
if err != nil {
return err
}
if v1.kind == v2.kind {
switch v1.kind {
case STRING:
err = globalStack.Push(token{BOOL, v1.val.(string) < v2.val.(string), line, fp})
case INT:
err = globalStack.Push(token{BOOL, v1.val.(int) < v2.val.(int), line, fp})
case FLOAT:
err = globalStack.Push(token{BOOL, v1.val.(float64) < v2.val.(float64), line, fp})
case BOOL:
err = globalStack.Push(token{BOOL, false, line, fp})
default:
return fmt.Errorf("%s is not a comparable type", kindToString(v1.kind))
}
} else if v1.kind == INT && v2.kind == FLOAT {
err = globalStack.Push(token{BOOL, float64(v1.val.(int)) < v2.val.(float64), line, fp})
} else if v1.kind == FLOAT && v2.kind == INT {
err = globalStack.Push(token{BOOL, v1.val.(float64) < float64(v2.val.(int)), line, fp})
} else {
return fmt.Errorf("%s and %s cannot be compared", kindToString(v1.kind), kindToString(v2.kind))
}
if err != nil {
return err
}
return nil
}
func libStackDump() error {
fmt.Fprintf(os.Stderr, "\n%s\n", globalStack.String())
return nil
}
func libClearStack() error {
globalStack.sp = 0
return nil
}
func libLength(line int, fp string) error {
t, err := globalStack.Pop()
if err != nil {
return err
}
switch t.kind {
case LIST:
l := t.val.(list)
err = globalStack.Push(token{INT, len(l.body), line, fp})
case STRING:
s := t.val.(string)
err = globalStack.Push(token{INT, len(s), line, fp})
case DICT:
d := t.val.(dict)
err = globalStack.Push(token{INT, len(d.body), line, fp})
default:
err = fmt.Errorf("%s does not have a length", kindToString(t.kind))
}
return err
}
func libConvertType(line int, fp string) error {
typeToken, err := globalStack.Pop()
if err != nil {
return err
}
if typeToken.kind != TYPE {
return fmt.Errorf("`cast` expected a TYPE on top of stack")
}
fromToken, err := globalStack.Pop()
if err != nil {
return err
}
toKind, fromKind := typeToken.val.(int), fromToken.kind
if fromKind == toKind {
err = globalStack.Push(fromToken)
} else if fromKind == INT && toKind == FLOAT {
err = globalStack.Push(token{FLOAT, float64(fromToken.val.(int)), line, fp})
} else if fromKind == FLOAT && toKind == INT {
err = globalStack.Push(token{INT, int(math.Floor(fromToken.val.(float64))), line, fp})
} else if toKind == STRING {
err = globalStack.Push(token{STRING, toString(fromToken, false), line, fp})
} else if fromKind == STRING && toKind == INT {
i, err := tokenStringToInt(fromToken.val.(string))
if err != nil {
return fmt.Errorf("%s (STRING: %q)", err.Error(), fromToken.val.(string))
}
err = globalStack.Push(token{INT, i, line, fp})
} else if fromKind == STRING && toKind == FLOAT {
f, err := strconv.ParseFloat(fromToken.val.(string), 64)
if err != nil {
return fmt.Errorf("Could not convert STRING to FLOAT (STRING: %q)", fromToken.val.(string))
}
err = globalStack.Push(token{FLOAT, f, line, fp})
} else if toKind == BOOL {
err = globalStack.Push(toBool(fromToken))
} else if toKind == TYPE {
err = globalStack.Push(token{TYPE, fromToken.kind, line, fp})
} else {
return fmt.Errorf("Could not convert %s to %s", kindToString(fromKind), kindToString(toKind))
}
if err != nil {
return err
}
return nil
}
func libFileExists(line int, fp string) error {
p, err := globalStack.Pop()
if err != nil {
return err
}
if p.kind != STRING {
return fmt.Errorf("`file-exists?` expected a STRING path on TOS but got %s", kindToString(p.kind))
}
path := ExpandedAbsFilepath(p.val.(string))
_, err = os.Stat(path)
if err == nil {
err = globalStack.Push(token{BOOL, true, line, fp})
} else {
err = globalStack.Push(token{BOOL, false, line, fp})
}
if err != nil {
return err
}
return nil
}
func libFileRemove(line int, fp string) error {
p, err := globalStack.Pop()
if err != nil {
return err
}
if p.kind != STRING {
return fmt.Errorf("`file-remove` expected a STRING path on TOS but got %s", kindToString(p.kind))
}
path := ExpandedAbsFilepath(p.val.(string))
err = os.RemoveAll(path) // Will remove DIRs as well, be careful
if err != nil {
return fmt.Errorf("`file-remove` could not remove the file(s): %s", path)
}
return nil
}
func libFileAppend(line int, fp string, doAppend bool) error {
p, err := globalStack.Pop()
if err != nil {
return err
}
if p.kind != STRING {
return fmt.Errorf("`file-append` expected a STRING path on TOS but got %s", kindToString(p.kind))
}
data, err := globalStack.Pop()
if err != nil {
return err
}
path := ExpandedAbsFilepath(p.val.(string))
s := toString(data, false)
var caller string
var f *os.File
if doAppend {
f, err = os.OpenFile(path, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
caller = "file-append"
} else {
f, err = os.OpenFile(path, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0644)
caller = "file-write"
}
if err != nil {
return fmt.Errorf("`%s` could not open or create the path: %s", caller, path)
}
defer f.Close()
if err != nil {
return fmt.Errorf("`%s` opened but could not write to the path: %s", caller, path)
}
_, err = f.WriteString(s)
return nil
}
func libFileRead(line int, fp string) error {
p, err := globalStack.Pop()
if err != nil {
return err
}
if p.kind != STRING {
return fmt.Errorf("`file-read` expected a STRING path on TOS but got %s", kindToString(p.kind))
}
path := ExpandedAbsFilepath(p.val.(string))
b, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("`file-read` could not read from the file at path: %s", path)
}
err = globalStack.Push(token{STRING, string(b), line, fp})
if err != nil {
return err
}
return nil
}
func libDocstring(line int, r *tokenReader, en *env, fp string) error {
t, err := r.Read()
if err != nil {
return fmt.Errorf("Unexpectedly reached EOF")
}
// There is a situation here where the parser does not expect
// to see a structure (proc, while, if) here and may mangle
// parsing, since there will be no close to the declaration.
// Make the parser smart enough to handle this case.
if t.kind == KEYWORD || t.kind == PROC || t.kind == IF || t.kind == WHILE {
var s string
switch t.kind {
case IF:
s = "if"
case WHILE:
s = "while"
case PROC:
if t.val.(proc).hasArg {
s = "proc!"
} else {
s = "proc"
}
default:
s = t.val.(string)
}
v, ok := kwDocstrings[s]
if ok {
err = globalStack.Push(token{STRING, fmt.Sprintf("`%s`\n%s", s, v), line, fp})
if err != nil {
return err
}
return nil
}
} else if t.kind != SYMBOL && t.kind != KEYWORD {
return fmt.Errorf("`docstring!` expected to read in a SYMBOL or KEYWORD but found %s", kindToString(t.kind))
}
targetTokenEnv, err := en.Find(t.val.(string))
if err != nil {
return err
}
targetToken := targetTokenEnv.vars[t.val.(string)]
if targetToken.kind != PROC {
return fmt.Errorf("`docstring!` cannot read a docstring from %s", kindToString(targetToken.kind))
}
p := targetToken.val.(proc)
s := fmt.Sprintf("`%s`\n%s", t.val.(string), p.doc)
err = globalStack.Push(token{STRING, s, line, fp})
if err != nil {
return err
}
return nil
}
func libInput(line int, fp string) error {
scanner := bufio.NewScanner(os.Stdin)
scanner.Scan()
text := scanner.Text()
err := globalStack.Push(token{STRING, text, line, fp})
if err != nil {
return err
}
return nil
}
func libReMatch(line int, fp string) error {
t2, err := globalStack.Pop()
if err != nil {
return err
}
t1, err := globalStack.Pop()
if err != nil {
return err
}
if !(t1.kind == STRING && t2.kind == STRING) {
return fmt.Errorf("`re-match?` expected two strings from the stack but found %s and %s", kindToString(t1.kind), kindToString(t2.kind))
}
re, err := regexp.Compile(t1.val.(string))
if err != nil {
return err
}
err = globalStack.Push(token{BOOL, re.MatchString(t2.val.(string)), line, fp})
if err != nil {
return err
}
return nil
}
func libReFind(line int, fp string, all bool) error {
t2, err := globalStack.Pop()
if err != nil {
return err
}
t1, err := globalStack.Pop()
if err != nil {
return err
}
if !(t1.kind == STRING && t2.kind == STRING) {
return fmt.Errorf("`re-find` expected two strings from the stack but found %s and %s", kindToString(t1.kind), kindToString(t2.kind))
}
re, err := regexp.Compile(t1.val.(string))
if err != nil {
return err
}
var out token
if all {
matches := re.FindAllStringSubmatch(t2.val.(string), -1)
tokens := make([]token, len(matches))
for i := range matches {
subtokens := make([]token, len(matches[i]))
for ii, ss := range matches[i] {
subtokens[ii] = token{STRING, ss, line, fp}
}
tokens[i] = token{LIST, list{subtokens}, line, fp}
}
out = token{LIST, list{tokens}, line, fp}
} else {
matches := re.FindStringSubmatch(t2.val.(string))
tokens := make([]token, len(matches))
for i, s := range matches {
tokens[i] = token{STRING, s, line, fp}
}
out = token{LIST, list{tokens}, line, fp}
}
err = globalStack.Push(out)
if err != nil {
return err
}
return nil
}
func libReReplace(line int, fp string) error {
replacement, err := globalStack.Pop()
if err != nil {
return err
}
input, err := globalStack.Pop()
if err != nil {
return err
}
pattern, err := globalStack.Pop()
if err != nil {
return err
}
re, err := regexp.Compile(toString(pattern, false))
if err != nil {
return err
}
s := re.ReplaceAllString(toString(input, false), toString(replacement, false))
err = globalStack.Push(token{STRING, s, line, fp})
if err != nil {
return err
}
return nil
}
func libSlice(line int, fp string) error {
to, err := globalStack.Pop()
if err != nil {
return err
}
from, err := globalStack.Pop()
if err != nil {
return err
}
if to.kind != INT || from.kind != INT {
return fmt.Errorf("`slice` expected two integers from the stack but found %s and %s", kindToString(from.kind), kindToString(to.kind))
}
data, err := globalStack.Pop()
if err != nil {
return err
}
t := to.val.(int)
f := from.val.(int) - 1
switch data.kind {
case STRING:
s := data.val.(string)
if t < 0 {
t = len(s) + t + 1
}
if t > len(s) {
t = len(s)
}
if f < 0 {
f = 0
}
if f > t {
f = t
}
err = globalStack.Push(token{STRING, s[f:t], line, fp})
case LIST:
li := data.val.(list)
l := li.body
if t < 0 {
t = len(l) + t + 1
}
if t > len(l) {
t = len(l)
}
if f < 0 {
f = 0
}
if f > t {
f = t
}
err = globalStack.Push(token{LIST, list{l[f:t]}, line, fp})
default:
return fmt.Errorf("Cannot `slice` %s\n", line, kindToString(data.kind))
}
if err != nil {
return err
}
return nil
}
func libStackDepth(line int, fp string) error {
err := globalStack.Push(token{INT, globalStack.Depth(), line, fp})
if err != nil {
return err
}
return nil
}
func libType(line int, fp string) error {
val, err := globalStack.Pop()
if err != nil {
return err
}
err = globalStack.Push(token{TYPE, val.kind, line, fp})
if err != nil {
return err
}
return nil
}
func libNetGet(line int, fp string) error {
val, err := globalStack.Pop()
if err != nil {
return err
}
respList := dict{make(map[string]token)}
respList.body["success"] = token{BOOL, false, line, fp}
respList.body["status"] = token{INT, -1, line, fp}
respList.body["body"] = token{STRING, "The given string could not be parsed as a url", line, fp}
u, err := url.Parse(toString(val, false))
if err != nil {
err = globalStack.Push(token{DICT, respList, line, fp})
if err != nil {
return err
}
return nil
}
switch u.Scheme {
case "http", "https":
resp, err := http.Get(u.String())
if err != nil {
respList.body["status"] = token{STRING, err.Error(), line, fp}
break
}
defer resp.Body.Close()
success := true
status := resp.StatusCode
if status < 200 || status > 299 {
success = false
}
body, err := io.ReadAll(resp.Body)
if err != nil {
respList.body["status"] = token{INT, status, line, fp}
respList.body["body"] = token{STRING, "Could not read response body, or reading was interuppted", line, fp}
break
}
respList.body["success"] = token{BOOL, success, line, fp}
respList.body["status"] = token{INT, status, line, fp}
respList.body["body"] = token{STRING, string(body), line, fp}
case "gemini":
status, resp := GeminiRequest(u, 0)
if status == 1 || status == 2 {
respList.body["success"] = token{BOOL, true, line, fp}
} else {
respList.body["success"] = token{BOOL, false, line, fp}
}
respList.body["status"] = token{INT, status, line, fp}
respList.body["body"] = token{STRING, resp, line, fp}
case "gopher":
if u.Port() == "" {
u.Host = u.Host + ":70"
}
conn, err := net.Dial("tcp", u.Host)
if err != nil {
respList.body["body"] = token{STRING, err.Error(), line, fp}
break
}
defer conn.Close()
p := u.Path
if len(u.Path) < 2 {
p = "/" + "\n"
} else {
p = p[2:] + "\n"
}
_, err = conn.Write([]byte(p))
if err != nil {
respList.body["body"] = token{STRING, err.Error(), line, fp}
break
}
resp, err := io.ReadAll(conn)
if err != nil {
respList.body["body"] = token{STRING, err.Error(), line, fp}
break
}
respList.body["success"] = token{BOOL, true, line, fp}
respList.body["body"] = token{STRING, string(resp), line, fp}
default:
return fmt.Errorf("Unsupported URL scheme: %s\n", line, u.Scheme)
}
err = globalStack.Push(token{DICT, respList, line, fp})
if err != nil {
return err
}
return nil
}
func libThrow(line int, fp string) error {
e, err := globalStack.Pop()
if err != nil {
return err
}
return fmt.Errorf("%s", toString(e, false))
}
func libImport(line int, fp string, en *env) error {
t, err := globalStack.Pop()
if err != nil {
return err
}
if t.kind != STRING {
return fmt.Errorf("`import` expects a STRING on TOS, not %s", kindToString(t.kind))
}
if en.Imported(t.val.(string)) {
return nil
}
switch t.val.(string) {
case "std":
err = lexParseInterpret(stdImport, en, "std")
if err != nil {
return err
}
en.imports[t.val.(string)] = true
case "math":
err = lexParseInterpret(mathImport, en, "math")
if err != nil {
return err
}
en.imports[t.val.(string)] = true
case "stack":
err = lexParseInterpret(stackImport, en, "stack")
if err != nil {
return err
}
en.imports[t.val.(string)] = true
case "paths":
err = lexParseInterpret(pathsImport, en, "paths")
if err != nil {
return err
}
en.imports[t.val.(string)] = true
case "strings":
err = lexParseInterpret(stringsImport, en, "strings")
if err != nil {
return err
}
en.imports[t.val.(string)] = true
default:
p := ExpandedAbsFilepath(t.val.(string))
s, err := readFile(p)
if err != nil {
return err
}
err = lexParseInterpret(s, en, p)
if err != nil {
return err
}
en.imports[t.val.(string)] = true
}
return nil
}
func libEach(line int, r *tokenReader, en *env, fp string) error {
li, err := globalStack.Pop()
if err != nil {
return err
}
if li.kind != LIST {
return fmt.Errorf("'each!' expected a LIST on TOS but found %s", kindToString(li.kind))
}
l := li.val.(list).body
t, err := r.Read()
if err != nil {
return fmt.Errorf("Unexpectedly reached EOF")
}
if t.kind == SYMBOL {
e, err := en.Find(t.val.(string))
if err != nil {
return err
}
val := e.vars[t.val.(string)]
if val.kind != PROC {
return fmt.Errorf("Cannot apply %s to list item in `each!`, expected PROC or KEYWORD", kindToString(val.kind))
}
} else if t.kind != KEYWORD {
return fmt.Errorf("A non-SYMBOL/non-KEYWORD value was given as an identifier to `each!`")
}
for i := range l {
err = interpret([]token{l[i], t}, en)
if err != nil {
return err
}
}
return nil
}
func libFilter(line int, r *tokenReader, en *env, fp string) error {
li, err := globalStack.Pop()
if err != nil {
return err
}
if li.kind != LIST {
return fmt.Errorf("'filter!' expected a LIST on TOS but found %s", kindToString(li.kind))
}
l := li.val.(list).body
t, err := r.Read()
if err != nil {
return fmt.Errorf("Unexpectedly reached EOF")
}
if t.kind == SYMBOL {
e, err := en.Find(t.val.(string))
if err != nil {
return err
}
val := e.vars[t.val.(string)]
if val.kind != PROC {
return fmt.Errorf("Cannot apply %s to list item in `filter!`, expected PROC or KEYWORD", kindToString(val.kind))
}
} else if t.kind != KEYWORD {
return fmt.Errorf("A non-SYMBOL/non-KEYWORD value was given as an identifier to `filter!`")
}
newList := make([]token, 0, len(l))
for i := range l {
err = interpret([]token{l[i], t}, en)
if err != nil {
return err
}
b, err := globalStack.Pop()
if err != nil {
return err
}
if toBoolRaw(b) {
newList = append(newList, l[i])
}
}
err = globalStack.Push(token{LIST, list{newList}, line, fp})
if err != nil {
return err
}
return nil
}
func libTime(line int, fp string) error {
return globalStack.Push(token{INT, int(time.Now().Unix()), line, fp})
}
func libToChar(line int, fp string) error {
t, err := globalStack.Pop()
if err != nil {
return err
}
if t.kind == STRING {
if len(t.val.(string)) == 0 {
return fmt.Errorf("Cannot convert empty STRING to char INT")
}
return globalStack.Push(token{INT, int(rune(t.val.(string)[0])), line, fp})
} else if t.kind == INT {
if t.val.(int) < 0 {
return fmt.Errorf("Cannot convert negative INT to STRING as a char")
}
return globalStack.Push(token{STRING, string(rune(t.val.(int))), line, fp})
}
return fmt.Errorf("Cannot convert %s to or from a char", kindToString(t.kind))
}
func libEnvGet(line int, fp string) error {
t, err := globalStack.Pop()
if err != nil {
return err
}
if t.kind != STRING {
return fmt.Errorf("Non-STRING value on TOS")
}
v := os.Getenv(t.val.(string))
return globalStack.Push(token{STRING, v, line, fp})
}
func libEnvSet(line int, fp string) error {
t, err := globalStack.Pop()
if err != nil {
return err
}
if t.kind != STRING {
return fmt.Errorf("Non-STRING value on TOS")
}
val, err := globalStack.Pop()
if err != nil {
return err
}
err = os.Setenv(t.val.(string), toString(val, false))
if err != nil {
return err
}
return nil
}
func libChangeDir(line int, fp string) error {
t, err := globalStack.Pop()
if err != nil {
return err
}
if t.kind != STRING {
return fmt.Errorf("`cd` expected a STRING on TOS but found %s on line %d of %s", kindToString(t.kind), line, fp)
}
err = os.Chdir(ExpandedAbsFilepath(toString(t, false)))
if err != nil {
return fmt.Errorf("Could not change directory to %s, line %d of %s", toString(t, true), line, fp)
}
return nil
}
func libPresentDir(line int, fp string) error {
d, err := os.Getwd()
if err != nil {
return err
}
return globalStack.Push(token{STRING, d, line, fp})
}
func libSubprocess(line int, fp string, asReturn bool, takeInput bool) error {
t, err := globalStack.Pop()
if err != nil {
return err
}
var in string
if takeInput {
i, err := globalStack.Pop()
if err != nil {
return err
}
in = toString(i, false)
}
path := ""
var cmd *exec.Cmd
var args []string
if t.kind == LIST {
// If the list is empty, just return nil
if len(t.val.(list).body) == 0 {
return nil
}
path = toString(t.val.(list).body[0], false)
argSlice := t.val.(list).body[1:]
args = make([]string, 0, 3)
for _, v := range argSlice {
args = append(args, toString(v, false))
}
if len(args) == 0 {
cmd = exec.Command(path)
} else {
cmd = exec.Command(path, args...)
}
} else {
path = toString(t, false)
cmd = exec.Command(path)
}
if takeInput {
cmd.Stdin = strings.NewReader(in)
} else {
cmd.Stdin = os.Stdin
}
if asReturn {
var sout strings.Builder
var eout strings.Builder
cmd.Stdout = &sout
cmd.Stderr = &eout
err = cmd.Run()
out := token{
LIST,
list{
[]token{
token{STRING, sout.String(), line, fp},
token{STRING, eout.String(), line, fp},
token{INT, 0, line, fp}}},
line,
fp}
if err != nil {
if exiterr, ok := err.(*exec.ExitError); ok {
if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
out.val.(list).body[2].val = status.ExitStatus()
}
} else {
return err
}
}
return globalStack.Push(out)
} else {
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err = cmd.Run()
out := token{INT, 0, line, fp}
if err != nil {
if exiterr, ok := err.(*exec.ExitError); ok {
if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
out.val = status.ExitStatus()
}
} else {
return err
}
}
return globalStack.Push(out)
}
}