diff --git a/dev/TODO.txt b/dev/TODO.txt index fab682e14..ad0b6f199 100644 --- a/dev/TODO.txt +++ b/dev/TODO.txt @@ -21,29 +21,6 @@ - Easier to refer people who want to know what is planned and/or want to help. - Right now, searches for keywords hit both code and this TODO.txt. -- switch on type. - see some discussion here: https://github.com/aardappel/lobster/issues/113 - tagged unions: https://github.com/aardappel/lobster/discussions/242 - What people really seem to want is a guaranteed exhaustive type switch. - We should first add that for objects. - The way that would work is that it would expect all subclasses to be covered by a type, - including subclasses of subclasses. - To make this manageable, we should first implement abstract classes, since that way the - base class doesn't need to feature in the cases. - Actually, Lobster already knows if a base class is never instanced, but it be nice to give - people the option to enforce it. - We would give never-instanced the same power for convenience, with of course the downside - that if anyone ever tries to instance one suddenly all type switches start complaining - about non-exhaustiveness, but I think this is managable. - For sub-subclasses, the presence of the middle type in the cases will make the sub-subclasses - entirely optional, but if present, should of course get precedence. - Typically though these middle types would be marked abstract if intended to be used with - type switches a lot, which would force cases for the sub-subclasses. - Implementation wise this would go through the same logic that currently constructs vtable - entries, but instead results in case-integers instead of function pointers. That way - this is guaranteed fast no matter the number of cases, though we could choose to bypass - the vtable for 3 or less cases or so. - - We should be making extensive use of https://devblogs.microsoft.com/cppblog/introducing-vector-calling-convention/ Especially since we're a closed static build we control, meaning we have no worries of ABI/calling convention mismatches. @@ -253,9 +230,7 @@ - Features to make it easier to use structs ADT style as opposed to inheritance - ADT is similar to a base class with no members and a fixed set of sub-classes. - So make that use case into a simpler declaration syntax. - - extend switch to do pattern matching. - - failing that, a switch that can use types as case labels would be a good start, - and simple to do given that we have type indices! + - extend switch to do ADTs. - Might be better to not piggy-back on struct inheritance. - It really does a different thing, where with normal inheritance the base class is not expected to be the max size of all subclasses. @@ -1358,8 +1333,6 @@ - Make includes explicitly depend on eachother. -- switch jump table based version. - - We have a check in IdentRef::TypeCheck to avoid escaping lambdas, but this only works for static ones, not for calls over a function type. @@ -1539,8 +1512,6 @@ - if your forget ":" after "else", it is going to try and declare a following identifier as a function parameter, see FIXME in ParseTrailingFunctionValues -- extend switch with pattern match facility? - - Should consider to allow on-the fly data structures as well. Writing new_type { field1: val, field2: val } would add such specialization to new_type even if it doesn't exist yet. @@ -2181,7 +2152,6 @@ - make comparators non-associative? -- some form or switch or pattern match facility? - pattern matching by failing a function into the next - make bytecode, not intcode. diff --git a/dev/src/disasm.cpp b/dev/src/disasm.cpp index 3cc65e639..9cd132578 100644 --- a/dev/src/disasm.cpp +++ b/dev/src/disasm.cpp @@ -164,6 +164,18 @@ const int *DisAsmIns(NativeRegistry &nfr, string &sd, const int *ip, const int * break; } + case IL_JUMP_TABLE_DISPATCH: { + auto vtable_idx = *ip++; + auto mini = *ip++; + auto maxi = *ip++; + auto n = maxi - mini + 2; + append(sd, vtable_idx, "/", n - 1, " [ "); + while (n--) + append(sd, *ip++, " "); + sd += "]"; + break; + } + case IL_FUNSTART: { auto fidx = *ip++; sd += (fidx >= 0 ? bcf->functions()->Get(fidx)->name()->string_view() : "__dummy"); diff --git a/dev/src/lobster/codegen.h b/dev/src/lobster/codegen.h index e4f6de8ee..6bf7bd9d6 100644 --- a/dev/src/lobster/codegen.h +++ b/dev/src/lobster/codegen.h @@ -34,7 +34,7 @@ struct CodeGen { vector node_context; int keepvars = 0; int runtime_checks; - vector vtables; + vector vtables; // -1 = uninit, -2 and lower is case idx, positive is code offset. vector tstack; size_t tstack_max = 0; int dummyfun = -1; @@ -312,6 +312,10 @@ struct CodeGen { if (de.sf) { vtables[udt->vtable_start + i] = de.sf->subbytecodestart ? de.sf->subbytecodestart : dummyfun; + assert(!de.is_switch_dispatch); + } else if (de.case_index >= 0) { + vtables[udt->vtable_start + i] = -de.case_index - 2; + assert(de.is_switch_dispatch); } } } @@ -1599,7 +1603,12 @@ void Break::Generate(CodeGen &cg, size_t retval) const { void Switch::Generate(CodeGen &cg, size_t retval) const { cg.Gen(value, 1); cg.TakeTemp(1, false); - // See if we should do a jump table version. + // See if we do a type dispatch (always a jump table). + if (value->exptype->t == V_CLASS) { + GenerateTypeDispatch(cg, retval); + return; + } + // See if we should do an integer jump table version. if (GenerateJumpTable(cg, retval)) return; // Do slow default implementation for sparse integers, expressions and strings. @@ -1672,20 +1681,22 @@ void Switch::Generate(CodeGen &cg, size_t retval) const { bs.Exit(cg); } +pair get_range(Node *c) { + auto start = c; + auto end = c; + if (auto r = Is(c)) { + start = r->start; + end = r->end; + } + return { Is(start), Is(end) }; +}; + + bool Switch::GenerateJumpTable(CodeGen &cg, size_t retval) const { if (value->exptype->t != V_INT) return false; int64_t mini = INT64_MAX / 2, maxi = INT64_MIN / 2; int64_t num = 0; - auto get_range = [&](Node *c) -> pair { - auto start = c; - auto end = c; - if (auto r = Is(c)) { - start = r->start; - end = r->end; - } - return { Is(start), Is(end) }; - }; for (auto n : cases->children) { auto cas = AssertIs(n); for (auto c : cas->pattern->children) { @@ -1712,19 +1723,28 @@ bool Switch::GenerateJumpTable(CodeGen &cg, size_t retval) const { cg.EmitOp(IL_JUMP_TABLE); cg.Emit((int)mini); cg.Emit((int)maxi); + GenerateJumpTableMain(cg, retval, (int)range, (int)mini); + return true; +} + +void Switch::GenerateJumpTableMain(CodeGen & cg, size_t retval, int range, int mini) const { auto table_start = cg.Pos(); - for (int i = 0; i < (int)range + 1; i++) cg.Emit(-1); + for (int i = 0; i < range + 1; i++) cg.Emit(-1); vector exitswitch; int default_pos = -1; CodeGen::BlockStack bs(cg.tstack); - for (auto n : cases->children) { + for (auto [i, n] : enumerate(cases->children)) { bs.Start(); auto cas = AssertIs(n); for (auto c : cas->pattern->children) { - auto [istart, iend] = get_range(c); - assert(istart && iend); - for (auto i = istart->integer; i <= iend->integer; i++) { - cg.code[table_start + (int)i - (int)mini] = cg.Pos(); + if (value->exptype->t == V_CLASS) { + cg.code[table_start + (int)i] = cg.Pos(); + } else { + auto [istart, iend] = get_range(c); + assert(istart && iend); + for (auto i = istart->integer; i <= iend->integer; i++) { + cg.code[table_start + (int)i - mini] = cg.Pos(); + } } } if (cas->pattern->children.empty()) default_pos = cg.Pos(); @@ -1741,12 +1761,25 @@ bool Switch::GenerateJumpTable(CodeGen &cg, size_t retval) const { cg.EmitOp(IL_JUMP_TABLE_END); cg.SetLabels(exitswitch); if (default_pos < 0) default_pos = cg.Pos(); - for (int i = 0; i < (int)range + 1; i++) { + for (int i = 0; i < range + 1; i++) { if (cg.code[table_start + i] == -1) cg.code[table_start + i] = default_pos; } bs.Exit(cg); - return true; +} + +void Switch::GenerateTypeDispatch(CodeGen &cg, size_t retval) const { + auto dispatch_udt = value->exptype->udt; + auto &dt = dispatch_udt->dispatch_table[vtable_idx]; + assert(dt.is_dispatch_root && dt.is_switch_dispatch && + dt.subudts_size == dispatch_udt->subudts.size()); + (void)dt; + cg.EmitOp(IL_JUMP_TABLE_DISPATCH); + cg.Emit(vtable_idx); + cg.Emit(0); + int range = (int)cases->children.size(); + cg.Emit(range - 1); + GenerateJumpTableMain(cg, retval, range, 0); } void Case::Generate(CodeGen &/*cg*/, size_t /*retval*/) const { diff --git a/dev/src/lobster/idents.h b/dev/src/lobster/idents.h index fb57ec601..44db89afc 100644 --- a/dev/src/lobster/idents.h +++ b/dev/src/lobster/idents.h @@ -171,7 +171,9 @@ struct GenericTypeVariable { }; struct DispatchEntry { - SubFunction *sf = nullptr; + SubFunction *sf = nullptr; // if !is_switch_dispatch + int case_index = -1; // if is_switch_dispatch + bool is_switch_dispatch = false; bool is_dispatch_root = false; // Shared return type if root of dispatch. TypeRef returntype = nullptr; diff --git a/dev/src/lobster/il.h b/dev/src/lobster/il.h index bf8c5eb74..ad0d0ebb2 100644 --- a/dev/src/lobster/il.h +++ b/dev/src/lobster/il.h @@ -153,6 +153,7 @@ enum MathOp { #define ILVARARGNAMES \ F(JUMP_TABLE, ILUNKNOWN, 1, 0) \ + F(JUMP_TABLE_DISPATCH, ILUNKNOWN, 1, 0) \ F(FUNSTART, ILUNKNOWN, 0, 0) #define ILJUMPNAMES1 \ diff --git a/dev/src/lobster/node.h b/dev/src/lobster/node.h index cc1a8a96b..9f0aba4e0 100644 --- a/dev/src/lobster/node.h +++ b/dev/src/lobster/node.h @@ -278,7 +278,12 @@ BINARY_NODE_T(While, "while", false, Node, condition, Block, wbody, RETURNSMETHO BINARY_NODE_T(For, "for", false, Node, iter, Block, fbody, ) ZERO_NODE(ForLoopElem, "for loop element", false, ) ZERO_NODE(ForLoopCounter, "for loop counter", false, ) -BINARY_NODE_T(Switch, "switch", false, Node, value, List, cases, RETURNSMETHOD bool GenerateJumpTable(CodeGen &cg, size_t retval) const;) +BINARY_NODE_T(Switch, "switch", false, Node, value, List, cases, \ + int vtable_idx = -1; \ + RETURNSMETHOD \ + bool GenerateJumpTable(CodeGen &cg, size_t retval) const; \ + void GenerateTypeDispatch(CodeGen &cg, size_t retval) const; \ + void GenerateJumpTableMain(CodeGen &cg, size_t retval, int range, int mini) const;) BINARY_NODE_T(Case, "case", false, List, pattern, Node, cbody, ) BINARY_NODE(Range, "range", false, start, end, ) ZERO_NODE(Break, "break", false, RETURNSMETHOD) diff --git a/dev/src/lobster/parser.h b/dev/src/lobster/parser.h index 744bf778d..ff81a63b1 100644 --- a/dev/src/lobster/parser.h +++ b/dev/src/lobster/parser.h @@ -1397,10 +1397,20 @@ struct Parser { } else { Expect(T_CASE); for (;;) { - auto f = ParseUnary(); - if (lex.token == T_DOTDOT) { - lex.Next(); - f = new Range(lex, f, ParseUnary()); + Node *f = nullptr; + if (lex.token == T_IDENT) { + auto udt = st.LookupSpecialization(lex.sattr); + if (udt) { + lex.Next(); + f = new UDTRef(lex, udt); + } + } + if (!f) { + f = ParseUnary(); + if (lex.token == T_DOTDOT) { + lex.Next(); + f = new Range(lex, f, ParseUnary()); + } } pattern->Add(f); if (lex.token == T_COLON) break; diff --git a/dev/src/lobster/tonative.h b/dev/src/lobster/tonative.h index 0b42c4ed6..783ae4488 100644 --- a/dev/src/lobster/tonative.h +++ b/dev/src/lobster/tonative.h @@ -47,6 +47,15 @@ inline int ParseOpAndGetArity(int opc, const int *&ip, int ®so) { arity = int(ip - ips); break; } + case IL_JUMP_TABLE_DISPATCH: { + ip++; // vtable_idx + auto mini = *ip++; + auto maxi = *ip++; + auto n = maxi - mini + 2; + ip += n; + arity = int(ip - ips); + break; + } case IL_FUNSTART: { ip++; // function idx. ip++; // max regs. diff --git a/dev/src/lobster/typecheck.h b/dev/src/lobster/typecheck.h index 5ec4e96b9..fc3c9c744 100644 --- a/dev/src/lobster/typecheck.h +++ b/dev/src/lobster/typecheck.h @@ -85,6 +85,7 @@ struct TypeChecker { set> integer_literal_warnings; Query *query; bool full_error; + Switch *switch_case_context = nullptr; TypeChecker(Parser &_p, SymbolTable &_st, size_t retreq, Query *query, bool full_error) : parser(_p), st(_st), query(query), full_error(full_error) { @@ -1250,7 +1251,7 @@ struct TypeChecker { // TODO: we chould check for a superclass vtable entry also, but chances // two levels will be present are low. if (disp.sf && disp.sf->method_of == &dispatch_udt && disp.is_dispatch_root && - &f == disp.sf->parent) { + !disp.is_switch_dispatch && &f == disp.sf->parent) { for (auto [i, c] : enumerate(call_args.children)) { auto &arg = disp.sf->args[i]; if (i && !ConvertsTo(c->exptype, arg.type, CF_NONE)) @@ -2461,8 +2462,8 @@ Node *Switch::TypeCheck(TypeChecker &tc, size_t reqret) { tc.TT(value, 1, LT_BORROW); tc.DecBorrowers(value->lt, *this); auto ptype = value->exptype; - if (!ptype->Numeric() && ptype->t != V_STRING) - tc.Error(*this, "switch value must be int / float / string"); + if (!ptype->Numeric() && ptype->t != V_STRING && ptype->t != V_CLASS) + tc.Error(*this, "switch value must be int / float / string / class"); exptype = nullptr; bool have_default = false; vector enum_cases; @@ -2470,21 +2471,26 @@ Node *Switch::TypeCheck(TypeChecker &tc, size_t reqret) { cases->exptype = type_void; cases->lt = LT_ANY; for (auto &n : cases->children) { + tc.switch_case_context = this; tc.TT(n, reqret, LT_KEEP); auto cas = AssertIs(n); - if (cas->pattern->children.empty()) have_default = true; + if (!cas->pattern->Arity()) have_default = true; cas->pattern->exptype = type_void; cas->pattern->lt = LT_ANY; for (auto c : cas->pattern->children) { - tc.SubTypeT(c->exptype, ptype, *c, "", "case"); - tc.DecBorrowers(c->lt, *cas); - if (ptype->IsEnum()) { - assert(c->exptype->IsEnum()); - Value v = NilVal(); - if (c->ConstVal(&tc, v) != V_VOID) { - for (auto [i, ev] : enumerate(ptype->e->vals)) if (ev->val == v.ival()) { - enum_cases[i] = true; - break; + if (ptype->t == V_CLASS) { + if (!Is(c)) tc.Error(*c, "non-type value in switch on type"); + } else { + tc.SubTypeT(c->exptype, ptype, *c, "", "case"); + tc.DecBorrowers(c->lt, *cas); + if (ptype->IsEnum()) { + assert(c->exptype->IsEnum()); + Value v = NilVal(); + if (c->ConstVal(&tc, v) != V_VOID) { + for (auto [i, ev] : enumerate(ptype->e->vals)) if (ev->val == v.ival()) { + enum_cases[i] = true; + break; + } } } } @@ -2503,12 +2509,62 @@ Node *Switch::TypeCheck(TypeChecker &tc, size_t reqret) { } } if (exptype.Null()) exptype = type_void; // Empty switch or all return statements. - if (!have_default) { - if (reqret) tc.Error(*this, "switch that returns a value must have a default case"); - if (ptype->IsEnum()) { - for (auto [i, ev] : enumerate(ptype->e->vals)) { - if (!enum_cases[i]) - tc.Error(*value, "enum value ", Q(ev->name), " not tested in switch"); + if (ptype->t == V_CLASS) { + auto &dispatch_udt = *ptype->udt; + dispatch_udt.subudts_dispatched = true; + vtable_idx = -1; + vector case_picks; + for (auto udt : dispatch_udt.subudts) { + int pick = -1; + if (!udt->g.is_abstract) { + int best_dist = -1; + int default_case = -1; + for (auto [i, n] : enumerate(cases->children)) { + auto cas = AssertIs(n); + if (cas->pattern->Arity()) { + auto udtref = Is(cas->pattern->children[0]); + auto sdist = SuperDistance(udtref->udt, udt); + if (sdist >= 0 && (pick < 0 || best_dist >= sdist)) { + if (best_dist == sdist) + tc.Error(*udtref, "more than one case applies to ", Q(udt->name)); + pick = (int)i; + best_dist = sdist; + } + } else { + default_case = (int)i; + } + } + if (pick < 0) pick = default_case; + if (pick < 0) tc.Error(*this, "no case applies to ", Q(udt->name)); + } + case_picks.push_back(pick); + vtable_idx = std::max(vtable_idx, (int)udt->dispatch_table.size()); + } + // FIXME: check here if any vtable entries are equal so we don't need to store + // a new one. + assert(vtable_idx >= 0); + // Add cases to all vtables. + for (auto [i, udt] : enumerate(dispatch_udt.subudts)) { + auto &dt = udt->dispatch_table; + assert((int)dt.size() <= vtable_idx); // Double entry. + // FIXME: this is not great, wasting space, but only way to do this + // on the fly without tracking lots of things. + while ((int)dt.size() < vtable_idx) + dt.push_back({}); + dt.push_back({ nullptr, case_picks[i], true }); + } + auto de = &dispatch_udt.dispatch_table[vtable_idx]; + de->is_switch_dispatch = true; + de->is_dispatch_root = true; + de->subudts_size = dispatch_udt.subudts.size(); + } else { + if (!have_default) { + if (reqret) tc.Error(*this, "switch that returns a value must have a default case"); + if (ptype->IsEnum()) { + for (auto [i, ev] : enumerate(ptype->e->vals)) { + if (!enum_cases[i]) + tc.Error(*value, "enum value ", Q(ev->name), " not tested in switch"); + } } } } @@ -2520,8 +2576,17 @@ Node *Case::TypeCheck(TypeChecker &tc, size_t reqret) { // FIXME: Since string constants are the real use case, LT_KEEP would be more // natural here, as this will introduce a lot of keeprefs. Alternatively make sure // string consts don't introduce keeprefs. - tc.TypeCheckList(pattern, LT_BORROW); + auto sw = tc.switch_case_context; + tc.switch_case_context = nullptr; auto flowstart = tc.flowstack.size(); + if (pattern->Arity()) { + if (auto udtref = Is(pattern->children[0])) { + tc.CheckFlowTypeIdOrDot(*sw->value, &udtref->udt->thistype); + udtref->TypeCheck(tc, 0); + } else { + tc.TypeCheckList(pattern, LT_BORROW); + } + } tc.TT(cbody, reqret, LT_KEEP); tc.CleanUpFlow(flowstart); exptype = cbody->exptype; diff --git a/dev/src/lobster/vmdata.h b/dev/src/lobster/vmdata.h index dca1733dc..c50cd2fcf 100644 --- a/dev/src/lobster/vmdata.h +++ b/dev/src/lobster/vmdata.h @@ -1133,6 +1133,13 @@ VM_INLINE int RetSlots(VM &vm) { return vm.ret_slots; } +VM_INLINE int GetTypeSwitchID(VM &vm, Value self, int vtable_idx) { + auto start = self.oval()->ti(vm).vtable_start_or_bitmask; + auto id = (int)(size_t)vm.native_vtables[start + vtable_idx]; + assert(id >= 0); + return id; +} + VM_INLINE void PushFunId(VM &vm, const int *funstart, StackPtr locals) { vm.fun_id_stack.push_back({ funstart, locals, vm.last_line, vm.last_fileidx, #if LOBSTER_FRAME_PROFILER_FUNCTIONS diff --git a/dev/src/lobster/vmops.h b/dev/src/lobster/vmops.h index 238ff2c3c..0f5571b7b 100644 --- a/dev/src/lobster/vmops.h +++ b/dev/src/lobster/vmops.h @@ -768,6 +768,10 @@ VM_INLINE void U_JUMP_TABLE(VM &, StackPtr, const int *) { assert(false); } +VM_INLINE void U_JUMP_TABLE_DISPATCH(VM &, StackPtr, const int *) { + assert(false); +} + VM_INLINE void U_ISTYPE(VM &vm, StackPtr sp, int ty) { auto to = (type_elem_t)ty; auto v = Pop(sp); diff --git a/dev/src/tocpp.cpp b/dev/src/tocpp.cpp index 36884106f..dcb3f6bce 100644 --- a/dev/src/tocpp.cpp +++ b/dev/src/tocpp.cpp @@ -119,6 +119,7 @@ string ToCPP(NativeRegistry &natreg, string &sd, string_view bytecode_buffer, bo "extern StackPtr PopArg(VMRef, int, StackPtr);\n" "extern void SetLVal(VMRef, Value *);\n" "extern int RetSlots(VMRef);\n" + "extern int GetTypeSwitchID(VMRef, Value, int);\n" "extern void PushFunId(VMRef, const int *, StackPtr);\n" "extern void PopFunId(VMRef);\n" #if LOBSTER_FRAME_PROFILER @@ -317,6 +318,16 @@ string ToCPP(NativeRegistry &natreg, string &sd, string_view bytecode_buffer, bo // switch, and generate warnings/errors. Ideally not generate this block at all. append(sd, "block", id, ":;"); break; + case IL_JUMP_TABLE_DISPATCH: + if (cpp) { + append(sd, "switch (GetTypeSwitchID(vm, regs[", regso - 1, "], ", args[0], + ")) {"); + } else { + append(sd, "{ int top = GetTypeSwitchID(vm, regs[", regso - 1, "], ", args[0], + "); switch (top) {"); + } + jumptables.push_back(args + 1); + break; case IL_JUMP_TABLE: if (cpp) { append(sd, "switch (regs[", regso - 1, "].ival()) {"); @@ -513,6 +524,7 @@ string ToCPP(NativeRegistry &natreg, string &sd, string_view bytecode_buffer, bo for (auto id : *bcf->vtables()) { sd += " "; if (id >= 0) append(sd, "fun_", id); + else if (id <= -2) append(sd, "(fun_base_t)", -id - 2); // Bit of a hack, would be nice to separate. else sd += "0"; sd += ",\n"; } diff --git a/dev/src/vm.cpp b/dev/src/vm.cpp index f7b00733c..5a6650d2a 100644 --- a/dev/src/vm.cpp +++ b/dev/src/vm.cpp @@ -840,6 +840,7 @@ void CVM_RestoreBackup(VM *vm, int i) { RestoreBackup(*vm, i); } StackPtr CVM_PopArg(VM *vm, int i, StackPtr psp) { return PopArg(*vm, i, psp); } void CVM_SetLVal(VM *vm, Value *v) { SetLVal(*vm, v); } int CVM_RetSlots(VM *vm) { return RetSlots(*vm); } +int CVM_GetTypeSwitchID(VM *vm, Value self, int vtable_idx) { return GetTypeSwitchID(*vm, self, vtable_idx); } void CVM_PushFunId(VM *vm, const int *id, StackPtr locals) { PushFunId(*vm, id, locals); } void CVM_PopFunId(VM *vm) { PopFunId(*vm); } #if LOBSTER_FRAME_PROFILER @@ -904,6 +905,7 @@ const void *vm_ops_jit_table[] = { "PopArg", (void *)CVM_PopArg, "SetLVal", (void *)CVM_SetLVal, "RetSlots", (void *)CVM_RetSlots, + "GetTypeSwitchID", (void *)CVM_GetTypeSwitchID, "PushFunId", (void *)CVM_PushFunId, "PopFunId", (void *)CVM_PopFunId, #if LOBSTER_ENGINE diff --git a/docs/language_reference.html b/docs/language_reference.html index 4dc792b0e..f95d34ef6 100644 --- a/docs/language_reference.html +++ b/docs/language_reference.html @@ -613,6 +613,24 @@

Overloading and dynamic

That f() is statically dispatched to call A’s version of f (or its superclass, if it doesn’t have one).

+

You can also “dynamic dispatch” with switch ! You can +use class names as switch cases:

+
switch x:
+    case A: print x.field_in_a
+    case B: print x.field_in_b
+

As you can see, the type of variable switched on will be “upgraded” +to the type matched, so you can access its fields.

+

switches on types are “exhaustive”, meaning if you don’t use a +default case (and its a good habit to not use those) you +will get a compile-time error if a type is not covered by a switch (all +possible subclasses of the type of the switched on value).

+

Superclass cases can apply to subclass cases, and if both are +present, the most specific case will always be used. It is a good idea +to make superclasses abstract for use with +switch, that way you may omit a case for them, causing all +their subclasses to need their own case.

+

The actual implementation use vtables much like the above dynamic +dispatch, so is similar in speed too.

Functions with different number of arguments / default arguments.

diff --git a/docs/source/language_reference.md b/docs/source/language_reference.md index 59775deba..9c748c736 100644 --- a/docs/source/language_reference.md +++ b/docs/source/language_reference.md @@ -724,6 +724,30 @@ class B : A That `f()` is statically dispatched to call `A`'s version of `f` (or its superclass, if it doesn't have one). +You can also "dynamic dispatch" with `switch` ! You can use class names as +switch cases: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +switch x: + case A: print x.field_in_a + case B: print x.field_in_b +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +As you can see, the type of variable switched on will be "upgraded" to the type +matched, so you can access its fields. + +switches on types are "exhaustive", meaning if you don't use a `default` case +(and its a good habit to not use those) you will get a compile-time error if +a type is not covered by a switch (all possible subclasses of the type of the +switched on value). + +Superclass cases can apply to subclass cases, and if both are present, the +most specific case will always be used. It is a good idea to make superclasses +`abstract` for use with `switch`, that way you may omit a case for them, +causing all their subclasses to need their own case. + +The actual implementation use vtables much like the above dynamic dispatch, +so is similar in speed too. ### Functions with different number of arguments / default arguments. diff --git a/tests/typeswitch.lobster b/tests/typeswitch.lobster new file mode 100644 index 000000000..0b13bf1c8 --- /dev/null +++ b/tests/typeswitch.lobster @@ -0,0 +1,77 @@ +import std +import testing + +run_test("typeswitch"): + + abstract class A: + a = 0 + + class B : A + b = 1 + + abstract class C : A + c = 2 + + class D : C + d = 3 + + class E : C + e = 4 + + class F : E + f = 5 + + // 6 classes, but 2 are abstract, so this switch will demand that exactly 4 cases + // must always be covered! + + /* + + (A) + |\ + B (C) + |\ + D E + | + F + + */ + + let tests = [ B {}, D {}, E {}, F {} ] + + // Exactly 1 case per class. + let results1 = map(tests) t: + switch t: + case B: t.b + case D: t.d + case E: t.e + case F: t.f + // No default needed / allowed! + + assert equal(results1, [ 1, 3, 4, 5 ]) + + // Subclasses can be done by superclasses. + let results2 = map(tests) t: + switch t: + case B: t.b + case D: t.d + case E: t.e + // No default needed / allowed! + + assert equal(results2, [ 1, 3, 4, 4 ]) + + // Abstract base class may implement for all subclasses. + let results3 = map(tests) t: + switch t: + case B: t.b + case C: t.c + // No default needed / allowed! + + assert equal(results3, [ 1, 2, 2, 2 ]) + + // Probably bad practice, but defaults are still allowed. + let results4 = map(tests) t: + switch t: + default: t.a // Can't access anything else! + case F: t.f + + assert equal(results4, [ 0, 0, 0, 5 ]) diff --git a/tests/unittest.lobster b/tests/unittest.lobster index cb4237080..93e3c9bb0 100644 --- a/tests/unittest.lobster +++ b/tests/unittest.lobster @@ -27,5 +27,6 @@ import lifetimetest import builtintest import operators import matrixtest +import typeswitch "the end" // FIXME: a file can not be only import files?