more powerful interfaces using expected types
authorBrendan Hansen <brendan.f.hansen@gmail.com>
Sat, 15 Jan 2022 03:09:24 +0000 (21:09 -0600)
committerBrendan Hansen <brendan.f.hansen@gmail.com>
Sat, 15 Jan 2022 03:09:24 +0000 (21:09 -0600)
12 files changed:
core/container/iter.onyx
core/container/map.onyx
core/container/set.onyx
core/hash.onyx
docs/interfaces.onyx
examples/21_quick_functions.onyx
include/astnodes.h
src/checker.c
src/parser.c
src/symres.c
tests/complicated_polymorph.onyx
tests/interfaces.onyx

index 34df7f084b3a6f3e7178d731ce017fb40e7ee7c9..09d6bcc539415ed687a7ce8305f0d460af8055b4 100644 (file)
@@ -5,8 +5,8 @@ use package core.intrinsics.onyx { __zero_value }
 
 as_iterator :: #match {}
 
-Iterable :: interface (T: type_expr) {
-    as_iterator(T);
+Iterable :: interface (t: $T) {
+    as_iterator(t);
 }
 
 close :: (it: Iterator($T)) {
index 842a2cf29b3bdb284ffbf871e3af91dbb0edd841..c88017691a0ce263f09a9754dd3c663ad44d432c 100644 (file)
@@ -11,13 +11,13 @@ package core.map
 }
 
 #local {
-    ValidKey :: interface (T: type_expr) {
+    ValidKey :: interface (t: $T) {
         // In order to use a certain type as a key in a Map, you must
         // provide an implementation of core.hash.to_u32() for that type,
         // and you must provide an operator overload for ==.
 
-        hash.to_u32(T);
-        T == T;
+        { hash.to_u32(t) } -> u32;
+        { t == t         } -> bool;
     }
 }
 
index fcf4ac74a1063da673312f3f9119d43e3440f28d..f4374f42d770c51ffb08c93ee8340162dfd540ba 100644 (file)
@@ -8,9 +8,9 @@ package core.set
 }
 use package core.intrinsics.onyx { __zero_value }
 
-#local SetValue :: interface (T: type_expr) {
-    hash.to_u32(T);
-    T == T;
+#local SetValue :: interface (t: $T) {
+    { hash.to_u32(T) } -> u32;
+    { T == T } -> bool;
 }
 
 Set :: struct (Elem_Type: type_expr) where SetValue(Elem_Type) {
index a0e05e1f772f59a308dbb0116398af0c19dce9a1..f02695374cec60c34f832be85fc107f83885e6c3 100644 (file)
@@ -13,6 +13,6 @@ to_u32 :: #match {
     (key: type_expr) -> u32 do return to_u32(cast(u32) key);
 }
 
-Hashable :: interface (T: type_expr) {
-    to_u32(T);
-}
\ No newline at end of file
+Hashable :: interface (t: $T) {
+    { to_u32(t) } -> u32;
+}
index 7abe84d418f11a3a5eb4c6264944e8fea9ac9aa9..84ab346c0652d77a7cbb123c2c2e03884e404491 100644 (file)
@@ -39,222 +39,49 @@ ValidKey :: interface (T: type_expr) {
 }
 
 
+// I think that interfaces should be a little bit more powerful than they are now.
 
-// To remedy all of this, I think Onyx should have interfaces (or constraints,
-// I'm not sure what I want to call them yet). The idea is much like
-// an type class in Haskell where you must define a set of procedure types
-// that the implementer must define in order to provide the interface.
-// With these interfaces, I would get rid of #match and only provide
-// locally overloaded procedures (like print right now), as these are still
-// useful.
-//
-// One thing I was wondering was if this is how operator overloading should
-// work as well. I don't think so, but I do think that interfaces should
-// provide a mechanism for specifying that a certain operator must be defined
-// as well.
-
-// This is how interfaces will be declared. Interfaces must have at
-// least 1 parameter, and for now, all parameters must be type_exprs.
 Hashable :: interface (T: type_expr) {
 
-    // Each "method" listed in the interface is just a symbol name
-    // and a function type. It can use the parameters to the interface
-    // to describe the type.
-    hash :: (T) -> u32
-}
-
-// This is how you implement an interface for a certain set of type
-// arguments. All interface "methods" must be defined here and their
-// types must match the interface types listed above.
-implement Hashable(str) {
-    hash :: (s: str) -> i32 {
-        // ...
-    }
-}
-
-Vec2 :: struct (T: type_expr) {
-    x, y: T;
-}
-
-implement Hashable(Vec2($T)) {
-    hash :: (v: Vec2($T)) -> i32 where H :: Hashable(T) {
-        return H.hash(v.x) * H.hash(v.y);
-    }
-}
-
-v : Vec2(i32);
-h := Hashable(typeof v).hash(v);
-
-// This shows two things: interfaces can have constraints, and interfaces
-// can declare that a certain operator must be overloaded with a particular
-// type.
-Mappable :: interface (T: type_expr) where Hashable(T) {
-    #operator == (T, T) -> bool
-}
-
-Map :: struct (K: type_expr, V: type_expr) where Mappable(K) {
-}
-
-// This will have the "Hashable(K)" constraint of Map checked when
-// the Map(K, V) type is constructed for the parameter type.
-get :: (m: ^Map($K, $V), key: K) -> V where H :: Hashable(K) {
-    // ...
-    hash := H.hash(key);
-}
-
-// 'where' clauses are also allowed on procedures and can be bound
-// to a symbol in order to call the interface functions for that type.
-do_hash :: (h: $T) -> i32 where H :: Hashable(T) {
-    return H.hash(h);  // returns an i32;
-}
-
-// Interfaces don't have to have any methods inside of them.
-Numeric :: interface (T: type_expr) {}
-implement Numeric(i32) {}
-implement Numeric(f32) {}
-
-V2 :: struct { x, y: f32; }
-implement Numeric(V2) {}
-
-// This demonstrates the ability for a constraint to just exist as
-// a declaration that something must be numeric. The way you check
-// for something being numeric is if `impl Numeric(T) {}` has been
-// declared.
-add :: (a, b: $T) -> T where Numeric(T) {
-    return a + b;
-}
-
-
-// At the moment I do not plan on forcing the programmer to declare constaints
-// where they are present. Instead, they are primarily there in order to provide
-// better error messages. For example, this function calls do_hash, which
-// requires that T is hashable, but the caller does not need to add constraints
-// to itself because of this.
-compare_things :: (s1, s2: $T) -> bool {
-    h1 := do_hash(s1);
-    h2 := do_hash(s2);
-    return h1 == h2;
-}
-
-compare_strings :: (s1, s2: str) -> bool {
-    use Hashable(str);
-    return hash(s1) == hash(s2);
-}
-
-// As I'm thinking about whether or not this feature is worth implementing, I want
-// a list of interfaces that will be in the standard library.
-//
-// - Hashable    (hash)
-// - Stringable  (to_string)
-// - Comparable  (==)
-// - Writable    (write to io.Writer)
-// - Readable
-// - Iterable    (as_iter :: (t: T) -> Iterator())
-
-Stringable :: interface (T: type_expr) {
-    to_string :: (T, Allocator) -> str;
-}
-
-implement Stringable(i32) {
-    to_string :: (v: i32, a: Allocator) -> str {
-        buf: [128] u8;
-        s := conv.i64_to_str(~~v, 10, buf);
-        return string.alloc_copy(s, a);
-    }
-}
-
-
-// This is a more complicated case because it requires pattern matching
-// on the types given.
-Iterable :: interface (T: type_expr, V: type_expr) {
-    as_iter :: (T) -> Iterator(V)
-}
-
-implement Iterable([..] $T, T) {
-    as_iter :: (arr: [..] $T) -> Iterator(T) {
-        // ...
-    }
-}
-
-implement Iterable(^List($T), T) {
-    as_iter :: (package core.list).get_iterator
-}
-
-implement Iterable(range, i32) {
-    as_iter :: (r: range) -> Iterator(i32) {
-    }
-}
-
-iterate :: (i: $I) where Iter :: Iterable(I, $V) {
-    for it: Iter.as_iter(i) {
-        println(it);
-    }
-}
-
-
-
-Collecter :: interface (T: type_expr, V: type_expr) {
-    collect :: (T, V) -> void
-}
-
-implement Collecter(^[..] $T, T) {
-    collect :: array.push
-}
-
-implement Collecter(^List($T), T) {
-    collect :: (l: ^List($T), v: T) { /* .. */ }
-}
-
-gather_values :: (i: $Iter, c: $T) where I :: Iterable(Iter, $V),
-                                         C :: Collecter(T, V) {
-    for it: I.as_iter(i) {
-        C.collect(c, it);
-    }
-}
-
-
-
-For :: interface (T: type_expr) {
-    expand     :: (T, body: Code) -> void;
-    expand_ptr :: (T, body: Code) -> void;
-}
-
-implement For([..] $T) {
-    expand :: macro (arr: [..] $T, body: Code) {
-        for it: arr {
-            #insert body;
-        }
-    }
-
-    expand_ptr :: macro (arr: [..] $T, body: Code) {
-        for ^it: arr {
-            #insert body;
-        }
-    }
-}
-
-implement For(Map($K, $V)) {
-    expand :: macro (m: Map($K, $V), body: Code) {
-        for ^it: m.entries {
-            key   := it.key;
-            value := it.value;
-            #insert body;
-        }
-    }
-
-    expand_ptr :: macro (m: Map($K, $V), body: Code) {
-        for ^it: m.entries {
-            key   := ^it.key;
-            value := ^it.value;
-            #insert body;
-        }
-    }
-}
-
-for_ :: macro (a: $T, body: Code) where F :: For(T) {
-    F.expand(a, body);
-}
-
-for_ptr :: macro (a: $T, body: Code) where F :: For(T) {
-    F.expand_ptr(a, body);
+    // This syntax seems pretty clean, but it introduces some complications when
+    // you actually think about using the type T in the expected type expression.
+    { hash.to_u32(T) } -> u32;
+}
+
+Ring :: interface (T: type_expr) {
+    // This syntax is very clean and easy to read; however, when you think about
+    // it for a second, the meaning of T becomes ambiguous...
+    // In the {} T is a value of type T, while in the type expression T is a type.
+    // Is this okay? Or should you have to say `typeof T` in the type expression?
+    // Or should the syntax for interfaces change?
+
+    { T + T } -> T;
+    { T - T } -> T;
+    { T * T } -> T;
+}
+
+Ring :: interface (t: $T) {
+    // This syntax is slightly better, as it makes clear where the value is being
+    // used and where the type is being used.
+
+    // They polymorphic variable "$T" is just for syntactic consistency. The "$" will
+    // always be expected before types in interfaces. Also, all types should just be
+    // a single symbol.
+
+    { t + t } -> T;
+    { t - t } -> T;
+    { t * t } -> T;
+
+    //
+    // The only problem I have with this syntax is that it is a little confusing when
+    // using the constraint syntax:
+    //
+    // foo :: (x, y: $T) -> T where Ring(T) {
+    //     return x + y - x * y; 
+    // }
+    //
+    // Because you are "calling" Ring with a type, not a value of type T. This might
+    // be worth giving up the syntactic consistency tho, just to avoid confusion about
+    // if T is a type or a value.
+    //
 }
index 26905d7947e4ee949d75c54ccfd2372995316b53..2a4535779812ca5cdb246fb04a1787918893041b 100644 (file)
@@ -36,9 +36,9 @@ main :: (args) => {
 
     // This is an example of what the iter package looks like.
     val := iter.as_iterator(range.{ 20, 1, -1 })
-            |> iter.map((x) => x * 2)
-            |> iter.map((x) => cast(f32) x)
-            |> iter.filter((x) => x > 20)
+            |> iter.map(x => x * 2)
+            |> iter.map(x => cast(f32) x)
+            |> iter.filter(x => x > 20)
             |> iter.take(5)
             |> iter.fold(0.0f, (x, y) => x + y);
     println(val);
index ff6986b46fe4d636e2c30ba9d822e38306c10cec..03b6abcf1f78dfbb92042a22cdb9120766f10c3d 100644 (file)
@@ -1012,17 +1012,22 @@ struct AstOverloadedFunction {
 
 // @CLEANUP: Is this really necessary?
 typedef struct InterfaceParam {
-    OnyxToken *token;
+    OnyxToken *value_token;
     AstType   *type_node;
-    Type      *type;
 } InterfaceParam;
 
+typedef struct InterfaceConstraint {
+    AstTyped *expr;
+    AstType  *expected_type_expr;
+    Type     *expected_type;
+} InterfaceConstraint;
+
 struct AstInterface {
     AstTyped_base;
     char *name;
 
-    bh_arr(InterfaceParam) params;
-    bh_arr(AstTyped *)  exprs;
+    bh_arr(InterfaceParam)      params;
+    bh_arr(InterfaceConstraint) exprs;
 };
 
 typedef enum ConstraintPhase {
@@ -1043,7 +1048,7 @@ struct AstConstraint {
     ConstraintCheckStatus *report_status;
 
     Scope* scope;
-    bh_arr(AstTyped *) exprs;
+    bh_arr(InterfaceConstraint) exprs;
     u32 expr_idx;
 };
 
index 63ef8a18d9526861ac02c3c54691f1202db95431..a5afb5078dc870a113531f67749a5885099b30c9 100644 (file)
@@ -2607,8 +2607,11 @@ CheckStatus check_constraint(AstConstraint *constraint) {
             }
 
             bh_arr_new(global_heap_allocator, constraint->exprs, bh_arr_length(constraint->interface->exprs));
-            bh_arr_each(AstTyped *, expr, constraint->interface->exprs) {
-                bh_arr_push(constraint->exprs, (AstTyped *) ast_clone(context.ast_alloc, (AstNode *) *expr));
+            bh_arr_each(InterfaceConstraint, ic, constraint->interface->exprs) {
+                InterfaceConstraint new_ic = {0};
+                new_ic.expr = (AstTyped *) ast_clone(context.ast_alloc, (AstNode *) ic->expr);
+                new_ic.expected_type_expr = (AstType *) ast_clone(context.ast_alloc, (AstNode *) ic->expected_type_expr);
+                bh_arr_push(constraint->exprs, new_ic);
             }
 
             assert(constraint->interface->entity && constraint->interface->entity->scope);
@@ -2619,10 +2622,16 @@ CheckStatus check_constraint(AstConstraint *constraint) {
                 InterfaceParam *ip = &constraint->interface->params[i];
 
                 AstTyped *sentinel = onyx_ast_node_new(context.ast_alloc, sizeof(AstTyped), Ast_Kind_Constraint_Sentinel);
-                sentinel->token = ip->token;
+                sentinel->token = ip->value_token;
                 sentinel->type_node = constraint->type_args[i];
 
-                symbol_introduce(constraint->scope, ip->token, (AstNode *) sentinel);
+                assert(ip->type_node);
+                AstAlias *type_alias = onyx_ast_node_new(context.ast_alloc, sizeof(AstAlias), Ast_Kind_Alias);
+                type_alias->token = ip->type_node->token;
+                type_alias->alias = (AstTyped *) constraint->type_args[i];
+
+                symbol_introduce(constraint->scope, ip->value_token, (AstNode *) sentinel);
+                symbol_introduce(constraint->scope, ip->type_node->token, (AstNode *) type_alias);
             }
 
             assert(constraint->entity);
@@ -2634,20 +2643,38 @@ CheckStatus check_constraint(AstConstraint *constraint) {
 
         case Constraint_Phase_Checking_Expressions: {
             fori (i, constraint->expr_idx, bh_arr_length(constraint->exprs)) {
-                CheckStatus cs = check_expression(&constraint->exprs[i]);
+                InterfaceConstraint* ic = &constraint->exprs[i];
+
+                CheckStatus cs = check_expression(&ic->expr);
                 if (cs == Check_Return_To_Symres || cs == Check_Yield_Macro) {
                     return cs;
                 }
 
                 if (cs == Check_Error) {
-                    // HACK HACK HACK
-                    onyx_clear_errors();
+                    goto constraint_error;
+                }
 
-                    *constraint->report_status = Constraint_Check_Status_Failed;
-                    return Check_Failed;
+                if (ic->expected_type_expr) {
+                    ic->expected_type = type_build_from_ast(context.ast_alloc, ic->expected_type_expr);
+                    if (ic->expected_type == NULL) {
+                        printf("WEIRD CONDITION THAT SHOULD NEVER HAPPEN!\n");
+                        return Check_Yield_Macro;
+                    }
+
+                    TYPE_CHECK(&ic->expr, ic->expected_type) {
+                        goto constraint_error;
+                    }
                 }
 
                 constraint->expr_idx++;
+                continue;
+
+              constraint_error:
+                // HACK HACK HACK
+                onyx_clear_errors();
+
+                *constraint->report_status = Constraint_Check_Status_Failed;
+                return Check_Failed;
             }
 
             *constraint->report_status = Constraint_Check_Status_Success;
@@ -2670,7 +2697,7 @@ CheckStatus check_constraint_context(ConstraintContext *cc, Scope *scope, OnyxFi
                     fori (i, 0, bh_arr_length(constraint->type_args)) {
                         if (i != 0) strncat(constraint_map, ", ", 511);
 
-                        OnyxToken* symbol = constraint->interface->params[i].token;
+                        OnyxToken* symbol = constraint->interface->params[i].value_token;
                         token_toggle_end(symbol);
                         strncat(constraint_map, symbol->text, 511);
                         token_toggle_end(symbol);
@@ -2680,7 +2707,7 @@ CheckStatus check_constraint_context(ConstraintContext *cc, Scope *scope, OnyxFi
                         strncat(constraint_map, "'", 511);
                     }
 
-                    onyx_report_error(constraint->exprs[constraint->expr_idx]->token->pos, Error_Critical, "Failed to satisfy constraint where %s.", constraint_map);
+                    onyx_report_error(constraint->exprs[constraint->expr_idx].expr->token->pos, Error_Critical, "Failed to satisfy constraint where %s.", constraint_map);
                     onyx_report_error(constraint->token->pos, Error_Critical, "Here is where the interface was used.");
                     onyx_report_error(pos, Error_Critical, "Here is the code that caused this constraint to be checked.");
 
index 24944d6b5f83dd28ca9f582e57832e9783564f40..9f94184943ca41f7433c9b64cee991b9d9a3a0bf 100644 (file)
@@ -2078,9 +2078,12 @@ static AstInterface* parse_interface(OnyxParser* parser) {
         if (parser->hit_unexpected_token) return interface;
 
         InterfaceParam ip;
-        ip.token = expect_token(parser, Token_Type_Symbol);
+        ip.value_token = expect_token(parser, Token_Type_Symbol);
         expect_token(parser, ':');
-        ip.type_node = parse_type(parser);
+        expect_token(parser, '$');
+
+        OnyxToken* type_sym = expect_token(parser, Token_Type_Symbol);
+        ip.type_node = (AstType *) make_symbol(parser->allocator, type_sym);
 
         bh_arr_push(interface->params, ip);
 
@@ -2094,8 +2097,20 @@ static AstInterface* parse_interface(OnyxParser* parser) {
     while (!consume_token_if_next(parser, '}')) {
         if (parser->hit_unexpected_token) return interface;
 
-        AstTyped *expr = parse_expression(parser, 0);
-        bh_arr_push(interface->exprs, expr);
+        InterfaceConstraint ic = {0};
+        if (consume_token_if_next(parser, '{')) {
+            ic.expr = parse_expression(parser, 0);
+
+            expect_token(parser, '}');
+            expect_token(parser, Token_Type_Right_Arrow);
+
+            ic.expected_type_expr = parse_type(parser);
+
+        } else {
+            ic.expr = parse_expression(parser, 0);
+        }
+
+        bh_arr_push(interface->exprs, ic);
 
         expect_token(parser, ';');
     }
index d6e1ae509271131d40434403b23a315ca80600bc..153f182fe0aecfa59723aa1fd25b4cbb1d8d62e6 100644 (file)
@@ -1326,7 +1326,11 @@ static SymresStatus symres_constraint(AstConstraint* constraint) {
 
         case Constraint_Phase_Checking_Expressions: {
             fori (i, constraint->expr_idx, bh_arr_length(constraint->exprs)) {
-                SYMRES(expression, &constraint->exprs[i]);
+                SYMRES(expression, &constraint->exprs[i].expr);
+
+                if (constraint->exprs[i].expected_type_expr) {
+                    SYMRES(type, &constraint->exprs[i].expected_type_expr);
+                }
             }
 
             return Symres_Success;
index 41b457591d643d79e08b28cc324c7b17d7c247c2..cf08e60ef82c65ce3dd217b39a99a761153661f6 100644 (file)
@@ -14,8 +14,8 @@ main :: (args: [] cstr) {
         return g(f(a));
     }
 
-    Number :: interface (T: type_expr) {
-        T * T;
+    Number :: interface (t: $T) {
+        { t * t } -> T;
     }
 
     dumb :: (ZZZ: $T) -> #auto where Number(T) {
index 33e18fd811f3d89fcb2da327f83e8b36e1d02634..ea10259a3acb7943267c23660c81df8c667d866f 100644 (file)
@@ -2,8 +2,8 @@
 
 use package core
 
-Hashable :: interface (T: type_expr) {
-    hash.to_u32(T);
+Hashable :: interface (t: $T) {
+    { hash.to_u32(t) } -> u32;
 }
 
 try_hash :: #match {
@@ -16,8 +16,8 @@ try_hash :: #match {
     },
 }
 
-CanCastTo :: interface (T: type_expr, D: type_expr) {
-    cast(typeof D) T;
+CanCastTo :: interface (t: $T, d: $D) {
+    { cast(D) t } -> D;
 }
 
 // I don't know why this doesn't work... It is complaining that it couldn't match
@@ -37,20 +37,20 @@ do_math :: macro (x, y: $T) -> T where SemiRing(T) {
     return x + y + x * y;
 }
 
-SemiRing :: interface (T: type_expr) {
-    T + T;
-    T * T;
+SemiRing :: interface (t: $T) {
+    {t + t} -> T;
+    {t * t} -> T;
 }
 
 bit_math :: (x, y: $T) -> T where BitField(T) {
     return (x & y) | (x ^ y);
 }
 
-BitField :: interface (T: type_expr) {
-    ~T;
-    T & T;
-    T | T;
-    T ^ T;
+BitField :: interface (t: $T) {
+    { ~t } -> T;
+    { t & t } -> T;
+    { t | t } -> T;
+    { t ^ t } -> T;
 }
 
 Complex :: struct [conv.Custom_Format.{ format }] {