slope/lib.go

3542 lines
101 KiB
Go

package main
import (
"bufio"
"crypto/md5"
"crypto/sha256"
"crypto/tls"
"fmt"
"io"
"io/ioutil"
"math"
"math/rand"
"net"
"net/http"
"net/url"
"os"
"os/exec"
"os/user"
"path/filepath"
"reflect"
"regexp"
"strconv"
"strings"
"sync"
"syscall"
"time"
"unicode/utf8"
"git.rawtext.club/sloum/slope/termios"
)
var Sysout *IOHandle = &IOHandle{os.Stdout, true, "file (write-only)"}
var stdLibrary = vars{
"sysout": func(a ...expression) expression {
if len(a) == 0 {
return Sysout
}
switch io := a[0].(type) {
case *IOHandle:
if !io.Open {
return exception("'sysout' expected an open io-handle, but a closed io-handle was given")
}
Sysout = io
return make([]expression, 0)
default:
return exception("'sysout' expected an open io-handle, or nothing, but a non-io-handle was given")
}
},
"stdin": &IOHandle{os.Stdin, true, "file (read-only)"},
"stdout": &IOHandle{os.Stdout, true, "file (write-only)"},
"stderr": &IOHandle{os.Stderr, true, "file (write-only)"},
"devnull": func() *IOHandle {
devnull, _ := os.OpenFile(os.DevNull, os.O_WRONLY, 0755)
dn := &IOHandle{devnull, true, "file (write-only)"}
openFiles = append(openFiles, dn)
return dn
}(),
"mod-path": func(a ...expression) expression {
return getModBaseDir()
},
"__SIGINT": func(a ...expression) expression {
SafeExit(1)
return false
},
"signal-catch-sigint": func(a ...expression) expression {
if len(a) < 1 {
return exception("'signal-catch-sigint' expected a procedure but no value was given")
}
switch p := a[0].(type) {
case proc, func(...expression) expression:
globalenv.vars["__SIGINT"] = p
return true
default:
return exception("'signal-catch-sigint' expected a procedure, but a non-procedure value was given")
}
},
"PHI": number(math.Phi),
"PI": number(math.Pi),
"E": number(math.E),
"sys-args": func(a ...expression) expression {
args := os.Args
if len(args) > 0 && args[0] == "slope" || args[0] == "./slope" || args[0] == "../slope" {
args = args[1:]
}
return StringSliceToExpressionSlice(args)
}(),
"exception-mode-panic": func(a ...expression) expression {
panicOnException = true
return make([]expression, 0)
},
"exception-mode-pass": func(a ...expression) expression {
panicOnException = false
return make([]expression, 0)
},
"exception-mode-pass?": func(a ...expression) expression {
return panicOnException == false
},
"exception-mode-panic?": func(a ...expression) expression {
return panicOnException == true
},
"slope-version": func(a ...expression) expression {
out := make([]expression, 0, 4)
s := strings.SplitN(version, ".", -1)
for i := range s {
integer, err := strconv.ParseInt(s[i], 10, 64)
if err != nil {
continue
}
out = append(out, number(integer))
}
var maj, min, patch number
var ok bool
if len(a) == 0 {
return out
}
maj, ok = a[0].(number)
if !ok {
return exception("'slope-version' expected its first argument to be a number representing a major version, an invalid value was given")
}
if len(a) > 1 {
min, ok = a[1].(number)
if !ok {
return exception("'slope-version' expected its second argument to be a number representing a minor version, an invalid value was given")
}
}
if len(a) > 2 {
patch, ok = a[2].(number)
if !ok {
return exception("'slope-version' expected its third argument to be a number representing a patch version, an invalid value was given")
}
}
if out[0].(number) > maj || (out[0].(number) == maj && out[1].(number) > min) || (out[0].(number) == maj && out[1].(number) == min && out[2].(number) >= patch) {
return true
}
return exception(fmt.Sprintf("This program requires a slope interpreter version >= %d.%d.%d\nThe currently installed version is %s\nPlease update your slope interpreter to run this program", int(maj), int(min), int(patch), version))
},
"!": func(a ...expression) expression {
if len(a) < 0 {
return exception("'!' expects a value, no value was given")
}
if panicOnException {
panic(a[0])
}
return exception(String(a[0], false))
},
"positive?": func(a ...expression) expression {
if len(a) > 0 {
if n, ok := a[0].(number); ok {
return n > 0
}
}
return false
},
"negative?": func(a ...expression) expression {
if len(a) > 0 {
if n, ok := a[0].(number); ok {
return n < 0
}
}
return false
},
"zero?": func(a ...expression) expression {
if len(a) > 0 {
if n, ok := a[0].(number); ok {
return n == 0
}
}
return false
},
"exception?": func(a ...expression) expression {
if len(a) > 0 {
if _, ok := a[0].(exception); ok {
return true
}
}
return false
},
"abs": func(a ...expression) expression {
if len(a) < 1 {
return exception("'abs' expected a value, but no value was given")
}
i, ok := a[0].(number)
if !ok {
return exception("'abs' expected a number, but a non-number value was received")
}
if i < 0 {
i = i * -1
}
return i
},
"floor": func(a ...expression) expression {
if len(a) < 1 {
return exception("'floor' expected a value, but no value was given")
}
i, ok := a[0].(number)
if !ok {
return exception("'floor' expected a number, but a non-number value was received")
}
return number(math.Floor(float64(i)))
},
"ceil": func(a ...expression) expression {
if len(a) < 1 {
return exception("'ceil' expected a value, but no value was given")
}
i, ok := a[0].(number)
if !ok {
return exception("'ceil' expected a number, but a non-number value was received")
}
return number(math.Ceil(float64(i)))
},
"type": func(a ...expression) expression {
if len(a) == 0 {
return exception("'type' expected a value, but no value was given")
}
t := reflect.TypeOf(a[0]).String()
switch t {
case "main.number":
return "number"
case "[]main.expression":
return "list"
case "*main.IOHandle":
return "IOHandle"
case "main.symbol":
return "symbol"
case "func(...main.expression) main.expression":
return "compiled-procedure"
case "main.proc":
return "procedure"
case "main.exception":
return "exception"
case "main.macro":
return "macro"
case "main.GUI":
return "GUI"
case "main.guiWidget":
return "Widget"
case "fyne.Widget":
return "Widget"
case "fyne.CanvasObject":
return "Widget"
case "fyne.Container":
return "Container"
case "main.guiContainer":
return "Container"
default:
return t
}
},
"round": func(a ...expression) expression {
if len(a) < 1 {
return exception("'round' expected a value, but no value was given")
}
decimals := number(0.0)
if len(a) >= 2 {
if dec, ok := a[1].(number); ok {
decimals = dec
}
}
num, ok := a[0].(number)
if !ok {
return exception("'round' expected a number, but a non-number value was received")
}
return number(math.Round(float64(num)*math.Pow(10, float64(decimals))) / math.Pow(10, float64(decimals)))
},
"&": func(a ...expression) expression {
if len(a) < 2 {
return exception("'&' expected two numbers, insufficient arguments were given")
}
n1, ok := a[0].(number)
if !ok {
return exception("'&' expected argument one to be a number, a non-number value was given")
}
n2, ok2 := a[1].(number)
if !ok2 {
return exception("'&' expected argument two to be a number, a non-number value was given")
}
return number(int(n1) & int(n2))
},
"|": func(a ...expression) expression {
if len(a) < 2 {
return exception("'|' expected two numbers, insufficient arguments were given")
}
n1, ok := a[0].(number)
if !ok {
return exception("'|' expected argument one to be a number, a non-number value was given")
}
n2, ok2 := a[1].(number)
if !ok2 {
return exception("'|' expected argument two to be a number, a non-number value was given")
}
return number(int(n1) | int(n2))
},
"<<": func(a ...expression) expression {
if len(a) < 2 {
return exception("'<<' expected two numbers, insufficient arguments were given")
}
n1, ok := a[0].(number)
if !ok {
return exception("'<<' expected argument one to be a number, a non-number value was given")
}
n2, ok2 := a[1].(number)
if !ok2 {
return exception("'<<' expected argument two to be a number, a non-number value was given")
}
return number(int(n1) << int(n2))
},
">>": func(a ...expression) expression {
if len(a) < 2 {
return exception("'>>' expected two numbers, insufficient arguments were given")
}
n1, ok := a[0].(number)
if !ok {
return exception("'>>' expected argument one to be a number, a non-number value was given")
}
n2, ok2 := a[1].(number)
if !ok2 {
return exception("'>>' expected argument two to be a number, a non-number value was given")
}
return number(int(n1) >> int(n2))
},
"^": func(a ...expression) expression {
if len(a) < 1 {
return exception("'^' expected two numbers, insufficient arguments were given")
}
n1, ok := a[0].(number)
if !ok {
return exception("'^' expected argument one to be a number, a non-number value was given")
}
if len(a) == 1 {
return number(^int(n1))
}
n2, ok2 := a[1].(number)
if !ok2 {
return exception("'^' expected argument two to be a number, a non-number value was given")
}
return number(int(n1) ^ int(n2))
},
"+": func(a ...expression) expression {
if len(a) == 0 {
return number(0)
}
v, ok := a[0].(number)
if !ok {
return exception("'+' expected a number, but a non-number value was received")
}
for _, i := range a[1:] {
x, ok := i.(number)
if !ok {
return exception("'+' expected a number, but a non-number value was received")
}
v += x
}
return v
},
"-": func(a ...expression) expression {
if len(a) == 0 {
return exception("'-' expected a value, but no value was given")
}
v, ok := a[0].(number)
if !ok {
return exception("'-' expected a number, but a non-number value was received")
}
if len(a) == 1 {
return v * -1
}
for _, i := range a[1:] {
x, ok := i.(number)
if !ok {
return exception("'-' expected a number, but a non-number value was received")
}
v -= x
}
return v
},
"*": func(a ...expression) expression {
if len(a) == 0 {
return number(1)
}
v, ok := a[0].(number)
if !ok {
return exception("'*' expected a number, but a non-number value was received")
}
if len(a) == 1 {
return 1 / v
}
for _, i := range a[1:] {
x, ok := i.(number)
if !ok {
return exception("'*' expected a number, but a non-number value was received")
}
v *= x
}
return v
},
"/": func(a ...expression) expression {
if len(a) == 0 {
return exception("'/' expected a value, but no value was given")
}
v, ok := a[0].(number)
if !ok {
return exception("'/' expected a number, but a non-number value was received")
}
for _, i := range a[1:] {
x, ok := i.(number)
if !ok {
return exception("'/' expected a number, but a non-number value was received")
}
v /= x
}
return v
},
"%": func(a ...expression) expression {
if len(a) < 2 {
return exception("'%' expected two numbers, but too few values were given")
}
v, ok := a[0].(number)
if !ok {
return exception("'%' expected a number, but a non-number value was received")
}
v2, ok2 := a[1].(number)
if !ok2 {
return exception("'%' expected a number, but a non-number value was received")
}
out := math.Mod(float64(v), float64(v2))
if math.IsNaN(out) {
return number(0)
}
return number(out)
},
"exp": func(a ...expression) expression {
if len(a) < 2 {
return exception("'exp' expected two numbers, but too few values were given")
}
base, ok := a[0].(number)
if !ok {
return exception("'exp' expected a number as its first argument, a non-number value was given")
}
exp, ok2 := a[1].(number)
if !ok2 {
return exception("'exp' expected a number as its second argument, a non-number value was given")
}
return number(math.Pow(float64(base), float64(exp)))
},
"sqrt": func(a ...expression) expression {
if len(a) == 0 {
return exception("'sqrt' expected a number, but no value was given")
}
v, ok := a[0].(number)
if !ok {
return exception("'sqrt' expected a number, but a non-number value was received")
}
return number(math.Sqrt(float64(v)))
},
"log": func(a ...expression) expression {
if len(a) == 0 {
return exception("'log' expected a number, but no value was given")
}
v, ok := a[0].(number)
if !ok {
return exception("'log' expected a number, but a non-number value was received")
}
return number(math.Log(float64(v)))
},
"cos": func(a ...expression) expression {
if len(a) == 0 {
return exception("'cos' expected a number, but no value was given")
}
v, ok := a[0].(number)
if !ok {
return exception("'cos' expected a number, but a non-number value was received")
}
return number(math.Cos(float64(v)))
},
"sin": func(a ...expression) expression {
if len(a) == 0 {
return exception("'sin' expected a number, but no value was given")
}
v, ok := a[0].(number)
if !ok {
return exception("'sin' expected a number, but a non-number value was received")
}
return number(math.Sin(float64(v)))
},
"tan": func(a ...expression) expression {
if len(a) == 0 {
return exception("'tan' expected a number, but no value was given")
}
v, ok := a[0].(number)
if !ok {
return exception("'tan' expected a number, but a non-number value was received")
}
return number(math.Tan(float64(v)))
},
"atan": func(a ...expression) expression {
if len(a) == 0 {
return exception("'atan' expected at least one number, but no value was given")
}
v, ok := a[0].(number)
if !ok {
return exception("'atan' expected a number, but a non-number value was received")
}
if len(a) == 1 {
return number(math.Atan(float64(v)))
}
v2, ok2 := a[1].(number)
if !ok2 {
return exception("'atan' expected a number as its second argument, but a non-number value was received")
}
return number(math.Atan2(float64(v), float64(v2)))
},
"min": func(a ...expression) expression {
if len(a) == 0 {
return exception("'min' expected a value, but no value was given")
}
val := number(math.MaxFloat64)
for _, v := range a {
value, ok := v.(number)
if !ok {
return exception("'min' expected a number, but a non-number value was received")
}
if value < val {
val = value
}
}
return val
},
"max": func(a ...expression) expression {
if len(a) == 0 {
return exception("'max' expected a value, but no value was given")
}
val := number(math.SmallestNonzeroFloat64)
for _, v := range a {
value, ok := v.(number)
if !ok {
return exception("'max' expected a number, but a non-number value was received")
}
if value > val {
val = value
}
}
return val
},
"<=": func(a ...expression) expression {
if len(a) < 2 {
return exception("'<=' expected, but did not receive, two values")
}
n1, ok1 := a[0].(number)
n2, ok2 := a[1].(number)
if !ok1 || !ok2 {
return exception("'<=' expected numbers, but a non-number value was received")
}
return n1 <= n2
},
"<": func(a ...expression) expression {
if len(a) < 2 {
return exception("'<' expected, but did not receive, two values")
}
n1, ok1 := a[0].(number)
n2, ok2 := a[1].(number)
if !ok1 || !ok2 {
return exception("'<' expected numbers, but a non-number value was received")
}
return n1 < n2
},
">=": func(a ...expression) expression {
if len(a) < 2 {
return exception("'>=' expected, but did not receive, two values")
}
n1, ok1 := a[0].(number)
n2, ok2 := a[1].(number)
if !ok1 || !ok2 {
return exception("'>=' expected numbers, but a non-number value was received")
}
return n1 >= n2
},
">": func(a ...expression) expression {
if len(a) < 2 {
return exception("'>' expected, but did not receive, two values")
}
n1, ok1 := a[0].(number)
n2, ok2 := a[1].(number)
if !ok1 || !ok2 {
return exception("'>' expected numbers, but a non-number value was received")
}
return n1 > n2
},
"equal?": func(a ...expression) expression {
if len(a) < 1 {
return exception("'equal?' expected, but did not receive, at least one value")
} else if len(a) == 1 {
return true
}
eq := false
for i := 0; i < len(a)-1; i++ {
eq = reflect.DeepEqual(a[i], a[i+1])
if !eq {
return false
}
}
return true
},
"atom?": func(a ...expression) expression {
if len(a) == 0 {
return exception("'atom?' expected a value, but no value was given")
}
switch a[0].(type) {
case []expression:
return false
default:
return true
}
},
"assoc?": func(a ...expression) expression {
if len(a) == 0 {
return exception("'assoc?' expected a value, but no value was given")
}
list, ok := a[0].([]expression)
if !ok {
return false
}
for x := range list {
l, ok := list[x].([]expression)
if !ok || len(l) != 2 {
return false
}
}
return true
},
"assoc-has-key?": func(a ...expression) expression {
if len(a) < 2 {
return exception("'assoc-has-key' expected at least two values, an associative list and a key value; insufficient arguments were given")
}
list, ok := a[0].([]expression)
if !ok {
return exception("'assoc' expected argument 1 to be an association list, a non-list value was given")
}
for x := range list {
l, ok := list[x].([]expression)
if !ok || len(l) != 2 {
return exception("'assoc' expected argument 1 to be an association list, a list was given, but it is not an association list")
}
if reflect.DeepEqual(l[0], a[1]) {
return true
}
}
return false
},
"assoc": func(a ...expression) expression {
if len(a) < 2 {
return exception("'assoc' expected at least two values, a list and a value; insufficient arguments were given")
}
list, ok := a[0].([]expression)
if !ok {
return exception("'assoc' expected argument 1 to be an association list, a non-list value was given")
}
for x := range list {
l, ok := list[x].([]expression)
if !ok || len(l) != 2 {
return exception("'assoc' expected argument 1 to be an association list, a list was given, but it is not an association list")
}
if reflect.DeepEqual(l[0], a[1]) {
if len(a) >= 3 {
if len(a) > 3 && AnythingToBool(a[3]).(bool) {
l[1] = a[2]
list[x] = l
return list
} else {
cpy := make([]expression, len(list))
copy(cpy, list)
cpy[x] = []expression{l[0], a[2]}
return cpy
}
}
return l[1]
}
}
if len(a) >= 3 {
if len(a) > 3 && AnythingToBool(a[3]).(bool) {
// This will almost certainly create a new assoc
// and return it rather than adding to the existing
// assoc, and is thus similar to the else branch,
// but in theory has a chance that a copy will not
// be needed, thus potentially executing faster.
list = append(list, []expression{a[1], a[2]})
return list
} else {
cpy := make([]expression, len(list))
copy(cpy, list)
cpy = append(cpy, []expression{a[1], a[2]})
return cpy
}
}
return exception("'assoc' could not find the key: " + String(a[1], true))
},
"number?": func(a ...expression) expression {
if len(a) == 0 {
return exception("'number?' expected a value, but no value was given")
}
switch a[0].(type) {
case float64, number:
return true
default:
return false
}
},
"byte?": func(a ...expression) expression {
if len(a) == 0 {
return exception("'byte?' expected a value, but no value was given")
}
num, ok := a[0].(number)
if !ok {
return false
}
return int(num) >= 0 && int(num) <= 255
},
"string?": func(a ...expression) expression {
if len(a) == 0 {
return exception("'string?' expected a value, but no value was given")
}
switch a[0].(type) {
case string:
return true
default:
return false
}
},
"io-handle?": func(a ...expression) expression {
if len(a) == 0 {
return exception("'io-handle?' expected a value, but no value was given")
}
switch a[0].(type) {
case *IOHandle:
return true
default:
return false
}
},
"string-buf?": func(a ...expression) expression {
if len(a) == 0 {
return exception("'string-buf?' expected a value, but no value was given")
}
switch v := a[0].(type) {
case *IOHandle:
_, ok := v.Obj.(*strings.Builder)
return ok
default:
return false
}
},
"bool?": func(a ...expression) expression {
if len(a) == 0 {
return exception("'bool?' expected a value, but no value was given")
}
switch a[0].(type) {
case bool:
return true
default:
return false
}
},
"symbol?": func(a ...expression) expression {
if len(a) == 0 {
return exception("'symbol?' expected a value, but no value was given")
}
switch a[0].(type) {
case symbol:
return true
default:
return false
}
},
"null?": func(a ...expression) expression {
if len(a) == 0 {
return exception("'null?' expected a list, but no value was given")
}
switch i := a[0].(type) {
case []expression:
return len(i) == 0
default:
return false
}
},
"pair?": func(a ...expression) expression {
if len(a) == 0 {
return exception("'pair?' expected a value, but no value was given")
}
switch i := a[0].(type) {
case []expression:
return len(i) > 0
default:
return false
}
},
"list?": func(a ...expression) expression {
if len(a) == 0 {
return exception("'list?' expected a value, but no value was given")
}
switch a[0].(type) {
case []expression:
return true
default:
return false
}
},
"macro?": func(a ...expression) expression {
if len(a) == 0 {
return exception("'macro?' expected a value, but no value was given")
}
switch a[0].(type) {
case macro:
return true
default:
return false
}
},
"procedure?": func(a ...expression) expression {
if len(a) == 0 {
return exception("'proc?' expected a value, but no value was given")
}
switch a[0].(type) {
case proc, func(a ...expression) expression:
return true
default:
return false
}
},
"length": func(a ...expression) expression {
switch i := a[0].(type) {
case []expression:
return number(len(i))
case string:
return number(len([]rune(String(i, false))))
case exception:
return number(len([]rune(String(string(i), false))))
case *IOHandle:
switch h := i.Obj.(type) {
case *strings.Builder:
return number(len([]rune(String(h.String(), false))))
default:
return exception("'length' is not defined for a non-string-buf IOHandle")
}
default:
return exception("'length' expected a list or a string, but the given value was not a list or a string")
}
},
"string-make-buf": func(a ...expression) expression {
var b strings.Builder
if len(a) > 0 {
b.WriteString(String(a[0], false))
}
obj := &IOHandle{&b, true, "string-buf"}
openFiles = append(openFiles, obj)
return obj
},
"string-buf-clear": func(a ...expression) expression {
if len(a) == 0 {
return exception("'string-buf-clear' expects an IOHandle for a string-buf as an argument, no argument was given")
}
h, ok := a[0].(*IOHandle)
if !ok {
return exception("'string-buf-clear' expects an IOHandle as its argument, a non-IOHandle argument was given")
}
if !h.Open {
return exception("a closed IOHandle was given to 'string-buf-clear'")
}
switch buf := h.Obj.(type) {
case *strings.Builder:
buf.Reset()
default:
return exception("'string-buf-clear' expects an IOHandle representing a string-buf as its argument, a non-string-buf-IOHandle argument was given")
}
return make([]expression, 0)
},
"string-upper": func(a ...expression) expression {
if len(a) == 0 {
return exception("no value was given to 'string-upper'")
}
s, ok := a[0].(string)
if !ok {
return exception("'string-upper' expected a string but received a non-string value")
}
return strings.ToUpper(s)
},
"string-lower": func(a ...expression) expression {
if len(a) == 0 {
return exception("no value was given to 'string-lower'")
}
s, ok := a[0].(string)
if !ok {
return exception("'string-lower' expected a string but received a non-string value")
}
return strings.ToLower(s)
},
"string-format": func(a ...expression) expression {
if len(a) == 0 {
return exception("no value was given to 'string-format'")
}
format, ok := a[0].(string)
if !ok {
return exception("'string-format' expected a format string and was given a non-string value as its first argument")
}
if len(a) == 1 {
return format
}
var outputBuf, percentBuf strings.Builder
parsePercent := false
varNum := 1
for _, c := range format {
if parsePercent {
if c == 'v' {
parsePercent = false
if varNum < len(a) {
s, err := formatValue(percentBuf.String(), a[varNum])
if err != nil {
return exception("'string-format' was given an invalid formatting specifier")
}
outputBuf.WriteString(s)
percentBuf.Reset()
varNum++
} else {
return exception("'string-format' was not given enough values to format the string")
}
} else {
percentBuf.WriteRune(c)
}
} else if c == '%' {
parsePercent = true
} else if c == '%' {
outputBuf.WriteRune(c)
} else {
outputBuf.WriteRune(c)
}
}
return unescapeString(outputBuf.String())
},
"string-index-of": func(a ...expression) expression {
// (string-index-of [haystack] [needle])
if len(a) < 2 {
return exception("'string-index-of' expects two strings, a haystack and a needle, insufficient arguments were given")
}
haystack, ok := a[0].(string)
if !ok {
return exception("'string-index-of' expects a string as its first argument; a non-string value was given")
}
needle, ok := a[1].(string)
if !ok {
return exception("'string-index-of' expects a string as its second argument; a non-string value was given")
}
return number(strings.Index(haystack, needle))
},
"string-ref": func(a ...expression) expression {
// TODO remove this proc entirely someday?
return globalenv.vars["ref"].(func(...expression) expression)(a...)
},
"cons": func(a ...expression) expression {
if len(a) < 2 {
return exception(fmt.Sprintf("'cons' expected two arguments, %d given", len(a)))
}
switch car := a[0]; cdr := a[1].(type) {
case []expression:
return append([]expression{car}, cdr...)
default:
return []expression{car, cdr}
}
},
"car": func(a ...expression) expression {
if len(a) < 1 {
return exception(fmt.Sprintf("%q expected a list with at least one item in it, but a nil value was given", "car"))
}
switch i := a[0].(type) {
case []expression:
return a[0].([]expression)[0]
default:
return exception(fmt.Sprintf("%q expected a list, but a %T was given", "car", i))
}
},
"cdr": func(a ...expression) expression {
if len(a) < 1 {
return exception("\"cdr\" expected a list with at least one item in it, but a nil value was given")
}
switch i := a[0].(type) {
case []expression:
return a[0].([]expression)[1:]
default:
return exception(fmt.Sprintf("%q expected a list, but a %T was given", "car", i))
}
},
"list->string": func(a ...expression) expression {
if len(a) == 0 {
return exception("insufficient number of arguments given to 'list->string', expected a list and a string")
}
listIn, ok := a[0].([]expression)
if !ok {
return exception("'list->string' expected a list, but was given a non-list value")
}
joinOn := ""
if len(a) >= 2 {
switch sp := a[1].(type) {
case string:
joinOn = sp
case exception:
joinOn = string(sp)
case symbol:
joinOn = string(sp)
case number:
joinOn = strconv.Itoa(int(sp))
case []expression:
joinOn = String(sp, false)
default:
return exception("'list->string' could not convert the given join point to a string upon which to join")
}
}
sSlice := make([]string, len(listIn))
for i := range listIn {
sSlice[i] = String(listIn[i], false)
}
return strings.Join(sSlice, joinOn)
},
"range": func(a ...expression) expression {
if len(a) == 0 {
return make([]expression, 0)
}
start := number(0)
step := number(1)
count, ok := a[0].(number)
if !ok {
return exception("'range' expected a number representing a count, a non-number value was given")
}
if int(count) < 1 {
return exception("'range' expected a number greater than zero as its first argument, an invalid value was given")
}
if len(a) > 1 {
start, ok = a[1].(number)
if !ok {
return exception("'range' expected a number representing a starting point as its second argument, a non-number value was given")
}
}
if len(a) > 2 {
step, ok = a[2].(number)
if !ok {
return exception("'range' expected a number representing a step count as its third argument, a non-number value was given")
}
}
out := make([]expression, 0, int(count))
for ; count > 0; count-- {
out = append(out, start)
start += step
}
return out
},
"list-seed": func(a ...expression) expression {
if len(a) < 2 {
return exception("insufficient number of arguments given to 'list-seed', expected number and value")
}
count, ok := a[0].(number)
if !ok {
return exception("'list-seed' expected a number as its first argument, but was given a non-number value")
}
if count < 1 {
return exception("'list-seed' expects a positive non-zero number as its first argument, a number less than 1 was given")
}
l := make([]expression, int(count))
switch t := a[1].(type) {
case []expression:
for i := range l {
s := make([]expression, len(t))
copy(s, t)
l[i] = s
}
default:
for i := range l {
l[i] = t
}
}
return l
},
"list-sort": func(a ...expression) expression {
if len(a) == 0 {
return exception("insufficient number of arguments given to 'list-sort', expected a list")
}
listIn, ok := a[0].([]expression)
if !ok {
return exception("'list-sort' expected a list, but was given a non-list value")
}
if len(a) > 1 {
n, ok := a[1].(number)
if !ok {
return exception("'list-sort' expected a number as its optional second argument, but a non-number was given")
}
return MergeSort(listIn, int(n))
}
return MergeSort(listIn, -1)
},
"list-join": func(a ...expression) expression {
if len(a) == 0 {
return make([]expression, 0)
}
base, ok := a[0].([]expression)
if !ok {
return exception("'list-join' expected a list but was given a non-list value")
}
out := DeepCopySlice(base).([]expression)
for _, v := range a[1:] {
val, ok2 := v.([]expression)
if !ok2 {
return exception("'list-join' expected a list but was given a non-list value")
}
out = append(out, val...)
}
return out
},
"append": func(a ...expression) expression {
if len(a) == 0 {
return make([]expression, 0)
}
switch base := a[0].(type) {
case []expression:
base = append(base, a[1:]...)
return base
default:
var out strings.Builder
for i := range a {
out.WriteString(String(a[i], false))
}
return out.String()
}
},
"ref": func(a ...expression) expression {
if len(a) < 2 {
return exception("'ref' expects at least two arguments, a string|list and an index number, insufficient arguments were given")
}
switch t := a[1].(type) {
case number:
i := int(t)
switch v := a[0].(type) {
case []expression:
if i >= len(v) {
return exception("'ref' was given an index number that is out of bounds for the given list")
} else if i < 0 {
i = len(v) + i
if i < 0 {
return exception("'ref' was given an index number that is out of bounds for the given list")
}
}
if len(a) >= 3 {
if len(a) > 3 && AnythingToBool(a[3]).(bool) {
v[int(i)] = a[2]
return v
} else {
cpy := DeepCopySlice(v)
cpy.([]expression)[int(i)] = a[2]
return cpy
}
}
return v[int(i)]
case string:
if i >= len(v) {
return exception("'ref' was given an index number that is out of bounds for the given list")
} else if i < 0 {
i = len(v) + i
if i < 0 {
return exception("'ref' was given an index number that is out of bounds for the given list")
}
}
if len(a) >= 3 {
s := String(a[2], false)
if len(s) > 0 {
return v[:i] + s + v[i+1:]
} else {
return v[:i] + v[i+1:]
}
}
return string(v[int(i)])
default:
return exception("'ref' was given a non-string non-list value")
}
case []expression:
switch v := a[0].(type) {
case []expression:
if len(t) == 0 {
return exception("'ref' was given an index list that is empty")
}
var val expression
var copyList bool = !(len(a) > 3 && AnythingToBool(a[3]).(bool))
var newList expression
if copyList {
newList = DeepCopySlice(v)
val = newList
} else {
val = v
}
for count, i := range t {
if list, ok := val.([]expression); ok {
if num, ok := i.(number); ok {
if int(num) >= len(list) {
return exception("'ref' was given a list of indexes containing an out of bounds reference")
} else if int(num) < 0 {
num = number(len(list) + int(num))
if int(num) < 0 {
return exception("'ref' was given a list of indexes containing an out of bounds reference")
}
}
val = list[int(num)]
if len(a) >= 3 && count == len(t)-1 {
list[int(num)] = a[2]
if !copyList {
return v
} else {
return newList
}
}
} else {
return exception("'ref' was given a list of indexes containing a non-number value")
}
} else {
return exception("'ref' was given a list of indexes that does not correspond to the depth of the given list")
}
}
return val
case string:
if len(t) == 0 {
return exception("'ref' was given an empty string, which cannot be indexed")
}
i := int(t[0].(number))
if i >= len(v) {
return exception("'ref' was given an index number that is out of bounds for the given list")
} else if i < 0 {
i = len(v) + i
if i < 0 {
return exception("'ref' was given an index number that is out of bounds for the given list")
}
}
if len(a) >= 3 {
s := String(a[2], false)
if len(s) > 0 {
return v[:i] + s + v[i+1:]
} else {
return v[:i] + v[i+1:]
}
}
return string(v[int(i)])
default:
return exception("'ref' was given a non-string non-list value")
}
default:
return exception("'ref' expects argument two to be an index number or a list of index numbers (for sublists), a non-number/non-list argument was given")
}
},
"list": func(a ...expression) expression {
return a
},
"list-ref": func(a ...expression) expression {
// TODO remove this proc entirely someday?
return globalenv.vars["ref"].(func(...expression) expression)(a...)
},
"member?": func(a ...expression) expression {
if len(a) < 2 {
return exception("'member?' expects a list and a value to search for, insufficient arguments were given")
}
list, ok := a[0].([]expression)
if !ok {
return exception("'member?' expects argument one to be a list, a non-list argument was given")
}
for i := range list {
if reflect.DeepEqual(list[i], a[1]) {
return true
}
}
return false
},
"reverse": func(a ...expression) expression {
if len(a) == 0 {
return make([]expression, 0)
}
switch l := a[0].(type) {
case []expression:
out := make([]expression, len(l))
for i, n := range l {
j := len(l) - i - 1
out[j] = n
}
return out
case string:
var b strings.Builder
r := []rune(l)
for i := len(r) - 1; i >= 0; i-- {
b.WriteRune(r[i])
}
return b.String()
default:
return exception("'reverse' expected a list but was given a non-list value")
}
},
"not": func(a ...expression) expression {
if len(a) == 0 {
return exception("'not' expects a value but no value was given")
}
if i, ok := a[0].(bool); ok && !i {
return true
}
return false
},
"~bool": func(a ...expression) expression {
if len(a) == 0 {
return exception("`~bool` expects a value, but n value was given")
}
switch v := a[0].(type) {
case string:
if v == "" {
return false
}
return true
case number:
if v == 0 {
return false
}
return true
case *IOHandle:
return v.Open
case []expression:
if len(v) > 0 {
return true
}
return false
case bool:
return v
default:
return true
}
},
"filter": func(a ...expression) expression {
if len(a) < 2 {
return exception("'filter' expects two arguments: a procedure and an argument list, too few arguments were given")
}
switch p := a[0].(type) {
case proc, func(...expression) expression, macro:
list, ok := a[1].([]expression)
if !ok {
return exception("'filter' expects a list as its second argument, a non-list value was given")
}
out := make([]expression, 0, len(list))
for i := range list {
if AnythingToBool(apply(p, []expression{list[i]}, "Filter routine")).(bool) {
out = append(out, list[i])
}
}
return out
default:
return exception("filter expects a procedure followed by a list")
}
},
"map": func(a ...expression) expression {
if len(a) < 2 {
return exception("'map' expects a procedure followed by at least one list")
}
ml, err := MergeLists(a[1:])
if err != nil {
return exception("map error: " + err.Error())
}
mergedLists := ml.([]expression)
switch i := a[0].(type) {
case proc, func(...expression) expression:
out := make([]expression, 0, len(mergedLists))
for merge := range mergedLists {
out = append(out, apply(i, mergedLists[merge].([]expression), "Map routine"))
}
return out
default:
return exception("map expects a procedure followed by at least one list")
}
},
"reduce": func(a ...expression) expression {
if len(a) < 3 {
return exception("'reduce' expects a procedure followed by an initializer value and a list, too few arguments given")
}
switch p := a[0].(type) {
case proc, func(...expression) expression:
init := a[1]
list, ok := a[2].([]expression)
if !ok {
return exception("'reduce' expects a list as its third argument, a non-list value was given")
}
for i := range list {
init = apply(p, []expression{list[i], init}, "Reduce routine")
}
return init
default:
return exception("'reduce' expects a procedure as its first argument, a non-procedure value was given")
}
},
"for-each": func(a ...expression) expression {
if len(a) < 2 {
return exception("for-each expects a procedure followed by at least one list")
}
ml, err := MergeLists(a[1:])
if err != nil {
return exception("for-each error: " + err.Error())
}
mergedLists := ml.([]expression)
switch i := a[0].(type) {
case proc, func(...expression) expression:
for merge := range mergedLists {
apply(i, mergedLists[merge].([]expression), "For-each routine")
}
return make([]expression, 0)
default:
return exception("for-each expects a procedure followed by at least one list")
}
},
"display": func(a ...expression) expression {
var out strings.Builder
for i := range a {
out.WriteString(String(a[i], false))
}
SysoutPrint(out.String(), Sysout)
return make([]expression, 0)
},
"display-lines": func(a ...expression) expression {
var out strings.Builder
for i := range a {
switch val := a[i].(type) {
case *IOHandle:
buf, ok := val.Obj.(*strings.Builder)
if ok {
out.WriteString(buf.String())
} else {
out.WriteString(String(val, true))
}
out.WriteRune('\n')
default:
out.WriteString(String(val, false))
out.WriteRune('\n')
}
}
SysoutPrint(out.String(), Sysout)
return make([]expression, 0)
},
"write": func(a ...expression) expression {
if len(a) == 1 {
SysoutPrint(String(a[0], false), Sysout)
return make([]expression, 0)
} else if len(a) > 1 {
stringOut := String(a[0], false)
obj, ok := a[1].(*IOHandle)
if !ok {
return exception("'write' expected an IO handle as its second argument, but was not given an IO handle")
}
if !obj.Open {
return exception("'write' was given an IO handle that is already closed")
}
switch ft := obj.Obj.(type) {
case *os.File:
ft.WriteString(stringOut)
case *net.Conn:
(*ft).Write([]byte(stringOut))
case *tls.Conn:
ft.Write([]byte(stringOut))
case *strings.Builder:
ft.WriteString(stringOut)
default:
return exception("'write' was given an IO handle that is not supported")
}
return a[1].(*IOHandle)
}
return exception("'write' received insufficient arguments")
},
"write-bytes": func(a ...expression) expression {
if len(a) == 1 {
a = append(a, &IOHandle{os.Stdout, true, "file (write-only)"})
}
obj, ok := a[1].(*IOHandle)
if !ok {
return exception("'write-bytes' expected an IO handle as its argument, but was not given an IO handle")
}
if !obj.Open {
return exception("'write-bytes' was given an IO handle that is already closed")
}
switch val := a[0].(type) {
case number:
n := byte(val)
if n < 0 || n > 255 {
return exception("'write-bytes' was given a number that is not a byte")
}
switch ft := obj.Obj.(type) {
case *os.File:
ft.Write([]byte{n})
case *net.Conn:
(*ft).Write([]byte{n})
case *tls.Conn:
ft.Write([]byte{n})
case *strings.Builder:
ft.Write([]byte{n})
default:
return false
}
return a[1].(*IOHandle)
case []expression:
bys := make([]byte, 0, len(val))
for _, v := range val {
n, ok := v.(number)
if !ok {
return exception("'write-bytes' was given a non-number value (inside of a list)")
}
if n < 0 || n > 255 {
return exception("'write-bytes' was given a number that is not a byte")
}
bys = append(bys, byte(n))
}
switch ft := obj.Obj.(type) {
case *os.File:
ft.Write(bys)
case *net.Conn:
(*ft).Write(bys)
case *tls.Conn:
ft.Write(bys)
case *strings.Builder:
ft.Write(bys)
default:
return false
}
return a[1].(*IOHandle)
}
return make([]expression, 0)
},
"write-raw": func(a ...expression) expression {
if len(a) == 1 {
SysoutPrint(String(a[0], true), Sysout)
} else if len(a) >= 2 {
obj, ok := a[1].(*IOHandle)
if !ok {
return exception("'write-raw' expected an IO handle as its argument, but was not given an IO handle")
}
if !obj.Open {
return exception("'write-raw' was given an IO handle that is already closed")
}
switch ft := obj.Obj.(type) {
case *os.File:
ft.WriteString(String(a[0], true))
case *net.Conn:
(*ft).Write([]byte(String(a[0], true)))
case *tls.Conn:
ft.Write([]byte([]byte(String(a[0], true))))
case *strings.Builder:
ft.WriteString(String(a[0], true))
default:
return false
}
return a[1].(*IOHandle)
}
return make([]expression, 0)
},
"read-all": func(a ...expression) expression {
if len(a) == 0 {
text, err := ioutil.ReadAll(os.Stdin)
if err != nil {
return false
}
return string(text)
} else {
obj, ok := a[0].(*IOHandle)
if !ok {
return exception("'read-all' expected an IO handle as its argument, but was not given an IO handle")
}
if !obj.Open {
return exception("'read-all' was given an IO handle that is already closed")
}
switch f := obj.Obj.(type) {
case *os.File:
b, err := ioutil.ReadAll(f)
if err != nil {
return exception("'read-all' could not read from the given IOHandle")
}
return string(b)
case *net.Conn:
b, err := ioutil.ReadAll(*f)
if err != nil {
return exception("'read-all' could not read from the given IOHandle")
}
return string(b)
case *tls.Conn:
b, err := ioutil.ReadAll(f)
if err != nil {
return exception("'read-all' could not read from the given IOHandle")
}
return string(b)
case *strings.Builder:
return f.String()
default:
return exception("'read-all' has not been implemented for this object type")
}
}
},
"read-all-bytes": func(a ...expression) expression {
if len(a) == 0 {
a = append(a, &IOHandle{os.Stdin, true, "file (read-only)"})
}
obj, ok := a[0].(*IOHandle)
if !ok {
return exception("'read-all-bytes' expected an IO handle as its argument, but was not given an IO handle")
}
if !obj.Open {
return exception("'read-all-bytes' was given an IO handle that is already closed")
}
switch f := obj.Obj.(type) {
case *os.File:
b, err := ioutil.ReadAll(f)
if err != nil {
return exception("'read-all-bytes' could not read from the given IOHandle")
}
return ByteSliceToExpressionSlice(b)
case *net.Conn:
b, err := ioutil.ReadAll(*f)
if err != nil {
return exception("'read-all-bytes' could not read from the given IOHandle")
}
return ByteSliceToExpressionSlice(b)
case *tls.Conn:
b, err := ioutil.ReadAll(f)
if err != nil {
return exception("'read-all-bytes' could not read from the given IOHandle")
}
return ByteSliceToExpressionSlice(b)
case *strings.Builder:
return ByteSliceToExpressionSlice([]byte(f.String()))
default:
return exception("'read-all-bytes' has not been implemented for this object type")
}
},
"read-all-lines": func(a ...expression) expression {
if len(a) == 0 {
a = append(a, &IOHandle{os.Stdin, true, "file (read-only)"})
}
obj, ok := a[0].(*IOHandle)
if !ok {
return exception("'read-all-lines' expected an IO handle as its argument, but was not given an IO handle")
}
if !obj.Open {
return exception("'read-all-lines' was given an IO handle that is already closed")
}
switch f := obj.Obj.(type) {
case *os.File:
b, err := ioutil.ReadAll(f)
if err != nil {
return exception("'read-all-lines' could not read from the given IOHandle")
}
o := strings.SplitN(string(b), "\n", -1)
return StringSliceToExpressionSlice(o).([]expression)
case *net.Conn:
b, err := ioutil.ReadAll(*f)
if err != nil {
return exception("'read-all-lines' could not read from the given IOHandle")
}
o := strings.SplitN(string(b), "\n", -1)
return StringSliceToExpressionSlice(o).([]expression)
case *tls.Conn:
b, err := ioutil.ReadAll(f)
if err != nil {
return exception("'read-all-lines' could not read from the given IOHandle")
}
o := strings.SplitN(string(b), "\n", -1)
return StringSliceToExpressionSlice(o).([]expression)
case *strings.Builder:
s := f.String()
o := strings.SplitN(s, "\n", -1)
return StringSliceToExpressionSlice(o).([]expression)
default:
return exception("'read-all-lines' has not been implemented for this object type")
}
},
"read-bytes": func(a ...expression) expression {
if len(a) == 0 {
reader := bufio.NewReader(os.Stdin)
b, err := reader.ReadByte()
if err != nil {
return exception("'read-bytes' could not read from stdin")
}
return number(b)
} else {
obj, ok := a[0].(*IOHandle)
if !ok {
return exception("'read-char' expected an IO handle as its argument, but was not given an IO handle")
}
if !obj.Open {
return exception("'read-char' was given an IO handle that is already closed")
}
count := 1
if len(a) > 1 {
c, ok := a[1].(number)
if !ok {
return exception("'read-bytes' expected its optional second argument to be a number")
}
count = int(c)
}
if count < 1 {
return exception("'read-bytes' expected its second argument to be a number greater than 0")
}
b := make([]byte, count)
switch f := obj.Obj.(type) {
case *os.File:
n, err := f.Read(b)
if err != nil && err != io.EOF {
return exception("'read-byte' could not read from the given IOHandle")
} else if err != nil || n == 0 {
return false
}
if count > 1 {
return ByteSliceToExpressionSlice(b)
}
return number(b[0])
case *net.Conn:
n, err := (*f).Read(b)
if err != nil && err != io.EOF {
return exception("'read-byte' could not read from the given IOHandle")
} else if err != nil || n == 0 {
return false
}
if count > 1 {
return ByteSliceToExpressionSlice(b)
}
return number(b[0])
case *tls.Conn:
n, err := f.Read(b)
if err != nil && err != io.EOF {
return exception("'read-byte' could not read from the given IOHandle")
} else if err != nil || n == 0 {
return false
}
if count > 1 {
return ByteSliceToExpressionSlice(b)
}
return number(b[0])
default:
return exception("'read-byte' has not been implemented for this object type")
}
}
},
"read-char": func(a ...expression) expression {
if len(a) == 0 {
reader := bufio.NewReader(os.Stdin)
char, _, err := reader.ReadRune()
if err != nil {
return exception("'read-char' could not read from stdin")
}
return fmt.Sprintf("%c", char)
} else {
obj, ok := a[0].(*IOHandle)
if !ok {
return exception("'read-char' expected an IO handle as its argument, but was not given an IO handle")
}
if !obj.Open {
return exception("'read-char' was given an IO handle that is already closed")
}
// TODO add a strings.Builder to build output
// take a second arg for how many chars to read
// loop over readrune to read that many chars and
// build the buffer, then return the output. If an
// error is encountered while reading return the
// output if the len(buffer) > 0, otherwise an
// exception
byteBuf := make([]byte, 0, 3)
b := make([]byte, 1)
switch f := obj.Obj.(type) {
case *os.File:
for {
n, err := f.Read(b)
if err != nil && err != io.EOF {
return exception("'read-char' could not read from the given IOHandle")
} else if err != nil || n == 0 {
return false
}
byteBuf = append(byteBuf, b[0])
if utf8.FullRune(byteBuf) {
break
}
}
return string(byteBuf)
case *net.Conn:
for {
n, err := (*f).Read(b)
if err != nil && err != io.EOF {
return exception("'read-char' could not read from the given IOHandle")
} else if err != nil || n == 0 {
return false
}
byteBuf = append(byteBuf, b[0])
if utf8.FullRune(byteBuf) {
break
}
}
return string(byteBuf)
case *tls.Conn:
for {
n, err := f.Read(b)
if err != nil && err != io.EOF {
return exception("'read-char' could not read from the given IOHandle")
} else if err != nil || n == 0 {
return false
}
byteBuf = append(byteBuf, b[0])
if utf8.FullRune(byteBuf) {
break
}
}
return string(byteBuf)
default:
return exception("'read-char' has not been implemented for this object type")
}
}
},
"read-line": func(a ...expression) expression {
if len(a) == 0 {
a = append(a, &IOHandle{os.Stdin, true, "file (read-only)"})
}
obj, ok := a[0].(*IOHandle)
if !ok {
return exception("'read-line' expected an IO handle as its argument, but was not given an IO handle")
}
if !obj.Open {
return exception("'read-line' was given an IO handle that is already closed")
}
split := byte('\n')
if len(a) > 1 {
s := String(a[1], false)
if len(s) > 0 {
split = s[0]
}
}
switch f := obj.Obj.(type) {
case *os.File:
var o strings.Builder
for {
b := make([]byte, 1)
n, err := f.Read(b)
if n > 0 {
if b[0] == split {
break
}
o.Write(b)
}
if err != nil && err != io.EOF && o.Len() == 0 {
return exception("'read-line' could not read from the given IOHandle")
} else if err != nil && err == io.EOF {
return false
} else if err != nil {
break
}
}
return o.String()
case *net.Conn:
var o strings.Builder
for {
b := make([]byte, 1)
n, err := (*f).Read(b)
if n > 0 {
if b[0] == '\n' {
break
}
o.Write(b)
}
if err != nil && err != io.EOF && o.Len() == 0 {
return exception("'read-line' could not read from the given IOHandle")
} else if err != nil && err == io.EOF {
return false
} else if err != nil {
break
}
}
return o.String()
default:
return exception("'read-line' has not been implemented for this object type")
}
},
"newline": func(a ...expression) expression {
SysoutPrint("\r\n", Sysout)
return make([]expression, 0)
},
"exit": func(a ...expression) expression {
code := 0
if len(a) > 0 {
if v, ok := a[0].(number); ok {
code = int(v)
}
}
SafeExit(code)
return 0
},
"slice": func(a ...expression) expression {
if len(a) < 2 {
return exception("insufficient number of arguments given to 'slice', expected at least a list or string and at least one index number")
}
var n1, n2 number
var ok bool
switch val := a[0].(type) {
case []expression:
if n1, ok = a[1].(number); !ok {
return exception("'slice' expects a number as its second argument")
}
n2 = number(len(val))
if len(a) > 2 {
if n2, ok = a[2].(number); !ok {
return exception("'slice' expects a number as its third argument")
}
}
if int(n2) > len(val) {
n2 = number(len(val))
}
if n1 < 0 {
n1 = number(0)
}
if n1 >= n2 {
return make([]expression, 0)
}
return val[int(n1):int(n2)]
case string:
if n1, ok = a[1].(number); !ok {
return exception("'slice' expects a number as its second argument")
}
n2 = number(len(val))
if len(a) > 2 {
if n2, ok = a[2].(number); !ok {
return exception("'slice' expects a number as its third argument")
}
}
if int(n2) > len(val) {
n2 = number(len(val))
}
if n1 < 0 {
n1 = number(0)
}
if n1 >= n2 {
return ""
}
return val[int(n1):int(n2)]
default:
return exception("'slice' expects a string or list as its first argument, a non-string non-list value was given")
}
},
"substring": func(a ...expression) expression {
return globalenv.vars["slice"].(func(a ...expression) expression)(a...)
},
"string-trim-space": func(a ...expression) expression {
if len(a) == 0 {
return exception("insufficient number of arguments given to 'string-trim-space'")
}
if s, ok := a[0].(string); ok {
return strings.TrimSpace(s)
}
return exception("'string-trim-space' expected a string but was given a non-string value")
},
"string-append": func(a ...expression) expression {
return globalenv.vars["append"].(func(...expression) expression)(a...)
},
"rune->string": func(a ...expression) expression {
if len(a) == 0 {
return exception("'rune->string' expectes a number, no value was given")
}
n, ok := a[0].(number)
if !ok {
return exception("'rune->string' expectes a number, a non-number value was given")
}
return string(rune(n))
},
"string->rune": func(a ...expression) expression {
if len(a) == 0 {
return exception("'string->rune' expectes a string, no value was given")
}
s := String(a[0], false)
r := []rune(s)
if len(r) == 0 {
return number(0)
}
return number(r[0])
},
"rune->bytes": func(a ...expression) expression {
if len(a) == 0 {
return exception("insufficient number of arguments given to 'rune->bytes'")
}
r, ok := a[0].(number)
if !ok {
return exception("'rune->bytes' expectes a number, a non-number was given")
}
return ByteSliceToExpressionSlice([]byte(string(rune(r))))
},
"string->bytes": func(a ...expression) expression {
if len(a) == 0 {
return exception("insufficient number of arguments given to 'string->bytes'")
}
return ByteSliceToExpressionSlice([]byte(String(a[0], false)))
},
"bytes->string": func(a ...expression) expression {
if len(a) == 0 {
return exception("insufficient number of arguments given to 'bytes->string'")
}
switch b := a[0].(type) {
case number:
return string([]byte{byte(b)})
case []expression:
by := make([]byte, len(b))
for i := range b {
n, ok := b[i].(number)
if !ok {
return exception("'byte->string' encountered a non-number value in the given list")
}
by[i] = byte(n)
}
return string(by)
default:
return exception("'byte->string' expected a number or a list of numbers, an invalid item type was given")
}
},
"string->number": func(a ...expression) expression {
// (string->number [string] [[base: number]])
if len(a) == 0 {
return exception("insufficient number of arguments given to 'string->number'")
}
s, ok := a[0].(string)
if !ok {
return exception("'string->number' expects a string as its first argument, a non-string value was given")
}
base := 10
if len(a) > 1 {
b, ok2 := a[1].(number)
if !ok2 {
return exception("'string->number' expects a number representing the base being used for number parsing as its second argument, a non-number value was given")
}
base = int(b)
}
if len(a) == 1 || base == 10 {
f, err := strconv.ParseFloat(s, 64)
if err == nil {
return number(f)
}
}
i, err := strconv.ParseInt(s, base, 64)
if err != nil {
return false
}
return number(i)
},
"string-replace": func(a ...expression) expression {
if len(a) < 3 {
return exception("'string-replace' expected three values: a string, a thing to replace in that string, and a thing to replace it with. Insufficient values were given")
}
count := -1
if len(a) > 3 {
c, ok := a[3].(number)
if !ok {
return exception("'string-replace' expected its fourth argument to be a number representing a count of replacements to be made, a non-number value was given")
}
count = int(c)
}
return strings.Replace(String(a[0], false), String(a[1], false), String(a[2], false), count)
},
"string-fields": func(a ...expression) expression {
if len(a) == 0 {
return exception("insufficient number of arguments given to 'string-fields'")
}
stringIn, ok := a[0].(string)
if !ok {
return exception("'string-fields' expected a string, but was given a non-string value")
}
sf := strings.Fields(stringIn)
return StringSliceToExpressionSlice(sf).([]expression)
},
"string->list": func(a ...expression) expression {
if len(a) == 0 {
return exception("insufficient number of arguments given to 'string->list'")
}
stringIn, ok := a[0].(string)
if !ok {
return exception("'string->list' expected a string, but was given a non-string value")
}
splitPoint := ""
if len(a) >= 2 {
switch sp := a[1].(type) {
case string:
splitPoint = sp
case symbol:
splitPoint = string(sp)
case number:
splitPoint = strconv.Itoa(int(sp))
case []expression:
splitPoint = String(sp, true)
default:
return exception("'string->list' could not convert the given split point to a string upon which to split")
}
}
count := -1
if len(a) > 2 {
num, ok := a[2].(number)
if !ok {
return exception("'string->list' expected a number as its third argument, but a non-number value was given")
}
if num < 1 {
count = -1
} else {
count = int(num)
}
}
sSlice := strings.SplitN(stringIn, splitPoint, count)
return StringSliceToExpressionSlice(sSlice).([]expression)
},
"regex-match?": func(a ...expression) expression {
if len(a) < 2 {
return exception("'regex-match?' expects two arguments, a pattern string and an input string, insufficient arguments were given")
}
pattern, ok := a[0].(string)
if !ok {
return exception("'regex-match?' expects a string representing a regular expression pattern as its first argument, but was given a non-string value")
}
str, ok := a[1].(string)
if !ok {
return exception("'regex-match?' expects a string as its second argument, but was given a non-string value")
}
match, err := regexp.MatchString(pattern, str)
if err != nil {
return exception("'regex-match?' encountered a regular expression error while matching")
}
return match
},
"regex-find": func(a ...expression) expression {
if len(a) < 2 {
return exception("'regex-find' expects two arguments, a pattern string and an input string, insufficient arguments were given")
}
pattern, ok := a[0].(string)
if !ok {
return exception("'regex-find' expects a string representing a regular expression pattern as its first argument, but was given a non-string value")
}
str, ok := a[1].(string)
if !ok {
return exception("'regex-find' expects a string as its second argument, but was given a non-string value")
}
re := regexp.MustCompile(pattern)
found := re.FindAllString(str, -1)
if found == nil {
return make([]expression, 0)
}
return StringSliceToExpressionSlice(found).([]expression)
},
"regex-replace": func(a ...expression) expression {
if len(a) < 3 {
return exception("'regex-replace' expects three arguments: a pattern string, an input string, and a replacement string, insufficient arguments were given")
}
pattern, ok := a[0].(string)
if !ok {
return exception("'regex-replace' expects a string representing a regular expression pattern as its first argument, but was given a non-string value")
}
str, ok := a[1].(string)
if !ok {
return exception("'regex-find' expects a string as its second argument, but was given a non-string value")
}
replace, ok := a[2].(string)
if !ok {
return exception("'regex-find' expects a replacement string as its third argument, but was given a non-string value")
}
re := regexp.MustCompile(pattern)
return re.ReplaceAllString(str, replace)
},
"number->string": func(a ...expression) expression {
if len(a) == 0 {
return exception("insufficient number of arguments given to 'string->number'")
}
n, ok := a[0].(number)
if !ok {
return exception("'number->string' expected a number as its first argument, a non-number value was given")
}
if len(a) == 2 {
base, ok2 := a[1].(number)
if !ok2 {
return exception("'number->string' expected a number as its second argument, a non-number value was given")
}
if base != 10 {
return strconv.FormatInt(int64(n), int(base))
}
}
return strconv.FormatFloat(float64(n), 'f', -1, 64)
},
"file-create-temp": func(a ...expression) expression {
if len(a) == 0 {
return exception("'file-create-temp' expects a filename pattern as a string, no value was given")
}
fp, ok := a[0].(string)
if !ok {
return exception("'file-create-temp' expects a string representing a filename pattern, a non-string value was given")
}
f, err := os.CreateTemp("", fp)
if err != nil {
return exception("'file-create-temp' was unable to create the requested temporary file: " + err.Error())
}
obj := &IOHandle{f, true, "file (write-only)"}
openFiles = append(openFiles, obj)
return obj
},
"file-name": func(a ...expression) expression {
if len(a) == 0 {
return exception("'file-path' expects an IOHandle, no value was given")
}
ioh, ok := a[0].(*IOHandle)
if !ok {
return exception("'file-path' expects an IOHandle, a non-IOHandle value was given")
}
switch f := ioh.Obj.(type) {
case *os.File:
return f.Name()
default:
return exception("'file-path' encountered an IOHandle type that it cannot process")
}
},
"file-stat": func(a ...expression) expression {
if len(a) == 0 {
return exception("'file-stat' expects a filepath, no filepath was given")
}
fp, ok := a[0].(string)
if !ok {
return exception("'file-stat' expects a filepath as a string, a non-string value was given")
}
fp = ExpandedAbsFilepath(fp)
info, err := os.Stat(fp)
if err != nil {
if os.IsNotExist(err) {
return false
}
return exception("'file-stat' could not stat file: " + err.Error())
}
stat := info.Sys().(*syscall.Stat_t)
u := strconv.FormatUint(uint64(stat.Uid), 10)
g := strconv.FormatUint(uint64(stat.Gid), 10)
var userString string
usr, err := user.LookupId(u)
if err != nil {
userString = ""
} else {
userString = usr.Username
}
var groupString string
group, err := user.LookupGroupId(g)
if err != nil {
groupString = ""
} else {
groupString = group.Name
}
out := make([]expression, 10)
out[0] = []expression{"name", info.Name()}
out[1] = []expression{"size", number(info.Size())}
out[2] = []expression{"mode", number(info.Mode())}
out[3] = []expression{"mod-time", number(info.ModTime().Unix())}
out[4] = []expression{"is-dir?", info.IsDir()}
out[5] = []expression{"is-symlink?", false}
out[6] = []expression{"path", fp}
out[7] = []expression{"owner", userString}
out[8] = []expression{"group", groupString}
out[9] = []expression{"modestring", strconv.FormatUint(uint64(info.Mode()), 8)}
ln, err := os.Readlink(fp)
if err == nil {
out[5] = []expression{"is-symlink?", true}
if !filepath.IsAbs(ln) {
root := fp
if !info.IsDir() {
root = filepath.Dir(fp)
}
ln = ExpandedAbsFilepath(filepath.Join(root, ln))
}
out[6] = []expression{"path", ln}
}
return out
},
"file-create": func(a ...expression) expression {
if len(a) < 1 {
return exception("'file-create' expects a filepath, no filepath was given")
}
fp, ok := a[0].(string)
if !ok {
return exception("'file-create' expects a filepath as a string, a non-string value was given")
}
f, err := os.Create(fp)
if err != nil {
return exception("'file-create' encountered an error: " + err.Error())
}
obj := &IOHandle{f, true, "file (write-only)"}
openFiles = append(openFiles, obj)
return obj
},
"file-open-read": func(a ...expression) expression {
if len(a) == 0 {
return exception("'file-open-read' expects a filepath, no filepath was given")
}
if fp, ok := a[0].(string); ok {
f, err := os.Open(fp)
if err != nil {
return false
}
obj := &IOHandle{f, true, "file (read-only)"}
openFiles = append(openFiles, obj)
return obj
}
return exception("'file-open-read' expects a filepath as a string, a non-string value was given")
},
"file-open-write": func(a ...expression) expression {
// (file-open-write [filepath: string] [[truncate: bool]])
if len(a) == 0 {
return exception("'file-open-write' expects a filepath, no filepath was given")
}
if fp, ok := a[0].(string); ok {
flags := os.O_CREATE | os.O_WRONLY
if len(a) >= 2 {
if b, ok := a[1].(bool); ok && b {
flags = flags | os.O_TRUNC
}
}
f, err := os.OpenFile(fp, flags, 0664)
if err != nil {
return false
}
obj := &IOHandle{f, true, "file (write-only)"}
openFiles = append(openFiles, obj)
return obj
}
return exception("'file-write' expects a filepath as a string, a non-string value was given")
},
"file-append-to": func(a ...expression) expression {
// (file-append-to [filepath: string] [[string...]])
if len(a) < 2 {
return exception("'file-append-to' requires a filepath and at least one string")
}
if fp, ok := a[0].(string); ok {
f, err := os.OpenFile(fp, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return false
}
defer f.Close()
for i := range a[1:] {
f.WriteString(String(a[i+1], false))
}
return make([]expression, 0)
}
return exception("'file-append-to' expects a filepath, as a string, as its first argument")
},
"open?": func(a ...expression) expression {
if len(a) < 1 {
return exception("'open?' expects an IO handle, no argument was given")
}
if h, ok := a[0].(*IOHandle); ok {
return h.Open
}
return exception("'open?' expects an IO handle")
},
"close": func(a ...expression) expression {
if len(a) < 1 {
return exception("'close' expects an IO handle, no argument was given")
}
if f, ok := a[0].(*IOHandle); ok {
f.Open = false
switch o := f.Obj.(type) {
case *os.File:
o.Close()
case *net.Conn:
(*o).Close()
case *tls.Conn:
o.Close()
case *strings.Builder:
f.Open = false // duplicate here to not have a blank line
default:
return exception("'close' encountered an unsupported IO handle type")
}
} else {
return exception("'close' expects an IO handle")
}
return make([]expression, 0)
},
"path-exists?": func(a ...expression) expression {
if len(a) == 0 {
return exception("'path-exists' expects a filepath as a string, no value was supplied")
}
switch p := a[0].(type) {
case string:
_, err := os.Stat(p)
if err != nil {
return false
}
return true
case symbol:
_, err := os.Stat(string(p))
if err != nil {
return false
}
return true
default:
return exception("'path-exists' expected a string, but a non-string value was given")
}
},
"path-abs": func(a ...expression) expression {
if len(a) == 0 {
return exception("'path-abs' expects a filepath as a string, no value was given")
}
p, ok := a[0].(string)
if !ok {
return exception("'path-abs' expected a filepath as a string, a non-string value was given")
}
return ExpandedAbsFilepath(p)
},
"path-is-dir?": func(a ...expression) expression {
if len(a) == 0 {
return exception("'path-is-dir?' expects a filepath as a string, no value was given")
}
switch p := a[0].(type) {
case string:
f, err := os.Stat(ExpandedAbsFilepath(p))
if err != nil {
return exception(strings.Replace(err.Error(), "stat ", "", -1))
}
return f.IsDir()
case symbol:
f, err := os.Stat(ExpandedAbsFilepath(string(p)))
if err != nil {
return exception(strings.Replace(err.Error(), "stat ", "", -1))
}
return f.IsDir()
default:
return exception("'path-is-dir?' expected a string, but a non-string value was given")
}
},
"path-join": func(a ...expression) expression {
if len(a) == 0 {
return "/"
}
strs := make([]string, len(a))
for i := range a {
v, ok := a[i].(string)
if !ok {
return exception(fmt.Sprintf("Argument %d to 'path-join' is not a string", i+1))
}
strs[i] = v
}
return filepath.Join(strs...)
},
"path-extension": func(a ...expression) expression {
if len(a) == 0 {
return exception("'path-extension' expects a string and, optionally, a second string; no value was given")
}
p, ok := a[0].(string)
if !ok {
return exception("'path-extension' expects a path string as its first argument, a non-string value was given")
}
if len(a) > 1 {
e, ok := a[1].(string)
if !ok {
return exception("'path-extension' expects an extension as a string as its second argument, a non-string value was given")
}
ex := filepath.Ext(p)
if ex == "" {
return p + e
}
li := strings.LastIndex(p, ex)
if li < 0 {
return p
}
return p[:li] + e
}
return filepath.Ext(p)
},
"path-base": func(a ...expression) expression {
if len(a) == 0 {
return exception("'path-base' expects a string, no value was given")
}
p, ok := a[0].(string)
if !ok {
return exception("'path-base' expects a path string as its first argument, a non-string value was given")
}
return filepath.Base(p)
},
"path-dir": func(a ...expression) expression {
if len(a) == 0 {
return exception("'path-dir' expects a string, no value was given")
}
p, ok := a[0].(string)
if !ok {
return exception("'path-dir' expects a path string as its first argument, a non-string value was given")
}
return filepath.Dir(p)
},
"path-glob": func(a ...expression) expression {
if len(a) == 0 {
return exception("'path-glob' expects a filepath as a string, too few values given")
}
p, ok := a[0].(string)
if !ok {
return exception("'path-glob' expects a filepath as a string, a non-string value was given")
}
g, err := BetterGlob(p)
if err != nil {
return exception("'path-glob' received an invalid filepath glob pattern as a string")
}
return StringSliceToExpressionSlice(g)
},
"hostname": func(a ...expression) expression {
hostname, err := os.Hostname()
if err != nil {
return exception("'hostname' could not retrieve the hostname: " + err.Error())
}
return hostname
},
"net-post": func(a ...expression) expression {
if len(a) < 2 {
return []expression{false, "'net-post' expects a url string and a string or an open io-handle containing the post body/data, too few values were given"}
}
u, err := url.Parse(String(a[0], false))
if err != nil {
return []expression{false, "'net-post' was given a url string that could not be parsed as a url"}
}
switch u.Scheme {
case "http", "https":
mime := "text/plain"
if len(a) > 2 {
mime = String(a[2], false)
}
var resp *http.Response
switch data := a[1].(type) {
case *IOHandle:
if !data.Open {
return []expression{false, "'net-post' was given a closed io-handle"}
}
switch handleObj := data.Obj.(type) {
case *os.File:
resp, err = http.Post(u.String(), mime, handleObj)
case *net.Conn:
resp, err = http.Post(u.String(), mime, *handleObj)
case net.Conn:
resp, err = http.Post(u.String(), mime, handleObj)
case *tls.Conn:
resp, err = http.Post(u.String(), mime, handleObj)
case *strings.Builder:
resp, err = http.Post(u.String(), mime, strings.NewReader(handleObj.String()))
default:
return []expression{false, "'net-post' was given an unsupported io-handle type"}
}
default:
resp, err = http.Post(u.String(), mime, strings.NewReader(String(data, false)))
}
if err != nil {
return []expression{false, fmt.Sprintf("'net-post' error: %s", err.Error())}
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return []expression{false, fmt.Sprintf("'net-get' error: %s", err.Error())}
}
return []expression{true, string(body)}
case "gemini":
u.RawQuery = url.QueryEscape(String(a[1], false))
resp, mime, err := GeminiRequest(u, 0)
if err != nil {
return []expression{false, fmt.Sprintf("'net-post' error: %s", err.Error())}
}
return []expression{true, resp, mime}
case "gopher":
if u.Port() == "" {
u.Host = u.Host + ":70"
}
conn, err := net.Dial("tcp", u.Host)
if err != nil {
return []expression{false, fmt.Sprintf("'net-get' error: %s", err.Error())}
}
defer conn.Close()
p := u.Path
if len(u.Path) < 2 {
p = fmt.Sprintf("/\t%s\r\n", a[1])
} else {
p = fmt.Sprintf("%s\t%s\r\n", p[2:], a[1])
}
_, err = conn.Write([]byte(p))
if err != nil {
return []expression{false, fmt.Sprintf("'net-get' error: %s", err.Error())}
}
resp, err := io.ReadAll(conn)
if err != nil {
return []expression{false, fmt.Sprintf("'net-get' error: %s", err.Error())}
}
return []expression{true, string(resp)}
default:
return []expression{false, "'net-post' was given a url for an unsupported scheme: " + u.Scheme + ". Consider using 'net-conn' instead"}
}
},
"net-get": func(a ...expression) expression {
if len(a) < 1 {
return []expression{false, "'net-get' expects a url string, too few values were given"}