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?

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?

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.

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.

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.

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.