442 lines
11 KiB
Go
442 lines
11 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
|
|
"git.rawtext.club/sloum/slope/termios"
|
|
)
|
|
|
|
type proc struct {
|
|
params expression
|
|
body expression
|
|
en *env
|
|
}
|
|
|
|
func eval(exp expression, en *env) (value expression) {
|
|
switch e := exp.(type) {
|
|
case symbol:
|
|
value = en.Find(e).vars[e]
|
|
case string:
|
|
value = unescapeString(e)
|
|
case bool, number:
|
|
value = e
|
|
case exception:
|
|
if panicOnException {
|
|
panic(string(e))
|
|
}
|
|
value = e
|
|
case []expression:
|
|
switch car, _ := e[0].(symbol); car {
|
|
case "quote":
|
|
if len(e) < 2 {
|
|
value = exception("Invalid 'quote' syntax - too few arguments")
|
|
break
|
|
}
|
|
value = e[1]
|
|
case "if":
|
|
if len(e) < 3 {
|
|
value = exception("Invalid 'if' syntax - too few arguments")
|
|
break
|
|
}
|
|
if AnythingToBool(eval(e[1], en)).(bool) {
|
|
value = eval(e[2], en)
|
|
} else if len(e) > 3 {
|
|
value = eval(e[3], en)
|
|
} else {
|
|
value = make([]expression, 0)
|
|
}
|
|
case "and":
|
|
if len(e) < 2 {
|
|
value = exception("Invalid 'and' syntax - too few arguments")
|
|
break
|
|
}
|
|
OuterAnd:
|
|
for i := range e[1:] {
|
|
switch item := e[i+1].(type) {
|
|
case []expression:
|
|
value = eval(item, en)
|
|
if !AnythingToBool(value).(bool) {
|
|
break OuterAnd
|
|
}
|
|
default:
|
|
value = eval(item, en)
|
|
if !AnythingToBool(value).(bool) {
|
|
break OuterAnd
|
|
}
|
|
}
|
|
}
|
|
case "or":
|
|
if len(e) < 2 {
|
|
value = exception("Invalid 'or' syntax - too few arguments")
|
|
break
|
|
}
|
|
OuterOr:
|
|
for i := range e[1:] {
|
|
switch item := e[i+1].(type) {
|
|
case []expression:
|
|
value = eval(item, en)
|
|
if AnythingToBool(value).(bool) {
|
|
break OuterOr
|
|
}
|
|
default:
|
|
value = eval(item, en)
|
|
if AnythingToBool(value).(bool) {
|
|
break OuterOr
|
|
}
|
|
}
|
|
}
|
|
case "cond":
|
|
if len(e) < 2 {
|
|
value = exception("Invalid 'cond' syntax - too few arguments")
|
|
break
|
|
}
|
|
CondLoop:
|
|
for _, exp := range e[1:] {
|
|
switch i := exp.(type) {
|
|
case []expression:
|
|
if len(i) < 2 {
|
|
value = exception("Invalid 'cond' case, cases must take the form: `(<test> <expression>)")
|
|
break CondLoop
|
|
}
|
|
if i[0] == "else" || i[0] == symbol("else") {
|
|
value = eval(i[1], en)
|
|
break CondLoop
|
|
}
|
|
if AnythingToBool(eval(i[0], en)).(bool) {
|
|
value = eval(i[1], en)
|
|
break CondLoop
|
|
}
|
|
default:
|
|
value = exception("Invalid 'cond' case, cases must take the form: `(<test> <expression>)")
|
|
break CondLoop
|
|
}
|
|
}
|
|
case "set!":
|
|
if len(e) < 3 {
|
|
value = exception("Invalid 'set!' syntax - too few arguments")
|
|
break
|
|
}
|
|
v, ok := e[1].(symbol)
|
|
if !ok {
|
|
value = exception("'set!' expected a symbol as its first argument, a non-symbol was provided")
|
|
break
|
|
}
|
|
val := eval(e[2], en)
|
|
en.Find(v).vars[v] = val
|
|
value = val
|
|
case "define":
|
|
if len(e) < 3 {
|
|
value = exception("Invalid 'define' syntax - too few arguments")
|
|
break
|
|
}
|
|
if _, ok := e[1].(symbol); !ok {
|
|
value = exception("'define' expects a symbol as its first argument")
|
|
break
|
|
}
|
|
val := eval(e[2], en)
|
|
en.vars[e[1].(symbol)] = val
|
|
value = val
|
|
case "lambda", "λ":
|
|
if len(e) < 3 {
|
|
value = exception("'lambda' expects at least three arguments")
|
|
break
|
|
}
|
|
b := []expression{symbol("begin")}
|
|
b = append(b, e[2:]...)
|
|
value = proc{e[1], b, en}
|
|
case "begin":
|
|
for _, i := range e[1:] {
|
|
value = eval(i, en)
|
|
}
|
|
case "begin0":
|
|
for ii, i := range e[1:] {
|
|
if ii == 0 {
|
|
value = eval(i, en)
|
|
} else {
|
|
eval(i, en)
|
|
}
|
|
}
|
|
case "usage":
|
|
var procSigRE = regexp.MustCompile(`(?s)(\()([^() ]+\b)([^)]*)(\))(?:(\s*=>)([^\n]*))?(.*)`)
|
|
var replacer = "\033[40;33;1m$1\033[95m$2\033[92m$3\033[33m$4\033[94m$5\033[36m$6\033[0m$7"
|
|
if len(e) < 2 {
|
|
var out strings.Builder
|
|
header := "(usage [[procedure: symbol]])\n\n\033[1;4mKnown procedures\033[0m\n\n"
|
|
out.WriteString(procSigRE.ReplaceAllString(header, replacer))
|
|
keys := make([]string, 0, len(usageStrings))
|
|
for key, _ := range usageStrings {
|
|
keys = append(keys, key)
|
|
}
|
|
|
|
var width int = 60
|
|
if globalenv.vars[symbol("slope-interactive?")] != false {
|
|
width, _ = termios.GetWindowSize()
|
|
}
|
|
printedWidth := 0
|
|
sort.Strings(keys)
|
|
for i := range keys {
|
|
if printedWidth+26 >= width {
|
|
out.WriteRune('\n')
|
|
printedWidth = 0
|
|
}
|
|
out.WriteString(fmt.Sprintf("%-26s", keys[i]))
|
|
printedWidth += 26
|
|
}
|
|
|
|
if len(moduleUsageStrings) > 0 {
|
|
out.WriteString("\n\n\033[1;4mKnown modules\033[0m\n\n")
|
|
for k, _ := range moduleUsageStrings {
|
|
out.WriteString(k)
|
|
out.WriteRune('\n')
|
|
}
|
|
}
|
|
|
|
fmt.Println(out.String())
|
|
value = make([]expression, 0)
|
|
break
|
|
} else if len(e) == 2 {
|
|
proc, ok := e[1].(string)
|
|
if !ok {
|
|
p, ok2 := e[1].(symbol)
|
|
if !ok2 {
|
|
value = exception("'usage' expected a string or symbol as its first argument, a non-string non-symbol value was given")
|
|
break
|
|
}
|
|
proc = string(p)
|
|
}
|
|
v, ok := usageStrings[proc]
|
|
if !ok {
|
|
fmt.Printf("%q does not have a usage definition\n", proc)
|
|
} else {
|
|
fmt.Println(procSigRE.ReplaceAllString(v, replacer))
|
|
}
|
|
} else {
|
|
module, ok := e[1].(string)
|
|
if !ok {
|
|
m, ok2 := e[1].(symbol)
|
|
if !ok2 {
|
|
value = exception("'usage' expected a string or symbol as its first argument, a non-string non-symbol value was given")
|
|
break
|
|
}
|
|
module = string(m)
|
|
}
|
|
modMap, ok := moduleUsageStrings[module]
|
|
if !ok {
|
|
value = exception("'usage' expected a string or symbol representing the name of a loaded module as its first argument, there is no loaded module with the given name/value")
|
|
break
|
|
}
|
|
funcName := String(e[2], false)
|
|
if funcName == "" || funcName == "#f" {
|
|
fmt.Printf("\033[1;4m%s's Known Procedures\033[0m\n\n", module)
|
|
for k := range modMap {
|
|
fmt.Println(k)
|
|
}
|
|
} else {
|
|
subFunc, ok := modMap[funcName]
|
|
if !ok {
|
|
value = exception("'usage' could not find the requested symbol within the " + module + "module's usage data")
|
|
break
|
|
}
|
|
// TODO fix this RE mess, it is not working
|
|
fmt.Println(procSigRE.ReplaceAllString(subFunc, replacer))
|
|
}
|
|
}
|
|
value = make([]expression, 0)
|
|
case "load":
|
|
if en.outer != nil {
|
|
value = exception("'load' is only callable from the global/top-level")
|
|
break
|
|
}
|
|
for _, fp := range e[1:] {
|
|
if _, ok := fp.(string); !ok {
|
|
value = exception("'load' expects filepaths as a string, a non-string value was encountered")
|
|
break
|
|
}
|
|
}
|
|
loadFiles(e[1:])
|
|
value = symbol("ok")
|
|
case "load-mod":
|
|
fullLoadEnv := env{make(map[symbol]expression), &globalenv}
|
|
for _, fp := range e[1:] {
|
|
if p, ok := fp.(string); !ok {
|
|
panic("'load-mod' expects filepaths as a string, a non-string value was encountered")
|
|
} else {
|
|
modEnv, err := RunModule(p, false)
|
|
if err != nil {
|
|
panic(fmt.Errorf("'load-mod' failed loading module %s: %s", p, err.Error()))
|
|
}
|
|
for k, v := range modEnv.vars {
|
|
if k == "_USAGE" {
|
|
// Add helper text if available
|
|
helpOut := make(map[string]string)
|
|
if val, ok := v.([]expression); ok {
|
|
for _, helpPair := range val {
|
|
switch p := helpPair.(type) {
|
|
case []expression:
|
|
if len(p) < 2 {
|
|
break
|
|
}
|
|
helpOut[String(p[0], false)] = String(p[1], false)
|
|
}
|
|
}
|
|
}
|
|
if len(helpOut) > 0 {
|
|
moduleUsageStrings[p] = helpOut
|
|
}
|
|
} else {
|
|
// Otherwise add to the global symbol table
|
|
fullLoadEnv.vars[k] = v
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for k, v := range fullLoadEnv.vars {
|
|
globalenv.vars[k] = v
|
|
}
|
|
value = symbol("ok")
|
|
case "load-mod-file":
|
|
fullLoadEnv := env{make(map[symbol]expression), &globalenv}
|
|
for _, fp := range e[1:] {
|
|
if p, ok := fp.(string); !ok {
|
|
panic("'load-mod-file' expects relative filepaths as a string, a non-string value was encountered")
|
|
} else {
|
|
modEnv, err := RunModule(p, true)
|
|
if err != nil {
|
|
panic(fmt.Errorf("'load-mod-file' failed loading module %s: %s", p, err.Error()))
|
|
}
|
|
for k, v := range modEnv.vars {
|
|
fullLoadEnv.vars[k] = v
|
|
}
|
|
}
|
|
}
|
|
for k, v := range fullLoadEnv.vars {
|
|
globalenv.vars[k] = v
|
|
}
|
|
value = symbol("ok")
|
|
case "filter":
|
|
if len(e) < 3 {
|
|
value = exception("'filter' expects two arguments: a procedure and an argument list, too few arguments were given")
|
|
break
|
|
}
|
|
proc := eval(e[1], en)
|
|
list, ok := eval(e[2], en).([]expression)
|
|
if !ok {
|
|
value = exception("'filter' expects a list as its second argument, a non-list value was given")
|
|
break
|
|
}
|
|
val := make([]expression, 0)
|
|
for i := range list {
|
|
app := apply(proc, list[i:i+1])
|
|
v := AnythingToBool(app).(bool)
|
|
if v {
|
|
val = append(val, list[i])
|
|
}
|
|
}
|
|
value = val
|
|
case "apply":
|
|
if len(e) < 3 {
|
|
value = exception("'apply' expects two arguments: a procedure and an argument list, too few arguments were given")
|
|
break
|
|
}
|
|
args := eval(e[2], en)
|
|
switch item := args.(type) {
|
|
case []expression:
|
|
value = apply(eval(e[1], en), item)
|
|
default:
|
|
value = apply(eval(e[1], en), []expression{item})
|
|
}
|
|
case "eval":
|
|
if len(e) < 1 {
|
|
value = exception("'eval' expects a string and an optional boolean to indicate that a string should be parsed and evaluated, but was not given any arguments")
|
|
}
|
|
sParse := false
|
|
if len(e) >= 3 {
|
|
v, ok := e[2].(bool)
|
|
if ok {
|
|
sParse = v
|
|
}
|
|
}
|
|
switch item := eval(e[1], en).(type) {
|
|
case []expression:
|
|
if _, ok := item[0].(symbol); !ok {
|
|
value = item
|
|
} else {
|
|
value = eval(item, en)
|
|
v, ok := value.(string)
|
|
if ok && sParse {
|
|
p := Parse(v)
|
|
value = eval(p.([]expression)[0], en)
|
|
}
|
|
}
|
|
case string:
|
|
if sParse {
|
|
p := Parse(item)
|
|
if l, ok := p.([]expression)[0].([]expression); ok {
|
|
if _, ok := l[0].(symbol); ok {
|
|
value = eval(p.([]expression)[0], en)
|
|
} else {
|
|
value = l
|
|
}
|
|
} else {
|
|
value = eval(l[0], en)
|
|
}
|
|
} else {
|
|
value = item
|
|
}
|
|
default:
|
|
value = item
|
|
}
|
|
default:
|
|
operands := e[1:]
|
|
values := make([]expression, len(operands))
|
|
for i, x := range operands {
|
|
values[i] = eval(x, en)
|
|
}
|
|
value = apply(eval(e[0], en), values)
|
|
}
|
|
default:
|
|
panic("Unknown expression type encountered during EVAL")
|
|
}
|
|
|
|
if e, ok := value.(exception); panicOnException && ok {
|
|
panic(string(e))
|
|
}
|
|
return
|
|
}
|
|
|
|
func apply(procedure expression, args []expression) (value expression) {
|
|
switch p := procedure.(type) {
|
|
case func(...expression) expression:
|
|
value = p(args...)
|
|
case proc: // Mostly used by lambda
|
|
en := &env{make(vars), p.en}
|
|
switch params := p.params.(type) {
|
|
case []expression:
|
|
if len(params)-variadic(params) > len(args) {
|
|
return exception(fmt.Sprintf("Lambda expected %d arguments but received %d", len(params), len(args)))
|
|
}
|
|
for i, param := range params {
|
|
if param.(symbol) == symbol("args-list") {
|
|
if len(args) >= len(params) {
|
|
en.vars[param.(symbol)] = args[i:]
|
|
} else {
|
|
en.vars[param.(symbol)] = make([]expression, 0)
|
|
}
|
|
break
|
|
}
|
|
en.vars[param.(symbol)] = args[i]
|
|
}
|
|
default:
|
|
en.vars[params.(symbol)] = args
|
|
}
|
|
value = eval(p.body, en)
|
|
default:
|
|
panic("Unknown procedure type encountered during APPLY: " + String(procedure, true))
|
|
}
|
|
return
|
|
}
|