diff --git a/include/cpp/jank/read/lex.hpp b/include/cpp/jank/read/lex.hpp index aea9af002..f184ce0ff 100644 --- a/include/cpp/jank/read/lex.hpp +++ b/include/cpp/jank/read/lex.hpp @@ -21,6 +21,7 @@ namespace jank::read::lex meta_hint, reader_macro, reader_macro_comment, + reader_macro_conditional, /* Has string data. */ comment, nil, diff --git a/include/cpp/jank/read/parse.hpp b/include/cpp/jank/read/parse.hpp index 8edd2a7b5..52b5f70c4 100644 --- a/include/cpp/jank/read/parse.hpp +++ b/include/cpp/jank/read/parse.hpp @@ -49,6 +49,7 @@ namespace jank::read::parse object_result parse_reader_macro(); object_result parse_reader_macro_set(); object_result parse_reader_macro_comment(); + object_result parse_reader_macro_conditional(); object_result parse_symbol(); object_result parse_nil(); object_result parse_boolean(); diff --git a/src/cpp/jank/read/lex.cpp b/src/cpp/jank/read/lex.cpp index 7de6713ce..934efa6b9 100644 --- a/src/cpp/jank/read/lex.cpp +++ b/src/cpp/jank/read/lex.cpp @@ -563,10 +563,21 @@ namespace jank::read auto const oc(peek()); ++pos; - if(oc.is_some() && oc.unwrap() == '_') + if(oc.is_some()) { - ++pos; - return ok(token{ token_start, pos - token_start, token_kind::reader_macro_comment }); + switch(oc.unwrap()) + { + case '_': + ++pos; + return ok( + token{ token_start, pos - token_start, token_kind::reader_macro_comment }); + case '?': + ++pos; + return ok( + token{ token_start, pos - token_start, token_kind::reader_macro_conditional }); + default: + break; + } } return ok(token{ token_start, pos - token_start, token_kind::reader_macro }); diff --git a/src/cpp/jank/read/parse.cpp b/src/cpp/jank/read/parse.cpp index 3c9386d6b..d3cfef07b 100644 --- a/src/cpp/jank/read/parse.cpp +++ b/src/cpp/jank/read/parse.cpp @@ -170,6 +170,15 @@ namespace jank::read::parse } return res; } + case lex::token_kind::reader_macro_conditional: + { + auto res(parse_reader_macro_conditional()); + if(res.is_ok() && res.expect_ok() == nullptr) + { + continue; + } + return res; + } case lex::token_kind::nil: return parse_nil(); case lex::token_kind::boolean: @@ -458,6 +467,57 @@ namespace jank::read::parse return ok(nullptr); } + processor::object_result processor::parse_reader_macro_conditional() + { + auto const start_token(token_current.latest.unwrap().expect_ok()); + ++token_current; + + auto list_result(next()); + if(list_result.is_err()) + { + return list_result; + } + else if(list_result.expect_ok() == nullptr) + { + return err( + error{ start_token.pos, native_persistent_string{ "value after #? must be present" } }); + } + else if(list_result.expect_ok()->type != runtime::object_type::persistent_list) + { + return err( + error{ start_token.pos, native_persistent_string{ "value after #? must be a list" } }); + } + + auto const list(runtime::expect_object(list_result.expect_ok())); + + if(list.data->count() % 2 == 1) + { + return err( + error{ start_token.pos, native_persistent_string{ "#? expects an even number of forms" } }); + } + + auto const jank_keyword(rt_ctx.intern_keyword("", "jank").expect_ok()); + auto const default_keyword(rt_ctx.intern_keyword("", "default").expect_ok()); + + for(auto it(list->fresh_seq()); it != nullptr;) + { + auto const kw(it->first()); + /* We take the first match, checking for :jank first. If there are duplicates, it doesn't + * matter. If :default comes first, we'll always take it. In short, order is important. This + * matches Clojure's behavior. */ + if(runtime::detail::equal(kw, jank_keyword) || runtime::detail::equal(kw, default_keyword)) + { + it = it->next_in_place(); + auto const form(it->first()); + return ok(form); + } + + it = it->next_in_place()->next_in_place(); + } + + return ok(nullptr); + } + processor::object_result processor::parse_nil() { ++token_current; diff --git a/test/cpp/jank/read/lex.cpp b/test/cpp/jank/read/lex.cpp index 1deb2ed1e..edaee3620 100644 --- a/test/cpp/jank/read/lex.cpp +++ b/test/cpp/jank/read/lex.cpp @@ -921,6 +921,56 @@ namespace jank::read::lex })); } + SUBCASE("Comment") + { + SUBCASE("Empty") + { + processor p{ "#_" }; + native_vector> tokens(p.begin(), p.end()); + CHECK(tokens + == make_tokens({ + {0, 2, token_kind::reader_macro_comment} + })); + } + + SUBCASE("No whitespace after") + { + processor p{ "#_[]" }; + native_vector> tokens(p.begin(), p.end()); + CHECK(tokens + == make_tokens({ + {0, 2, token_kind::reader_macro_comment}, + {2, 1, token_kind::open_square_bracket}, + {3, 1, token_kind::close_square_bracket} + })); + } + } + + SUBCASE("Conditional") + { + SUBCASE("Empty") + { + processor p{ "#?" }; + native_vector> tokens(p.begin(), p.end()); + CHECK(tokens + == make_tokens({ + {0, 2, token_kind::reader_macro_conditional} + })); + } + + SUBCASE("With following list") + { + processor p{ "#?()" }; + native_vector> tokens(p.begin(), p.end()); + CHECK(tokens + == make_tokens({ + {0, 2, token_kind::reader_macro_conditional}, + {2, 1, token_kind::open_paren}, + {3, 1, token_kind::close_paren} + })); + } + } + SUBCASE("Set") { SUBCASE("Empty") diff --git a/test/cpp/jank/read/parse.cpp b/test/cpp/jank/read/parse.cpp index acbc0fd09..9e0313b47 100644 --- a/test/cpp/jank/read/parse.cpp +++ b/test/cpp/jank/read/parse.cpp @@ -542,7 +542,6 @@ namespace jank::read::parse rt_ctx.intern_keyword(runtime::obj::symbol{ "foo" }).expect_ok(), runtime::obj::boolean::true_const()))); } - /* TODO: With codegen? */ } TEST_CASE("Reader macro") @@ -663,6 +662,105 @@ namespace jank::read::parse CHECK(r.is_err()); } } + + SUBCASE("Conditional") + { + SUBCASE("EOF") + { + lex::processor lp{ "#?" }; + runtime::context rt_ctx; + processor p{ rt_ctx, lp.begin(), lp.end() }; + auto const r(p.next()); + CHECK(r.is_err()); + } + + SUBCASE("Non-list after") + { + lex::processor lp{ "#?[]" }; + runtime::context rt_ctx; + processor p{ rt_ctx, lp.begin(), lp.end() }; + auto const r(p.next()); + CHECK(r.is_err()); + } + + SUBCASE("No match") + { + lex::processor lp{ "[#?(:clj 0 :cljs 1) 9]" }; + runtime::context rt_ctx; + processor p{ rt_ctx, lp.begin(), lp.end() }; + auto const r(p.next()); + CHECK(r.is_ok()); + CHECK(r.expect_ok() != nullptr); + CHECK(runtime::detail::equal( + r.expect_ok(), + make_box(std::in_place, make_box(9)))); + } + + SUBCASE("Default match") + { + lex::processor lp{ "[#?(:clj 0 :cljs 1 :default 8) 9]" }; + runtime::context rt_ctx; + processor p{ rt_ctx, lp.begin(), lp.end() }; + auto const r(p.next()); + CHECK(r.is_ok()); + CHECK(r.expect_ok() != nullptr); + CHECK(runtime::detail::equal( + r.expect_ok(), + make_box(std::in_place, make_box(8), make_box(9)))); + } + + SUBCASE("jank match") + { + lex::processor lp{ "[#?(:clj 0 :cljs 1 :jank 7 :default 8) 9]" }; + runtime::context rt_ctx; + processor p{ rt_ctx, lp.begin(), lp.end() }; + auto const r(p.next()); + CHECK(r.is_ok()); + CHECK(r.expect_ok() != nullptr); + CHECK(runtime::detail::equal( + r.expect_ok(), + make_box(std::in_place, make_box(7), make_box(9)))); + } + + SUBCASE("First match picked") + { + lex::processor lp{ "[#?(:default -1 :clj 0 :cljs 1 :jank 7 :default 8) 9]" }; + runtime::context rt_ctx; + processor p{ rt_ctx, lp.begin(), lp.end() }; + auto const r(p.next()); + CHECK(r.is_ok()); + CHECK(r.expect_ok() != nullptr); + CHECK(runtime::detail::equal( + r.expect_ok(), + make_box(std::in_place, make_box(-1), make_box(9)))); + } + + SUBCASE("Nested") + { + lex::processor lp{ "[#?(:clj 0 :cljs 1 :jank #?(:default 5) :default 8) 9]" }; + runtime::context rt_ctx; + processor p{ rt_ctx, lp.begin(), lp.end() }; + auto const r(p.next()); + CHECK(r.is_ok()); + CHECK(r.expect_ok() != nullptr); + CHECK(runtime::detail::equal( + r.expect_ok(), + make_box(std::in_place, make_box(5), make_box(9)))); + } + + SUBCASE("Other reader macro") + { + lex::processor lp{ "#?(:default #{1})" }; + runtime::context rt_ctx; + processor p{ rt_ctx, lp.begin(), lp.end() }; + auto const r(p.next()); + CHECK(r.is_ok()); + CHECK(r.expect_ok() != nullptr); + CHECK(runtime::detail::equal( + r.expect_ok(), + make_box(std::in_place, make_box(1)))); + } + } } } }