OpenDiablo2/d2common/d2calculation/d2parser/parser.go

313 lines
6.2 KiB
Go

package d2parser
import (
"log"
"strconv"
"strings"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2calculation"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2calculation/d2lexer"
)
// Parser is a parser for calculations used for skill and missiles.
type Parser struct {
lex *d2lexer.Lexer
binaryOperations map[string]binaryOperation
unaryOperations map[string]unaryOperation
ternaryOperations map[string]ternaryOperation
fixedFunctions map[string]func(v1, v2 int) int
currentType string
currentName string
}
// New creates a new parser.
func New() *Parser {
return &Parser{
binaryOperations: getBinaryOperations(),
unaryOperations: getUnaryOperations(),
ternaryOperations: getTernaryOperations(),
fixedFunctions: getFunctions(),
}
}
// SetCurrentReference sets the current reference type and name, such as "skill" and skill name.
func (parser *Parser) SetCurrentReference(propType, propName string) {
parser.currentType = propType
parser.currentName = propName
}
// Parse parses the calculation string and creates a Calculation tree.
func (parser *Parser) Parse(calc string) d2calculation.Calculation {
calc = strings.TrimSpace(calc)
if calc == "" {
return &d2calculation.ConstantCalculation{Value: 0}
}
defer func() {
if r := recover(); r != nil {
log.Printf("Error parsing calculation: %v", calc)
}
}()
parser.lex = d2lexer.New([]byte(calc))
return parser.parseLevel(0)
}
func (parser *Parser) peek() d2lexer.Token {
return parser.lex.Peek()
}
func (parser *Parser) consume() d2lexer.Token {
return parser.lex.NextToken()
}
func (parser *Parser) parseLevel(level int) d2calculation.Calculation {
node := parser.parseProduction()
t := parser.peek()
if t.Type == d2lexer.EOF {
return node
}
for {
if t.Type != d2lexer.Symbol {
break
}
op, ok := parser.binaryOperations[t.Value]
if !ok || op.Precedence < level {
break
}
parser.consume()
var nextLevel int
if op.IsRightAssociated {
nextLevel = op.Precedence
} else {
nextLevel = op.Precedence + 1
}
otherCalculation := parser.parseLevel(nextLevel)
node = &d2calculation.BinaryCalculation{
Left: node,
Right: otherCalculation,
Op: op.Function,
}
t = parser.peek()
}
for {
if t.Type != d2lexer.Symbol {
break
}
op, ok := parser.ternaryOperations[t.Value]
if !ok || op.Precedence < level {
break
}
parser.consume()
var nextLevel int
if op.IsRightAssociated {
nextLevel = op.Precedence
} else {
nextLevel = op.Precedence + 1
}
middleCalculation := parser.parseLevel(nextLevel)
t = parser.peek()
if t.Type != d2lexer.Symbol || t.Value != op.Marker {
panic("Invalid ternary! " + t.Value + ", expected: " + op.Marker)
}
parser.consume()
rightCalculation := parser.parseLevel(nextLevel)
node = &d2calculation.TernaryCalculation{
Left: node,
Middle: middleCalculation,
Right: rightCalculation,
Op: op.Function,
}
t = parser.peek()
}
return node
}
func (parser *Parser) parseProduction() d2calculation.Calculation {
t := parser.peek()
switch {
case t.Type == d2lexer.Symbol:
if t.Value == "(" {
parser.consume()
node := parser.parseLevel(0)
t = parser.peek()
if t.Type != d2lexer.Symbol ||
t.Value != ")" {
if t.Type == d2lexer.EOF { // Ignore unclosed final parenthesis due to syntax error in original Fire Wall calculation.
return node
}
panic("Parenthesis not closed!")
}
parser.consume()
return node
}
op, ok := parser.unaryOperations[t.Value]
if !ok {
panic("Invalid unary symbol: " + t.Value)
}
parser.consume()
node := parser.parseLevel(op.Precedence)
return &d2calculation.UnaryCalculation{
Child: node,
Op: op.Function,
}
case t.Type == d2lexer.Name || t.Type == d2lexer.Number:
return parser.parseLeafCalculation()
default:
panic("Expected parenthesis, unary operator, function or value!")
}
}
func (parser *Parser) parseLeafCalculation() d2calculation.Calculation {
t := parser.peek()
if t.Type == d2lexer.Number {
val, err := strconv.Atoi(t.Value)
if err != nil {
panic("Invalid number: " + t.Value)
}
parser.consume()
return &d2calculation.ConstantCalculation{Value: val}
}
if t.Value == "skill" ||
t.Value == "miss" ||
t.Value == "stat" {
return parser.parseProperty()
}
if parser.fixedFunctions[t.Value] != nil {
return parser.parseFunction(t.Value)
}
if t.Type == d2lexer.Name {
parser.consume()
return &d2calculation.PropertyReferenceCalculation{
Type: parser.currentType,
Name: parser.currentName,
Qualifier: t.Value,
}
}
panic(t.Value + " is not a function, property, or number!")
}
func (parser *Parser) parseFunction(name string) d2calculation.Calculation {
function := parser.fixedFunctions[name]
parser.consume()
t := parser.peek()
if t.Value != "(" {
panic("Invalid function!")
}
parser.consume()
firstParam := parser.parseLevel(0)
t = parser.peek()
if t.Type != d2lexer.Symbol || t.Value != "," {
panic("Invalid function!")
}
parser.consume()
secondParam := parser.parseLevel(0)
t = parser.peek()
if t.Value != ")" {
panic("Invalid function!")
}
parser.consume()
return &d2calculation.BinaryCalculation{
Left: firstParam,
Right: secondParam,
Op: function,
}
}
func (parser *Parser) parseProperty() d2calculation.Calculation {
t := parser.peek()
propType := t.Value
t = parser.consume()
t = parser.peek()
if t.Value != "(" {
panic("Invalid property: " + propType + ", open parenthesis missing.")
}
parser.consume()
t = parser.peek()
if t.Type != d2lexer.String {
panic("Property name must be in quotes: " + propType)
}
propName := t.Value
parser.consume()
t = parser.peek()
if t.Type != d2lexer.Symbol || t.Value != "." {
panic("Property name must be followed by dot: " + propType)
}
parser.consume()
t = parser.peek()
if t.Type != d2lexer.Name {
panic("Invalid propery qualifier: " + propType)
}
propQual := t.Value
parser.consume()
t = parser.peek()
if t.Value != ")" {
panic("Invalid property: " + propType + ", closed parenthesis missing.")
}
parser.consume()
return &d2calculation.PropertyReferenceCalculation{
Type: propType,
Name: propName,
Qualifier: propQual,
}
}