melt/Script/main.go

478 lines
9.6 KiB
Go

package main
import (
"fmt"
"os"
"strings"
c "github.com/TwiN/go-color"
)
type Token struct {
kind string
value string
line int
column int
}
type Node interface{}
// Who needs statements when you can have expressions?
type Expr interface {
Node
}
type Identifier struct {
startToken Token
name string
}
type AssignmentExpr struct {
startToken Token
left Expr
right Expr
}
type BinaryExpr struct {
startToken Token
left Expr
right Expr
}
type UnaryExpr struct {
startToken Token
expr Expr
}
type IfExpr struct {
startToken Token
condition Expr
block BlockExpr
}
type ElseIfExpr struct {
startToken Token
condition Expr
block BlockExpr
}
type ElseExpr struct {
startToken Token
block BlockExpr
}
type BlockExpr struct {
startNode Node
expressions []Expr
}
func (e Identifier) Kind() string { return "Identifier" }
func (e AssignmentExpr) Kind() string { return "AssignmentExpr" }
func (e BinaryExpr) Kind() string { return "BinaryExpr" }
func (e UnaryExpr) Kind() string { return "UnaryExpr" }
func (e IfExpr) Kind() string { return "IfExpr" }
func (e ElseIfExpr) Kind() string { return "ElseIfExpr" }
func (e ElseExpr) Kind() string { return "ElseExpr" }
func (e BlockExpr) Kind() string { return "BlockExpr" }
const (
EOF = "EOF"
INDENT = "INDENT"
SPACE = "SPACE"
NEWLINE = "NEWLINE"
// Literals
IDENTIFIER = "IDENTIFIER"
NUMBER = "NUMBER"
COMMENT = "COMMENT"
STRING = "STRING"
KEYWORD = "KEYWORD"
// Operators
TEXTOPERATOR = "TEXTOPERATOR"
EQUALS = "EQUALS"
PLUS = "PLUS"
PLUSPLUS = "PLUSPLUS"
PLUSEQUALS = "PLUSEQUALS"
MINUS = "MINUS"
MINUSMINUS = "MINUSMINUS"
MINUSEQUALS = "MINUSEQUALS"
TIMES = "TIMES"
DIVIDE = "DIVIDE"
MODULO = "MODULO"
// OPEN_BRACE = "OPEN_BRACE"
// CLOSE_BRACE = "CLOSE_BRACE"
)
var keywords = map[string]bool{
"if": true,
"elseif": true,
"else": true,
"loop": true,
"for": true,
"break": true,
"continue": true,
}
var textOperators = map[string]bool{
"is": true,
"and": true,
"or": true,
"not": true,
}
func generate(program []Expr) string {
var output string
for i := 0; i < len(program); i++ {
expr := program[i]
fmt.Println(expr)
}
return output
}
func parse(tokens []Token) []Expr {
var program []Expr
addExpr := func(expr Expr) {
program = append(program, expr)
}
for i := 0; i < len(tokens); i++ {
token := tokens[i]
currentIndent := 0
getBlock := func() []Token {
// get tokens until the end of the block (which is the same indent level as the if statement)
var blockTokens []Token
blockIndent := 0
// skip newline at start
i++
for i < len(tokens) {
if tokens[i].kind == NEWLINE {
blockIndent = 0
// check next few tokens to see if they're indented
for j := i + 1; j < len(tokens) && tokens[j].kind == INDENT; j++ {
blockIndent++
}
if blockIndent <= currentIndent {
break
}
}
blockTokens = append(blockTokens, tokens[i])
i++
}
return blockTokens
}
getCondition := func() []Token {
var condTokens []Token
// skip the keyword
i++
// get all tokens until the end of the line
for i < len(tokens) && tokens[i+1].kind != NEWLINE {
i++
condTokens = append(condTokens, tokens[i])
}
// skip the newline
i++
return condTokens
}
switch token.kind {
case INDENT:
currentIndent++
case NEWLINE:
currentIndent = 0
case KEYWORD:
switch token.value {
case "if":
condTokens := getCondition()
blockTokens := getBlock()
addExpr(IfExpr{
startToken: token,
condition: parse(condTokens),
block: BlockExpr{
startNode: token,
expressions: parse(blockTokens),
},
})
case "elseif":
condTokens := getCondition()
blockTokens := getBlock()
addExpr(ElseIfExpr{
startToken: token,
condition: parse(condTokens),
block: BlockExpr{
startNode: token,
expressions: parse(blockTokens),
},
})
case "else":
// skip newline
i++
blockTokens := getBlock()
addExpr(ElseExpr{
startToken: token,
block: BlockExpr{
startNode: token,
expressions: parse(blockTokens),
},
})
}
}
}
return program
}
func lex(source string) []Token {
var tokens []Token
last := func(n int) Token {
return tokens[len(tokens)-n]
}
line := 1
column := 0
addToken := func(kind string, value string, linecol ...int) {
currentLine := line
currentColumn := column
if len(linecol) > 0 {
currentLine = linecol[0]
}
if len(linecol) > 1 {
currentColumn = linecol[1]
}
tokens = append(tokens, Token{
kind: kind,
value: value,
line: currentLine,
column: currentColumn,
})
}
ParseLoop:
for i := 0; i < len(source); i++ {
char := source[i]
column++
switch char {
case '=':
addToken(EQUALS, "=")
case '\n':
addToken(NEWLINE, "\n")
line++
column = 0
case ' ':
addToken(SPACE, " ")
case '\t':
// only if last token is a newline or an indent
if last(1).kind == NEWLINE || last(1).kind == INDENT {
addToken(INDENT, "\t")
column += 3
} else {
addToken(SPACE, "\t")
}
case ';':
// parse till end of line
startColumn := column
i++ // skip the semicolon
var comment string
for i < len(source) && source[i] != '\n' {
comment += string(source[i])
column++
i++
}
column--
i--
addToken(COMMENT, comment, line, startColumn)
case '"':
startLine := line
startColumn := column
var stringLiteral string
column++
i++ // skip the first quote
for i < len(source) && source[i] != '"' {
stringLiteral += string(source[i])
column++
i++
}
if i == len(source) {
fmt.Println(c.InRed("unclosed string literal"))
os.Exit(1)
}
addToken(STRING, stringLiteral, startLine, startColumn)
case '+':
// check if it's a ++ or += or just a +
if i+1 < len(source) && source[i+1] == '+' {
addToken(PLUSPLUS, "++")
i++
column++
} else if i+1 < len(source) && source[i+1] == '=' {
addToken(PLUSEQUALS, "+=")
i++
column++
} else {
addToken(PLUS, "+")
}
case '-':
// check if it's a -- or -= or just a -
if i+1 < len(source) && source[i+1] == '-' {
addToken(MINUSMINUS, "--")
i++
column++
} else if i+1 < len(source) && source[i+1] == '=' {
addToken(MINUSEQUALS, "-=")
i++
column++
} else {
addToken(MINUS, "-")
}
case '*':
addToken(TIMES, "*")
case '/':
addToken(DIVIDE, "/")
case '%':
addToken(MODULO, "%")
default:
if char >= '0' && char <= '9' {
startLine := line
startColumn := column
var number string
// keep going until we hit a non-number
for i < len(source) && source[i] >= '0' && source[i] <= '9' {
number += string(source[i])
column++
i++
}
column--
i--
addToken(NUMBER, number, startLine, startColumn)
} else if char >= 'a' && char <= 'z' || char >= 'A' && char <= 'Z' {
startLine := line
startColumn := column
// keep going until we hit a non-letter
var identifierOrKeyword string
for i < len(source) &&
(source[i] >= 'a' && source[i] <= 'z' ||
source[i] >= 'A' && source[i] <= 'Z' ||
source[i] >= '0' && source[i] <= '9') {
identifierOrKeyword += string(source[i])
column++
i++
}
if i == len(source) {
// you can't end a program with an identifier (yet)
fmt.Println(c.InRed("cant end program with identifier"))
os.Exit(1)
}
column--
i--
// check if it's a text operator
if textOperators[identifierOrKeyword] {
addToken(TEXTOPERATOR, identifierOrKeyword, startLine, startColumn)
continue ParseLoop
}
// check if it's a keyword
if keywords[identifierOrKeyword] {
addToken(KEYWORD, identifierOrKeyword, startLine, startColumn)
continue ParseLoop
}
addToken(IDENTIFIER, identifierOrKeyword, startLine, startColumn)
} else {
fmt.Println(c.InRed("that isnt a valid character"), c.InYellow(string(char)))
os.Exit(1)
}
}
}
return tokens
}
func main() {
if len(os.Args) < 2 {
fmt.Println(c.InRed("No target file specified!"))
fmt.Println(c.InBlue("Run 'melt-script help' for more information."))
os.Exit(1)
}
target := os.Args[1]
fi, err := os.Stat(target)
if err != nil {
fmt.Println(c.InRed("Target file ") + c.InUnderline(c.InPurple(target)) + c.InRed(" does not exist!"))
os.Exit(1)
}
if fi.IsDir() {
fmt.Println(c.InUnderline(c.InPurple(target)) + c.InRed(" is a directory, please choose a file to compile!"))
os.Exit(1)
}
source, err := os.ReadFile(target)
if err != nil {
fmt.Println(c.InRed("Failed to read target file ") + c.InUnderline(c.InPurple(target)) + c.InRed("!"))
os.Exit(1)
}
// replace \r\n with \n
sourceString := strings.Replace(string(source), "\r\n", "\n", -1)
// remove trailing newlines
sourceString = strings.TrimRight(sourceString, "\n")
tokens := lex(sourceString)
for _, token := range tokens {
if token.kind == "SPACE" {
continue
}
if token.kind == "NEWLINE" {
fmt.Println("────────────────┼───────────────┼─────────────────────────────")
continue
}
toPrint := []any{
fmt.Sprintf("%s:%d:%d", target, token.line, token.column),
c.InYellow(token.kind),
c.InPurple(token.value),
}
// print in a nice format
fmt.Printf("%-15s │ %-22s │ %s\n", toPrint...)
}
program := parse(tokens)
out := generate(program)
fmt.Println(out)
}