jckarter / clay

The Clay programming language
http://claylabs.com/clay
Other
404 stars 34 forks source link

infixOperator handles multivalue - fixes issue #270 #375

Closed ghost closed 12 years ago

ghost commented 12 years ago

Use a modified shunting yard algorithm to parse/eval infixOperator expressions. Handles multivalue arguments. However, due to precedence of the unpack operator an unpack expression must be used in parens e.g. var ..a = integers(#4); var b = 3 + (..a);

Operator test updated.

jckarter commented 12 years ago

Very nice. Does the shunting yard algorithm with post-variadic matching improve compile performance much?

jckarter commented 12 years ago

As far as requiring parens around variadic operator arguments goes, I think that's OK.

ghost commented 12 years ago

I haven't noticed a change in performance. I suppose the potential exists, although it's unlikely to ever be an issue.

ghost commented 12 years ago

What would be nice though is to improve overload resolution and pattern matching performance. Not sure how to achieve that at the moment though.

jckarter commented 12 years ago

One way to improve overload resolution performance would be to add some indexing so that it doesn't need to do a linear search through the entire overload set on every call. You could index the root symbol of each argument position and then use those indices to filter the overload set before doing the linear search. If you have overloads like these:

overload foo(x:Int, y:Float) {} //A
[T when Integer?(T)] overload foo(x:Vector[T], y:Float) {} //B
[T] overload foo(x:Vector[T], y:Int) {} //C

and you're looking for foo(Vector[Int], Float), then the arg-0 index would find B and C, and the arg-1 index would find A and B, so you'd only then have to match B.

jckarter commented 12 years ago

I just did a before-and-after comparison of the test suite and I see 327s before, 279s after, which is a pretty good speedup.

ghost commented 12 years ago

Excellent. Btw, do you know what happened to dembits?

jckarter commented 12 years ago

I think claywheel has some bugs that are causing it to fall over and stop updating. I'll ping @erg to try to start his claywheels again.

ghost commented 12 years ago

A bit of profiling shows the bottleneck in overload resolution to be predicate evaluation, not pattern matching or size of the overload set.

jckarter commented 12 years ago

Interesting find. Just goes to show you should always measure first. I think the evaluator could benefit from a better allocation policy. Right now I think static values are always allocated as ValueHolders, which are refcounted and allocated on the heap, when in most cases they could probably be allocaed or allocated on a special stack during the normal course of evaluation (since compile-time local values have stack extent just like run-time local values). ValueHolders should only be necessary when a static value becomes part of an InvokeEntry, symbol instance, or other global state.

jckarter commented 12 years ago

If you want to get more ambitious in language design, concepts/type classes/interfaces/traits/whatever you want to call them could be a useful feature that, in addition to be prettier and more theoretically sound than predicates for many generic programming patterns, could also greatly improve compile performance. Matching types to traits could potentially be a lot faster than testing predicates.

ghost commented 12 years ago

Maybe something like this:


define Integer {
    integerAdd(a, b);
    integerSubtract(a, b);  
    etc.
}

[T:Integer]
define Number[T] { // defines interfaces for type-class Number
    (+)(a:T, b:T) : T;
}

[T:Number[T]]
define Foo[T] { // defines interfaces for type-class Foo which is dependent on Number
    bar(a:T, b:T) : T;
}

registertypes Integer (Int32);

[T:Integer, U:Integer]
overload (+)(a:T, b:U) : T = integerAdd(a, b);

[T]
overload bar(a:T, b:T) : T = a+b;

[T:Foo[T]] // Check for implementation of bar for type T
someFunc(a:T, b:T) = bar(a, b); 

main () {

    var a:Int, b:Int  = 2, 4;
    var c = someFunc(a, b); // Works

    var d:Int64, e:Int64  = 2l, 4l;
    var f = someFunc(d, e); // Doesn't work as Int64 not registered as Integer

}
jckarter commented 12 years ago

Here are some notes from an earlier attempt to design a type class feature: https://github.com/jckarter/clay/wiki/Protocols I like the idea of reusing define—independent generic functions can behave as single-function type classes (an overload defines an instance), and you could just extend it to let you define a group of related generics as a group. You shouldn't need anything like registertypes—a type becomes an instance of a class by providing implementations of its member functions.

A good test case is to try defining the sequence and iterator protocols, since they require parametric classes and benefit from support for functional dependencies.

ghost commented 12 years ago

The previously proposed 'of' syntax isn't great. Maybe re-use of define and as could work (although of could also be used in this context without the definition/constraint mash-up) e.g. define T as TypeClass {} and all pattern constraints could be specified with the pattern definitions. Using this the protocols would maybe look like this:

// Protocol definition using typeclasses

[I, E]
define I as IteratorType[E] {
    // required operations
    next(i:I) : E;
    hasNext?(i:I) : Bool;

    // derived definitions
    IteratorTargetType(I) = E;
}

[S, E, I:IteratorType[E]]
define S as SequenceType[I] {
    // required operations
    iterator(x:S) : I;

    // derived definitions
    SequenceIteratorType(S) = I;
    SequenceElementType(S) = IteratorTargetType(I);
}

[C, E]
define C as CoordinateType[E] {
    dereference(c:C) : E;
    inc(ref c:C) : ;
    dec(ref c:C) : ;

    CoordinateTargetType(C) = E;
}

[E, C:CoordinateType[E]] 
define C as RandomAccessCoordinateType[E] {
    [I:Integer] add(c:C, i:I) : C;
    [I:Integer] subtract(c:C, i:I) : C;
                subtract(c:C, c:C) : Int;
}

[S, E, C:CoordinateType[E]]
define S as CoordinateSequenceType[C] {
    begin(s:S) : C;
    end(s:S) : C;

    SequenceCoordinateType(S) = C;
}

This seems pretty consistent with the current Clay syntax and would still allow predicate definitions like [T:Integer when T==Int64]. One possible issue is specification of multiple type-class constraints on a single pattern. I do like the idea of type-classes being pervasive with no explicit requirement for formal instance definitions.

So . . .

[T]
define T as FooType {
    foo?(x:T, y:Int) : Bool;
}

overload foo?(x:Foo, y:Int) = x[y] == 0; // Foo is now implicitly an instance of FooType

I haven't really given much thought to type-class extension. I suppose you could overload the type-class definition. Any thoughts on this?

jckarter commented 12 years ago

This looks pretty decent. You could support multiple constraints on a single variable by perhaps allowing a variable to be declared multiple times with different constraints. Supporting multi-in-parameter classes could also be useful (though it brings up annoying semantic and implementation challenges). For example:

[A:Foo, A:Bar, (A, B):Bas]

could constrain A as an instance of Foo and Bar, and A and B together as an instance of Bas. For type class extension, would it be sufficient to declare a constraint on the extending class? For example:

[T:Base]
define T as Derived {
    ...
}

would declare Derived as a subclass of Base.

jckarter commented 12 years ago

I created issue #382 for further discussion/implementation talk.

ghost commented 11 years ago

I'm not shure if I like type classes. but that's mainly because i can't imagine how well things play together. specifically type classes, predicates, records, tuples and other statics in combination. possibly you can imagine full examples for me?

say, I want to write a labyrinth game with walls, keys and stuff. say, I want the development most intuitive so that I could possibly write things like:

[O where isWall?(O)] overload isMovable?(O) = false;

(...)

if ( isMovable?(obj) ) { moveOn(obj, directionOf(p1)); moveOn(p1, directionOf(p1)); } else beep();

silly example, I know! the point is that I'd like to have an interface very native to the toy I am programming. Below that interface, the situation is very different. Because the labyrint is a grid and all graphics share the same size and basic logics, there could be only one record named gameObject (or so) that can represent all the different objects. basically, it holds a pointer to a set of different pixmap objects for different animations, if any, an ID field to identify the type of that object (wall, key...) and some state. besides there are some generic interfaces like above, which are all the same for all objects i.e. isDeadly?, isFetchable?, destroy, moveOn, triggerAnim, whatever...

currently I'd have a problem to distinct the objects if the record is the same. records are also not clone'able or subtype'able. there are no predicates for checking aliases and so forth. type classes now seem to solve the issue. but how do they if the only suitable difference for a check is the ID in the record? how would I subtype the objects to make them distinct though their memory layout is the same? got the point?

can someone please show me how type classes play in here and how i should lay out the code?

ghost commented 11 years ago

sorry, got the page wrong, have posted twice in #382