diff --git a/pkg/gcs/eval/stmt.go b/pkg/gcs/eval/stmt.go index 079676d0e..22126c463 100644 --- a/pkg/gcs/eval/stmt.go +++ b/pkg/gcs/eval/stmt.go @@ -19,8 +19,7 @@ func evalFromStmt(n ast.Stmt, env *Env) evalNode { case *ast.WhileStmt: return whileStmtEval(v, env) case *ast.ForStmt: - //TODO: for stmt - return nil + return forStmtEval(v, env) case *ast.AssignStmt: return assignStmtEval(v, env) case *ast.SwitchStmt: diff --git a/pkg/gcs/eval/stmt_for.go b/pkg/gcs/eval/stmt_for.go new file mode 100644 index 000000000..0be88e0bc --- /dev/null +++ b/pkg/gcs/eval/stmt_for.go @@ -0,0 +1,129 @@ +package eval + +import "github.com/genshinsim/gcsim/pkg/gcs/ast" + +type forStmtEvalNode struct { + *ast.ForStmt + state nodeStateFn + env *Env + + lastRes Obj + + initNode evalNode + condNode evalNode + postNode evalNode + bodyNode evalNode +} + +type nodeStateFn func() (nodeStateFn, Obj, error) + +func forStmtEval(n *ast.ForStmt, env *Env) evalNode { + f := &forStmtEvalNode{ + ForStmt: n, + env: NewEnv(env), // for has it's own scope in order to handle init + lastRes: &null{}, + } + // start state is init + f.state = f.init + f.initNode = evalFromStmt(f.Init, f.env) + f.reset() + + return f +} + +func (f *forStmtEvalNode) nextAction() (Obj, bool, error) { + for { + next, res, err := f.state() + // we're done if any of the following is true: + // - err is not nil + // - next is nil + // - res is an action + if err != nil { + return nil, false, err + } + if next == nil { + // here the res could be an action, or may not be, it doesn't matter + // regardless, we are done with this node + return res, true, nil + } + f.state = next + if res.Typ() == typAction { + // this will effectively pause the execution + return res, false, nil + } + } +} + +func (f *forStmtEvalNode) reset() { + f.condNode = evalFromExpr(f.Cond, f.env) + f.postNode = evalFromStmt(f.Post, f.env) + f.bodyNode = evalFromStmt(f.Body, f.env) +} + +func (f *forStmtEvalNode) init() (nodeStateFn, Obj, error) { + // init is simply a stmt that must be evaluated before anything happens + res, done, err := f.initNode.nextAction() + if done { + return f.cond, res, err + } + return f.init, res, err +} + +func (f *forStmtEvalNode) cond() (nodeStateFn, Obj, error) { + // if condition evaluates to false then we should simply exit + res, done, err := f.condNode.nextAction() + // if not done we really don't care what the res or err is at this step + if !done { + return f.cond, res, err + } + // otherwise we want to make sure no error before checking results + if err != nil { + return nil, nil, err + } + switch v := res.(type) { + case *number: + if v.fval == 0 && v.ival == 0 { + return nil, f.lastRes, nil + } + case *strval: + if v.str == "" { + return nil, f.lastRes, nil + } + default: + // default is always false + return nil, f.lastRes, nil + } + // at this point condition is true so we should loop + return f.loop, res, nil +} + +func (f *forStmtEvalNode) loop() (nodeStateFn, Obj, error) { + res, done, err := f.bodyNode.nextAction() + f.lastRes = res + if !done { + return f.loop, res, err + } + if err != nil { + return nil, nil, err + } + if res.Typ() == typRet { + return nil, res, nil + } + if r, ok := res.(*ctrl); ok && r.typ == ast.CtrlBreak { + return nil, &null{}, nil + } + return f.post, res, err +} + +func (f *forStmtEvalNode) post() (nodeStateFn, Obj, error) { + res, done, err := f.postNode.nextAction() + if !done { + return f.post, res, err + } + if err != nil { + return nil, nil, err + } + // reset states + f.reset() + return f.cond, res, nil +} diff --git a/pkg/gcs/eval/stmt_for_test.go b/pkg/gcs/eval/stmt_for_test.go new file mode 100644 index 000000000..852ca419c --- /dev/null +++ b/pkg/gcs/eval/stmt_for_test.go @@ -0,0 +1,119 @@ +package eval + +import ( + "fmt" + "testing" + + "github.com/genshinsim/gcsim/pkg/gcs/ast" +) + +func TestEvalForStmt(t *testing.T) { + prog := ` + for let i = 0; i < 5; i = i + 1 { + print(i); + i; + } + ` + p := ast.New(prog) + _, gcsl, err := p.Parse() + if err != nil { + t.Fatal(err) + } + fmt.Println("program:") + fmt.Println(gcsl.String()) + eval, err := NewEvaluator(gcsl, nil) + if err != nil { + t.Error(err) + t.FailNow() + } + res, _, err := eval.base.nextAction() + if err != nil { + t.Error(err) + t.FailNow() + } + val, ok := res.(*number) + if !ok { + t.Errorf("res is not number, got %v", res.Typ()) + t.FailNow() + } + if val.ival != 4 { + t.Errorf("expecting res to be 4, got %v", val.ival) + } +} + +func TestEvalForStmtBreak(t *testing.T) { + prog := ` + let i = 0; + for i = 0; i < 5; i = i + 1 { + print(i); + if i > 1 { + break; + } + } + i; + ` + p := ast.New(prog) + _, gcsl, err := p.Parse() + if err != nil { + t.Fatal(err) + } + fmt.Println("program:") + fmt.Println(gcsl.String()) + eval, err := NewEvaluator(gcsl, nil) + if err != nil { + t.Error(err) + t.FailNow() + } + res, _, err := eval.base.nextAction() + if err != nil { + t.Error(err) + t.FailNow() + } + val, ok := res.(*number) + if !ok { + t.Errorf("res is not number, got %v", res.Typ()) + t.FailNow() + } + if val.ival != 2 { + t.Errorf("expecting res to be 2, got %v", val.ival) + } +} + +func TestEvalForStmtContinue(t *testing.T) { + prog := ` + let x = 0; + for let i = 0; i < 5; i = i + 1 { + if i < 2 { + continue; + } + x = x + 1; + print(x); + } + x; + ` + p := ast.New(prog) + _, gcsl, err := p.Parse() + if err != nil { + t.Fatal(err) + } + fmt.Println("program:") + fmt.Println(gcsl.String()) + eval, err := NewEvaluator(gcsl, nil) + if err != nil { + t.Error(err) + t.FailNow() + } + res, _, err := eval.base.nextAction() + if err != nil { + t.Error(err) + t.FailNow() + } + val, ok := res.(*number) + if !ok { + t.Errorf("res is not number, got %v", res.Typ()) + t.FailNow() + } + if val.ival != 3 { + t.Errorf("expecting res to be 3, got %v", val.ival) + } +}