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) }, }