🐕

Rustで作る自作言語(4)

2023/03/20に公開
1

前回: https://zenn.dev/taka2/articles/9d3c61e6a2e30c
次回: https://zenn.dev/taka2/articles/97df820e0c38be

今回の目標

関数を定義、使用することが出来るようになる

関数定義の文法

let add(a, b) = a + b;

ASTの定義

関数を表す式のASTを定義する。

pub enum Expr {
    ...
    EFun(String, Box<Expr>)
}

次の関数定義は、

let add(a, b) = a + b;

次のようなASTとなる。

Assign("add", EFun("a", EFun("b", EBinOp("+", EVar("a"), EVar("b")))))

関数定義のパーサーの定義

まず上記関数定義のパーサーを定義します。

pub fn fun_def(input: &str) -> IResult<&str, Statement> {
    let (input, _) = keyword("let")(input)?;
    let (input, id) = identifier(input)?;
    let (input, _) = symbol("(")(input)?;
    let (input, args) = many0(identifier)(input)?;
    let (input, _) = symbol(")")(input)?;
    let (input, _) = symbol("=")(input)?;
    let (input, expr) = parser_expr(input)?;
    let mut result = expr;
    for i in (0..args.len()).rev() {
        result = Expr::EFun(args[i].clone(), Box::new(result))
    }
    Ok((input, Statement::Assign(id, result)))
}

rustでのfoldr的な関数が分からないので手続き的に書いてしまいました。

次に、前回の代入文も含めた文のパーサを定義した。

pub fn parser_statement(input: &str) -> IResult<&str, Statement> {
    let (input, statement) = alt((fun_def, statement_assign))(input)?;
    Ok((input, statement))
}

あと、今までパーサーの入力がすべて消費されていなくてもパースエラーになっていなかったのが気になっていたので、そのような入力がエラーとなるようにした。

pub fn parser_statements(input: &str) -> IResult<&str, Statements> {
    let (input, statements) = many1(parser_statement)(input)?;
    let (input, _) = eof(input)?;
    Ok((input, statements))
}
pub fn parser_for_repl(input: &str) -> IResult<&str, StatementOrExpr> {
    let (input, stmt) = parser_statement_or_expr(input)?;
    let (input, _) = eof(input)?;
    Ok((input, stmt))
}

型推論の実装

まず、関数の型を定義する。

TFun(Box<Type>, Box<Type>),

次に、typeinfer_exprのastのマッチアームにEFunの場合を追加します。

            Expr::EFun(arg, e) => {
                self.env.new_enclosed_env();
                let ty = self.newTVar();
                self.env.insert(arg.to_string(), ty.clone());
                let result_ty = self.typeinfer_expr(e)?;
                self.env = *self.env.outer.clone().unwrap();
                Ok(t_fun(ty, result_ty))
            }

cloneが多くて何とかしたいです...
TypeEnvの定義を変えて、

struct TypeEnv {
    env: HashMap<String, Type>,
    outer: Option<Box<TypeEnv>>,
}

新しいメソッドを定義します。

    pub fn new_enclosed_env(&mut self) {
        self.outer = Some(Box::new(self.clone()));
        self.env = HashMap::new();
    }

getの方も修正

    pub fn get(&self, name: String) -> Result<Type, TypeInferError> {
        match self.env.get(&name) {
            Some(ty) => Ok(ty.clone()),
            None => match &self.outer {
                None => Err(TypeInferError::UndefinedVariable(name)),
                Some(env) => env.get(name),
            },
        }
    }

関数定義の評価

続いて評価の方の実装をする。
まずEval型のenvをRc<Refcell<..>>で包む。

pub struct Eval {
    env: Rc<RefCell<Environment>>,
}

そして、Environmentにouterを追加する。

impl Environment {
    pub fn new() -> Self {
        Environment {
            env: HashMap::new(),
            outer: None,
        }
    }
    pub fn get(&self, name: String) -> Result<Value, EvalError> {
        match self.env.get(&name) {
            Some(value) => Ok(value.clone()),
            None => match &self.outer {
                None => Err(EvalError::UndefinedVariable(name)),
                Some(env) => env.borrow().get(name),
            },
        }
    }
    pub fn insert(&mut self, name: String, val: Value) {
        self.env.insert(name, val);
    }
    pub fn new_enclosed_env(env: Rc<RefCell<Environment>>) -> Self {
        Environment {
            env: HashMap::new(),
            outer: Some(env),
        }
    }
}

(Rustでの『Go言語で作るインタプリタ』のネット上での実装を真似した)
このRc<RefCell<..>>を使ったやり方で、型推論の方も書き直してみる。

pub struct TypeInfer {
    env: Rc<RefCell<TypeEnv>>,
    unassigned_num: u64,
}
struct TypeEnv {
    env: HashMap<String, Type>,
    outer: Option<Rc<RefCell<TypeEnv>>>,
}
impl TypeInfer {
    fn from(env: TypeEnv, unassigned_num: u64) -> Self {
        TypeInfer {
            env: Rc::new(RefCell::new(env)),
            unassigned_num,
        }
    }
    pub fn typeinfer_expr(&mut self, ast: &Expr) -> Result<Type, TypeInferError> {
    ... {
            Expr::EFun(arg, e) => {
                let mut typeinfer = TypeInfer::from(
                    TypeEnv::new_enclosed_env(Rc::clone(&self.env)),
                    self.unassigned_num,
                );
                let ty = self.newTVar();
                typeinfer
                    .env
                    .borrow_mut()
                    .insert(arg.to_string(), ty.clone());
                let result_ty = typeinfer.typeinfer_expr(e)?;
                self.unassigned_num = typeinfer.unassigned_num;
                Ok(t_fun(ty, result_ty))
            }
	}
    }
}

関数適用のパーサーの定義

pub fn fun_app(input: &str) -> IResult<&str, Expr> {
    let (input, e) = alt((delimited(symbol("("), parser_expr, symbol(")")), |input| {
        let (input, ident) = identifier(input)?;
        Ok((input, Expr::EVar(ident)))
    }))(input)?;
    let (input, _) = symbol("(")(input)?;
    let (input, args) = separated_list0(symbol(","), parser_expr)(input)?;
    let (input, _) = symbol(")")(input)?;
    Ok((
        input,
        args.iter()
            .fold(e, |acc, expr| e_fun_app(acc, expr.clone())),
    ))
}

関数適用の型推論

            Expr::EFunApp(e1, e2) => {
                let t1 = self.typeinfer_expr(e1)?;
                let t2 = self.typeinfer_expr(e2)?;
                let t3 = self.newTVar();
                unify(t1, t_fun(t2, t3.clone()))?;
                Ok(t3)
            }

簡単ですね!
replで実験していたら奇妙なバグを発見した

Welcome to lunalang repl!
>>let id(x) = x;
>>:t id  
a1 -> a1
>>:t id(3)
a1
>>:t id(3)
Int
>>

一回目の:t id(3)でもIntと表示されるはずが何故かa1になってしまっている...

            Expr::EFun(arg, e) => {
                let mut typeinfer = TypeInfer::from(
                    TypeEnv::new_enclosed_env(Rc::clone(&self.env)),
                    self.unassigned_num,
                );
                let ty = typeinfer.newTVar(); //ここがself.newTVar()になっていたのが原因
                typeinfer
                    .env
                    .borrow_mut()
                    .insert(arg.to_string(), ty.clone());
                let result_ty = typeinfer.typeinfer_expr(e)?;
                self.unassigned_num = typeinfer.unassigned_num;
                Ok(t_fun(ty, result_ty))
            }

直った
もう一つバグを発見

>>let f(x) = x(x);

この入力がType Errorにならない(occur errorになるはず)

関数適用の評価

関数適用の評価の本体

             )),
+            Expr::EFunApp(e1, e2) => {
+                let v1 = self.eval_expr(*e1)?;
+                let v2 = self.eval_expr(*e2)?;
+                match v1 {
+                    Value::VFun(arg, expr, env) => {
+                        let eval = Eval::from(env);
+                        eval.env.borrow_mut().insert(arg, v2);
+                        eval.eval_expr(expr)
+                    }
+                    _ => Err(EvalError::InternalTypeError),
+                }
+            }
         }

Eval::fromは次のような実装となっている

+    fn from(env: Environment) -> Eval {
+        Eval {
+            env: Rc::new(RefCell::new(env)),
+        }
+    }

これで、再帰関数や高階関数も動く。

Welcome to lunalang repl!
>>let fib(n) = if (n==1) 1 else if (n==2) 1 else fib(n-1) + fib(n-2);
>>fib(10)
Ok(VInt(55))
>>let double(f, x) = f(f(x));
>>double(fib, 3)
Ok(VInt(1))
>>let succ(n) = n+1;
>>double(succ, 3)
Ok(VInt(5))

ついでにテストを見やすくした。

バグの修正

>>let f(x) = x(x);

これがエラーにならないバグを修正した。

             Ok((_, StatementOrExpr::Statement(stmt))) => {
-                repl.typeinfer.typeinfer_statement(&stmt);
-                repl.eval.eval_statement(stmt);
+                match repl.typeinfer.typeinfer_statement(&stmt) {
+                    Ok(()) => match repl.eval.eval_statement(stmt) {
+                        Ok(()) => (),
+                        Err(err) => println!("eval error: {}", err),
+                    },
+                    Err(err) => println!("type error: {}", err),
+                }
             }

replでのStatementOrExprのStatement側での処理にて、typeinferのtype errorが無視されていたのが原因だった。

次回の目標

リファクタリング
文字列型を追加する

Discussion

sh9sh9

foldr相当としてDoubleEndedIterator::rfold()があり、またとても単純な発想ですがrev()してfold()/reduce()でも行けます(多分)scan()のr版は標準だと(多分)無いのでこちらで対応出来ますが、素直にライブラリ使うか、自前で実装するのも容易なので定義したほうが早い・速いです。先行評価での副作用の順序の期待とずれるのとオーバーヘッドがあるので。