329 lines
8.4 KiB
Go
329 lines
8.4 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"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
/*
|
|
TODO
|
|
Ditch these vars and use a stack. If there is a
|
|
close to a scope that function (eatIf, etc) can
|
|
check that the top value on the stack is of that
|
|
sort. If not, it is an error. `parse` should be
|
|
adjusted to return `([]token, error)`
|
|
|
|
Alternately, keep the vars, but update parse to
|
|
return an error so that a go stackdump does not
|
|
occur and panic does not need to be used
|
|
*/
|
|
var (
|
|
ifDepth int
|
|
whileDepth int
|
|
listDepth int
|
|
tryDepth int
|
|
dictDepth int
|
|
procDepth int
|
|
)
|
|
|
|
func parse(r *tokenReader) ([]token, error) {
|
|
out := make([]token, 0, len(r.tokens))
|
|
var err error
|
|
Loop:
|
|
for {
|
|
var t token
|
|
t, err = r.Read()
|
|
if err != nil {
|
|
err = nil
|
|
break
|
|
}
|
|
switch t.kind {
|
|
case END:
|
|
return out, nil
|
|
case RBRACKET:
|
|
if listDepth == 0 {
|
|
return []token{}, fmt.Errorf("Token `]` found outside of a list (Line %d of %s)", t.line, t.file)
|
|
}
|
|
return out, nil
|
|
case LBRACKET:
|
|
list, err := eatList(r, t.line, t.file)
|
|
if err != nil {
|
|
return []token{}, err
|
|
}
|
|
out = append(out, list)
|
|
case RCURLY:
|
|
if dictDepth == 0 {
|
|
return []token{}, fmt.Errorf("Token `}` found outside of a dict (Line %d of %s)", t.line, t.file)
|
|
}
|
|
// TODO out will not work here
|
|
return out, nil
|
|
case LCURLY:
|
|
dict, err := eatDict(r, t.line, t.file)
|
|
if err != nil {
|
|
return []token{}, err
|
|
}
|
|
// TODO out will not work here
|
|
out = append(out, dict)
|
|
case KEYWORD:
|
|
if t.val.(string) == "docstring!" {
|
|
out = append(out, t)
|
|
var t2 token
|
|
t2, err = r.Read()
|
|
if err != nil {
|
|
break Loop
|
|
}
|
|
out = append(out, t2)
|
|
} else if t.val.(string) == "proc" {
|
|
pt, err := eatProcedure(r, t.line, false, t.file)
|
|
if err != nil {
|
|
return []token{}, err
|
|
}
|
|
out = append(out, pt)
|
|
} else if t.val.(string) == "proc!" {
|
|
pt, err := eatProcedure(r, t.line, true, t.file)
|
|
if err != nil {
|
|
return []token{}, err
|
|
}
|
|
out = append(out, pt)
|
|
} else if t.val.(string) == "break" {
|
|
if whileDepth <= 0 {
|
|
return []token{}, fmt.Errorf("`break` found outside of `while`/`dowhile` ... `.` construct (Line %d of %s)", t.line, t.file)
|
|
}
|
|
out = append(out, t)
|
|
} else if t.val.(string) == "catch" {
|
|
if tryDepth <= 0 {
|
|
return []token{}, fmt.Errorf("`catch` found outside of `try` ... `.` construct (Line %d of %s)", t.line, t.file)
|
|
}
|
|
out = append(out, t)
|
|
return out, nil
|
|
} else if t.val.(string) == "else" {
|
|
if ifDepth <= 0 {
|
|
return []token{}, fmt.Errorf("`else` found outside of `if` .. `.` construct on line %d of %s", t.line, t.file)
|
|
}
|
|
out = append(out, t)
|
|
return out, nil
|
|
} else if t.val.(string) == "if" {
|
|
it, err := eatIf(r, t.line, t.file)
|
|
if err != nil {
|
|
return []token{}, err
|
|
}
|
|
out = append(out, it)
|
|
} else if t.val.(string) == "while" {
|
|
wt, err := eatWhile(r, t.line, false, t.file)
|
|
if err != nil {
|
|
return []token{}, err
|
|
}
|
|
out = append(out, wt)
|
|
} else if t.val.(string) == "dowhile" {
|
|
wt, err := eatWhile(r, t.line, true, t.file)
|
|
if err != nil {
|
|
return []token{}, err
|
|
}
|
|
out = append(out, wt)
|
|
} else if t.val.(string) == "try" {
|
|
tt, err := eatTry(r, t.line, t.file)
|
|
if err != nil {
|
|
return []token{}, nil
|
|
}
|
|
out = append(out, tt)
|
|
} else {
|
|
out = append(out, t)
|
|
}
|
|
case INT, FLOAT, STRING, BOOL, TYPE, SYMBOL:
|
|
out = append(out, t)
|
|
case DOCSTRING:
|
|
if debug {
|
|
fmt.Fprintf(os.Stderr, "WARN │ Line %d\n │ docstring found outside of proc body\n", t.line)
|
|
}
|
|
default:
|
|
if debug {
|
|
fmt.Fprintf(os.Stderr, "WARN │ Line %d\n │ Unimplemented type found: %s\n", kindToString(t.kind))
|
|
}
|
|
out = append(out, t)
|
|
}
|
|
}
|
|
if ifDepth > 0 {
|
|
return []token{}, fmt.Errorf("Reached parser EOF without closing `if`")
|
|
}
|
|
if whileDepth > 0 {
|
|
return []token{}, fmt.Errorf("Reached parser EOF without closing `while` or `dowhile`")
|
|
}
|
|
if listDepth > 0 {
|
|
return []token{}, fmt.Errorf("Reached parser EOF without closing LIST literal")
|
|
}
|
|
if dictDepth > 0 {
|
|
return []token{}, fmt.Errorf("Reached parser EOF without closing DICT literal")
|
|
}
|
|
if tryDepth > 0 {
|
|
return []token{}, fmt.Errorf("Reached parser EOF without closing `try`/`catch`")
|
|
}
|
|
if procDepth > 0 {
|
|
return []token{}, fmt.Errorf("Reached parser EOF without closing `proc` or `proc!`")
|
|
}
|
|
return out, err
|
|
}
|
|
|
|
func eatList(r *tokenReader, line int, f string) (token, error) {
|
|
listDepth++
|
|
l := list{}
|
|
|
|
var err error
|
|
l.body, err = parse(r)
|
|
if err != nil {
|
|
return token{}, err
|
|
}
|
|
listDepth--
|
|
tok := token{LIST, l, line, f}
|
|
return tok, nil
|
|
}
|
|
|
|
func eatDict(r *tokenReader, line int, f string) (token, error) {
|
|
dictDepth++
|
|
d := NewDict()
|
|
tokens, err := parse(r)
|
|
if err != nil {
|
|
return token{}, err
|
|
}
|
|
// Loop over and have evens be keys and odds be vals
|
|
var k string
|
|
onKey := true
|
|
for _, t := range tokens {
|
|
if onKey {
|
|
if t.kind == STRING {
|
|
k = t.val.(string)
|
|
} else {
|
|
return token{}, fmt.Errorf("Non-string key found, %s, while parsing DICT on line %d of %s", toString(t, true), t.line, t.file)
|
|
}
|
|
} else {
|
|
d.body[k] = t
|
|
}
|
|
onKey = !onKey
|
|
}
|
|
if !onKey {
|
|
return token{}, fmt.Errorf("Unbalanced DICT declaration (a key with no value) while parsing DICT on line %d of %s", line, f)
|
|
}
|
|
dictDepth--
|
|
return token{DICT, d, line, f}, nil
|
|
}
|
|
|
|
func eatIf(r *tokenReader, line int, f string) (token, error) {
|
|
ifDepth++
|
|
i := condIf{}
|
|
truthy, err := parse(r)
|
|
if err != nil {
|
|
return token{}, err
|
|
}
|
|
if len(truthy) != 0 {
|
|
last := truthy[len(truthy)-1]
|
|
if last.kind == KEYWORD && last.val.(string) == "else" {
|
|
i.hasFalseBranch = true
|
|
truthy = truthy[:len(truthy)-1]
|
|
i.falsy, err = parse(r)
|
|
if err != nil {
|
|
return token{}, err
|
|
}
|
|
last := i.falsy[len(i.falsy)-1]
|
|
if last.kind == KEYWORD && last.val.(string) == "else" {
|
|
return token{}, fmt.Errorf("Extra call to `else` found on line %d of %s", last.line, f)
|
|
}
|
|
}
|
|
}
|
|
i.truthy = truthy
|
|
ifDepth--
|
|
return token{IF, i, line, f}, nil
|
|
}
|
|
|
|
func eatTry(r *tokenReader, line int, f string) (token, error) {
|
|
tryDepth++
|
|
t := try{}
|
|
main, err := parse(r)
|
|
if err != nil {
|
|
return token{}, err
|
|
}
|
|
if len(main) != 0 {
|
|
last := main[len(main)-1]
|
|
if last.kind == KEYWORD && last.val.(string) == "catch" {
|
|
main = main[:len(main)-1]
|
|
t.catch, err = parse(r)
|
|
if err != nil {
|
|
return token{}, err
|
|
}
|
|
last := t.catch[len(t.catch)-1]
|
|
if last.kind == KEYWORD && last.val.(string) == "catch" {
|
|
return token{}, fmt.Errorf("Extra call to `catch` found on line %d of %s", last.line, f)
|
|
}
|
|
} else {
|
|
return token{}, fmt.Errorf("`try` with no `catch` found on line %d of %s", line, f)
|
|
}
|
|
}
|
|
t.try = main
|
|
tryDepth--
|
|
return token{TRY, t, line, f}, nil
|
|
}
|
|
|
|
func eatWhile(r *tokenReader, line int, doWhile bool, f string) (token, error) {
|
|
whileDepth++
|
|
w := while{}
|
|
w.doWhile = doWhile
|
|
var err error
|
|
w.body, err = parse(r)
|
|
if err != nil {
|
|
return token{}, err
|
|
}
|
|
whileDepth--
|
|
return token{WHILE, w, line, f}, nil
|
|
}
|
|
|
|
func eatProcedure(r *tokenReader, line int, hasArg bool, f string) (token, error) {
|
|
procDepth++
|
|
p := proc{}
|
|
name, err := r.Read()
|
|
if name.kind != SYMBOL || err != nil {
|
|
return token{}, fmt.Errorf("proc expected a name on line %d of %s", name.line, f)
|
|
}
|
|
p.name = name.val.(string)
|
|
if hasArg && !strings.HasSuffix(p.name, "!") {
|
|
return token{}, fmt.Errorf("proc! was passed a name (%q) that does not end with a '!' on line %d of %s", p.name, name.line, f)
|
|
} else if !hasArg && strings.HasSuffix(p.name, "!") {
|
|
return token{}, fmt.Errorf("proc was passed a name (%q) that ends with a '!' on line %d of %s", p.name, name.line, f)
|
|
}
|
|
doc, err := r.Read()
|
|
if err != nil {
|
|
return token{}, fmt.Errorf("eof reached without closing of procedure near line %d of %s", line, f)
|
|
} else if doc.kind == DOCSTRING {
|
|
p.doc = doc.val.(string)
|
|
} else {
|
|
r.Unread()
|
|
}
|
|
body, err := parse(r)
|
|
if err != nil {
|
|
return token{}, err
|
|
}
|
|
p.hasArg = hasArg
|
|
p.body = body
|
|
procDepth--
|
|
return token{PROC, p, line, f}, nil
|
|
}
|
|
|
|
func resetParseCounters() {
|
|
ifDepth = 0
|
|
whileDepth = 0
|
|
listDepth = 0
|
|
tryDepth = 0
|
|
dictDepth = 0
|
|
procDepth = 0
|
|
}
|