354 lines
7.0 KiB
Go
354 lines
7.0 KiB
Go
/*
|
|
Copyright (C) 2023 Brian Evans (aka sloum). All rights reserved.
|
|
|
|
This source code is available under the terms of the ffsl, or,
|
|
Floodgap Free Software License. A copy of the license has been
|
|
provided as the file 'LICENSE' in the same folder as this source
|
|
code file. If for some reason it is not present, you can find the
|
|
terms of version 1 of the FFSL at the following URL:
|
|
|
|
https://www.floodgap.com/software/ffsl/license.html
|
|
*/
|
|
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
/*
|
|
value represents any runtime value
|
|
that the interpreter can represent
|
|
*/
|
|
type value interface{}
|
|
|
|
type dict struct {
|
|
body map[string]token
|
|
}
|
|
|
|
func (d dict) String() string {
|
|
var b strings.Builder
|
|
b.WriteString("{ ")
|
|
for k, v := range d.body {
|
|
b.WriteString(fmt.Sprintf("\"%s\" %s ", k, v.String(true)))
|
|
}
|
|
b.WriteString("} ")
|
|
return b.String()
|
|
}
|
|
|
|
func (d *dict) Update(k, v token) error {
|
|
if k.kind != STRING {
|
|
return fmt.Errorf("DICT keys must be of type STRING, not %s", kindToString(k.kind))
|
|
}
|
|
d.body[k.val.(string)] = v
|
|
return nil
|
|
}
|
|
|
|
func (d dict) Get(k string) (token, error) {
|
|
if v, ok := d.body[k]; ok {
|
|
return v, nil
|
|
}
|
|
return token{}, fmt.Errorf("Invalid reference. DICT does not have key %q", k)
|
|
}
|
|
|
|
func (d *dict) DeleteKey(k string) {
|
|
delete(d.body, k)
|
|
}
|
|
|
|
func NewDict() dict {
|
|
return dict{make(map[string]token)}
|
|
}
|
|
|
|
type list struct {
|
|
body []token
|
|
}
|
|
|
|
func (l list) String() string {
|
|
var b strings.Builder
|
|
b.WriteString("[ ")
|
|
for _, t := range l.body {
|
|
b.WriteString(toString(t, true))
|
|
b.WriteRune(' ')
|
|
}
|
|
b.WriteString("] ")
|
|
return b.String()
|
|
}
|
|
|
|
func (l list) Join(d string) string {
|
|
sl := make([]string, len(l.body))
|
|
for i, t := range l.body {
|
|
sl[i] = t.String(false)
|
|
}
|
|
return strings.Join(sl, d)
|
|
}
|
|
|
|
func (l *list) Append(val token) error {
|
|
switch val.kind {
|
|
case INT, FLOAT, STRING, BOOL, TYPE, LIST, DICT:
|
|
break
|
|
default:
|
|
return fmt.Errorf("Lists cannot contain %s", kindToString(val.kind))
|
|
}
|
|
l.body = append(l.body, val)
|
|
return nil
|
|
}
|
|
|
|
func (l list) Get(ind int) (token, error) {
|
|
if ind < 0 {
|
|
ind = len(l.body) + ind + 1
|
|
}
|
|
if ind > len(l.body) || ind < 1 {
|
|
return token{}, fmt.Errorf("Invalid index %d for length %d", ind, len(l.body))
|
|
}
|
|
return l.body[ind-1], nil
|
|
}
|
|
|
|
func (l *list) Set(ind int, val token) error {
|
|
if ind < 0 {
|
|
ind = len(l.body) + ind + 1
|
|
}
|
|
if ind > len(l.body) || ind < 1 {
|
|
return fmt.Errorf("Invalid index %d for length %d", ind, len(l.body))
|
|
}
|
|
l.body[ind-1] = val
|
|
return nil
|
|
}
|
|
|
|
func NewList(kind int) list {
|
|
return list{make([]token, 0, 10)}
|
|
}
|
|
|
|
/*
|
|
try/catch is the primary error handling mechanism
|
|
in felise. It is basically a short circuiting `if`
|
|
where if an error is encountered in the try, the
|
|
catch will be run
|
|
*/
|
|
type try struct {
|
|
try []token
|
|
catch []token
|
|
}
|
|
|
|
func (t try) String() string {
|
|
var b strings.Builder
|
|
b.WriteString("TRY ")
|
|
for i := range t.try {
|
|
b.WriteString(toString(t.try[i], true))
|
|
b.WriteRune(' ')
|
|
}
|
|
if len(t.catch) > 0 {
|
|
b.WriteString("catch ")
|
|
for i := range t.catch {
|
|
b.WriteString(toString(t.catch[i], true))
|
|
b.WriteRune(' ')
|
|
}
|
|
}
|
|
b.WriteRune('.')
|
|
return b.String()
|
|
}
|
|
|
|
/*
|
|
conditionals are implemented here as
|
|
a special form handled by the interpreter
|
|
and using the type system to provide
|
|
that feature
|
|
*/
|
|
type condIf struct {
|
|
truthy []token
|
|
falsy []token
|
|
hasFalseBranch bool
|
|
}
|
|
|
|
func (ci condIf) String() string {
|
|
var b strings.Builder
|
|
b.WriteString("if ")
|
|
for i := range ci.truthy {
|
|
b.WriteString(toString(ci.truthy[i], true))
|
|
b.WriteRune(' ')
|
|
}
|
|
if len(ci.falsy) > 0 {
|
|
b.WriteString("then ")
|
|
for i := range ci.falsy {
|
|
b.WriteString(toString(ci.falsy[i], true))
|
|
b.WriteRune(' ')
|
|
}
|
|
}
|
|
b.WriteRune('.')
|
|
return b.String()
|
|
}
|
|
|
|
type while struct {
|
|
doWhile bool // When true, functions as `do ... while` rather than `while ... do`
|
|
body []token
|
|
}
|
|
|
|
func (w while) String() string {
|
|
var b strings.Builder
|
|
b.WriteString("while ")
|
|
for i := range w.body {
|
|
b.WriteString(toString(w.body[i], true))
|
|
b.WriteRune(' ')
|
|
}
|
|
b.WriteRune('.')
|
|
return b.String()
|
|
}
|
|
|
|
/*
|
|
proc represents a procedure
|
|
the kind will always be == PROC (defined
|
|
as a constant in globals.go)
|
|
*/
|
|
type proc struct {
|
|
body []token
|
|
name string
|
|
doc string
|
|
hasArg bool
|
|
}
|
|
|
|
func (p proc) String() string {
|
|
var b strings.Builder
|
|
for i := range p.body {
|
|
b.WriteString(toString(p.body[i], true))
|
|
b.WriteRune(' ')
|
|
}
|
|
ptype := "proc"
|
|
if p.hasArg {
|
|
ptype = "proc!"
|
|
}
|
|
if p.doc != "" {
|
|
return fmt.Sprintf("%s %s\n |%s|\n %s\n.", ptype, p.name, p.doc, b.String())
|
|
}
|
|
return fmt.Sprintf("%s %s\n %s\n.", ptype, p.name, b.String())
|
|
}
|
|
|
|
func (p proc) injectToken(t token) []token {
|
|
out := make([]token, len(p.body))
|
|
copy(out, p.body)
|
|
out = replacer(out, t)
|
|
return out
|
|
}
|
|
|
|
func replacer(tokens []token, t token) []token {
|
|
tr := NewTokenReader(tokens)
|
|
for {
|
|
tok, err := tr.Read()
|
|
if err != nil {
|
|
break
|
|
}
|
|
if tok.kind == SYMBOL && tok.val.(string) == "procarg" {
|
|
tr.Update(t)
|
|
} else if tok.kind == IF {
|
|
tr.Update(token{tok.kind, condIf{replacer(tok.val.(condIf).truthy, t), replacer(tok.val.(condIf).falsy, t), tok.val.(condIf).hasFalseBranch}, tok.line, tok.file})
|
|
} else if tok.kind == WHILE {
|
|
tr.Update(token{tok.kind, while{tok.val.(while).doWhile, replacer(tok.val.(while).body, t)}, tok.line, tok.file})
|
|
}
|
|
}
|
|
return tr.Tokens()
|
|
}
|
|
|
|
/*
|
|
token is the basic unit that the interpreter
|
|
understands
|
|
*/
|
|
type token struct {
|
|
kind int
|
|
val value
|
|
line int
|
|
file string
|
|
}
|
|
|
|
func (t token) String(litRep bool) string {
|
|
var repr string
|
|
switch t.kind {
|
|
case INT:
|
|
repr = strconv.Itoa(t.val.(int))
|
|
case FLOAT:
|
|
repr = fmt.Sprintf("%f", t.val.(float64))
|
|
case BOOL:
|
|
if t.val.(bool) {
|
|
repr = "true"
|
|
} else {
|
|
repr = "false"
|
|
}
|
|
case STRING:
|
|
if litRep {
|
|
repr = fmt.Sprintf(`"%s"`, t.val.(string))
|
|
} else {
|
|
repr = fmt.Sprintf("%s", t.val.(string))
|
|
}
|
|
case LIST:
|
|
l := t.val.(list)
|
|
repr = l.String()
|
|
case IF:
|
|
repr = t.val.(condIf).String()
|
|
case WHILE:
|
|
repr = t.val.(while).String()
|
|
case KEYWORD:
|
|
repr = fmt.Sprintf("%s", t.val.(string))
|
|
case PROC:
|
|
repr = t.val.(proc).String()
|
|
case DICT:
|
|
d := t.val.(dict)
|
|
repr = d.String()
|
|
default:
|
|
panic("Issue while stringifying token")
|
|
}
|
|
|
|
if debug {
|
|
return fmt.Sprintf("TOKEN %-10s Ln: %-3d %-10s\n", kindToString(t.kind), t.line, repr)
|
|
} else {
|
|
return repr
|
|
}
|
|
}
|
|
|
|
/*
|
|
tokenReader creates a readable token stream
|
|
that can be moved in both directions
|
|
*/
|
|
type tokenReader struct {
|
|
tokens []token
|
|
p int
|
|
}
|
|
|
|
func (t *tokenReader) Read() (token, error) {
|
|
t.p++
|
|
if t.p-1 >= len(t.tokens) {
|
|
t.p--
|
|
return token{}, fmt.Errorf("Token EOF")
|
|
}
|
|
return t.tokens[t.p-1], nil
|
|
}
|
|
|
|
func (t *tokenReader) Unread() error {
|
|
t.p--
|
|
if t.p < 0 {
|
|
return fmt.Errorf("Token underflow")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (t *tokenReader) Update(tok token) {
|
|
t.tokens[t.p-1] = tok
|
|
}
|
|
|
|
func (t tokenReader) Current() int {
|
|
return t.p
|
|
}
|
|
|
|
func (t *tokenReader) Jump(i int) {
|
|
if i < 0 || i > len(t.tokens) {
|
|
panic("Token jump error")
|
|
}
|
|
t.p = i
|
|
}
|
|
|
|
func (t tokenReader) Tokens() []token {
|
|
return t.tokens
|
|
}
|
|
|
|
func NewTokenReader(tokens []token) *tokenReader {
|
|
return &tokenReader{tokens, 0}
|
|
}
|