diff --git a/compiler+runtime/include/cpp/jank/analyze/expr/case.hpp b/compiler+runtime/include/cpp/jank/analyze/expr/case.hpp new file mode 100644 index 000000000..9b2f10518 --- /dev/null +++ b/compiler+runtime/include/cpp/jank/analyze/expr/case.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include +#include + +namespace jank::analyze::expr +{ + using namespace jank::runtime; + + template + struct case_ : expression_base + { + native_box value_expr{}; + native_integer shift{}; + native_integer mask{}; + native_box default_expr{}; + native_vector keys{}; + native_vector> exprs{}; + + void propagate_position(expression_position const pos) + { + default_expr->propagate_position(pos); + for(auto &expr : exprs) + { + expr->propagate_position(pos); + } + position = pos; + } + + object_ptr to_runtime_data() const + { + return merge(static_cast(this)->to_runtime_data(), + obj::persistent_array_map::create_unique(make_box("__type"), + make_box("expr::case"), + make_box("value_expr"), + value_expr->to_runtime_data(), + make_box("shift"), + make_box(shift), + make_box("mask"), + make_box(mask), + make_box("default_expr"), + default_expr->to_runtime_data())); + } + }; +} diff --git a/compiler+runtime/include/cpp/jank/analyze/expression.hpp b/compiler+runtime/include/cpp/jank/analyze/expression.hpp index 0dbeaa4b6..9675fb002 100644 --- a/compiler+runtime/include/cpp/jank/analyze/expression.hpp +++ b/compiler+runtime/include/cpp/jank/analyze/expression.hpp @@ -21,6 +21,7 @@ #include #include #include +#include namespace jank::analyze { @@ -46,7 +47,8 @@ namespace jank::analyze expr::do_, expr::if_, expr::throw_, - expr::try_>; + expr::try_, + expr::case_>; static constexpr native_bool pointer_free{ false }; diff --git a/compiler+runtime/include/cpp/jank/analyze/processor.hpp b/compiler+runtime/include/cpp/jank/analyze/processor.hpp index 9731e9d7e..6de915ae1 100644 --- a/compiler+runtime/include/cpp/jank/analyze/processor.hpp +++ b/compiler+runtime/include/cpp/jank/analyze/processor.hpp @@ -141,7 +141,13 @@ namespace jank::analyze option const &, native_bool needs_box); - /* Returns whether or not the form is a special symbol. */ + expression_result analyze_case(runtime::obj::persistent_list_ptr const &, + local_frame_ptr &, + expression_position, + option const &, + native_bool needs_box); + + /* Returns whether the form is a special symbol. */ native_bool is_special(runtime::object_ptr form); using special_function_type diff --git a/compiler+runtime/include/cpp/jank/c_api.h b/compiler+runtime/include/cpp/jank/c_api.h index 51c4a02b2..0de981a4a 100644 --- a/compiler+runtime/include/cpp/jank/c_api.h +++ b/compiler+runtime/include/cpp/jank/c_api.h @@ -255,6 +255,10 @@ extern "C" jank_native_bool jank_truthy(jank_object_ptr o); jank_native_bool jank_equal(jank_object_ptr l, jank_object_ptr r); jank_native_hash jank_to_hash(jank_object_ptr o); + jank_native_integer jank_to_integer(jank_object_ptr o); + jank_native_integer jank_shift_mask_case_integer(jank_object_ptr o, + jank_native_integer shift, + jank_native_integer mask); void jank_set_meta(jank_object_ptr o, jank_object_ptr meta); diff --git a/compiler+runtime/include/cpp/jank/codegen/llvm_processor.hpp b/compiler+runtime/include/cpp/jank/codegen/llvm_processor.hpp index 9b379313c..c41ff84c6 100644 --- a/compiler+runtime/include/cpp/jank/codegen/llvm_processor.hpp +++ b/compiler+runtime/include/cpp/jank/codegen/llvm_processor.hpp @@ -112,6 +112,8 @@ namespace jank::codegen analyze::expr::function_arity const &); llvm::Value *gen(analyze::expr::try_ const &, analyze::expr::function_arity const &); + llvm::Value *gen(analyze::expr::case_ const &, + analyze::expr::function_arity const &); llvm::Value *gen_var(obj::symbol_ptr qualified_name) const; llvm::Value *gen_c_string(native_persistent_string const &s) const; diff --git a/compiler+runtime/include/cpp/jank/error.hpp b/compiler+runtime/include/cpp/jank/error.hpp index 2996874da..0088b8bce 100644 --- a/compiler+runtime/include/cpp/jank/error.hpp +++ b/compiler+runtime/include/cpp/jank/error.hpp @@ -50,6 +50,7 @@ namespace jank::error parse_invalid_keyword, internal_parse_failure, + analysis_invalid_case, analysis_invalid_def, analysis_invalid_fn, analysis_invalid_fn_parameters, @@ -158,6 +159,8 @@ namespace jank::error return "parse/invalid-keyword"; case kind::internal_parse_failure: return "internal/parse-failure"; + case kind::analysis_invalid_case: + return "analysis/invalid-case"; case kind::analysis_invalid_def: return "analysis/invalid-def"; case kind::analysis_invalid_fn: diff --git a/compiler+runtime/include/cpp/jank/error/analyze.hpp b/compiler+runtime/include/cpp/jank/error/analyze.hpp index 9f6a1404e..d2d05d2ab 100644 --- a/compiler+runtime/include/cpp/jank/error/analyze.hpp +++ b/compiler+runtime/include/cpp/jank/error/analyze.hpp @@ -1,10 +1,11 @@ #pragma once #include -#include namespace jank::error { + error_ptr + analysis_invalid_case(native_persistent_string const &message, read::source const &source); error_ptr analysis_invalid_def(native_persistent_string const &message, read::source const &source); error_ptr diff --git a/compiler+runtime/include/cpp/jank/evaluate.hpp b/compiler+runtime/include/cpp/jank/evaluate.hpp index f4fcbb5cf..c447445d3 100644 --- a/compiler+runtime/include/cpp/jank/evaluate.hpp +++ b/compiler+runtime/include/cpp/jank/evaluate.hpp @@ -36,4 +36,5 @@ namespace jank::evaluate runtime::object_ptr eval(analyze::expr::if_ const &); runtime::object_ptr eval(analyze::expr::throw_ const &); runtime::object_ptr eval(analyze::expr::try_ const &); + runtime::object_ptr eval(analyze::expr::case_ const &); } diff --git a/compiler+runtime/include/cpp/jank/runtime/obj/nil.hpp b/compiler+runtime/include/cpp/jank/runtime/obj/nil.hpp index d99464f30..e9497e11a 100644 --- a/compiler+runtime/include/cpp/jank/runtime/obj/nil.hpp +++ b/compiler+runtime/include/cpp/jank/runtime/obj/nil.hpp @@ -47,7 +47,6 @@ namespace jank::runtime::obj /* behavior::sequenceable */ nil_ptr first() const; nil_ptr next() const; - obj::cons_ptr conj(object_ptr head) const; /* behavior::sequenceable_in_place */ nil_ptr next_in_place(); diff --git a/compiler+runtime/src/cpp/clojure/core_native.cpp b/compiler+runtime/src/cpp/clojure/core_native.cpp index f52cf8a4f..078c7b709 100644 --- a/compiler+runtime/src/cpp/clojure/core_native.cpp +++ b/compiler+runtime/src/cpp/clojure/core_native.cpp @@ -491,6 +491,8 @@ jank_object_ptr jank_load_clojure_core_native() intern_fn("prefers", &core_native::prefers); intern_val("int-min", std::numeric_limits::min()); intern_val("int-max", std::numeric_limits::max()); + intern_val("int32-min", std::numeric_limits::min()); + intern_val("int32-max", std::numeric_limits::max()); intern_fn("sleep", &core_native::sleep); intern_fn("current-time", &core_native::current_time); intern_fn("create-ns", &core_native::intern_ns); diff --git a/compiler+runtime/src/cpp/jank/analyze/processor.cpp b/compiler+runtime/src/cpp/jank/analyze/processor.cpp index 2e4785e66..6260087d9 100644 --- a/compiler+runtime/src/cpp/jank/analyze/processor.cpp +++ b/compiler+runtime/src/cpp/jank/analyze/processor.cpp @@ -47,6 +47,7 @@ namespace jank::analyze { make_box("var"), make_fn(&processor::analyze_var_call) }, { make_box("throw"), make_fn(&processor::analyze_throw) }, { make_box("try"), make_fn(&processor::analyze_try) }, + { make_box("case*"), make_fn(&processor::analyze_case) }, }; } @@ -166,6 +167,125 @@ namespace jank::analyze }); } + processor::expression_result processor::analyze_case(obj::persistent_list_ptr const &o, + local_frame_ptr &f, + expression_position const position, + option const &fc, + native_bool const needs_box) + { + if(auto const length(o->count()); length != 6) + { + return error::analysis_invalid_case("Invalid case*: exactly 6 parameters are needed.", + meta_source(o->meta)); + } + + auto it{ o->data.rest() }; + if(it.first().is_none()) + { + return error::analysis_invalid_case("Value expression is missing.", meta_source(o->meta)); + } + auto const value_expr_obj{ it.first().unwrap() }; + auto const value_expr{ analyze(value_expr_obj, f, expression_position::value, fc, needs_box) }; + if(value_expr.is_err()) + { + return error::analysis_invalid_case(value_expr.expect_err()->message, meta_source(o->meta)); + } + + it = it.rest(); + if(it.first().is_none()) + { + return error::analysis_invalid_case("Shift value is missing.", meta_source(o->meta)); + } + auto const shift_obj{ it.first().unwrap() }; + if(shift_obj.data->type != object_type::integer) + { + return error::analysis_invalid_case("Shift value should be an integer.", + meta_source(o->meta)); + } + auto const shift{ runtime::expect_object(shift_obj) }; + + it = it.rest(); + if(it.first().is_none()) + { + return error::analysis_invalid_case("Mask value is missing.", meta_source(o->meta)); + } + auto const mask_obj{ it.first().unwrap() }; + if(mask_obj.data->type != object_type::integer) + { + return error::analysis_invalid_case("Mask value should be an integer.", meta_source(o->meta)); + } + auto const mask{ runtime::expect_object(mask_obj) }; + + it = it.rest(); + if(it.first().is_none()) + { + return error::analysis_invalid_case("Default expression is missing.", meta_source(o->meta)); + } + auto const default_expr_obj{ it.first().unwrap() }; + auto const default_expr{ analyze(default_expr_obj, f, position, fc, needs_box) }; + + it = it.rest(); + if(it.first().is_none()) + { + return error::analysis_invalid_case("Keys and expressions are missing.", + meta_source(o->meta)); + } + auto const imap_obj{ it.first().unwrap() }; + + struct keys_and_exprs + { + native_vector keys{}; + native_vector exprs{}; + }; + + auto const keys_exprs{ visit_map_like( + [&](auto const typed_imap_obj) -> string_result { + keys_and_exprs ret{}; + for(auto seq{ typed_imap_obj->seq() }; seq != nullptr; seq = seq->next()) + { + auto const e{ seq->first() }; + auto const k_obj{ runtime::nth(e, make_box(0)) }; + auto const v_obj{ runtime::nth(e, make_box(1)) }; + if(k_obj.data->type != object_type::integer) + { + return err("Map key for case* is expected to be an integer."); + } + auto const key{ runtime::expect_object(k_obj) }; + auto const expr{ analyze(v_obj, f, position, fc, needs_box) }; + if(expr.is_err()) + { + return err(expr.expect_err()->message); + } + ret.keys.push_back(key->data); + ret.exprs.push_back(expr.expect_ok()); + } + return ret; + }, + [&]() -> string_result { + return err("Case keys and expressions should be a map-like."); + }, + imap_obj) }; + + if(keys_exprs.is_err()) + { + return error::analysis_invalid_case(keys_exprs.expect_err(), meta_source(o->meta)); + } + + auto case_expr{ + make_box(expr::case_{ + expression_base{ {}, position, f, needs_box }, + value_expr.expect_ok(), + shift->data, + mask->data, + default_expr.expect_ok(), + keys_exprs.expect_ok().keys, + keys_exprs.expect_ok().exprs, + } + ) + }; + return case_expr; + } + processor::expression_result processor::analyze_symbol(runtime::obj::symbol_ptr const &sym, local_frame_ptr ¤t_frame, expression_position const position, diff --git a/compiler+runtime/src/cpp/jank/c_api.cpp b/compiler+runtime/src/cpp/jank/c_api.cpp index 7d1544a4d..86f2d1c90 100644 --- a/compiler+runtime/src/cpp/jank/c_api.cpp +++ b/compiler+runtime/src/cpp/jank/c_api.cpp @@ -817,6 +817,44 @@ extern "C" return to_hash(o_obj); } + static native_integer to_integer_or_hash(object const *o) + { + if(o->type == object_type::integer) + { + return expect_object(o)->data; + } + + return to_hash(o); + } + + jank_native_integer jank_to_integer(jank_object_ptr const o) + { + auto const o_obj(reinterpret_cast(o)); + return to_integer_or_hash(o_obj); + } + + jank_native_integer jank_shift_mask_case_integer(jank_object_ptr const o, + jank_native_integer const shift, + jank_native_integer const mask) + { + auto const o_obj(reinterpret_cast(o)); + auto integer{ to_integer_or_hash(o_obj) }; + if(mask != 0) + { + if(o_obj->type == object_type::integer) + { + /* We don't hash the integer if it's an int32 value. This is to be consistent with how keys are hashed in jank's + * case macro. */ + integer = (integer >= std::numeric_limits::min() + && integer <= std::numeric_limits::max()) + ? integer + : hash::integer(integer); + } + integer = (integer >> shift) & mask; + } + return integer; + } + void jank_set_meta(jank_object_ptr const o, jank_object_ptr const meta) { auto const o_obj(reinterpret_cast(o)); diff --git a/compiler+runtime/src/cpp/jank/codegen/llvm_processor.cpp b/compiler+runtime/src/cpp/jank/codegen/llvm_processor.cpp index 40509bcab..a2436e025 100644 --- a/compiler+runtime/src/cpp/jank/codegen/llvm_processor.cpp +++ b/compiler+runtime/src/cpp/jank/codegen/llvm_processor.cpp @@ -4,7 +4,6 @@ #include #include #include -#include #include #include #include @@ -958,6 +957,76 @@ namespace jank::codegen return call; } + llvm::Value *llvm_processor::gen(expr::case_ const &expr, + expr::function_arity const &arity) + { + auto const current_fn(ctx->builder->GetInsertBlock()->getParent()); + auto const position{ expr.position }; + auto const value(gen(expr.value_expr, arity)); + auto const is_return{ position == expression_position::tail }; + auto const integer_fn_type(llvm::FunctionType::get( + ctx->builder->getInt64Ty(), + { ctx->builder->getPtrTy(), ctx->builder->getInt64Ty(), ctx->builder->getInt64Ty() }, + false)); + auto const fn( + ctx->module->getOrInsertFunction("jank_shift_mask_case_integer", integer_fn_type)); + llvm::SmallVector const args{ + value, + llvm::ConstantInt::getSigned(ctx->builder->getInt64Ty(), expr.shift), + llvm::ConstantInt::getSigned(ctx->builder->getInt64Ty(), expr.mask) + }; + auto const call(ctx->builder->CreateCall(fn, args)); + auto const switch_val(ctx->builder->CreateIntCast(call, ctx->builder->getInt64Ty(), true)); + auto const default_block{ llvm::BasicBlock::Create(*ctx->llvm_ctx, "default", current_fn) }; + auto const switch_{ ctx->builder->CreateSwitch(switch_val, default_block, expr.keys.size()) }; + auto const merge_block{ is_return + ? nullptr + : llvm::BasicBlock::Create(*ctx->llvm_ctx, "merge", current_fn) }; + + ctx->builder->SetInsertPoint(default_block); + auto const default_val{ gen(expr.default_expr, arity) }; + if(!is_return) + { + ctx->builder->CreateBr(merge_block); + } + auto const default_block_exit{ ctx->builder->GetInsertBlock() }; + + llvm::SmallVector case_blocks; + llvm::SmallVector case_values; + for(size_t block_counter{}; block_counter < expr.keys.size(); ++block_counter) + { + auto const block_name{ fmt::format("case_{}", block_counter) }; + auto const block{ llvm::BasicBlock::Create(*ctx->llvm_ctx, block_name, current_fn) }; + switch_->addCase( + llvm::ConstantInt::getSigned(ctx->builder->getInt64Ty(), expr.keys[block_counter]), + block); + + ctx->builder->SetInsertPoint(block); + auto const case_val{ gen(expr.exprs[block_counter], arity) }; + case_values.push_back(case_val); + if(!is_return) + { + ctx->builder->CreateBr(merge_block); + } + case_blocks.push_back(ctx->builder->GetInsertBlock()); + } + + if(!is_return) + { + ctx->builder->SetInsertPoint(merge_block); + auto const phi{ + ctx->builder->CreatePHI(ctx->builder->getPtrTy(), expr.keys.size() + 1, "switch_tmp") + }; + phi->addIncoming(default_val, default_block_exit); + for(size_t i{}; i < case_blocks.size(); ++i) + { + phi->addIncoming(case_values[i], case_blocks[i]); + } + return phi; + } + return nullptr; + } + llvm::Value *llvm_processor::gen_var(obj::symbol_ptr const qualified_name) const { auto const found(ctx->var_globals.find(qualified_name)); diff --git a/compiler+runtime/src/cpp/jank/error.cpp b/compiler+runtime/src/cpp/jank/error.cpp index 1406fc6f9..e247b8b07 100644 --- a/compiler+runtime/src/cpp/jank/error.cpp +++ b/compiler+runtime/src/cpp/jank/error.cpp @@ -91,6 +91,8 @@ namespace jank::error case kind::internal_parse_failure: return "Internal parse failure"; + case kind::analysis_invalid_case: + return "Invalid case"; case kind::analysis_invalid_def: return "Invalid def"; case kind::analysis_invalid_fn: diff --git a/compiler+runtime/src/cpp/jank/error/analyze.cpp b/compiler+runtime/src/cpp/jank/error/analyze.cpp index 36ce08125..453feb93a 100644 --- a/compiler+runtime/src/cpp/jank/error/analyze.cpp +++ b/compiler+runtime/src/cpp/jank/error/analyze.cpp @@ -2,6 +2,16 @@ namespace jank::error { + error_ptr + analysis_invalid_case(native_persistent_string const &message, read::source const &source) + { + return make_error( + kind::analysis_invalid_case, + message, + source, + note{ "Consider using the 'case' macro instead of using 'case*' directly.", source }); + } + error_ptr analysis_invalid_def(native_persistent_string const &message, read::source const &source) { diff --git a/compiler+runtime/src/cpp/jank/evaluate.cpp b/compiler+runtime/src/cpp/jank/evaluate.cpp index 78106049f..811c01be6 100644 --- a/compiler+runtime/src/cpp/jank/evaluate.cpp +++ b/compiler+runtime/src/cpp/jank/evaluate.cpp @@ -646,4 +646,9 @@ namespace jank::evaluate e); } } + + object_ptr eval(expr::case_ const &expr) + { + return dynamic_call(eval(wrap_expression(expr, "case", {}))); + } } diff --git a/compiler+runtime/src/cpp/jank/runtime/obj/nil.cpp b/compiler+runtime/src/cpp/jank/runtime/obj/nil.cpp index 003990f7d..dacedc6fd 100644 --- a/compiler+runtime/src/cpp/jank/runtime/obj/nil.cpp +++ b/compiler+runtime/src/cpp/jank/runtime/obj/nil.cpp @@ -98,11 +98,6 @@ namespace jank::runtime::obj return nullptr; } - cons_ptr nil::conj(object_ptr const head) const - { - return make_box(head, nullptr); - } - nil_ptr nil::next_in_place() { return nullptr; diff --git a/compiler+runtime/src/cpp/jank/runtime/obj/persistent_list.cpp b/compiler+runtime/src/cpp/jank/runtime/obj/persistent_list.cpp index 3ccafd957..dbcd92967 100644 --- a/compiler+runtime/src/cpp/jank/runtime/obj/persistent_list.cpp +++ b/compiler+runtime/src/cpp/jank/runtime/obj/persistent_list.cpp @@ -31,7 +31,7 @@ namespace jank::runtime::obj [](auto const typed_s) -> persistent_list_ptr { using T = typename decltype(typed_s)::value_type; - if constexpr(behavior::sequenceable) + if constexpr(behavior::sequenceable || std::same_as) { native_vector v; for(auto i(typed_s->fresh_seq()); i != nullptr; i = runtime::next_in_place(i)) diff --git a/compiler+runtime/src/jank/clojure/core.jank b/compiler+runtime/src/jank/clojure/core.jank index fe35d03e5..f255df656 100644 --- a/compiler+runtime/src/jank/clojure/core.jank +++ b/compiler+runtime/src/jank/clojure/core.jank @@ -3445,34 +3445,80 @@ (def ~name ~expr)))) ;; Case. -(defn- shift-mask [shift mask x] - (-> x (bit-shift-right shift) (bit-and mask))) - (def ^:private max-mask-bits 13) (def ^:private max-switch-table-size (bit-shift-left 1 max-mask-bits)) (def ^:private int-min-value clojure.core-native/int-min) (def ^:private int-max-value clojure.core-native/int-max) +(def ^:private int32-min-value clojure.core-native/int32-min) +(def ^:private int32-max-value clojure.core-native/int32-max) + +(defn- shift-mask [shift mask x] + (-> x (bit-shift-right shift) (bit-and mask))) + +(defn- case-hash + "Returns the input if it is within the range of a 32-bit signed integer, otherwise returns the hash of the + input. The native hash returns a int32 int so this is to make sure that (case-hash (case-hash x)) == (case-hash x). + A key may be hashed more than once and we need to make sure its value does not change in later hashing." + [input] + (if (and (integer? input) + (>= input int32-min-value) + (<= input int32-max-value)) + input + (hash input))) (defn- maybe-min-hash "Takes a collection of hashes and returns [shift mask] or nil if none found" [hashes] (first - (filter (fn [[s m]] - (apply distinct? (map #(shift-mask s m %) hashes))) - (for [mask (map #(dec (bit-shift-left 1 %)) (range 1 (inc max-mask-bits))) - shift (range 0 31)] - [shift mask])))) - -(defn- case-map - "Transforms a sequence of test constants and a corresponding sequence of then - expressions into a sorted map to be consumed by case*. The form of the map - entries are {(case-f test) [(test-f test) then]}." - [case-f test-f tests thens] + (filter + (fn [[s m]] + (apply distinct? (map #(shift-mask s m %) hashes))) + (for [mask + (map #(dec (bit-shift-left 1 %)) (range 1 (inc max-mask-bits))) + shift + (range 0 31)] + [shift mask])))) + +(defn- case-map-with-check + "Transforms a sequence of test constants and their corresponding branch expressions + into a sorted map for consumption by `case*`. This version is used when no hash collisions + are detected (i.e. the skip-check set is empty). + + Returns a sorted map where each key is the transformed test constant and each value is a `condp` form + that checks the original expression against the test constant before selecting the branch." + [expr-sym default case-f test-f tests thens] (into (sorted-map) (zipmap (map case-f tests) - (map vector - (map test-f tests) - thens)))) + (map + (fn [key then] + (let [key + (if (symbol? key) + (list 'quote key) + key)] + `(condp = ~expr-sym ~key ~then + ~default))) + (map test-f tests) + thens)))) + +(defn- case-map-collison-merged + "Transforms a sequence of test constants and their corresponding branch expressions + into a sorted map for consumption by `case*`. + + This version is selected when hash collisions have been detected (i.e. the skip-check set is nonempty) + and resolved (typically via merging in `merge-hash-collisions`). In these cases, the branch expressions + have already been pre-wrapped in `condp` forms to handle multiple colliding constants. Therefore, no additional + wrapping is needed here-the transformed test constants are mapped directly to their corresponding branch expressions. + + Returns a sorted map mapping each transformed test constant directly to its branch expression." + [case-f test-f tests thens] + (into (sorted-map) + (zipmap (map case-f tests) thens))) + +(defn- case-map + [expr-sym default case-f test-f tests thens skip-check] + (if (empty? skip-check) + (case-map-with-check expr-sym default case-f test-f tests thens) + (case-map-collison-merged case-f test-f tests thens))) (defn- fits-table? "Returns true if the collection of ints can fit within the @@ -3482,19 +3528,20 @@ (defn- prep-ints "Takes a sequence of int-sized test constants and a corresponding sequence of - then expressions. Returns a tuple of [shift mask case-map switch-type] where - case-map is a map of int case values to [test then] tuples, and switch-type - is either :sparse or :compact." - [tests thens] + then expressions. Returns a tuple of [shift mask case-map] where + case-map is a map of int case values to [test then] tuples" + [expr-sym default tests thens] (if (fits-table? tests) ; compact case ints, no shift-mask - [0 0 (case-map int int tests thens) :compact] + [0 0 (case-map expr-sym default int int tests thens #{})] (let [[shift mask] (or (maybe-min-hash (map int tests)) [0 0])] (if (zero? mask) ; sparse case ints, no shift-mask - [0 0 (case-map int int tests thens) :sparse] + [0 0 (case-map expr-sym default int int tests thens #{})] ; compact case ints, with shift-mask - [shift mask (case-map #(shift-mask shift mask (int %)) int tests thens) :compact])))) + [shift + mask + (case-map expr-sym default #(shift-mask shift mask (int %)) int tests thens #{})])))) (defn- merge-hash-collisions "Takes a case expression, default expression, and a sequence of test constants @@ -3510,53 +3557,65 @@ The skip-check is a set of case ints for which post-switch equivalence checking must not be done (the cases holding the above condp thens)." [expr-sym default tests thens] - (let [buckets (loop [m {} ks tests vs thens] - (if (and ks vs) - (recur - (update m (hash (first ks)) (fnil conj []) [(first ks) (first vs)]) - (next ks) (next vs)) - m)) - assoc-multi (fn [m h bucket] - (let [testexprs (mapcat (fn [kv] [(list 'quote (first kv)) (second kv)]) bucket) - expr `(condp = ~expr-sym ~@testexprs ~default)] - (assoc m h expr))) - hmap (reduce (fn [m [h bucket]] - (if (== 1 (count bucket)) - (assoc m (ffirst bucket) (second (first bucket))) - (assoc-multi m h bucket))) - {} buckets) - skip-check (->> buckets - (filter #(< 1 (count (second %)))) - (map first) - (into #{}))] + (let [buckets + (loop [m {} + ks tests + vs thens] + (if (and ks vs) + (recur + (update m (case-hash (first ks)) (fnil conj []) [(first ks) (first vs)]) + (next ks) (next vs)) + m)) + assoc-multi + (fn [m h bucket] + (let [testexprs + (mapcat (fn [kv] [(list 'quote (first kv)) (second kv)]) bucket) + expr + `(condp = ~expr-sym ~@testexprs ~default)] + (assoc m h expr))) + hmap + (reduce + (fn [m [h bucket]] + (if (== 1 (count bucket)) + (assoc m (ffirst bucket) (second (first bucket))) + (assoc-multi m h bucket))) + {} buckets) + skip-check + (->> buckets + (filter #(< 1 (count (second %)))) + (map first) + (into #{}))] [(keys hmap) (vals hmap) skip-check])) (defn- prep-hashes "Takes a sequence of test constants and a corresponding sequence of then - expressions. Returns a tuple of [shift mask case-map switch-type skip-check] - where case-map is a map of int case values to [test then] tuples, switch-type - is either :sparse or :compact, and skip-check is a set of case ints for which - post-switch equivalence checking must not be done (occurs with hash - collisions)." - [expr-sym default tests thens] - (let [hashes (into #{} (map hash tests))] + expressions. Returns a tuple of [shift mask case-map] + where case-map is a map of int case values to [test then] tuples." + [expr-sym default tests thens skip-check] + (let [hashes (into #{} (map case-hash tests))] (if (== (count tests) (count hashes)) (if (fits-table? hashes) ; compact case ints, no shift-mask - [0 0 (case-map hash identity tests thens) :compact] + [0 0 + (case-map expr-sym default case-hash identity tests thens skip-check)] (let [[shift mask] (or (maybe-min-hash hashes) [0 0])] (if (zero? mask) ; sparse case ints, no shift-mask - [0 0 (case-map hash identity tests thens) :sparse] + [0 0 + (case-map expr-sym default case-hash identity tests thens skip-check)] ; compact case ints, with shift-mask - [shift mask (case-map #(shift-mask shift mask (hash %)) identity tests thens) :compact]))) + [shift mask + (case-map expr-sym default #(shift-mask shift mask (case-hash %)) identity tests thens skip-check)]))) ; resolve hash collisions and try again - (let [[tests thens skip-check] (merge-hash-collisions expr-sym default tests thens) - [shift mask case-map switch-type] (prep-hashes expr-sym default tests thens) - skip-check (if (zero? mask) - skip-check - (into #{} (map #(shift-mask shift mask %) skip-check)))] - [shift mask case-map switch-type skip-check])))) + (let [[tests thens skip-check] + (merge-hash-collisions expr-sym default tests thens) + [shift mask case-map] + (prep-hashes expr-sym default tests thens skip-check) + skip-check + (if (zero? mask) + skip-check + (into #{} (map #(shift-mask shift mask %) skip-check)))] + [shift mask case-map])))) (defmacro case "Takes an expression, and a set of clauses. @@ -3584,45 +3643,57 @@ {:added "1.2"} [e & clauses] - (let [ge (gensym) - default (if (odd? (count clauses)) - (last clauses) - `(throw (str "No matching clause: " ~ge)))] + (let [ge + (gensym) + default + (if (odd? (count clauses)) + (last clauses) + `(throw (str "No matching clause: " ~ge)))] (if (> 2 (count clauses)) `(let [~ge ~e] ~default) - (let [pairs (partition 2 clauses) - assoc-test (fn assoc-test [m test expr] - (if (contains? m test) - (throw (str "Duplicate case test constant: " test)) - (assoc m test expr))) - pairs (reduce - (fn [m [test expr]] - (if (seq? test) - (reduce #(assoc-test %1 %2 expr) m test) - (assoc-test m test expr))) - {} pairs) - tests (keys pairs) - thens (vals pairs) - mode (cond - (every? #(and (integer? %) (<= int-min-value % int-max-value)) - tests) - :ints - - (every? keyword? tests) - :identity - - :else - :hashes)] + (let [pairs + (partition 2 clauses) + assoc-test + (fn assoc-test [m test expr] + (if (contains? m test) + (throw (str "Duplicate case test constant: " test)) + (assoc m test expr))) + pairs + (reduce + (fn [m [test expr]] + (if (seq? test) + (reduce #(assoc-test %1 %2 expr) m test) + (assoc-test m test expr))) + {} pairs) + tests + (keys pairs) + thens + (vals pairs) + mode + (cond + (every? #(and (integer? %) (<= int-min-value % int-max-value)) + tests) + :ints + + (every? keyword? tests) + :identity + + :else + :hashes)] (condp = mode - :ints - (let [[shift mask imap switch-type] (prep-ints tests thens)] - `(let [~ge ~e] (case* ~ge ~shift ~mask ~default ~imap ~switch-type :int))) - :hashes - (let [[shift mask imap switch-type skip-check] (prep-hashes ge default tests thens)] - `(let [~ge ~e] (case* ~ge ~shift ~mask ~default ~imap ~switch-type :hash-equiv ~skip-check))) - :identity - (let [[shift mask imap switch-type skip-check] (prep-hashes ge default tests thens)] - `(let [~ge ~e] (case* ~ge ~shift ~mask ~default ~imap ~switch-type :hash-identity ~skip-check)))))))) + :ints + (let [[shift mask imap] (prep-ints ge default tests thens)] + `(let [~ge ~e] (case* ~ge ~shift ~mask ~default ~imap))) + :hashes + (let [[shift mask imap] + (prep-hashes ge default tests thens #{})] + `(let [~ge ~e] + (case* ~ge ~shift ~mask ~default ~imap))) + :identity + (let [[shift mask imap] + (prep-hashes ge default tests thens #{})] + `(let [~ge ~e] + (case* ~ge ~shift ~mask ~default ~imap)))))))) ;; Miscellaneous. ; TODO: jank.core @@ -7298,15 +7369,10 @@ fails, attempts to require sym's namespace and retries." "Parse strings \"true\" or \"false\" and return a boolean, or nil if invalid" [s] (if (string? s) - ;; TODO: switch `parse-boolean` to case implementation when avaiable - #_(case s + (case s "true" true "false" false nil) - (cond - (= "true" s) true - (= "false" s) false - :else nil) (throw (parsing-err s)))) (def NaN? diff --git a/compiler+runtime/test/jank/form/case/case-star/fail-empty.jank b/compiler+runtime/test/jank/form/case/case-star/fail-empty.jank new file mode 100644 index 000000000..8d8ae70fc --- /dev/null +++ b/compiler+runtime/test/jank/form/case/case-star/fail-empty.jank @@ -0,0 +1 @@ +(case*) diff --git a/compiler+runtime/test/jank/form/case/case-star/fail-mask-not-int.jank b/compiler+runtime/test/jank/form/case/case-star/fail-mask-not-int.jank new file mode 100644 index 000000000..b4d9a1c8b --- /dev/null +++ b/compiler+runtime/test/jank/form/case/case-star/fail-mask-not-int.jank @@ -0,0 +1,3 @@ +(case* 1 0 0.1 :default + {1 (clojure.core/condp clojure.core/= 1 1 1 + :default)}) diff --git a/compiler+runtime/test/jank/form/case/case-star/fail-no-default.jank b/compiler+runtime/test/jank/form/case/case-star/fail-no-default.jank new file mode 100644 index 000000000..e8c584bc9 --- /dev/null +++ b/compiler+runtime/test/jank/form/case/case-star/fail-no-default.jank @@ -0,0 +1,3 @@ +(case* 1 0 0 + {1 (clojure.core/condp clojure.core/= 1 1 1 + :default)}) diff --git a/compiler+runtime/test/jank/form/case/case-star/fail-no-map.jank b/compiler+runtime/test/jank/form/case/case-star/fail-no-map.jank new file mode 100644 index 000000000..9bc1eb99f --- /dev/null +++ b/compiler+runtime/test/jank/form/case/case-star/fail-no-map.jank @@ -0,0 +1 @@ +(case* 1 0 0 :default [1 1]) diff --git a/compiler+runtime/test/jank/form/case/case-star/fail-non-int-key.jank b/compiler+runtime/test/jank/form/case/case-star/fail-non-int-key.jank new file mode 100644 index 000000000..e1fe19817 --- /dev/null +++ b/compiler+runtime/test/jank/form/case/case-star/fail-non-int-key.jank @@ -0,0 +1,4 @@ +(case* 1 0 0 :default + {1.1 (clojure.core/condp clojure.core/= 1 1 1 + :default)} + :int) diff --git a/compiler+runtime/test/jank/form/case/case-star/fail-shift-not-int.jank b/compiler+runtime/test/jank/form/case/case-star/fail-shift-not-int.jank new file mode 100644 index 000000000..18b5a73b7 --- /dev/null +++ b/compiler+runtime/test/jank/form/case/case-star/fail-shift-not-int.jank @@ -0,0 +1,3 @@ +(case* 1 0.1 0 :default + {1 (clojure.core/condp clojure.core/= 1 1 1 + :default)}) diff --git a/compiler+runtime/test/jank/form/case/case-star/fail-too-many-args.jank b/compiler+runtime/test/jank/form/case/case-star/fail-too-many-args.jank new file mode 100644 index 000000000..968fdb7b9 --- /dev/null +++ b/compiler+runtime/test/jank/form/case/case-star/fail-too-many-args.jank @@ -0,0 +1,4 @@ +(case* 1 0 0 :default + {1 (clojure.core/condp clojure.core/= 1 1 1 + :default)} + :int) diff --git a/compiler+runtime/test/jank/form/case/fail-duplicate-keys.jank b/compiler+runtime/test/jank/form/case/fail-duplicate-keys.jank new file mode 100644 index 000000000..c206dc735 --- /dev/null +++ b/compiler+runtime/test/jank/form/case/fail-duplicate-keys.jank @@ -0,0 +1,4 @@ +(case 1 + 1 :one + 1 :duplicate-one + :default) diff --git a/compiler+runtime/test/jank/form/case/fail-empty-with-default.jank b/compiler+runtime/test/jank/form/case/fail-empty-with-default.jank new file mode 100644 index 000000000..010fd27ad --- /dev/null +++ b/compiler+runtime/test/jank/form/case/fail-empty-with-default.jank @@ -0,0 +1 @@ +(case :default) diff --git a/compiler+runtime/test/jank/form/case/fail-empty.jank b/compiler+runtime/test/jank/form/case/fail-empty.jank new file mode 100644 index 000000000..0e616f2ac --- /dev/null +++ b/compiler+runtime/test/jank/form/case/fail-empty.jank @@ -0,0 +1 @@ +(case) diff --git a/compiler+runtime/test/jank/form/case/fail-group-duplicates.jank b/compiler+runtime/test/jank/form/case/fail-group-duplicates.jank new file mode 100644 index 000000000..a450fc12b --- /dev/null +++ b/compiler+runtime/test/jank/form/case/fail-group-duplicates.jank @@ -0,0 +1,3 @@ +(case 2 + (1 2 2) :one-or-two + :default) diff --git a/compiler+runtime/test/jank/form/case/fail-non-literal-value.jank b/compiler+runtime/test/jank/form/case/fail-non-literal-value.jank new file mode 100644 index 000000000..12060c8bb --- /dev/null +++ b/compiler+runtime/test/jank/form/case/fail-non-literal-value.jank @@ -0,0 +1,3 @@ +(let [x 1] + (case 1 + x :one)) diff --git a/compiler+runtime/test/jank/form/case/fail-overlapping-values.jank b/compiler+runtime/test/jank/form/case/fail-overlapping-values.jank new file mode 100644 index 000000000..48e8282d7 --- /dev/null +++ b/compiler+runtime/test/jank/form/case/fail-overlapping-values.jank @@ -0,0 +1,4 @@ +(case 1 + (1 2) :one-or-two + (2 3) :two-or-three + :default) diff --git a/compiler+runtime/test/jank/form/case/pass-bool.jank b/compiler+runtime/test/jank/form/case/pass-bool.jank new file mode 100644 index 000000000..4e33f65dd --- /dev/null +++ b/compiler+runtime/test/jank/form/case/pass-bool.jank @@ -0,0 +1,8 @@ +(assert + (= + (case true true :yes + false :no + :default) + :yes)) + +:success diff --git a/compiler+runtime/test/jank/form/case/pass-default-as-val.jank b/compiler+runtime/test/jank/form/case/pass-default-as-val.jank new file mode 100644 index 000000000..eb96baebe --- /dev/null +++ b/compiler+runtime/test/jank/form/case/pass-default-as-val.jank @@ -0,0 +1,9 @@ +(assert + (= + (case :default + :default :redefined-default + :something-else :ok + :default) + :redefined-default)) + +:success diff --git a/compiler+runtime/test/jank/form/case/pass-escape-char.jank b/compiler+runtime/test/jank/form/case/pass-escape-char.jank new file mode 100644 index 000000000..9c5518e49 --- /dev/null +++ b/compiler+runtime/test/jank/form/case/pass-escape-char.jank @@ -0,0 +1,10 @@ +; This should work, but not yet with jank. +; TODO(investigate and support using escaped char in case) +#_(assert + (= + (case \a \a \b + \c \d + :not-found) + \b)) + +:success diff --git a/compiler+runtime/test/jank/form/case/pass-int.jank b/compiler+runtime/test/jank/form/case/pass-int.jank new file mode 100644 index 000000000..223bbf33a --- /dev/null +++ b/compiler+runtime/test/jank/form/case/pass-int.jank @@ -0,0 +1,9 @@ +(assert + (= + (case 0 0 :zero + 1 :one + 2 :two + :default) + :zero)) + +:success diff --git a/compiler+runtime/test/jank/form/case/pass-keywords.jank b/compiler+runtime/test/jank/form/case/pass-keywords.jank new file mode 100644 index 000000000..1327a74e6 --- /dev/null +++ b/compiler+runtime/test/jank/form/case/pass-keywords.jank @@ -0,0 +1,9 @@ +(assert + (= + (case :foo + :foo :bar + :baz :quux + :none) + :bar)) + +:success diff --git a/compiler+runtime/test/jank/form/case/pass-map.jank b/compiler+runtime/test/jank/form/case/pass-map.jank new file mode 100644 index 000000000..be23ee407 --- /dev/null +++ b/compiler+runtime/test/jank/form/case/pass-map.jank @@ -0,0 +1,9 @@ +(assert + (= + (case {:a 1 :b 2} + {:a 1 :b 2} :map-match + {:a 3 :b 4} :not-matched + :default) + :map-match)) + +:success diff --git a/compiler+runtime/test/jank/form/case/pass-mixed-int-bool.jank b/compiler+runtime/test/jank/form/case/pass-mixed-int-bool.jank new file mode 100644 index 000000000..680844fda --- /dev/null +++ b/compiler+runtime/test/jank/form/case/pass-mixed-int-bool.jank @@ -0,0 +1,9 @@ +(assert + (= + (case true + true :true + 2 :two + :default) + :true)) + +:success diff --git a/compiler+runtime/test/jank/form/case/pass-mixed-int-ratio.jank b/compiler+runtime/test/jank/form/case/pass-mixed-int-ratio.jank new file mode 100644 index 000000000..ad323ba45 --- /dev/null +++ b/compiler+runtime/test/jank/form/case/pass-mixed-int-ratio.jank @@ -0,0 +1,9 @@ +(assert + (= + (case 3/4 + 3/4 :three-fourths + 2 :two + :default) + :three-fourths)) + +:success diff --git a/compiler+runtime/test/jank/form/case/pass-mixed-int-real.jank b/compiler+runtime/test/jank/form/case/pass-mixed-int-real.jank new file mode 100644 index 000000000..a3662ca5a --- /dev/null +++ b/compiler+runtime/test/jank/form/case/pass-mixed-int-real.jank @@ -0,0 +1,9 @@ +(assert + (= + (case 3.4 + 3.4 :three-point-four + 2 :two + :default) + :three-point-four)) + +:success diff --git a/compiler+runtime/test/jank/form/case/pass-mixed-int-string.jank b/compiler+runtime/test/jank/form/case/pass-mixed-int-string.jank new file mode 100644 index 000000000..98203de2d --- /dev/null +++ b/compiler+runtime/test/jank/form/case/pass-mixed-int-string.jank @@ -0,0 +1,10 @@ +(assert + (= + (case 1 + 1 :one + 2 :two + "3" :three + :default) + :one)) + +:success diff --git a/compiler+runtime/test/jank/form/case/pass-multivalues.jank b/compiler+runtime/test/jank/form/case/pass-multivalues.jank new file mode 100644 index 000000000..71cbab618 --- /dev/null +++ b/compiler+runtime/test/jank/form/case/pass-multivalues.jank @@ -0,0 +1,9 @@ +(assert + (= + (case 2 + (0 1) :zero-or-one + (2 3) :two-or-three + :default) + :two-or-three)) + +:success diff --git a/compiler+runtime/test/jank/form/case/pass-nested-case.jank b/compiler+runtime/test/jank/form/case/pass-nested-case.jank new file mode 100644 index 000000000..1782824f8 --- /dev/null +++ b/compiler+runtime/test/jank/form/case/pass-nested-case.jank @@ -0,0 +1,10 @@ +(assert + (= + (case 1 + 1 (case 2 + 2 :inner-match + :inner-default) + :outer-default) + :inner-match)) + +:success diff --git a/compiler+runtime/test/jank/form/case/pass-nil.jank b/compiler+runtime/test/jank/form/case/pass-nil.jank new file mode 100644 index 000000000..d6b8b2217 --- /dev/null +++ b/compiler+runtime/test/jank/form/case/pass-nil.jank @@ -0,0 +1,8 @@ +(assert + (= + (case nil nil :empty + false :false-value + :default) + :empty)) + +:success diff --git a/compiler+runtime/test/jank/form/case/pass-no-default.jank b/compiler+runtime/test/jank/form/case/pass-no-default.jank new file mode 100644 index 000000000..46f33c544 --- /dev/null +++ b/compiler+runtime/test/jank/form/case/pass-no-default.jank @@ -0,0 +1,8 @@ +(assert + (= + (case 1 + 1 :one + 2 :two) + :one)) + +:success diff --git a/compiler+runtime/test/jank/form/case/pass-ratio.jank b/compiler+runtime/test/jank/form/case/pass-ratio.jank new file mode 100644 index 000000000..4de3b8fe1 --- /dev/null +++ b/compiler+runtime/test/jank/form/case/pass-ratio.jank @@ -0,0 +1,9 @@ +(assert + (= + (case 3/4 + 4/3 :four-thirds + 3/4 :three-fourths + :default) + :three-fourths)) + +:success diff --git a/compiler+runtime/test/jank/form/case/pass-real.jank b/compiler+runtime/test/jank/form/case/pass-real.jank new file mode 100644 index 000000000..efb4ac9b3 --- /dev/null +++ b/compiler+runtime/test/jank/form/case/pass-real.jank @@ -0,0 +1,9 @@ +(assert + (= + (case 3.14 3.14 :pi + 2.71 :e + 1.61 :phi + :default) + :pi)) + +:success diff --git a/compiler+runtime/test/jank/form/case/pass-return-position.jank b/compiler+runtime/test/jank/form/case/pass-return-position.jank new file mode 100644 index 000000000..3bcba23a9 --- /dev/null +++ b/compiler+runtime/test/jank/form/case/pass-return-position.jank @@ -0,0 +1,11 @@ +; The `case` is the return value of a function +(assert + (= + ((fn [] + (case 2 + 1 :one + 2 :two + :default))) + :two)) + +:success diff --git a/compiler+runtime/test/jank/form/case/pass-set.jank b/compiler+runtime/test/jank/form/case/pass-set.jank new file mode 100644 index 000000000..f4f72cf12 --- /dev/null +++ b/compiler+runtime/test/jank/form/case/pass-set.jank @@ -0,0 +1,9 @@ +(assert + (= + (case #{1 2} + #{1 2} :set-match + #{3 2} :vector-match + :default) + :set-match)) + +:success diff --git a/compiler+runtime/test/jank/form/case/pass-statement-position.jank b/compiler+runtime/test/jank/form/case/pass-statement-position.jank new file mode 100644 index 000000000..fa8de8279 --- /dev/null +++ b/compiler+runtime/test/jank/form/case/pass-statement-position.jank @@ -0,0 +1,9 @@ +(let [xfn (fn [] + (case 3/4 + 4/3 :four-thirds + 3/4 (def ret :success) + :default) + :arbitrary-value)] + (assert (= (xfn) :arbitrary-value))) + +ret diff --git a/compiler+runtime/test/jank/form/case/pass-string.jank b/compiler+runtime/test/jank/form/case/pass-string.jank new file mode 100644 index 000000000..a34ba52c7 --- /dev/null +++ b/compiler+runtime/test/jank/form/case/pass-string.jank @@ -0,0 +1,8 @@ +(assert + (= + (case "AaAaAaAa" "AaAaAaAa" :hasha + "BBBBBBBB" :hashb + :default) + :hasha)) + +:success diff --git a/compiler+runtime/test/jank/form/case/pass-symbol.jank b/compiler+runtime/test/jank/form/case/pass-symbol.jank new file mode 100644 index 000000000..b36ff2249 --- /dev/null +++ b/compiler+runtime/test/jank/form/case/pass-symbol.jank @@ -0,0 +1,8 @@ +(assert + (= + (case 'symbol symbol :symbol-match + other :symbol-no-match + :symbol-fallback) + :symbol-match)) + +:success diff --git a/compiler+runtime/test/jank/form/case/pass-unicode-char.jank b/compiler+runtime/test/jank/form/case/pass-unicode-char.jank new file mode 100644 index 000000000..9f7653f94 --- /dev/null +++ b/compiler+runtime/test/jank/form/case/pass-unicode-char.jank @@ -0,0 +1,8 @@ +(assert + (= + (case "你好" "你好" :nihao + "こんにちは" :konnichiwa + :default) + :nihao)) + +:success diff --git a/compiler+runtime/test/jank/form/case/pass-value-position.jank b/compiler+runtime/test/jank/form/case/pass-value-position.jank new file mode 100644 index 000000000..57956de41 --- /dev/null +++ b/compiler+runtime/test/jank/form/case/pass-value-position.jank @@ -0,0 +1,7 @@ +(let [x (case 1 + 1 1 + 2 2 + :default)] + (assert (= (+ x 1) 2))) + +:success diff --git a/compiler+runtime/test/jank/form/case/pass-vector.jank b/compiler+runtime/test/jank/form/case/pass-vector.jank new file mode 100644 index 000000000..9e70d1e0b --- /dev/null +++ b/compiler+runtime/test/jank/form/case/pass-vector.jank @@ -0,0 +1,9 @@ +(assert + (= + (case [:key1 :key2] + [:key1 :key2] :matched + :key :not-matched + :default) + :matched)) + +:success