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"}
}
u, err := url.Parse(String(a[0], false))
if err != nil {
return []expression{false, "'net-get' was given a url string that could not be parsed as a url"}
}
switch u.Scheme {
case "http", "https":
resp, err := http.Get(u.String())
if err != nil {
return []expression{false, fmt.Sprintf("'net-get' 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":
resp, mime, err := GeminiRequest(u, 0)
if err != nil {
return []expression{false, fmt.Sprintf("'net-get' 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 = "/" + "\n"
} else {
p = p[2:] + "\n"
}
_, 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-get' was given a URL for an unsupported scheme: " + u.Scheme + ". Consider using 'net-conn' instead"}
}
},
"net-listen": func(a ...expression) expression {
if len(a) < 3 {
return exception(`'net-listen' expects a string or symbol ("tcp", "tcp4", "tcp6", "unix", or "unixpacket"), an address string, and a procedure or macro that accepts an io-handle. Too few values were given`)
}
network := strings.ToLower(String(a[0], false))
switch network {
case "tcp", "tcp4", "tcp6", "unix", "unixpacket":
break
default:
return exception(`'net-listen' expected the first value given to be a string equal to "tcp", "tcp4", "tcp6", "unix", or "unixpacket". An invalid value was given`)
}
address := String(a[1], false)
server, err := net.Listen(network, address)
if err != nil {
return exception(`'net-listen' could not listen at the given address: ` + address)
}
defer server.Close()
var callback func(conn *IOHandle)
switch f := a[2].(type) {
case func(...expression) expression:
callback = func(conn *IOHandle) {
f(IOHandle{&conn, true, "net-conn"})
}
case proc:
callback = func(conn *IOHandle) {
apply(f, []expression{conn}, "net-listen callback")
}
case macro:
callback = func(conn *IOHandle) {
apply(f, []expression{conn}, "net-listen callback")
}
default:
return exception(`'net-listen' expected a built-in, proc, or macro as its third value; an invalid value was given`)
}
if len(a) > 3 {
switch f := a[3].(type) {
case func(...expression) expression:
f(network, address)
case proc:
apply(f, []expression{network, address}, "net-listen init callback")
case macro:
apply(f, []expression{network, address}, "net-listen init callback")
}
}
for {
conn, err := server.Accept()
if err != nil {
return exception("'net-listen' encountered an error with a connection: " + err.Error())
}
go callback(&IOHandle{&conn, true, "net-conn"})
}
},
"net-conn": func(a ...expression) expression {
// (net-conn host port use-tls timeout-seconds)
// (net-conn string string bool number)
if len(a) < 2 {
return exception("'net-conn' expects a host and a port as a string, too few values given")
}
host, ok := a[0].(string)
if !ok {
return exception("'net-conn' expects a host as a string, a non-string value was given")
}
var port string
switch p := a[1].(type) {
case string:
port = p
case number:
port = strconv.Itoa(int(p))
default:
return exception("'net-conn' expects a port as a string, a non-string value was given")
}
usetls := false
if len(a) >= 3 {
t, ok := a[2].(bool)
if !ok {
return exception("'net-conn' expects a boolean value as the third argument (use-tls), a non-bool value was given")
}
usetls = t
}
timeout := -1
if len(a) >= 4 {
switch to := a[3].(type) {
case number:
timeout = int(to)
case string:
t, err := strconv.Atoi(to)
if err != nil {
return exception("'net-conn' was given a timeout string that does not cast to an integer")
}
timeout = t
default:
return exception("'net-conn' expects a string or number value representing a timeout, in seconds; a non-string non-number value was given ")
}
}
var conn net.Conn
var tlsconn *tls.Conn
var err error
addr := fmt.Sprintf("%s:%s", host, port)
if usetls {
conf := &tls.Config{InsecureSkipVerify: true}
if timeout > 0 {
tlsconn, err = tls.DialWithDialer(&net.Dialer{Timeout: time.Duration(timeout) * time.Second}, "tcp", addr, conf)
if err != nil {
return false
}
} else {
tlsconn, err = tls.Dial("tcp", addr, conf)
if err != nil {
return false
}
}
} else {
if timeout > 0 {
conn, err = net.DialTimeout("tcp", addr, time.Duration(timeout)*time.Second)
if err != nil {
return false
}
} else {
conn, err = net.Dial("tcp", addr)
if err != nil {
return false
}
}
}
// based on the tls value use tls or not, same with the timeout value for adding a timeout
var handle *IOHandle
if usetls {
handle = &IOHandle{tlsconn, true, "net-conn (tls)"}
} else {
handle = &IOHandle{&conn, true, "net-conn"}
}
openFiles = append(openFiles, handle)
return handle
},
"url-host": func(a ...expression) expression {
if len(a) == 0 {
return exception("'url-host' expects a url as a string, no value was given")
}
rawURL, ok := a[0].(string)
if !ok {
return exception("'url-host' expects a url as a string, a non-string value was given")
}
u, err := url.Parse(rawURL)
if err != nil {
return false
}
if len(a) >= 2 {
p := ":" + u.Port()
if p == ":" {
p = ""
}
switch s := a[1].(type) {
case string:
u.Host = s + p
case symbol:
u.Host = string(s) + p
default:
return exception("'url-host' expects either a string or a symbol as its second argument, a non-string non-symbol value was given")
}
return u.String()
}
return u.Hostname()
},
"url-port": func(a ...expression) expression {
if len(a) == 0 {
return exception("'url-port' expects a url as a string, no value was given")
}
rawURL, ok := a[0].(string)
if !ok {
return exception("'url-port' expects a url as a string, a non-string value was given")
}
u, err := url.Parse(rawURL)
if err != nil {
return false
}
if len(a) >= 2 {
switch s := a[1].(type) {
case string:
u.Host = u.Hostname() + ":" + s
case symbol:
u.Host = u.Hostname() + ":" + string(s)
case number:
u.Host = u.Hostname() + ":" + strconv.Itoa(int(s))
default:
return exception("'url-port' expects either a string or a symbol as its second argument, a non-string non-symbol value was given")
}
return u.String()
}
return u.Port()
},
"url-query": func(a ...expression) expression {
if len(a) == 0 {
return exception("'url-query' expects a url as a string, no value was given")
}
rawURL, ok := a[0].(string)
if !ok {
return exception("'url-query' expects a url as a string, a non-string value was given")
}
u, err := url.Parse(rawURL)
if err != nil {
return false
}
if len(a) >= 2 {
switch s := a[1].(type) {
case string:
u.RawQuery = s
case symbol:
u.RawQuery = string(s)
default:
return exception("'url-query' expects either a string or a symbol as its second argument, a non-string non-symbol value was given")
}
return u.String()
}
return u.RawQuery
},
"url-path": func(a ...expression) expression {
if len(a) == 0 {
return exception("'url-path' expects a url as a string, no value was given")
}
rawURL, ok := a[0].(string)
if !ok {
return exception("'url-path' expects a url as a string, a non-string value was given")
}
u, err := url.Parse(rawURL)
if err != nil {
return false
}
if len(a) >= 2 {
switch s := a[1].(type) {
case string:
u.Path = s
case symbol:
u.Path = string(s)
default:
return exception("'url-path' expects either a string or a symbol as its second argument, a non-string non-symbol value was given")
}
return u.String()
}
return u.EscapedPath()
},
"url-scheme": func(a ...expression) expression {
if len(a) == 0 {
return exception("'url-scheme' expects a url as a string, no value was given")
}
rawURL, ok := a[0].(string)
if !ok {
return exception("'url-scheme' expects a url as a string, a non-string value was given")
}
u, err := url.Parse(rawURL)
if err != nil {
return false
}
if len(a) >= 2 {
switch s := a[1].(type) {
case string:
u.Scheme = s
case symbol:
u.Scheme = string(s)
default:
return exception("'url-scheme' expects either a string or a symbol as its second argument, a non-string non-symbol value was given")
}
return u.String()
}
return u.Scheme
},
"license": func(a ...expression) expression {
SysoutPrint(licenseText, Sysout)
return make([]expression, 0)
},
"chdir": func(a ...expression) expression {
if len(a) == 0 {
return exception("'chdir' expects a path as a string, no value was given")
}
p, ok := a[0].(string)
if !ok {
return exception("'chdir' expects a string; a non-string value was given")
}
err := os.Chdir(ExpandedAbsFilepath(p))
if err != nil {
return exception(err.Error())
}
return make([]expression, 0)
},
"chmod": func(a ...expression) expression {
if len(a) < 2 {
return exception("'chmod' expects a path as a string and a file mode as a number, no value was given")
}
p, ok := a[0].(string)
if !ok {
return exception("'chdir' expects a string; a non-string value was given")
}
n, ok := a[1].(number)
err := os.Chmod(ExpandedAbsFilepath(p), os.FileMode(n))
if err != nil {
return exception(err.Error())
}
return make([]expression, 0)
},
"env": func(a ...expression) expression {
if len(a) == 0 {
e := make([]expression, 0, 20)
env := os.Environ()
for _, v := range env {
sp := strings.Split(v, "=")
if len(sp) < 2 {
sp = append(sp, "")
}
e = append(e, []expression{sp[0], sp[1]})
}
return e
}
if len(a) == 1 {
switch i := a[0].(type) {
case string:
return os.Getenv(i)
default:
return exception("Invalid argument to single argument 'env' call, was not given a string")
}
}
variable, ok := a[0].(string)
if !ok {
return exception("Invalid argument to dual argument 'env' call, was not given a string as the first argument")
}
err := os.Setenv(variable, String(a[1], false))
if err != nil {
return exception(err.Error())
}
return make([]expression, 0)
},
"mkdir": func(a ...expression) expression {
if len(a) == 0 {
return exception("'mkdir' expects a path string and a number representing file permissions and optionally a bool; no value was given")
}
path, ok1 := a[0].(string)
if !ok1 {
return exception("'mkdir' expects a path string as its first argument, a non-string value was given")
}
perms, ok2 := a[1].(number)
if !ok2 {
return exception("'mkdir' expects a number representing a file permissions setting as its second argument, a non-number value was given")
}
prmInt := os.FileMode(perms)
var ok3, mkdirAll bool
if len(a) > 2 {
mkdirAll, ok3 = a[2].(bool)
if !ok3 {
return exception("'mkdir' expects a bool as its third argument, a non-bool value was given")
}
}
var err error
if mkdirAll {
err = os.MkdirAll(ExpandedAbsFilepath(path), prmInt)
} else {
err = os.Mkdir(ExpandedAbsFilepath(path), prmInt)
}
if err != nil {
return exception("'mkdir' could not create directories for the given path with the given permissions: " + err.Error())
}
return make([]expression, 0)
},
"rm": func(a ...expression) expression {
if len(a) == 0 {
return exception("'rm' expects a path string and, optionally, a bool; no value was given")
}
path, ok1 := a[0].(string)
if !ok1 {
return exception("'rm' expects a path string as its first argument, a non-string value was given")
}
var ok2, mkdirAll bool
if len(a) > 1 {
mkdirAll, ok2 = a[1].(bool)
if !ok2 {
return exception("'rm' expects a bool as its second argument, a non-bool value was given")
}
}
var err error
if mkdirAll {
err = os.RemoveAll(ExpandedAbsFilepath(path))
} else {
err = os.Remove(ExpandedAbsFilepath(path))
}
if err != nil {
return exception("'mkdir' could not create directories for the given path with the given permissions: " + err.Error())
}
return make([]expression, 0)
},
"pwd": func(a ...expression) expression {
wd, err := os.Getwd()
if err != nil {
return exception("'pwd' could not retrieve the current working directory: " + err.Error())
}
return wd
},
"mv": func(a ...expression) expression {
if len(a) < 2 {
return exception("'mv' expected two path strings (from and to), but was given an insufficient number of arguments")
}
from, ok := a[0].(string)
if !ok {
return exception("'mv' expected a path string as its first argument, a non-string value was given")
}
to, ok2 := a[1].(string)
if !ok2 {
return exception("'mv' expected a path string as its second argument, a non-string value was given")
}
err := os.Rename(from, to)
if err != nil {
return exception("'mv' could not perform the requested action: " + err.Error())
}
return make([]expression, 0)
},
"subprocess": func(a ...expression) expression {
if len(a) == 0 {
return exception("'subprocess' expects a list containing the process to start and any arguments, as strings. It can also, optionally, take 1 or 2 file handles to write Stdout and Stderr to. No arguments were given.")
}
processList, ok := a[0].([]expression)
if !ok {
return exception("'subprocess' expects a list containing the process to start and any arguments as separate strings in the list. A non-list was given.")
}
process := make([]string, len(processList))
for i := range processList {
if s, ok := processList[i].(string); ok {
process[i] = s
} else {
return exception("a list passed to 'subprocess' contained the non-string value: " + String(processList[i], true))
}
}
var o, e, i *IOHandle
if len(a) > 3 {
i1, i1ok := a[3].(*IOHandle)
if !i1ok {
if b, ok := a[3].(bool); !ok || (ok && b) {
return exception("'subprocess' was given an input redirection that is not an IOHandle or the boolean '#f'")
}
} else if !i1.Open {
return exception("'subprocess' was given an input redirection IOHandle that is already closed")
}
i = i1
}
if len(a) > 2 {
e1, e1ok := a[2].(*IOHandle)
if !e1ok {
if b, ok := a[2].(bool); !ok || (ok && b) {
return exception("'subprocess' was given an error redirection that is not an IOHandle or the boolean '#f'")
}
} else if !e1.Open {
return exception("'subprocess' was given an error redirection IOHandle that is already closed")
}
e = e1
}
if len(a) > 1 {
o1, o1ok := a[1].(*IOHandle)
if !o1ok {
if b, ok := a[1].(bool); !ok || (ok && b) {
return exception("'subprocess' was given an output redirection that is not an IOHandle or the boolean '#f'")
}
} else if !o1.Open {
return exception("'subprocess' was given an output redirection IOHandle that is already closed")
}
o = o1
}
cmd := exec.Command(process[0], process[1:]...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if o != nil {
switch io := o.Obj.(type) {
case *os.File:
cmd.Stdout = io
case *net.Conn:
cmd.Stdout = (*io)
case *tls.Conn:
cmd.Stdout = io
case *strings.Builder:
cmd.Stdout = io
default:
return exception("'subprocess' was given an output redirection IOHandle of an unknown type")
}
}
if e != nil {
switch io := e.Obj.(type) {
case *os.File:
cmd.Stderr = io
case *net.Conn:
cmd.Stderr = (*io)
case *tls.Conn:
cmd.Stderr = io
case *strings.Builder:
cmd.Stderr = io
default:
return exception("'subprocess' was given an error redirection IOHandle of an unknown type")
}
}
if i != nil {
switch io := i.Obj.(type) {
case *os.File:
cmd.Stdin = io
case *net.Conn:
cmd.Stdin = (*io)
case *tls.Conn:
cmd.Stdin = io
case *strings.Builder:
cmd.Stdin = strings.NewReader(io.String())
default:
return exception("'subprocess' was given an input redirection IOHandle of an unknown type")
}
}
err := cmd.Run()
if err != nil {
if exiterr, ok := err.(*exec.ExitError); ok {
if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
return number(status.ExitStatus())
}
} else {
return exception("'subprocess' encountered an error running the given process and the process could not be run")
}
}
return number(0)
},
"term-size": func(a ...expression) expression {
cols, rows := termios.GetWindowSize()
return []expression{number(cols), number(rows)}
},
"term-restore": func(a ...expression) expression {
termios.Restore()
return make([]expression, 0)
},
"term-char-mode": func(a ...expression) expression {
termios.SetCharMode()
return make([]expression, 0)
},
"term-raw-mode": func(a ...expression) expression {
termios.SetRawMode()
return make([]expression, 0)
},
"term-sane-mode": func(a ...expression) expression {
termios.SetSaneMode()
return make([]expression, 0)
},
"term-cooked-mode": func(a ...expression) expression {
termios.SetCookedMode()
return make([]expression, 0)
},
"timestamp": func(a ...expression) expression {
return number(time.Now().Unix())
},
"date": func(a ...expression) expression {
// (date [[format: string]])
if len(a) == 0 {
return time.Now().Format("Mon Jan 2 15:04:05 -0700 MST 2006")
}
layout, ok := a[0].(string)
if !ok {
return exception("'date' was given a non-string layout value")
}
return time.Now().Format(createTimeFormatString(layout))
},
"timestamp->date": func(a ...expression) expression {
// (timestamp->date [timestamp: string] [[layout: string]])
if len(a) >= 1 {
layout := "Mon Jan 2 15:04:05 -0700 MST 2006"
var dateint int64
switch d := a[0].(type) {
case string:
di, err := strconv.ParseInt(d, 10, 64)
if err != nil {
return exception("'timestamp->date' was given an invalid timestamp string, it could not be converted to a number")
}
dateint = di
case number:
dateint = int64(d)
default:
return exception("'timestamp->date' was given a non-string non-number value as a timestamp")
}
if len(a) >= 2 {
var ok1 bool
layout, ok1 = a[1].(string)
if !ok1 {
return exception("'timestamp->date' was given a non-string layout value")
}
layout = createTimeFormatString(layout)
}
t := time.Unix(dateint, 0)
return t.Local().Format(layout)
}
return exception("'timestamp->date' received an insufficient argument count, expected 1 or 2 arguments")
},
"date-format": func(a ...expression) expression {
// (date-format [input-format: string] [input-date: string] [output-format: string])
if len(a) == 3 {
// datestring->datestring
layout1, ok1 := a[0].(string)
datestring, ok2 := a[1].(string)
layout2, ok3 := a[2].(string)
if !ok1 || !ok2 || !ok3 {
return exception("'date-format' was given a non-string argument, it expects strings")
}
t, err := time.ParseInLocation(createTimeFormatString(layout1), datestring, time.Now().Location())
if err != nil {
return exception("'date-format' could not parse the input date to a time value based on the given layout string")
}
return t.Format(createTimeFormatString(layout2))
}
return exception("'date-format' received an unexpected number of arguments")
},
"date->timestamp": func(a ...expression) expression {
// (date->timestamp [date: string] [format: string])
if len(a) == 2 {
layout1, ok1 := a[1].(string)
datestring, ok2 := a[0].(string)
if !ok1 || !ok2 {
return exception("'date->timestamp' was given a non-string argument, it expects strings")
}
t, err := time.ParseInLocation(createTimeFormatString(layout1), datestring, time.Now().Location())
if err != nil {
return exception("'date->timestamp' could not parse the input date to a time value based on the given layout string")
}
return number(t.Unix())
}
return exception("'date' received an unexpected number of arguments")
},
"date-default-format": func(a ...expression) expression {
return "%w %f %d %g:%I:%S %O %Z %Y"
},
"sleep": func(a ...expression) expression {
// (sleep [ms: number])
if len(a) > 0 {
ms, ok := a[0].(number)
if !ok {
return exception("'sleep' was given a non-number value")
}
time.Sleep(time.Duration(ms) * time.Millisecond)
}
return make([]expression, 0)
},
"rand-seed": func(a ...expression) expression {
if len(a) == 0 {
return exception("'rand-seed' expected a number, no value was given")
}
seed, ok := a[0].(number)
if !ok {
return exception("'rand-seed' expected a number, a non-number value was given")
}
rand.Seed(int64(seed))
return true
},
"rand": func(a ...expression) expression {
if len(a) == 0 {
// equal to: (rand 1 0)
return number(rand.Float64())
}
var max, min number
var ok bool
if len(a) >= 1 {
// equal to: (rand [max] 0)
max, ok = a[0].(number)
if !ok {
return exception("'rand' expected a number as its first argument, a non-number value was given")
}
}
if len(a) == 1 {
return number(rand.Float64() * float64(max))
}
// equal to: (rand [max] [min])
min, ok = a[1].(number)
if !ok {
return exception("'rand' expected a number as its second argument, a non-number value was given")
}
return number(rand.Float64()*(float64(max)-float64(min)) + float64(min))
},
"string->md5": func(a ...expression) expression {
if len(a) == 0 {
return exception("'string->md5' expects a string, no value was given")
}
s, ok := a[0].(string)
if !ok {
return exception("'string->md5' expected a string as its first argument, a non-string argument was given")
}
return fmt.Sprintf("%x", md5.Sum([]byte(s)))
},
"string->sha256": func(a ...expression) expression {
if len(a) == 0 {
return exception("'string->sha256' expects a string, no value was given")
}
s, ok := a[0].(string)
if !ok {
return exception("'string->sha256' expected a string as its first argument, a non-string argument was given")
}
return fmt.Sprintf("%x", sha256.Sum256([]byte(s)))
},
"ns-create": func(a ...expression) expression {
if len(a) == 0 {
return exception("'ns-create' expects a string, no argument was given")
}
s := String(a[0], false)
if strings.ContainsAny(s, " ()\"#':") {
return exception("'ns-create' cannot create an env with a name containing any of the following characters: \033[1m:'( )\"#\033[0m")
}
if _, ok := namespaces[s]; ok {
return exception("'ns-create' cannot create an env with a name that is already in use")
}
namespaces[s] = env{vars(map[symbol]expression{}), &globalenv}
// Add magic string (slope custom env)
namespaces[s].vars[magicCustomEnv] = true
return true
},
"ns-delete": func(a ...expression) expression {
if len(a) == 0 {
return exception("'ns-delete' expects a string representing an env name and, optionally, a string representing a symbol in the given env. No argument was given")
}
s := String(a[0], false)
if _, ok := namespaces[s]; ok {
if _, ok := namespaces[s].vars[magicCustomEnv]; ok {
if len(a) > 1 {
delete(namespaces[s].vars, symbol(String(a[1], false)))
} else {
delete(namespaces, s)
}
return true
} else {
return exception("'ns-delete' cannot delete loaded environments, only custom environments made with 'ns-create'")
}
}
return exception("'ns-delete' could not find the env in question")
},
"ns-list": func(a ...expression) expression {
if len(a) > 0 {
s := String(a[0], false)
if ns, ok := namespaces[s]; ok {
if _, magicPresent := ns.vars[magicCustomEnv]; magicPresent {
symbols := make([]expression, 0, 5)
for k, _ := range ns.vars {
if k == magicCustomEnv {
continue
}
symbols = append(symbols, string(k))
}
return symbols
} else {
return exception("'ns-list' cannot list loaded environments, only custom environments made with 'ns-create'")
}
} else {
return exception("'ns-list' could not find the requested env")
}
} else {
envs := make([]expression, 0, 5)
for k, v := range namespaces {
if _, ok := v.vars[magicCustomEnv]; ok {
envs = append(envs, k)
}
}
return envs
}
},
"ns-define": func(a ...expression) expression {
if len(a) < 3 {
return exception("'ns-define' expects three values: the env name, the symbol name, and a value. Insufficient arguments were given")
}
envS := String(a[0], false)
symS := String(a[1], false) // FIXME this should ensure string or symbol and not coerce
if _, ok := namespaces[envS]; ok {
if _, ok := namespaces[envS].vars[magicCustomEnv]; ok {
namespaces[envS].vars[symbol(symS)] = a[2]
return a[2]
} else {
return exception("'ns-define' can only define new variables for custom environments made with 'ns-create'")
}
}
return exception("'ns-define' could not find the env in question")
},
"ns->string": func(a ...expression) expression {
if len(a) < 1 {
return exception("'ns->string' requires the name of an env, no arguments were given")
}
s := String(a[0], false)
var buf strings.Builder
ns, ok := namespaces[s]
if !ok {
return exception("'ns->string' could not find the requested env")
}
if _, magicPresent := ns.vars[magicCustomEnv]; magicPresent {
buf.WriteString(";;; ")
buf.WriteString(s)
buf.WriteString("\n;; Exported via ns->string:\n\n")
buf.WriteString(strings.Join(ExpressionSliceToStringSlice(a[1:]), "\n\n"))
if len(a[1:]) > 0 {
buf.WriteString("\n\n")
}
for k, v := range ns.vars {
if k == magicCustomEnv {
continue
}
buf.WriteString("(define ")
buf.WriteString(string(k))
buf.WriteRune(' ')
buf.WriteString(strings.ReplaceAll(String(v, true), fmt.Sprintf("%s::", s), ""))
buf.WriteString(")\n\n")
}
return buf.String()
}
return exception("'ns->string' cannot export loaded environments, only custom environments made with 'ns-create'")
},
"coeval": func(a ...expression) expression {
// (coeval [proc [args]]...)
// Takes a variable number of args. All args should
// be a list containing a proc and a list of args to
// that proc
//
// Use apply instead of eval (which was used in the special form)
// Pass 'coeval - arg (#)' as the name for apply
if len(a) == 0 {
return exception("'coeval' expects at least one argument (a list containing a procedure and a list of arguments to the procedure)")
}
for i := range a {
if list, ok := a[i].([]expression); ok {
if len(list) == 0 {
return exception("'coeval' expects lists with a proc and a list of arguments, an empty list was given")
}
_, procOk := list[0].(proc)
_, funcOk := list[0].(func(...expression) expression)
if !procOk && !funcOk {
return exception("'The first item in a list given to 'coeval' should be a procedure, a non-procedure value was given")
}
} else {
return exception("'coeval' expects its arguments to be lists, a non-list value was given")
}
}
// Validate args here before moving on
wg := new(sync.WaitGroup)
wg.Add(len(a))
for i := range a {
input := a[i].([]expression)
var list []expression
if len(input) == 1 {
list = make([]expression, 0)
} else {
switch item := input[1].(type) {
case []expression:
list = item
default:
list = []expression{item}
}
}
go func(w *sync.WaitGroup, p expression, args []expression, name expression) {
defer w.Done()
apply(p, args, name)
}(wg, input[0], list, fmt.Sprintf("Coeval procedure argument %d", i))
}
wg.Wait()
return make([]expression, 0)
},
}