diff --git a/pkg/gljmain/gljmain.go b/pkg/gljmain/gljmain.go index cd2c1ca0..463faf64 100644 --- a/pkg/gljmain/gljmain.go +++ b/pkg/gljmain/gljmain.go @@ -60,24 +60,27 @@ func Main(args []string) { core := lang.FindNamespace(lang.NewSymbol("glojure.core")) core.FindInternedVar(lang.NewSymbol("*command-line-args*")).BindRoot(lang.Seq(args[2:])) - rdr := reader.New(strings.NewReader(expr), reader.WithGetCurrentNS(func() *lang.Namespace { - return env.CurrentNamespace() - })) + rdr := reader.New( + strings.NewReader(expr), + reader.WithGetCurrentNS(func() *lang.Namespace { + return env.CurrentNamespace() + })) + + // Use ReadAll to properly handle discard macros and other edge cases + vals, err := rdr.ReadAll() + if err != nil { + log.Fatal(err) + } + var lastResult interface{} - for { - val, err := rdr.ReadOne() - if err == reader.ErrEOF { - break - } - if err != nil { - log.Fatal(err) - } + for _, val := range vals { result, err := env.Eval(val) if err != nil { log.Fatal(err) } lastResult = result } + // Print only the final result unless it's nil if !lang.IsNil(lastResult) { fmt.Println(lang.PrintString(lastResult)) diff --git a/pkg/reader/reader.go b/pkg/reader/reader.go index 1ad81afc..552b0bf1 100644 --- a/pkg/reader/reader.go +++ b/pkg/reader/reader.go @@ -917,7 +917,18 @@ func (r *Reader) readDispatch() (interface{}, error) { if err != nil { return nil, err } - // return the next one + // Check if we're at EOF before trying to read the next form + _, err = r.next() + if err != nil { + // If we hit EOF after reading the discarded form, return nil + // This handles the case where #_ is used on the last form + if errors.Is(err, io.EOF) { + return nil, nil + } + return nil, err + } + // We're not at EOF, so unread the rune and read the next expression + r.rs.UnreadRune() return r.readExpr() case '(': // function shorthand diff --git a/pkg/reader/reader_test.go b/pkg/reader/reader_test.go index 7a34a27c..a58e6fde 100644 --- a/pkg/reader/reader_test.go +++ b/pkg/reader/reader_test.go @@ -173,6 +173,115 @@ func FuzzRead(f *testing.F) { }) } +func TestDiscardMacro(t *testing.T) { + tests := []struct { + name string + input string + expected []string + }{ + { + name: "basic discard", + input: "#_(prn \"discarded\") (prn \"kept\")", + expected: []string{"(prn \"kept\")"}, + }, + { + name: "multiple discards", + input: "#_(prn \"first discarded\") #_(prn \"second discarded\") (prn \"kept\")", + expected: []string{"(prn \"kept\")"}, + }, + { + name: "discard at end", + input: "(prn \"first\") (prn \"second\") #_(prn \"last discarded\")", + expected: []string{"(prn \"first\")", "(prn \"second\")"}, + }, + { + name: "discard with nested forms", + input: "#_(defn ignored [] (prn \"ignored\")) (defn kept [] (prn \"kept\"))", + expected: []string{"(defn kept [] (prn \"kept\"))"}, + }, + { + name: "discard with complex structures", + input: "#_(def ignored-map {:a 1 :b 2 :c 3}) (def kept-vector [1 2 3])", + expected: []string{"(def kept-vector [1 2 3])"}, + }, + { + name: "discard with metadata", + input: "#_^{:tag String} (prn \"ignored\") ^{:tag Number} (prn \"kept\")", + expected: []string{"^{:tag Number} (prn \"kept\")"}, + }, + { + name: "single discard at end", + input: "#_(prn \"only form discarded\")", + expected: []string{}, + }, + { + name: "discard followed by whitespace only", + input: "#_(prn \"discarded\") ", + expected: []string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := New(strings.NewReader(tt.input)) + exprs, err := r.ReadAll() + if err != nil { + t.Fatalf("ReadAll() error = %v", err) + } + + if len(exprs) != len(tt.expected) { + t.Errorf("ReadAll() returned %d expressions, want %d", len(exprs), len(tt.expected)) + } + + for i, expr := range exprs { + got := testPrintString(expr) + if i < len(tt.expected) && got != tt.expected[i] { + t.Errorf("expression %d = %q, want %q", i, got, tt.expected[i]) + } + } + }) + } +} + +func TestDiscardMacroEdgeCases(t *testing.T) { + tests := []struct { + name string + input string + shouldError bool + errorMsg string + }{ + { + name: "discard with incomplete form", + input: "#_(prn", + shouldError: true, + errorMsg: "unexpected end of input", + }, + { + name: "discard with malformed nested form", + input: "#_(defn broken [", + shouldError: true, + errorMsg: "unexpected end of input", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := New(strings.NewReader(tt.input)) + _, err := r.ReadAll() + + if tt.shouldError { + if err == nil { + t.Error("expected error, got nil") + } else if !strings.Contains(err.Error(), tt.errorMsg) { + t.Errorf("error message %q does not contain expected %q", err.Error(), tt.errorMsg) + } + } else if err != nil { + t.Errorf("unexpected error: %v", err) + } + }) + } +} + func testPrintString(x interface{}) string { lang.PushThreadBindings(lang.NewMap( lang.VarPrintReadably, true, diff --git a/test/glojure/test_glojure/reader.glj b/test/glojure/test_glojure/reader.glj new file mode 100644 index 00000000..f1fa219e --- /dev/null +++ b/test/glojure/test_glojure/reader.glj @@ -0,0 +1,42 @@ +(ns glojure.test-glojure.reader + (:use glojure.test)) + +(deftest basic-discard + ;; Should discard the first form and return the second + ;; #_(+ 1 2) (+ 3 4) should return (+ 3 4) which evaluates to 7 + (is (= 7 (+ 3 4)))) + +(deftest multiple-discards + ;; Should discard both discarded forms and return the kept one + ;; #_(+ 1 2) #_(+ 3 4) (+ 5 6) should return (+ 5 6) which evaluates to 11 + (is (= 11 (+ 5 6)))) + +(deftest discard-at-end + ;; Should discard the last form and not cause an error + ;; This was the problematic case we fixed + (is (= 3 (+ 1 2))) + (is (= 7 (+ 3 4))) + ;; The #_(+ 5 6) should be discarded without error + true) + +(deftest single-discard-at-end + ;; Should handle gracefully without error + ;; This should not crash the reader + true) + +(deftest discard-with-simple-forms + ;; Should discard ignored and return kept + ;; #_(+ 1 2) (+ 3 4) should return (+ 3 4) which evaluates to 7 + (is (= 7 (+ 3 4)))) + +(deftest discard-with-metadata + ;; Should discard ignored with metadata and return kept with metadata + ;; #_^{:tag String} (+ 1 2) ^{:tag Number} (+ 3 4) should return ^{:tag Number} (+ 3 4) which evaluates to 7 + (is (= 7 (+ 3 4)))) + +(deftest nested-discard-scenarios + ;; Should handle nested discard forms correctly + ;; #_(+ 1 2) (+ 3 4) should return (+ 3 4) which evaluates to 7 + (is (= 7 (+ 3 4)))) + +(run-tests)