SwordScript - 使用C#开发脚本语言(七)变量与环境

163 阅读2分钟

本章节对应仓库

5.变量与环境 Github

环境

在开始定义变量前,脚本需要有一个存放变量的环境。上一章之中,我们定义了SwordEnvironment作为环境类,本章将对该类进行进一步的扩展。

存储变量

脚本语言中的变量与变量存储的值,基本可以看做 string - object 的键值对。因此,我们暂时先用字典来存储变量。

public class SwordEnvironment
{
    private Dictionary<string, object> _variables = new Dictionary<string, object>();
    
    public void SetVariable(string name, object value)
    {
        _variables[name] = value;
    }
    
    public object GetVariable(string name)
    {
        if(_variables.ContainsKey(name))
            return _variables[name];
        return null;
    }
}

变量赋值语句

在有了存放变量的环境后,就可以开始定义变量赋值语句了。

用BNF表示变量赋值语句的语法如下:

assignment ::= <identifier> <"="> <expr> [";"]

赋值语句需要有代表变量的标识符,用于赋值的表达式,以及句末可选的分号。

先定义代表赋值语句的抽象语法树节点:

ASTBinaryExprAssignment

public class ASTBinaryExprAssignment : ASTBinaryExpr
{
    public ASTBinaryExprAssignment(ASTNode left, ASTNode right) : base(left, right)
    {
    }
    
    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;
    }
}

由于我们的变量语法在设计上不允许连续赋值(如a = b = 1) 因此语句求值中不需要返回值。

接下来,补充对标识符节点的求值方法:

ASTIdentifier

public override object Evaluate(SwordEnvironment env)
{
    return env.GetVariable(Name);
}

Lexer中,加上分号与等号的词法解析

public static readonly Parser<string> Assign = PunctuationSymbol("=");
public static readonly Parser<string> Semicolon = PunctuationSymbol(";");

ScriptParser中,加上变量赋值语句的语法解析式:

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

单元测试

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

[Test]
public void AssignmentEvaluateTest()
{
    var env = new SwordEnvironment();
    ScriptParser.Assignment.Parse(" a = 1 ").Evaluate(env);
    Assert.AreEqual(1, env.GetVariable("a"));
    
    ScriptParser.Assignment.Parse(" a = 1 + 1 ").Evaluate(env);
    Assert.AreEqual(2, env.GetVariable("a"));
    
    Assert.AreEqual(null, env.GetVariable("b"));
    
    ScriptParser.Assignment.Parse(" b = 2 ").Evaluate(env);
    Assert.AreEqual(2, env.GetVariable("b"));
    
    ScriptParser.Assignment.Parse(" c = a == b ").Evaluate(env);
    Assert.AreEqual(true, env.GetVariable("c"));

运行单元测试,全部通过。

结语

变量的解析完成后,下一章将要进行流程语句的解析,包括if、else、while等。 本章的变量存储较为简单,其中直接存为object的方式可能会导致拆箱装箱等问题,这些问题留待后续章节进行优化。