Shunting yard algoritmus - 2. díl vlastní kalkulačky

Go

Možností, jak zpracovat vstupní tokeny ve správném pořadí, je hned několik. Jedním z nejznámějších algoritmů pro zpracování matematických výrazů je Shunting Yard algoritmus. Ten je pojmenován po seřaďovacím nádraží, protože rovněž přesouvá tokeny mezi třemi "kolejemi".

Shunting yard algoritmus - 2. díl vlastní kalkulačky

Article in English can be found on dev.to/arxeiss/shunting-yard-parser-expression-calculator-cik

Tento článek patří do seriálu Vlastní kalkulačka s výrazy. Ostatní články seriálu:


Výstupem lexikální analýzy z minulého dílu je seznam tokenů, což je jen přeformátovaný vstup. Stále není vyřešen problém závorek ani priority a asociativity operátorů. A přesně to je úkol parseru. Výstupem ale není konkrétní hodnota, nýbrž taková datová struktura, kterou lze jednoduše evaluovat. Například proměnné jsou v této struktuře stále zastoupeny pouze svým názvem. Je tedy možné evaluaci provádět i opakovaně jen s jinými vstupními hodnotami.

Zmíněný algoritmus Shunting Yard nejčastěji převádí infixovou notaci na Reverzní polskou notací, známou též jako postfixová notace. Mezi její nevýhody ale patří nemožnost spolehlivě rozeznat binární a unární operátory + a -. Nebo rozpoznat počet argumentů, které funkce přijímá, protože RPN vůbec nepoužívá závorky. Proto moje implementace převádí vstup přímo do Abstraktního syntaktického stromu, u kterého tyto problémy odpadají.

Algoritmus níže je velmi zjednodušený, kompletní implementace včetně komentářů a ukázkového kódu je opět dostupná na github.com/arxeiss/go-expression-calculator

Parser - Shunting Yard

Algoritmus Shunting Yard vynalezl E. Dijkstra a inspiroval se seřaďovacím nářadím. Protože celý průběh je pouze o přemisťování tokenů mezi vstupní frontou, zásobníkem operátorů a výstupní frontou či zásobníkem. Implementace níže produkuje AST, který se tvoří ve výstupním zásobníku.

func (p *Parser) Parse(tokenList []*lexer.Token) (ast.Node, error) {
	output := make([]ast.Node, 0)
	opStack := make([]*lexer.Token, 0)
	var err error

tokenLoop:
	for i := 0; i < len(tokenList); i++ {
		curToken := tokenList[i]
		switch curToken.Type() {
		case lexer.Number:
			// if the token is a number, then: push it to the output queue
			output = append(output, ast.NumericNode(curToken.Value()))

		case lexer.Identifier:
			//  if the token is a function then: push it onto the operator stack
			if tokenList[i+1].Type() == lexer.LPar {
				// Expecting the identifier is function name, because is followed by (
				opStack = append(opStack, tokenList[i])
			} else {
				// if the token is a variable name, then: push it to the output queue.
				output = append(output, ast.VariableNode(tokenList[i].Identifier()))
			}

		case lexer.Addition, lexer.Substraction, lexer.Exponent, lexer.Multiplication,
			lexer.Division, lexer.FloorDiv, lexer.Modulus:
			// if the token is an operator then: while there is an operator at the top of the operator stack
			for len(opStack) > 0 {
				topStackEl := opStack[len(opStack)-1]
				if topStackEl.Type() == lexer.LPar {
					break
				}
				// If the operator at the top of the operator stack has greater precedence
				// OR
				// The operator at the top of the operator stack has equal precedence AND the token is left associative
				if p.priorities.GetPrecedence(topStackEl.Type()) > p.priorities.GetPrecedence(curToken.Type()) ||
					(p.priorities.GetPrecedence(topStackEl.Type()) == p.priorities.GetPrecedence(curToken.Type()) &&
						p.priorities.GetAssociativity(curToken.Type()) == parser.LeftAssociativity) {

					// pop operators from the operator stack onto the output queue
					output, err = p.addToOutput(output, topStackEl)
					opStack = opStack[:len(opStack)-1]
					continue
				}
				break
			}
			// push token onto the operator stack
			opStack = append(opStack, curToken)

		case lexer.LPar:
			// if the token is a left parenthesis, then: push it onto the operator stack
			opStack = append(opStack, curToken)

		case lexer.RPar:
			// if the token is a right parenthesis, then:
			// while the operator at the top of the operator stack is not a left parenthesis:
			for len(opStack) > 0 {
				topStackEl := opStack[len(opStack)-1]
				if topStackEl.Type() == lexer.LPar {
					break
				}
				// pop the operator from the operator stack onto the output queue.
				output, err = p.addToOutput(output, topStackEl)
				opStack = opStack[:len(opStack)-1]
			}
			// if there is a left parenthesis at the top of the operator stack, then:
			// pop the operator from the operator stack and discard it
			opStack = opStack[:len(opStack)-1]
			// if there is a function token at the top of the operator stack, then:
			// pop the function from the operator stack onto the output queue.
			if opStack[len(opStack)-1].Type() == lexer.Identifier {
				output, err = p.addToOutput(output, opStack[len(opStack)-1])
				opStack = opStack[:len(opStack)-1]
			}
		}
		if err != nil {
			return nil, err
		}
	}

	// while there are still operator tokens on the stack
	for len(opStack) > 0 {
		// pop the operator from the operator stack onto the output queue
		topStackEl := opStack[len(opStack)-1]
		output, err = p.addToOutput(output, topStackEl)
		opStack = opStack[:len(opStack)-1]
	}
	return opStack, err
}
// addToOutput is converting nodes in output list into a AST
func (p *Parser) addToOutput(output []ast.Node, token *lexer.Token) ([]ast.Node, error) {
	var op ast.Operation
	switch t := token.Type(); t {
	case lexer.UnaryAddition, lexer.UnarySubstraction:
		output[len(output)-1] = ast.NewUnaryNode(t, output[len(output)-1])
	case lexer.Addition, lexer.Substraction, lexer.Multiplication, lexer.Division, lexer.Exponent,
		lexer.FloorDiv, lexer.Modulus:

		r := output[len(output)-1]
		l := output[len(output)-2]
		output = output[:len(output)-1]
		output[len(output)-1] = ast.NewBinaryNode(t, l, r)
	case lexer.Identifier:
		// Current functions support only 1 parameter
		output[len(output)-1] = ast.NewFunctionNode(token.Identifier(), output[len(output)-1])
	}
	return output, nil
}

Úpravy algoritmu a možná rozšíření

Algoritmus výše vychází z pseudokódu na anglické Wikipedii, který ale nebude plně fungovat pro vstupy jako 5 9 88 nebo ++/*--*+/66. Nekontroluje totiž, jestli se po tokenu čísla nebo proměnné nachází operátor a naopak. Nezvládá také zpracovat unární operátory + a - nebo funkce s proměnným počtem parametrů.

První dva problémy jsou vyřešené pomocí proměnné expect. Tato stavová proměnná se přepíná mezi stavy ExpectOperand a ExpectOperator a kontroluje, zda se zpracovává token správného typu. Rozšíření je detailněji popsané na StackOverflow a pomocí této metody lze také rozšířit algoritmus a přidat podporu pro unární operátory.

Rozšíření algoritmu a přidání podpory pro proměnný počet argumentů funkce bude popsáno v jednom z dalších dílů seriálu.


S názory na článek nebo algoritmus samotný se můžete podělit v komentářích

K tomuto článku již není možné přidávat další komentáře