diff --git a/crates/g3-core/tests/code_search_test.rs b/crates/g3-core/tests/code_search_test.rs index 992ad34..13167f1 100644 --- a/crates/g3-core/tests/code_search_test.rs +++ b/crates/g3-core/tests/code_search_test.rs @@ -612,4 +612,117 @@ async fn test_racket_search() { .filter_map(|m| m.captures.get("name").map(|s| s.as_str())) .collect(); assert!(names.contains(&"greet")); + assert!(names.contains(&"add")); + assert!(names.contains(&"factorial")); + assert!(names.contains(&"person-greet")); + assert!(names.contains(&"describe-list")); + assert!(names.contains(&"sum-squares")); +} + +#[tokio::test] +async fn test_racket_structs() { + // Get the workspace root (where Cargo.toml is) + let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); + let workspace_root = std::path::Path::new(&manifest_dir) + .parent() + .and_then(|p| p.parent()) + .unwrap(); + let test_code_path = workspace_root.join("examples/test_code"); + + let request = CodeSearchRequest { + searches: vec![SearchSpec { + name: "racket_structs".to_string(), + query: r#"(list . (symbol) @kw (#eq? @kw "struct") . (symbol) @name)"#.to_string(), + language: "racket".to_string(), + paths: vec![test_code_path.to_string_lossy().to_string()], + context_lines: 0, + }], + max_concurrency: 4, + max_matches_per_search: 500, + }; + + let response = execute_code_search(request).await.unwrap(); + assert_eq!(response.searches.len(), 1); + assert!(response.searches[0].matches.len() > 0); + + // Should find person and point structs + let names: Vec<&str> = response.searches[0] + .matches + .iter() + .filter_map(|m| m.captures.get("name").map(|s| s.as_str())) + .collect(); + assert!(names.contains(&"person"), "Should find 'person' struct, found: {:?}", names); + assert!(names.contains(&"point"), "Should find 'point' struct, found: {:?}", names); +} + +#[tokio::test] +async fn test_racket_macros() { + // Get the workspace root (where Cargo.toml is) + let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); + let workspace_root = std::path::Path::new(&manifest_dir) + .parent() + .and_then(|p| p.parent()) + .unwrap(); + let test_code_path = workspace_root.join("examples/test_code"); + + let request = CodeSearchRequest { + searches: vec![SearchSpec { + name: "racket_macros".to_string(), + query: r#"(list . (symbol) @kw (#eq? @kw "define-syntax-rule") . (list . (symbol) @name))"#.to_string(), + language: "racket".to_string(), + paths: vec![test_code_path.to_string_lossy().to_string()], + context_lines: 0, + }], + max_concurrency: 4, + max_matches_per_search: 500, + }; + + let response = execute_code_search(request).await.unwrap(); + assert_eq!(response.searches.len(), 1); + assert!(response.searches[0].matches.len() > 0, "Should find macros, error: {:?}", response.searches[0].error); + + // Should find swap! and unless macros + let names: Vec<&str> = response.searches[0] + .matches + .iter() + .filter_map(|m| m.captures.get("name").map(|s| s.as_str())) + .collect(); + assert!(names.contains(&"swap!"), "Should find 'swap!' macro, found: {:?}", names); + assert!(names.contains(&"unless"), "Should find 'unless' macro, found: {:?}", names); +} + +#[tokio::test] +async fn test_racket_contracts() { + // Get the workspace root (where Cargo.toml is) + let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); + let workspace_root = std::path::Path::new(&manifest_dir) + .parent() + .and_then(|p| p.parent()) + .unwrap(); + let test_code_path = workspace_root.join("examples/test_code"); + + let request = CodeSearchRequest { + searches: vec![SearchSpec { + name: "racket_contracts".to_string(), + query: r#"(list . (symbol) @kw (#eq? @kw "define/contract") . (list . (symbol) @name))"#.to_string(), + language: "racket".to_string(), + paths: vec![test_code_path.to_string_lossy().to_string()], + context_lines: 0, + }], + max_concurrency: 4, + max_matches_per_search: 500, + }; + + let response = execute_code_search(request).await.unwrap(); + assert_eq!(response.searches.len(), 1); + assert!(response.searches[0].matches.len() > 0, "Should find contract functions, error: {:?}", response.searches[0].error); + + // Should find safe-divide and non-negative-add + let names: Vec<&str> = response.searches[0] + .matches + .iter() + .filter_map(|m| m.captures.get("name").map(|s| s.as_str())) + .collect(); + assert!(names.contains(&"safe-divide"), "Should find 'safe-divide', found: {:?}", names); + assert!(names.contains(&"non-negative-add"), "Should find 'non-negative-add', found: {:?}", names); } diff --git a/docs/CODE_SEARCH.md b/docs/CODE_SEARCH.md index 93e5a6d..9d3a27f 100644 --- a/docs/CODE_SEARCH.md +++ b/docs/CODE_SEARCH.md @@ -40,7 +40,8 @@ g3 includes a syntax-aware code search tool powered by tree-sitter. Unlike text- - Java - C - C++ -- Kotlin +- Racket +- Scheme ## Basic Usage @@ -231,6 +232,65 @@ Tree-sitter queries use S-expression syntax. The basic pattern is: (namespace_definition name: (identifier) @name) ``` +### Racket + +Racket uses an S-expression grammar where code is represented as nested lists. The tree-sitter-racket parser represents most forms as `(list (symbol) ...)` nodes. + +```lisp +;; Function definitions: (define (name args...) body) +(list . (symbol) @kw (#eq? @kw "define") . (list . (symbol) @name)) + +;; Variable definitions: (define name value) +(list . (symbol) @kw (#eq? @kw "define") . (symbol) @name) + +;; Struct definitions: (struct name (fields...)) +(list . (symbol) @kw (#eq? @kw "struct") . (symbol) @name) + +;; Lambda expressions +(list . (symbol) @kw (#match? @kw "^(lambda|λ)$")) + +;; Let bindings +(list . (symbol) @kw (#match? @kw "^(let|let\\*|letrec)$")) + +;; Require statements +(list . (symbol) @kw (#eq? @kw "require")) + +;; Provide statements +(list . (symbol) @kw (#eq? @kw "provide")) + +;; Module definitions +(list . (symbol) @kw (#match? @kw "^module")) + +;; Contracts +(list . (symbol) @kw (#eq? @kw "define/contract") . (list . (symbol) @name)) + +;; Macros +(list . (symbol) @kw (#match? @kw "^(define-syntax|define-syntax-rule)$") . (symbol) @name) + +;; For loops +(list . (symbol) @kw (#match? @kw "^for")) + +;; Match expressions +(list . (symbol) @kw (#eq? @kw "match")) + +;; Class definitions +(list . (symbol) @kw (#match? @kw "^class")) +``` + +**Note**: The `.` (dot) in queries like `(list . (symbol))` means "first child" - it matches the symbol that appears immediately after the opening parenthesis. + +### Scheme + +Scheme uses similar patterns to Racket: + +```lisp +;; Function definitions +(list . (symbol) @kw (#eq? @kw "define") . (list . (symbol) @name)) + +;; Lambda expressions +(list . (symbol) @kw (#eq? @kw "lambda")) +``` + ## Advanced Queries ### Wildcards diff --git a/examples/test_code/example.rkt b/examples/test_code/example.rkt index 64fe470..a236e05 100644 --- a/examples/test_code/example.rkt +++ b/examples/test_code/example.rkt @@ -1,5 +1,11 @@ #lang racket +;; ============================================ +;; Example Racket file for g3 code search tests +;; ============================================ + +;; --- Basic Functions --- + (define (greet name) (printf "Hello, ~a!\n" name)) @@ -11,14 +17,116 @@ 1 (* n (factorial (- n 1))))) +;; --- Variable Definitions --- + +(define pi 3.14159) +(define greeting "Hello, World!") + +;; --- Structs --- + (struct person (name age) #:transparent) +(struct point (x y) #:transparent) (define (person-greet p) (printf "Hello, I'm ~a\n" (person-name p))) -(greet "World") -(displayln (add 5 3)) -(displayln (factorial 5)) +;; --- Pattern Matching --- -(define alice (person "Alice" 30)) -(person-greet alice) +(define (describe-list lst) + (match lst + ['() "empty"] + [(list x) (format "singleton: ~a" x)] + [(list x y) (format "pair: ~a, ~a" x y)] + [_ "many elements"])) + +(define (point-quadrant p) + (match p + [(point (? positive?) (? positive?)) 'first] + [(point (? negative?) (? positive?)) 'second] + [(point (? negative?) (? negative?)) 'third] + [(point (? positive?) (? negative?)) 'fourth] + [_ 'origin-or-axis])) + +;; --- Lambda and Higher-Order Functions --- + +(define double (lambda (x) (* x 2))) +(define triple (λ (x) (* x 3))) + +(define (apply-twice f x) + (f (f x))) + +;; --- Let Bindings --- + +(define (circle-area radius) + (let ([pi 3.14159]) + (* pi radius radius))) + +(define (swap-and-sum a b) + (let* ([temp a] + [a b] + [b temp]) + (+ a b))) + +;; --- For Loops --- + +(define (sum-squares n) + (for/sum ([i (in-range 1 (add1 n))]) + (* i i))) + +(define (collect-evens n) + (for/list ([i (in-range n)] + #:when (even? i)) + i)) + +(define (matrix-coords rows cols) + (for*/list ([r (in-range rows)] + [c (in-range cols)]) + (cons r c))) + +;; --- Macros --- + +(define-syntax-rule (swap! x y) + (let ([tmp x]) + (set! x y) + (set! y tmp))) + +(define-syntax-rule (unless condition body ...) + (when (not condition) + body ...)) + +;; --- Contracts --- + +(define/contract (safe-divide x y) + (-> number? (and/c number? (not/c zero?)) number?) + (/ x y)) + +(define/contract (non-negative-add a b) + (-> (>=/c 0) (>=/c 0) (>=/c 0)) + (+ a b)) + +;; --- Require and Provide --- + +(require racket/string) +(require racket/list) + +;; --- Module --- + +(module+ test + (require rackunit) + + (check-equal? (add 2 3) 5) + (check-equal? (factorial 5) 120) + (check-equal? (sum-squares 3) 14) + (check-equal? (describe-list '()) "empty") + (check-equal? (describe-list '(1)) "singleton: 1")) + +(module+ main + (greet "World") + (displayln (add 5 3)) + (displayln (factorial 5)) + + (define alice (person "Alice" 30)) + (person-greet alice) + + (displayln (sum-squares 10)) + (displayln (collect-evens 10)))