www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - RFC: Parcel: A scripting language in development

reply Burton Radons <burton-radons smocky.com> writes:
I've been working on a scripting language and thought I'd bounce it off 
the people here.  It's basically a subset of Cecil.

Basic type declarations are of the form "name : bases...;":

    // Declare the type A
    A;

    // Declare the type B inheriting from A and String.
    B : A, String;

You can also add a new supertype to a previously-declared type with 
"name +: bases;":

    // Add the integral interface to the integer built-in type.
    Integer +: Integral;

There are no member functions or fields; everything is in methods at the 
top level and uses static typing to determine which is the appropriate 
method to call.  Member functions and fields are artificial mechanics 
that have nothing to do with object-oriented programming.

Methods are declared with "name (arguments...) : return_type".  An 
argument can either be "name", "name : type", or ": type", which limits 
it to inheritors of that type.  The return type can be omitted if it 
returns anything, and the arguments list can be omitted if it takes no 
arguments.

It's then followed with a method body:

     foo (a, b : String)
     {
     }

The most-specialised method is chosen when sending a message; if two 
methods are equally specialised, the message fails.  This doesn't use 
the type an object poses as to find a message, it uses the type the 
object is composed of.

Fields are declared like methods with ":= value" instead of a body 
following them:

     // Declare a new field of Integer literals.
     bar (: Integer) := 8;

The initialiser is evaluated the first time it's read for an object.

Method bodies are composed of a series of messages separated with ";"; 
the last message is the return value, unless if the "return (value);" or 
"return ();" messages are used.  Messages can be sent in many ways. 
Each block here is composed of equivalent messages:

     foo;
     foo ();

     foo.bar;
     bar (foo);

     foo.bar (4);
     bar (foo, 4);

     foo = 4;
     foo (4);

     foo.bar = 8;
     bar (foo, 8);

     - bar;
     - (bar); // That is to say, call the "-" method with bar as the 
argument.

     bar * baz;
     * (bar, baz);
     bar.* (baz);

Therefore field accesses and writes are really just method calls, and 
operator overloading is straightforward.  Types are instantiated by 
using a "new" method it defines - for example:

     A; // Type named A.

     a := A.new; // Create an instance of A.

There are no control statements; everything's handled through closures. 
  For example:

     // Convert a Collection to a string of the form "[a, b, c]".
     to_string (list : Collection) : String
     {
         // Declare a local variable.
         text := "[";

         // Iterate over each item in the list; the closure is called
         // with index and item arguments.
         list.do ((index, item)
         {
             if (index != 0, { text = text + ", "; });
             text = text + item.to_string;
         });

         text + "]";
     }

Finally, you often need to treat an object as another type for a method 
call, such as to call a base method.  This is done by specifying the 
type to send it as.  For example:

     A;
     B : A;

     foo (s:A) { }

     foo (s:B)
     {
         // Send the message using A as the type to search for a match.
         foo (s:A);
     }

That's it to the language.

All of this functionality is complete, I just need to move the library 
into a DLL, make it easy to create new DLLs implementing further 
functionality, have some way to import, and fill out a standard library. 
  Unsurprisingly I use templating to make writing methods in D easy, 
fast, and type-safe.

I will eventually allow defining new operators (you'd also define their 
precedence relative to other operators), predicate types (an object can 
be consideed to be a type if it succeeds a predicate test - very useful 
for self-documentation and static code checking), and parametric types 
(to give further type information).  Regular expressions might be 
supported directly PERL-style to maximise their speed and avoid 
backslash madness.

This language is more static than it appears to be; it's susceptible to 
type analysis and aggressive profiling, and a small language change 
allows discovery of value types.  Experience with Self and Cecil show 
that even with a pure object-oriented language you can produce 
high-speed machine code.  I would like to start doing this in the 
future, although it will always be unnoticeable from the client's 
perspective; his code will simply start going faster and faster as it 
executes and the library learns more about the code it's been given.

Comments?
Aug 01 2005
next sibling parent zwang <nehzgnaw gmail.com> writes:
Burton Radons wrote:
 I've been working on a scripting language and thought I'd bounce it off 
 the people here.  It's basically a subset of Cecil.
 <snip>
 Comments?

Interesting. What kind of application is Parcel best suited for?
Aug 01 2005
prev sibling next sibling parent reply "Ben Hinkle" <ben.hinkle gmail.com> writes:
Very interesting! My first question is that reading

 There are no member functions or fields; everything is in methods at the 
 top level and uses static typing to determine which is the appropriate 
 method to call.  Member functions and fields are artificial mechanics that 
 have nothing to do with object-oriented programming.

and
 Finally, you often need to treat an object as another type for a method 
 call, such as to call a base method.  This is done by specifying the type 
 to send it as.  For example:

     A;
     B : A;

     foo (s:A) { }

     foo (s:B)
     {
         // Send the message using A as the type to search for a match.
         foo (s:A);
     }

I don't see how to implement the equivalent to the D code class Component { void draw(){...} } class Button:Component{ void draw(){...} } class Scrollbar:Component{ void draw(){...} } etc. so that when one has a Component and calls "draw" it actually calls the most specific "draw". In other words can the user dispatch based on the dynamic type instead of the static type without adding a switch statement to Component.draw?
Aug 02 2005
parent Burton Radons <burton-radons smocky.com> writes:
Ben Hinkle wrote:
 Very interesting! My first question is that reading
 
 
There are no member functions or fields; everything is in methods at the 
top level and uses static typing to determine which is the appropriate 
method to call.  Member functions and fields are artificial mechanics that 
have nothing to do with object-oriented programming.

and
Finally, you often need to treat an object as another type for a method 
call, such as to call a base method.  This is done by specifying the type 
to send it as.  For example:

    A;
    B : A;

    foo (s:A) { }

    foo (s:B)
    {
        // Send the message using A as the type to search for a match.
        foo (s:A);
    }

I don't see how to implement the equivalent to the D code class Component { void draw(){...} } class Button:Component{ void draw(){...} } class Scrollbar:Component{ void draw(){...} } etc. so that when one has a Component and calls "draw" it actually calls the most specific "draw". In other words can the user dispatch based on the dynamic type instead of the static type without adding a switch statement to Component.draw?

You'd just use: Component; Button : Component; Scrollbar : Component; draw (self : Component) { } draw (self : Button) { } draw (self : Scrollbar) { } It uses the most-specialised method in a dispatch based on the types of the objects themselves, not what type they are posing as (the static type). Currently it does this by scoring an argument based upon how far the specified type is on the chain from the type of the message and summing the arguments; the lowest-scoring method wins. That might change, it's just what I threw in there. The second section you pointed out was just to describe how to disambiguate. Most of the time you wouldn't use that functionality, but Parcel will not attempt to solve an ambiguous dispatch automatically. For example: A; B; C : A, B; foo (:A) { } foo (:B) { } // Error, both methods are equally specialised. // Parcel does not consider base types on the same level to // be ordered. main () { foo (C); } // It has to be either of these: main () { foo (C:A); foo (C:B); } Here's an example of the other main way to make an ambiguity: A; B : A; C : A; foo (:A, :B) { } foo (:C, :A) { } // Error, both methods are equally specialised. // Parcel does not prioritise arguments in method lookup. main () { foo (C, B); } The interpreter can report ambiguous method dispatches with a great deal of cogency; a Smalltalk-like environment could even allow you to click on what you intended and it'd insert a disambiguating method declaration and continue execution. Languages which solve these ambiguities without any user input (usually through prioritisation) tend to create bugs that are some of the most difficult to hunt down. The question may come up as to how to actually cast as opposed to just pose. This could be simply done with a cast method: // This is the basic undefined form. // cast (type, value) cast_to (value, type) { cast (type, value); } cast (:Float, :Integer) ... cast (String, 4.5), or String.cast (4.5), or 4.5.cast_to (String) Notice how the "cast (type, value)" form would be defined as throwing an error in other languages; instead this is omitted entirely, which allows the compiler to detect the error statically if it can determine that no method would match. It would also provide much better diagnostics than your exception could. I might extend the language with properties, so you could add this: [abstract] cast (type, value); Which is really just self-documentation. Given this, think about how you would solve the same problem with a language like D or a more flexible language like C++, or even Python. How would you add a new cast, either from or to a new type? Does it require explicit support from the interpreter? How would you add operator overloading - not just of the form "YourType * BuiltinType" but "BuiltinType * YourType"? Does it require additional syntax? Does it involve complex magic semantics builtin to the language? Does it break virtualisation? Is it more difficult than: * (a : YourType, b : BuiltinType) : YourType { ... } * (a : BuiltinType, b : YourType) : YourType { b * a; } The Cecil model might not seem impressive at first glance because a lot of the problems with the member function model have workarounds that it's become second nature to deal with, but they're ugly and they're limiting. zwang wrote:
 Burton Radons wrote:

 I've been working on a scripting language and thought I'd bounce it
 off the people here.  It's basically a subset of Cecil.
 <snip>
 Comments?

Interesting. What kind of application is Parcel best suited for?

Presently scripting, rapid prototyping, and control flow logic - same as Python - because of its poor speed. Once it's compiling to machine code and optimising and there are the expected extensions I am optimistic that it could be used as a development language. Fundamentally Parcel is Cecil; it's the irreduceable core of its semantics with a keywordless syntax. Some of those additional semantics are good to have but I'm ignoring them at the present time to discover what would be optimal for its new environment.
Aug 02 2005
prev sibling next sibling parent Burton Radons <burton-radons smocky.com> writes:
Bit of an update.  I just this minute got Parcel-to-C translation in
using TinyCC.  Eventually translation will take a few runs through a
function before it starts so that it can gather profiling information;
it could also periodically go back to interpreting a method to gather
more information as the program executes, but when in C mode its only
concern will be to execute quickly (although it could ping the method
when its assumptions are failing a lot to indicate to it that the
profile is no longer applicable and it's wasting a lot of time).  If a
method is added or overwritten, then all dependencies on that slot will
be recompiled the next time they're executed, so compilation doesn't
affect dynamicism at all.  Compilation is purely on a need-to-use basis.

For now it can certainly generate better code than the interpreter can
manage (it can bind a message directly to a method in some circumstances 
already), but it won't be brilliant for a good long while.

I've decided that I'm going to need parametric types, type 
substitutions, type specifiers, and predicate types early on in the 
game, and I'm putting in method references and currying.  Here's an 
example of most of them together:

     Collection ['T];

     // A collection is a non-empty collection if it has content.
     // Predicate types can have fields and methods.

     NonEmpty Collection ['T] : Collection [T] ? (self) { !self.is empty; };

     // Only support this operation if the collection's items are
     // Numeric.
     + (a : Collection ['T <= Numeric], b : T) : Collection [T]
     {
         // Curry the + method reference so that it returns a method
         // that takes one argument but calls + with two arguments, one
         // already specified.

         // One way to apply currying in languages without it is to have
         // a closure that calls the method with the necessary arguments.
         // However, I think that closures won't be able to be returned,
         // so currying will be very handy.

         a.do_copy ('+ (, b));
     }

     // Here's a user of the predicate type.  If the compiler can
     // determine statically that the type will fail a predicate,
     // then it can halt compilation at that point and have a much
     // better context for providing an error code.
     //
     // Predicates can also improve code generation by using optimised
     // code paths, if it can be determined that the object will match a
     // predicate over the course of some code.  Doing that is
     // easy because Parcel always knows the full program to optimise.

     reduce (self : NonEmpty Collection ['T], closure : &(:T, :T):T) : 
NonEmpty Collection [T]
     {
     }

As you can imagine, a library optimised for these structures would be 
quite a lot different than a library which doesn't, but they're 
important for writing good production code and they're hugely important 
for the later stages of optimisation.
Aug 03 2005
prev sibling parent reply Burton Radons <burton-radons smocky.com> writes:
I've just started a near-contentless site for the language at 
(http://www.smocky.com/parcel); I'll be working on it over the next week 
as much as I can, it'll ultimately have the language specification, 
library, etcetera.  I would really appreciate comments on the talk 
pages; a ten second effort for you commenting on what you found 
confusing or would like to know more about is a major favour to me.
Aug 09 2005
parent Chuck.Esterbrook /at/ gmail /dot/ com <Chuck.Esterbrook_member pathlink.com> writes:
In article <ddb5ii$fa6$1 digitaldaemon.com>, Burton Radons says...
I've just started a near-contentless site for the language at 
(http://www.smocky.com/parcel); I'll be working on it over the next week 
as much as I can, it'll ultimately have the language specification, 
library, etcetera.  I would really appreciate comments on the talk 
pages; a ten second effort for you commenting on what you found 
confusing or would like to know more about is a major favour to me.

Burton, this looks like interesting stuff, but I wonder if langsmiths would be a better place for this discussion: http://groups.yahoo.com/group/langsmiths/ Chuck
Aug 13 2005