From: Brendan Hansen Date: Sat, 15 Jan 2022 03:09:24 +0000 (-0600) Subject: more powerful interfaces using expected types X-Git-Url: https://git.brendanfh.com/?a=commitdiff_plain;h=0905adba9a038b37a7e89d85992a4e183a812c95;p=onyx.git more powerful interfaces using expected types --- diff --git a/core/container/iter.onyx b/core/container/iter.onyx index 34df7f08..09d6bcc5 100644 --- a/core/container/iter.onyx +++ b/core/container/iter.onyx @@ -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)) { diff --git a/core/container/map.onyx b/core/container/map.onyx index 842a2cf2..c8801769 100644 --- a/core/container/map.onyx +++ b/core/container/map.onyx @@ -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; } } diff --git a/core/container/set.onyx b/core/container/set.onyx index fcf4ac74..f4374f42 100644 --- a/core/container/set.onyx +++ b/core/container/set.onyx @@ -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) { diff --git a/core/hash.onyx b/core/hash.onyx index a0e05e1f..f0269537 100644 --- a/core/hash.onyx +++ b/core/hash.onyx @@ -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; +} diff --git a/docs/interfaces.onyx b/docs/interfaces.onyx index 7abe84d4..84ab346c 100644 --- a/docs/interfaces.onyx +++ b/docs/interfaces.onyx @@ -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. + // } diff --git a/examples/21_quick_functions.onyx b/examples/21_quick_functions.onyx index 26905d79..2a453577 100644 --- a/examples/21_quick_functions.onyx +++ b/examples/21_quick_functions.onyx @@ -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); diff --git a/include/astnodes.h b/include/astnodes.h index ff6986b4..03b6abcf 100644 --- a/include/astnodes.h +++ b/include/astnodes.h @@ -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; }; diff --git a/src/checker.c b/src/checker.c index 63ef8a18..a5afb507 100644 --- a/src/checker.c +++ b/src/checker.c @@ -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."); diff --git a/src/parser.c b/src/parser.c index 24944d6b..9f941849 100644 --- a/src/parser.c +++ b/src/parser.c @@ -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, ';'); } diff --git a/src/symres.c b/src/symres.c index d6e1ae50..153f182f 100644 --- a/src/symres.c +++ b/src/symres.c @@ -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; diff --git a/tests/complicated_polymorph.onyx b/tests/complicated_polymorph.onyx index 41b45759..cf08e60e 100644 --- a/tests/complicated_polymorph.onyx +++ b/tests/complicated_polymorph.onyx @@ -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) { diff --git a/tests/interfaces.onyx b/tests/interfaces.onyx index 33e18fd8..ea10259a 100644 --- a/tests/interfaces.onyx +++ b/tests/interfaces.onyx @@ -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 }] {