1749 lines
44 KiB
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)
|
|
}
|
|
}
|