felise/parser.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
}