From e8582732476ad58c3c8ae7956c67398aa1f5ee5a Mon Sep 17 00:00:00 2001 From: oyamo Date: Sun, 18 Jan 2026 20:44:36 +0300 Subject: [PATCH 1/5] ft: add cmd arguments for testing --- main.go | 59 ++++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 46 insertions(+), 13 deletions(-) diff --git a/main.go b/main.go index ed36acb..dff3f7e 100644 --- a/main.go +++ b/main.go @@ -30,6 +30,28 @@ var ( styles.HelpStyle.Bold(true).Render("nuru --toleo"))) ) +func hasValidExtension(file string) bool { + return strings.HasSuffix(file, ".nr") || strings.HasSuffix(file, ".sw") +} + +func readFile(file string) (string, error) { + if !hasValidExtension(file) { + return "", fmt.Errorf("'%s' sii faili sahihi. Tumia faili la '.nr' au '.sw'", file) + } + + contents, err := os.ReadFile(file) + if err != nil { + return "", fmt.Errorf("Error: Nuru imeshindwa kusoma faili: %s", file) + } + + return string(contents), nil +} + +func printError(err error) { + fmt.Println(styles.ErrorStyle.Render(err.Error())) + os.Exit(1) +} + func main() { args := os.Args @@ -51,23 +73,34 @@ func main() { repl.Docs() default: file := args[1] + if content, err := readFile(file); err == nil { + repl.Read(content) + } else { + printError(err) + } + } - if strings.HasSuffix(file, "nr") || strings.HasSuffix(file, ".sw") { - contents, err := os.ReadFile(file) - if err != nil { - fmt.Println(styles.ErrorStyle.Render("Error: Nuru imeshindwa kusoma faili: ", args[1])) - os.Exit(1) - } + return + } - repl.Read(string(contents)) + if len(args) > 2 { + switch args[1] { + case "pima": + file := args[2] + if content, err := readFile(file); err == nil { + repl.Test(content) } else { - fmt.Println(styles.ErrorStyle.Render("'"+file+"'", "sii faili sahihi. Tumia faili la '.nr' au '.sw'")) - os.Exit(1) + printError(err) } + default: + printError(fmt.Errorf("Error: Operesheni uliyochagua haijaumdwa")) } - } else { - fmt.Println(styles.ErrorStyle.Render("Error: Operesheni imeshindikana boss.")) - fmt.Println(Help) - os.Exit(1) + + return } + + fmt.Println(styles.ErrorStyle.Render("Error: Operesheni imeshindikana boss.")) + fmt.Println(Help) + os.Exit(1) + } From 06ee3313b1d626a6392ff7264dca97a2c366862d Mon Sep 17 00:00:00 2001 From: oyamo Date: Sun, 18 Jan 2026 20:45:40 +0300 Subject: [PATCH 2/5] ft: define test functions for the pima module --- module/pima.go | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 module/pima.go diff --git a/module/pima.go b/module/pima.go new file mode 100644 index 0000000..978d320 --- /dev/null +++ b/module/pima.go @@ -0,0 +1,50 @@ +package module + +import ( + "github.com/NuruProgramming/Nuru/object" +) + +type ReporterFunc func(pass bool, message string) + +var TestReporter ReporterFunc + +var TestFunctions = map[string]object.ModuleFunction{} + +func init() { + TestFunctions["hakiki"] = assert +} + +func assert(args []object.Object, defs map[string]object.Object) object.Object { + if len(args) < 1 { + return &object.Error{Message: "Hakiki inahitaji condition."} + } + + condition := args[0] + isTrue := true + + switch val := condition.(type) { + case *object.Boolean: + isTrue = val.Value + case *object.Null: + isTrue = false + default: + isTrue = true + } + + msg := "Jaribio" + if len(args) > 1 { + if m, ok := args[1].(*object.String); ok { + msg = m.Value + } + } + + if TestReporter != nil { + TestReporter(isTrue, msg) + return &object.Boolean{Value: true} + } + + if isTrue { + return &object.Boolean{Value: true} + } + return &object.Error{Message: msg} +} From c584fe02d9303e5e349e6be4d1adaffa2734498b Mon Sep 17 00:00:00 2001 From: oyamo Date: Sun, 18 Jan 2026 20:46:05 +0300 Subject: [PATCH 3/5] ft: add pima module to module list --- module/module.go | 1 + 1 file changed, 1 insertion(+) diff --git a/module/module.go b/module/module.go index f4c17c7..cdd825b 100644 --- a/module/module.go +++ b/module/module.go @@ -10,4 +10,5 @@ func init() { Mapper["mtandao"] = &object.Module{Name: "net", Functions: NetFunctions} Mapper["jsoni"] = &object.Module{Name: "json", Functions: JsonFunctions} Mapper["hisabati"] = &object.Module{Name: "hisabati", Functions: MathFunctions} + Mapper["pima"] = &object.Module{Name: "pima", Functions: TestFunctions} } From 0563402119dfdc1e84e78df27b67f2ae05336e59 Mon Sep 17 00:00:00 2001 From: oyamo Date: Sun, 18 Jan 2026 20:47:15 +0300 Subject: [PATCH 4/5] ft: add parser and executor for test files --- repl/test.go | 232 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100644 repl/test.go diff --git a/repl/test.go b/repl/test.go new file mode 100644 index 0000000..550a857 --- /dev/null +++ b/repl/test.go @@ -0,0 +1,232 @@ +package repl + +import ( + "fmt" + "strings" + "time" + + "github.com/NuruProgramming/Nuru/ast" + "github.com/NuruProgramming/Nuru/evaluator" + "github.com/NuruProgramming/Nuru/lexer" + "github.com/NuruProgramming/Nuru/module" + "github.com/NuruProgramming/Nuru/object" + "github.com/NuruProgramming/Nuru/parser" + "github.com/NuruProgramming/Nuru/token" + "github.com/charmbracelet/lipgloss" +) + +const ( + MsgNoTests = "Hakuna majaribio yaliyopatikana" + MsgSyntaxErrors = "Kuna Makosa Yafuatayo:" + + TxtPass = " IMEPITA" + TxtFail = " IMEFELI" + + HeaderStart = "============================= JARIBIO LIMEANZA =============================" + HeaderFailures = "========================= MAJARIBIO YALIYOSHINDWA ==========================" + + FmtCollected = "imekusanya vipengele %d\n\n" + FmtFailDivider = "_________________________ %s _________________________" + FmtFailDetail = "> %s\n\n" + FmtSummaryFail = "%d imeshindwa" + FmtSummaryPass = "%d imefaulu" + FmtSummaryTime = "kwa muda wa %.2fs" + FmtSummaryTotal = "======================= %s ========================" + FmtTestVerbose = "%-30s %s" +) + +var ( + stylePass = lipgloss.NewStyle().Foreground(lipgloss.Color("42")) + styleFail = lipgloss.NewStyle().Foreground(lipgloss.Color("196")) + styleFailBold = styleFail.Copy().Bold(true) + stylePassBold = stylePass.Copy().Bold(true) + styleHeader = lipgloss.NewStyle().Bold(true) +) + +type testFailure struct { + name string + msg string +} + +type testEvent struct { + pass bool + msg string +} + +func Test(source string) bool { + env := object.NewEnvironment() + var events []testEvent + + teardown := attachReporter(&events) + defer teardown() + + program, errs := parse(source) + if len(errs) > 0 { + reportSyntaxErrors(errs) + return false + } + + hoistFunctions(program, env) + evaluator.Eval(program, env) + + tests := collectTests(program) + if len(tests) == 0 { + fmt.Println(MsgNoTests) + return true + } + + printSessionHeader(len(tests)) + + startTotal := time.Now() + failures := runTestLoop(tests, env, &events) + totalDuration := time.Since(startTotal).Seconds() + + fmt.Println("") + + if len(failures) > 0 { + printFailuresSection(failures) + } + + return printSessionSummary(len(tests), len(failures), totalDuration) +} + +func attachReporter(events *[]testEvent) func() { + module.TestReporter = func(pass bool, message string) { + *events = append(*events, testEvent{pass, message}) + } + return func() { + module.TestReporter = nil + } +} + +func parse(source string) (*ast.Program, []string) { + l := lexer.New(source) + p := parser.New(l) + return p.ParseProgram(), p.Errors() +} + +func reportSyntaxErrors(errors []string) { + fmt.Println(styleFail.Render(MsgSyntaxErrors)) + for _, msg := range errors { + fmt.Println("\t" + styleFail.Render(msg)) + } +} + +func hoistFunctions(program *ast.Program, env *object.Environment) { + for _, stmt := range program.Statements { + exprStmt, ok := stmt.(*ast.ExpressionStatement) + if !ok { + continue + } + fn, ok := exprStmt.Expression.(*ast.FunctionLiteral) + if !ok { + continue + } + if fn.Name == "" { + continue + } + val := evaluator.Eval(fn, env) + env.Set(fn.Name, val) + } +} + +func collectTests(program *ast.Program) []string { + var tests []string + for _, stmt := range program.Statements { + name := identifyTest(stmt) + if name != "" { + tests = append(tests, name) + } + } + return tests +} + +func identifyTest(stmt ast.Statement) string { + var name string + switch s := stmt.(type) { + case *ast.ExpressionStatement: + if fn, ok := s.Expression.(*ast.FunctionLiteral); ok { + name = fn.Name + } + case *ast.LetStatement: + name = s.Name.Value + } + if strings.HasPrefix(name, "pima_") { + return name + } + return "" +} + +func runTestLoop(tests []string, env *object.Environment, events *[]testEvent) []testFailure { + var failures []testFailure + + for _, name := range tests { + *events = []testEvent{} + + call := &ast.CallExpression{ + Token: token.Token{Type: token.IDENT, Literal: name}, + Function: &ast.Identifier{Token: token.Token{Type: token.IDENT, Literal: name}, Value: name}, + Arguments: []ast.Expression{}, + } + evaluator.Eval(call, env) + + testFailed := false + var failureMsg string + + for _, e := range *events { + if !e.pass { + testFailed = true + failureMsg = e.msg + } + } + + if testFailed { + status := styleFail.Render(TxtFail) + fmt.Printf(FmtTestVerbose+"\n", name, status) + failures = append(failures, testFailure{name: name, msg: failureMsg}) + } else { + status := stylePass.Render(TxtPass) + fmt.Printf(FmtTestVerbose+"\n", name, status) + } + } + + return failures +} + +func printSessionHeader(count int) { + fmt.Println(styleHeader.Render(HeaderStart)) + fmt.Printf(FmtCollected, count) +} + +func printFailuresSection(failures []testFailure) { + fmt.Println("") + fmt.Println(styleFailBold.Render(HeaderFailures)) + + for _, f := range failures { + header := fmt.Sprintf(FmtFailDivider, f.name) + fmt.Println(styleFail.Render(header)) + fmt.Printf(FmtFailDetail, f.msg) + } +} + +func printSessionSummary(total, failed int, duration float64) bool { + passed := total - failed + + parts := []string{} + if failed > 0 { + parts = append(parts, fmt.Sprintf(FmtSummaryFail, failed)) + } + parts = append(parts, fmt.Sprintf(FmtSummaryPass, passed)) + parts = append(parts, fmt.Sprintf(FmtSummaryTime, duration)) + + summaryText := strings.Join(parts, ", ") + fullBar := fmt.Sprintf(FmtSummaryTotal, summaryText) + + if failed > 0 { + fmt.Println(styleFailBold.Render(fullBar)) + return false + } + + fmt.Println(stylePassBold.Render(fullBar)) + return true +} From 413689b2910b1628b2d68efe5b0358b3fe9ef89d Mon Sep 17 00:00:00 2001 From: oyamo Date: Sun, 18 Jan 2026 20:47:33 +0300 Subject: [PATCH 5/5] ft: add example test file --- examples/test.nr | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 examples/test.nr diff --git a/examples/test.nr b/examples/test.nr new file mode 100644 index 0000000..bf2c17c --- /dev/null +++ b/examples/test.nr @@ -0,0 +1,46 @@ +tumia pima + +unda pata_mraba(namba) { + rudisha namba * namba; +} + +unda pima_mraba() { + fanya x = pata_mraba(5); + pima.hakiki(x == 25, "Hesabu ya mraba sio sahihi"); + + x = pata_mraba(10); + pima.hakiki(x == 100, "Hesabu ya mraba sio sahihi"); + + x = pata_mraba(100); + pima.hakiki(x == 10000, "Hesabu ya mraba sio sahihi"); + +} + +unda pima_mraba2() { + fanya x = pata_mraba(5); + pima.hakiki(x == 25, "Hesabu ya mraba sio sahihi"); +} + +unda pima_mraba3() { + fanya x = pata_mraba(5); + pima.hakiki(x == 25, "Hesabu ya mraba sio sahihi"); +} + +unda pima_mraba4() { + fanya x = pata_mraba(5); + pima.hakiki(x == 25, "Hesabu ya mraba sio sahihi"); +} + +unda pima_ambayo_itafeli() { + pima.hakiki(pata_mraba(9) == 16, "Hesabu ya mraba sio sahihi"); + pima.hakiki(pata_mraba(10) == 16, "Hesabu ya mraba sio sahihi"); +} + + +unda pima_ambayo_itafeli2() { + pima.hakiki(pata_mraba(10) == 10 * 11, "Hesabu ya mraba sio sahihi"); +} + +unda pima_ambayo_itafeli3() { + pima.hakiki(pata_mraba(10) == 10 * 11, "Hesabu ya mraba sio sahihi"); +}