;; Parser tests for Ruby 2.7 subset. (define rb-deep=? (fn (a b) (cond ((= a b) true) ((and (dict? a) (dict? b)) (let ((ak (keys a)) (bk (keys b))) (if (not (= (len ak) (len bk))) false (every? (fn (k) (and (has-key? b k) (rb-deep=? (get a k) (get b k)))) ak)))) ((and (list? a) (list? b)) (if (not (= (len a) (len b))) false (let ((i 0) (ok true)) (define rb-de-loop (fn () (when (and ok (< i (len a))) (do (when (not (rb-deep=? (nth a i) (nth b i))) (set! ok false)) (set! i (+ i 1)) (rb-de-loop))))) (rb-de-loop) ok))) (:else false)))) (define rb-test-pass 0) (define rb-test-fail 0) (define rb-test-fails (list)) (define rb-test (fn (name actual expected) (if (rb-deep=? actual expected) (set! rb-test-pass (+ rb-test-pass 1)) (do (set! rb-test-fail (+ rb-test-fail 1)) (append! rb-test-fails {:name name :actual actual :expected expected}))))) ;; Shorthand: parse src and extract :stmts list (define rb-p-stmts (fn (src) (get (rb-parse-str src) :stmts))) ;; Shorthand: parse and get first statement (define rb-p-first (fn (src) (nth (rb-p-stmts src) 0))) ;; ── Literals ───────────────────────────────────────────────────────────────── (rb-test "int literal" (rb-p-first "42") {:type "lit-int" :value 42}) (rb-test "negative int" (rb-p-first "-7") {:type "unop" :op "-" :value {:type "lit-int" :value 7}}) (rb-test "float literal" (rb-p-first "3.14") {:type "lit-float" :value "3.14"}) (rb-test "string literal" (rb-p-first "\"hello\"") {:type "lit-str" :value "hello"}) (rb-test "symbol literal" (rb-p-first ":foo") {:type "lit-sym" :value "foo"}) (rb-test "nil literal" (rb-p-first "nil") {:type "lit-nil"}) (rb-test "true literal" (rb-p-first "true") {:type "lit-bool" :value true}) (rb-test "false literal" (rb-p-first "false") {:type "lit-bool" :value false}) (rb-test "self" (rb-p-first "self") {:type "self"}) (rb-test "%w[] words" (rb-p-first "%w[a b c]") {:type "lit-words" :elems (list "a" "b" "c")}) (rb-test "%i[] isymbols" (rb-p-first "%i[x y]") {:type "lit-isyms" :elems (list "x" "y")}) ;; ── Variables ───────────────────────────────────────────────────────────────── (rb-test "local var / send" (rb-p-first "x") {:type "send" :name "x" :args (list) :block nil}) (rb-test "ivar" (rb-p-first "@foo") {:type "ivar" :name "@foo"}) (rb-test "cvar" (rb-p-first "@@count") {:type "cvar" :name "@@count"}) (rb-test "gvar" (rb-p-first "$stdout") {:type "gvar" :name "$stdout"}) (rb-test "constant" (rb-p-first "Foo") {:type "const" :name "Foo"}) (rb-test "const path" (rb-p-first "Foo::Bar") {:type "const-path" :left {:type "const" :name "Foo"} :name "Bar"}) (rb-test "triple const path" (rb-p-first "A::B::C") {:type "const-path" :left {:type "const-path" :left {:type "const" :name "A"} :name "B"} :name "C"}) ;; ── Arrays and Hashes ───────────────────────────────────────────────────────── (rb-test "empty array" (rb-p-first "[]") {:type "array" :elems (list)}) (rb-test "array literal" (rb-p-first "[1, 2, 3]") {:type "array" :elems (list {:type "lit-int" :value 1} {:type "lit-int" :value 2} {:type "lit-int" :value 3})}) (rb-test "hash colon style" (get (rb-p-first "{a: 1}") :type) "hash") (rb-test "hash pair style" (get (nth (get (rb-p-first "{a: 1}") :pairs) 0) :style) "colon") (rb-test "hash symbol key" (get (get (nth (get (rb-p-first "{a: 1}") :pairs) 0) :key) :value) "a") ;; ── Binary operators ────────────────────────────────────────────────────────── (rb-test "addition" (rb-p-first "1 + 2") {:type "binop" :op "+" :left {:type "lit-int" :value 1} :right {:type "lit-int" :value 2}}) (rb-test "subtraction" (get (rb-p-first "a - b") :op) "-") (rb-test "multiplication" (get (rb-p-first "x * y") :op) "*") (rb-test "precedence: * before +" (rb-p-first "1 + 2 * 3") {:type "binop" :op "+" :left {:type "lit-int" :value 1} :right {:type "binop" :op "*" :left {:type "lit-int" :value 2} :right {:type "lit-int" :value 3}}}) (rb-test "power right-assoc" (rb-p-first "2 ** 3 ** 4") {:type "binop" :op "**" :left {:type "lit-int" :value 2} :right {:type "binop" :op "**" :left {:type "lit-int" :value 3} :right {:type "lit-int" :value 4}}}) (rb-test "equality" (get (rb-p-first "a == b") :op) "==") (rb-test "logical and" (get (rb-p-first "a && b") :op) "&&") (rb-test "logical or" (get (rb-p-first "a || b") :op) "||") (rb-test "range inclusive" (rb-p-first "1..5") {:type "range" :from {:type "lit-int" :value 1} :to {:type "lit-int" :value 5} :exclusive false}) (rb-test "range exclusive" (get (rb-p-first "1...5") :exclusive) true) ;; ── Assignment ──────────────────────────────────────────────────────────────── (rb-test "assign" (rb-p-first "x = 1") {:type "assign" :target {:type "send" :name "x" :args (list) :block nil} :value {:type "lit-int" :value 1}}) (rb-test "op-assign +=" (get (rb-p-first "x += 1") :type) "op-assign") (rb-test "op-assign op" (get (rb-p-first "x += 1") :op) "+") (rb-test "massign" (get (rb-p-first "a, b = 1, 2") :type) "massign") (rb-test "massign targets" (len (get (rb-p-first "a, b = 1, 2") :targets)) 2) (rb-test "massign value array" (get (get (rb-p-first "a, b = 1, 2") :value) :type) "array") ;; ── Method calls ────────────────────────────────────────────────────────────── (rb-test "call with parens" (rb-p-first "foo(1, 2)") {:type "send" :name "foo" :args (list {:type "lit-int" :value 1} {:type "lit-int" :value 2}) :block nil}) (rb-test "chained call" (get (rb-p-first "obj.foo") :type) "call") (rb-test "chained call method" (get (rb-p-first "obj.foo") :method) "foo") (rb-test "chained call with args" (len (get (rb-p-first "obj.foo(1, 2)") :args)) 2) (rb-test "no-paren call" (get (rb-p-first "puts \"hello\"") :type) "send") (rb-test "no-paren call name" (get (rb-p-first "puts \"hello\"") :name) "puts") (rb-test "no-paren call args" (len (get (rb-p-first "puts \"hello\"") :args)) 1) (rb-test "indexing" (get (rb-p-first "a[0]") :type) "index") ;; ── Unary operators ─────────────────────────────────────────────────────────── (rb-test "unary not" (rb-p-first "!x") {:type "unop" :op "!" :value {:type "send" :name "x" :args (list) :block nil}}) (rb-test "unary minus" (get (rb-p-first "-x") :op) "-") ;; ── Method def ──────────────────────────────────────────────────────────────── (rb-test "empty method def" (get (rb-p-first "def foo; end") :type) "method-def") (rb-test "method def name" (get (rb-p-first "def foo; end") :name) "foo") (rb-test "method def no params" (len (get (rb-p-first "def foo; end") :params)) 0) (rb-test "method def with params" (len (get (rb-p-first "def foo(a, b); end") :params)) 2) (rb-test "method def param-req" (get (nth (get (rb-p-first "def foo(a); end") :params) 0) :type) "param-req") (rb-test "method def param name" (get (nth (get (rb-p-first "def foo(a); end") :params) 0) :name) "a") (rb-test "method def optional param" (get (nth (get (rb-p-first "def foo(a, b=1); end") :params) 1) :type) "param-opt") (rb-test "method def splat" (get (nth (get (rb-p-first "def foo(*args); end") :params) 0) :type) "param-rest") (rb-test "method def double splat" (get (nth (get (rb-p-first "def foo(**opts); end") :params) 0) :type) "param-kwrest") (rb-test "method def block param" (get (nth (get (rb-p-first "def foo(&blk); end") :params) 0) :type) "param-block") (rb-test "method def all param types" (len (get (rb-p-first "def foo(a, b=1, *c, **d, &e); end") :params)) 5) (rb-test "method def singleton recv" (get (get (rb-p-first "def self.bar; end") :recv) :type) "self") (rb-test "method def body" (len (get (rb-p-first "def foo; 1; 2; end") :body)) 2) ;; ── Class def ──────────────────────────────────────────────────────────────── (rb-test "class def type" (get (rb-p-first "class Foo; end") :type) "class-def") (rb-test "class def name" (get (get (rb-p-first "class Foo; end") :name) :name) "Foo") (rb-test "class def no super" (nil? (get (rb-p-first "class Foo; end") :super)) true) (rb-test "class def with super" (get (get (rb-p-first "class Foo < Bar; end") :super) :name) "Bar") (rb-test "singleton class" (get (rb-p-first "class << self; end") :type) "sclass") ;; ── Module def ──────────────────────────────────────────────────────────────── (rb-test "module def type" (get (rb-p-first "module M; end") :type) "module-def") (rb-test "module def name" (get (get (rb-p-first "module M; end") :name) :name) "M") ;; ── Blocks ──────────────────────────────────────────────────────────────────── (rb-test "block do...end" (get (get (rb-p-first "foo do |x| x end") :block) :type) "block") (rb-test "block brace" (get (get (rb-p-first "foo { |x| x }") :block) :type) "block") (rb-test "block params" (len (get (get (rb-p-first "foo { |a, b| a }") :block) :params)) 2) (rb-test "block no params" (len (get (get (rb-p-first "foo { 42 }") :block) :params)) 0) ;; ── Control flow ────────────────────────────────────────────────────────────── (rb-test "return type" (get (rb-p-first "return 1") :type) "return") (rb-test "return value" (get (get (rb-p-first "return 1") :value) :value) 1) (rb-test "return nil" (nil? (get (rb-p-first "return") :value)) true) (rb-test "yield type" (get (rb-p-first "yield 1") :type) "yield") (rb-test "break type" (get (rb-p-first "break") :type) "break") (rb-test "next type" (get (rb-p-first "next") :type) "next") (rb-test "redo type" (get (rb-p-first "redo") :type) "redo") ;; ── Multi-statement program ─────────────────────────────────────────────────── (rb-test "two statements" (len (rb-p-stmts "1\n2")) 2) (rb-test "semi-separated" (len (rb-p-stmts "1; 2; 3")) 3) (rb-test "class with method" (let ((cls (rb-p-first "class Foo\n def bar\n 1\n end\nend"))) (len (get cls :body))) 1) (list rb-test-pass rb-test-fail)