SwordScript - 使用C#开发脚本语言(八)流程语句

192 阅读5分钟

本章节对应仓库

6.流程语句 Github

语句定义

在上一章中,我们仅定义了赋值语句。本章将会把所有的流程语句一一定义。

首先先定义基础流程语句的BNF:

condition ::= <"("> <expr> <")">
ifStatement ::= <"if"> <condition> <statement> [<else> <statement>]
whileStatement ::= <"while"> <condition> <statement>
doWhileStatement ::== <"do"> <statement> <"while"> <condition>

而引入了while时,相应的也可以引入break、continue等语句辅助循环

break ::= <"break"> [";"]
continue ::= <"continue"> [";"]

相应的,语句也有语句块,如用{}包裹的多个语句形成一个语句块。语句块也是一个语句

block ::= <"{"> {statement} <"}">

最后得出语句的定义:

statement ::= block | assignStatement | ifStatement | whileStatement | doWhileStatement | break | continue

综合如下:

statement ::= block | assignStatement | ifStatement | whileStatement | doWhileStatement | break | continue
block ::= <"{"> {statement} <"}">
break ::= <"break"> [";"]
continue ::= <"continue"> [";"]
condition ::= <"("> <expr> <")">
ifStatement ::= <"if"> <condition> <statement> [<else> <statement>]
whileStatement ::= <"while"> <condition> <statement>
doWhileStatement ::== <"do"> <statement> <"while"> <condition>

语法树定义

首先先定义语句语法树的基类:

ASTStatement

using System.Collections.Generic;

namespace SwordScript;

public abstract class ASTStatement : ASTList
{
    public ASTStatement()
    {
        
    }
    
    public ASTStatement(IEnumerable<ASTNode> children) : base(children)
    {
        
    }
}

将上一章之中定义的赋值语句重构如下:

Assignment => AssignStatement

namespace SwordScript;

public class ASTAssignStatement : ASTStatement
{
    public ASTAssignStatement(ASTNode left, ASTNode right) : base(new []{left, right})
    {
        
    }

    public ASTNode Left => this[0];
    
    public ASTNode Right => this[1];
    
    public override string ToString()
    {
        return $"{Left} = {Right};";
    }

    public override object Evaluate(SwordEnvironment env)
    {
        string variableName = ((ASTIdentifier)Left).Name;
        env.SetVariable(variableName, Right.Evaluate(env));
        
        return null;
    }
}

Block语句

Block语句负责的是执行一连串的语句:

using System.Collections.Generic;

namespace SwordScript;

public class ASTBlock : ASTStatement
{
    public ASTBlock(IEnumerable<ASTNode> children) : base(children)
    {
    }
    
    public override object Evaluate(SwordEnvironment env)
    {
        foreach (ASTNode child in Children)
        {
            child.Evaluate(env);
        }

        return null;
    }
    
    public override string ToString()
    {
        return "{ " + string.Join(" ", Children) + " }";
    }
}

if语句

在if语句中,只需要判断condition是否为true,不为true则执行else语句。

若condition不为bool值则可以直接抛出错误。

ASTIfStatement

using Sprache;

namespace SwordScript;

public class ASTIfStatement : ASTStatement
{
    public ASTIfStatement(ASTNode condition, ASTNode statement) : base(new []{ condition, statement})
    {
        HasElse = false;
    }
    
    public ASTIfStatement(ASTNode condition, ASTNode statement, ASTNode elseStatement) : base(new []{ condition, statement, elseStatement})
    {
        HasElse = true;
    }

    public bool HasElse;

    public ASTNode Condition => this[0];
    
    public ASTNode Statement => this[1];
    
    public ASTNode ElseStatement => HasElse ? this[2] : null;

    public override object Evaluate(SwordEnvironment env)
    {
        var result = Condition.Evaluate(env);
        if (result is not bool b)
        {
            throw new EvaluateException($"Condition '{Condition}' must return boolean");
        }

        if(b)
        {
            Statement.Evaluate(env);
        }
        else if(HasElse)
        {
            ElseStatement.Evaluate(env);
        }

        return null;
    }

    public override string ToString()
    {
        if(HasElse)
            return $"if ({Condition}) {Statement} else {ElseStatement}";
        
        return $"if ({Condition}) {Statement}";
    }
}

while与do...while语句

while语句定义前,先定义相关的break和continue语句:

ASTBreakStatement

namespace SwordScript;

public class ASTBreakStatement : ASTStatement
{
    public override object Evaluate(SwordEnvironment env)
    {
        throw new BreakException();
    }
    
    public override string ToString()
    {
        return "break;";
    }
}

ASTContinueStatement

namespace SwordScript;

public class ASTContinueStatement : ASTStatement
{
    public override object Evaluate(SwordEnvironment env)
    {
        throw new ContinueException();
    }
    
    public override string ToString()
    {
        return "continue;";
    }
}

这两个语句的作用是抛出异常,通过异常,实现中断while循环的作用。

异常的定义如下:

using System;

namespace SwordScript;

public class BreakException : Exception { }
using System;

namespace SwordScript;

public class ContinueException : Exception { }

随后,定义while循环的语法树节点: ASTWhileStatement

using System;

namespace SwordScript;

public class ASTWhileStatement : ASTStatement
{
    public ASTWhileStatement(ASTNode condition, ASTNode statement) : base(new []{condition, statement})
    {
    }

    public ASTNode Condition => this[0];
    public ASTNode Statement => this[1];

    public override object Evaluate(SwordEnvironment env)
    {
        var result = Condition.Evaluate(env);
        if (result is not bool b)
        {
            throw new EvaluateException($"Condition '{Condition}' must return boolean");
        }
        while (b)
        {
            try
            {
                Statement.Evaluate(env);
            }
            catch (BreakException)
            {
                break;
            }
            catch (ContinueException)
            {
                // ignore
            }
            
            result = Condition.Evaluate(env);
            if (result is not bool b2)
            {
                throw new EvaluateException($"Condition '{Condition}' must return boolean");
            }

            b = b2;
        }
        
        return null;
    }

    public override string ToString()
    {
        return $"while ({Condition}) {Statement}";
    }
}

ASTDoWhileStatement

namespace SwordScript;

public class ASTDoWhileStatement : ASTStatement
{
    public ASTDoWhileStatement(ASTNode condition, ASTNode statement) : base(new[] { condition, statement })
    {
    }
    
    public ASTNode Condition => this[0];
    public ASTNode Statement => this[1];

    public override object Evaluate(SwordEnvironment env)
    {
        bool b;
        do
        {
            try
            {
                Statement.Evaluate(env);
            }
            catch (BreakException)
            {
                break;
            }
            catch (ContinueException)
            {
                // ignore
            }
            
            var result = Condition.Evaluate(env);
            if (result is not bool b2)
            {
                throw new EvaluateException($"Condition '{Condition}' must return boolean");
            }

            b = b2;
        } while (b);
        
        return null;
    }
    
    public override string ToString()
    {
        return $"do {Statement} while ({Condition})";
    }
}

语法解析

在定义完语法后,便可以开始语法的解析了。

先在Words类里把新的关键词定义,并把这些关键词加入ALL_RESERVED_WORDS数组:

public const string IF = "if";
public const string ELSE = "else";
public const string DO = "do";
public const string WHILE = "while";
public const string BREAK = "break";
public const string CONTINUE = "continue";

在词法解析类Lexer里加上所有符号和关键词对应的解析:

public static readonly Parser<string> If = LetterSymbol(Words.IF);
public static readonly Parser<string> Else = LetterSymbol(Words.ELSE);
public static readonly Parser<string> Do = LetterSymbol(Words.DO);
public static readonly Parser<string> While = LetterSymbol(Words.WHILE);
public static readonly Parser<string> Break = LetterSymbol(Words.BREAK);
public static readonly Parser<string> Continue = LetterSymbol(Words.CONTINUE);

public static readonly Parser<string> LeftBrace = PunctuationSymbol("{");
public static readonly Parser<string> RightBrace = PunctuationSymbol("}");

ScriptParser中加入剩余的全部语法定义:

注:Assignment重构为了AssignStatement

public static readonly Parser<ASTStatement> Statement = Parse.Ref(() => Block)
    .Or(Parse.Ref(() => AssignStatement))
    .Or(Parse.Ref(() => IfStatement))
    .Or(Parse.Ref(() => WhileStatement))
    .Or(Parse.Ref(() => DoWhileStatement))
    .Or(Parse.Ref(() => ContinueStatement))
    .Or(Parse.Ref(() => BreakStatement));

public static readonly Parser<ASTStatement> Block = 
    from left in Lexer.LeftBrace
    from statements in Statement.Many()
    from right in Lexer.RightBrace
    select new ASTBlock(statements);

public static readonly Parser<ASTStatement> AssignStatement =
    from left in Identifier
    from assign in Lexer.Assign
    from right in Expr
    from _ in Lexer.Semicolon.Optional()
    select new ASTAssignStatement(left, right);

public static readonly Parser<ASTNode> Condition =
    from left in Lexer.LeftBracket
    from condition in Expr
    from right in Lexer.RightBracket
    select condition;

public static readonly Parser<ASTStatement> ElseStatement =
    from left in Lexer.Else
    from statement in Statement
    select statement;

public static readonly Parser<ASTStatement> IfStatement =
    from keywordIf in Lexer.If
    from condition in Condition
    from statement in Statement
    from elseStatement in ElseStatement.Optional()
    select elseStatement.IsEmpty 
        ? new ASTIfStatement(condition, statement) 
        : new ASTIfStatement(condition, statement, elseStatement.Get());

public static readonly Parser<ASTStatement> BreakStatement =
    from keywordBreak in Lexer.Break
    from _ in Lexer.Semicolon.Optional()
    select new ASTBreakStatement();

public static readonly Parser<ASTStatement> ContinueStatement =
    from keywordContinue in Lexer.Continue
    from _ in Lexer.Semicolon.Optional()
    select new ASTContinueStatement();

public static readonly Parser<ASTStatement> WhileStatement =
    from keywordWhile in Lexer.While
    from condition in Condition
    from statement in Statement
    select new ASTWhileStatement(condition, statement);

public static readonly Parser<ASTStatement> DoWhileStatement =
    from keywordDo in Lexer.Do
    from statement in Statement
    from keywordWhile in Lexer.While
    from condition in Condition
    select new ASTDoWhileStatement(condition, statement);

环境类改造

在进行单元测试前,还需要对上一章节定义的环境类进行一定的改造:

using System.Collections.Generic;

namespace SwordScript;

public class SwordEnvironment
{
    private Dictionary<string, object> _variables = new Dictionary<string, object>();
    
    public void SetVariable(string name, object value)
    {
        unchecked
        {
            switch (value)
            {
                case uint ui:
                    value = (long)ui;
                    break;
                case int i:
                    value = (long)i;
                    break;
                case ulong ul:
                    value = (long)ul;
                    break;
                case float f:
                    value = (double)f;
                    break;
                case char c:
                    value = c.ToString();
                    break;
            }
        }

        _variables[name] = value;
    }
    
    public object GetVariable(string name)
    {
        if(_variables.ContainsKey(name))
            return _variables[name];
        return null;
    }
}

可以看到,改动主要是对设置变量时接收的类型做了转化,以方便设置时不需要过于注重数值类型,也防止因为数值类型不同在运算时发生报错。

unchecked的作用是使类型转换时无视溢出等问题。

单元测试

新建StatementTest类,并将之前定义的变量赋值相关的测试移至该类。

测试用例如下:

using NUnit.Framework;
using Sprache;
using SwordScript;

namespace Tests;

public class StatementTest
{
    [Test]
    public void AssigStatementTest()
    {
        Assert.AreEqual("a = 1;", ScriptParser.AssignStatement.Parse(" a = 1 ").ToString());
        Assert.AreEqual("a = (1 + 1);", ScriptParser.AssignStatement.Parse(" a = 1 + 1; ").ToString());
        Assert.AreEqual("a = (2 == b);", ScriptParser.AssignStatement.Parse(" a = 2 == b ").ToString());
        Assert.Catch<ParseException>(() => ScriptParser.AssignStatement.Parse(" a + 1 = 1 "));
    }

    [Test]
    public void AssignStatementEvaluateTest()
    {
        var env = new SwordEnvironment();
        ScriptParser.AssignStatement.Parse(" a = 1 ").Evaluate(env);
        Assert.AreEqual(1, env.GetVariable("a"));

        ScriptParser.AssignStatement.Parse(" a = 1 + 1 ").Evaluate(env);
        Assert.AreEqual(2, env.GetVariable("a"));

        Assert.AreEqual(null, env.GetVariable("b"));

        ScriptParser.AssignStatement.Parse(" b = 2 ").Evaluate(env);
        Assert.AreEqual(2, env.GetVariable("b"));

        ScriptParser.AssignStatement.Parse(" c = a == b ").Evaluate(env);
        Assert.AreEqual(true, env.GetVariable("c"));
    }

    [Test]
    public void IfStatementTest()
    {
        Assert.AreEqual("if ((a == 1)) b = 1;", ScriptParser.IfStatement.Parse(" if (a == 1) \n b = 1 ").ToString());
        Assert.AreEqual("if ((a == 1)) b = 1; else c = 2;",
            ScriptParser.IfStatement.Parse(" if (a == 1) \n b = 1 \n else \n c = 2 ").ToString());
        Assert.AreEqual("if (a) { b = 0; } else { b = -100; }", ScriptParser.IfStatement.Parse(@"
            if(a)
            {
                b = 0;
            }
            else
            {
                b = -100;
            }
            ").ToString());
    }

    [Test]
    public void IfStatementEvaluateTest()
    {
        var env = new SwordEnvironment();
        env.SetVariable("a", 1);
        ScriptParser.IfStatement.Parse(" if (a == 1) \n b = 1 ").Evaluate(env);
        Assert.AreEqual(1, env.GetVariable("b"));

        ScriptParser.IfStatement.Parse(@"
            if(a == 2)
            {
                c = 100;
            }
            ").Evaluate(env);
        Assert.AreEqual(null, env.GetVariable("c"));

        ScriptParser.IfStatement.Parse(@"
            if(c != null)
            {
                d = 100;
            }
            else
            {
                d = -100;
            }
            ").Evaluate(env);
        Assert.AreEqual(-100, env.GetVariable("d"));
    }

    [Test]
    public void WhileStatementTest()
    {
        Assert.AreEqual("while ((a == 1)) a = 1;", ScriptParser.WhileStatement.Parse(" while (a == 1) \n a = 1 ").ToString());
        Assert.AreEqual("while ((a == 1)) { a = 1; }", ScriptParser.WhileStatement.Parse(@"
            while(a == 1)
            {
                a = 1;
            }
            ").ToString());
        
        Assert.AreEqual("while ((a == 1)) { a = 1; b = 2; }", ScriptParser.WhileStatement.Parse(@"
            while(a == 1)
            {
                a = 1;
                b = 2;
            }
            ").ToString());
    }

    [Test]
    public void WhileStatementEvaluateTest()
    {
        var env = new SwordEnvironment();
        env.SetVariable("a", 1);
        ScriptParser.WhileStatement.Parse(" while (a == 1) \n a = 2 ").Evaluate(env);
        Assert.AreEqual(2, env.GetVariable("a"));
        
        env.SetVariable("a", 1);
        ScriptParser.WhileStatement.Parse(@"
            while(a < 10)
            {
                a = a + 1;
            }").Evaluate(env);
        Assert.AreEqual(10, env.GetVariable("a"));
        
        env.SetVariable("a", 1);
        env.SetVariable("b", 0);
        ScriptParser.WhileStatement.Parse(@"
            while(a < 10)
            {
                b = b + a;
                a = a + 1;
            }").Evaluate(env);
        Assert.AreEqual(45, env.GetVariable("b"));
    }
    
    [Test]
    public void DoWhileStatementTest()
    {
        Assert.AreEqual("do b = 1; while ((a == 1))", ScriptParser.DoWhileStatement.Parse(" do \n b = 1 \n while (a == 1) ").ToString());
        Assert.AreEqual("do { a = 2; } while ((a == 1))", ScriptParser.DoWhileStatement.Parse(@"
            do
            {
                a = 2;
            }
            while(a == 1)
            ").ToString());
        
        Assert.AreEqual("do { a = 2; b = 4; } while ((a == 1))", ScriptParser.DoWhileStatement.Parse(@"
            do
            {
                a = 2;
                b = 4;
            }
            while(a == 1)
            ").ToString());
    }

    [Test]
    public void DoWhileStatementEvaluate()
    {
        var env = new SwordEnvironment();
        env.SetVariable("a", 1);
        ScriptParser.DoWhileStatement.Parse(" do \n a = 2 \n while (a == 1) ").Evaluate(env);
        Assert.AreEqual(2, env.GetVariable("a"));
        
        env.SetVariable("a", 1);
        ScriptParser.DoWhileStatement.Parse(@"
            do
            {
                a = 2;
            }
            while(a == 1)
            ").Evaluate(env);
        
        env.SetVariable("a", 1);
        env.SetVariable("b", 0);
        ScriptParser.DoWhileStatement.Parse(@"
            do
            {
                b = b + a;
                a = a + 1;
            }
            while(a < 10)
            ").Evaluate(env);
        Assert.AreEqual(45, env.GetVariable("b"));
    }
}

执行测试,全部通过。

结语

本章完成了语句解析,此时该脚本语言已经可以进行简单的流程运算了。不过距离一个完整的脚本语言,还差最关键的一步:函数

下一章当中,我们将开始对脚本语言进行函数定义,并让表达式与语句可以调用与执行函数。