module:
top_stmt:
Top level statements synthesize the project hierarchy (which is separate from the class hierarchy) and control semantic searches and object visibility in the project being compiled. Optional scope qualifiers on declarations, import, and class statements determine visibility. private-qualified statements can only be accessed within the source module. library-qualified statements can be accessed within the library or program but are not visible past any self containment boundaries. This is the default. public-qualified statements can be accessed externally through library imports.
Note that semantic scope can extend backwards through an import unless the module is asserted to be selfcontained. For example, take the following program:
import "list.l" as blah; import "cat.d";
In this program, the "cat.d" program module can access classes and certain declarations inside "list.l" by using the class path blah.id (or list.id if we did not have the as clause). This is the primary method of locating classes within a given program or library. There are no ordering constraints: you could easily import "cat.d" before "list.l". In Rune, semantic searches are always bottom-up and only push downward as each element matches. The first element of a class path is matched against semantic identifiers at the current level. If no match is found we recurse up to the parent and look for the identifier there, and so forth. It is important to note that Rune does not arbitrarily push into modules to locate an identifier. For example, Rune will not push into the list module when searching for fubar. Instead you have to explicitly push into the module by using blah.id. Rune provides the programmer with the ability to create shortcuts using typedef, alias, and a special import construct called as self;. Rune uses these features heavily in its system classes to allow the programmer to do things like declare 'int32 x = 4;' instead of 'sys.int32 x = 4;'.
The strict semantic search imposed by the language makes namespace conflicts virtually non-existent. More importantly, the namespace the application uses is under the control of the application programmer by virtue of being able to pull-up often-used elements, and not under the control of the low-level library.
You can recursively import the same module at several points in your code. If the module is self contained then the module never needs to semantically search above its own base Rune can use a single shared instance of the module no matter how many times it is imported. This is especially useful when importing libraries which have dependancies on other libraries because it allows library A to import library B directly, even if you also import library B in your project for other purposes. You can assert that a module is self-contained by using the import selfcontained; statement. This also has the effect of preventing Rune from searching above the module.
scope_qual* import "relative-path" [ as [ id | self ] ]";"
The import statement glues pieces of a project together and is the primary means by which a project is built. Elements within the import are accessed via the import's semantic identifier. For example, if you do 'import "sys";' then you can access elements within the system library using the sys identifier, as in 'sys.int32 x = 23;'.
Only relative paths may be used. The search path will always begin in the directory containing the current source file. Use of absolute paths are explicitly disallowed.
The statement may be used to import a directory, library, source file, or other intermediate Rune file for use in your project. By default the semantic identifier used to access the import is the last element of the path minus any file extension. To allow some distinction for readability within the source tree, the directory itself may be prefixed with 'lib'. So for example an import of "fubar" would match a directory called "libfubar" as well as a directory called "fubar".
The default identifier can be overridden with the as clause. Modules are considered to be a special case of a variable declaration so module names (the last component of the path) and identifiers must start with a lower-case character.
Module paths should never specify a trailing slash when referencing a directory. In addition, a module path in an import statement which references a directory may specify a suffix to provide Rune a file caching hint for generated intermediate files. In most cases you do not supply a suffix for directory references and simply allow Rune auto-optimize the caching itself. A directory path must never have a trailing slash and the actual directory in the filesystem must not have a terminal suffix that matches anything Rune understands. A directory called "sys.l" would be illegal.
Terminal source files, libraries, objects, or other intermediate files must be fully specified and may not omit their suffix. For example, "cat.d".
As we have mentioned previously, semantic searches are always bottom-up. This is usually what you want but it is sometimes extremely convenient to have Rune automatically push into an import to locate a semantic identifier. Rune provides a mechanism to do this. By doing an 'import ... as self;' you are telling Rune to make all the semantic elements at the top level of the imported entity directly available. In this case if you 'import "sys" as self;' you can then declare its top-level types directly, as in 'int32 x = 23;', instead of 'sys.int32 x = 23;'. The sys library is itself hierarchical but it uses typedef and import... as self to make certain deeper types directly available to you. This is why int32 can be resolved to a type. A semantic search normally recurses bottom-up. If you use the import .. as self mechanism in a module the semantic search will recurse downwarn into the module looking for your identifier.
You can use the import ... as self mechanism multiple times in a source file. However, Rune does not overload its namespaces. If Rune matches the first identifier in the class path (id.id...) you specify, the semantic search will not back-up again, even if it cannot resolve the remaining id's in that subtree and even if another subtree would be able to resolve the id path later on in the semantic search. This is done on purpose, not only because overloading becomes impossibly confusing when you allow that sort of thing, but also because it makes tracking dependancies nearly impossible and, really, Rune is designed to allow you to avoid having to overload identifiers so you really should.
Over-use of the as-self mechanism can cause extreme pollution of the namespace and it is recommended that you use typedef and alias to 'pull-up' elements to shorten identifier paths rather then the as-self import mechanism which pulls up all the identifiers.
When importing a directory, Rune actually imports "directory/main.d". main.d is typically a small Rune source module which contains a bunch of import ... as self; statements to aggregate the library's files together into a single cohesive whole.
Namespace collisions are fatal errors in Rune. You cannot overload semantic identifiers using import-as-self. As-self imports are not typically used at the project level when importing third party libraries, but it is often used to import the Rune language system library (called "sys") in order to avoid having to specify core types using long drawn own semantic paths, and almost always used within libraries to glue the library together.
Multiple imports of any self-contained entity shares one instance of that entity. This feature is universally used by a self-contained library to import other dependent libraries instead of depending on the parent to import the required dependent libraries. Imports may be inter-dependent and generate cycles. An infinite recursion will not result.
import "sys" as self; import "cat.d"; /* code in cat.d can just use 'int' instead of 'sys.int' */ int X; /* and so can we */
Finally, note that most system support libraries are self-contained. That is, they import whatever libraries they depend on, including "sys", and use the as-self feature to guarentee shareability. In Rune the namespace is searched semantically, NOT hierarchically by class. Subclasses that you create must be accessed via the module that created the subclass, not the module that created the original class. scope_qual* import selfcontained ";"
Asserts that the current module is self contained. This prevents any semantic search within this module from recursing above it, and marks the module as being shareable which means that many entities can import it and you still only have a single shared instance of the module. This feature is typically used in the "main.d" for a Rune library.
In particular, note that shortcuts constructed by the parent which imported the current source module will thus not be accessible. This is important due to the shareability... there can be many 'parents' importing the current module.
Rune can import shared (.so) and relinkable/relocateable (.r) libraries using the standard C dynamic link loader API. The symbols in these libraries will be bound to __clang scoped declarations in Rune. Note that such declarations will use a truncated version of Rune's semantic search mechanism to locate their proper bindings, only checking the current semantic layer and parent layers for imported DLL libraries.
It is important to note that Rune is unable to ref-count or bounds-check C language pointers, and that there will be differences in alignment between Rune arguments and structures and C arguments and structures. For this reason any raw extensions or interfaces you make to other languages should use a wrapper for each function which properly translates Rune types and structures to the language, and which handle all required reference counting and disposal.
scope_qual* class id "{" declaration_stmt* "}"
scope_qual* subclass id [ "."id ]* as id "{" declaration_stmt* "}"
Declare a new class or subclass. The first form creates a wholely new class. The second form subclasses the specified class. The identifier must begin with an upper-case character.
Rune uses a forest-of-classes topology, which means there is no single root. You can create new classes as easily as you can create new subclasses.
Keep in mind that you do not access your class name via the class hierarchy. That is, if you create a subclass of Integer called FixedInteger you cannot access your subclass using Integer.FixedInteger. In Rune identifiers are always located semantically. If you wish you can simulate a subclass hierarchy using typedef, but it shouldn't be necessary.
Here is an example of a class definition that includes a procedure, and a subclass definition that refines the procedure. Note that only method procedures include an object context and so only method procedures can access the this variable. While this document is not supposed to be a tutorial, I will also note that you can also have global method procedures where the this variable only has access to global elements of the class, and that you must explicitly use the refine qualifier for elements in the subclass which you wish to replace elements in the superclass for objects declared using the subclass, otherwise any superclass functions that propogate down to the subclass without being overridden will not 'see' the overrides that were made in the subclass. Finally, note that variable declarations made within procedures must have at least one storage or scope identifier in order to differentiate between declarations and expressions or statements.
#!/usr/local/rune/bin/rune # # Program to demonstrate classing and subclassing import "sys" as self; import "stdio"; class NoisyInt { int x = 0; method void print1() { stdio.stdout->show("Procedure #1 Value of X is:", this.x); } method void print2() { stdio.stdout->show("Procedure #2 Value of X is:", this.x); } } subclass NoisyInt as ReallyNoisyInt { refine int x = 1; refine method void print2() { stdio.stdout->show("Refined procedure #2 Value of X is:", this.x); } } void main() { NoisyInt a; ReallyNoisyInt b; a.print1(); # Procedure #1 Value of X is: 0 a.print2(); # Procedure #2 Value of X is: 0 b.print1(); # Procedure #1 Value of X is: 1 b.print2(); # Refined procedure #2 Value of X is: 1 }
It is important that you understand the difference between an overloaded variable and a refined variable in a subclass. The difference has to do with the visibility of the old and new versions of the variable from the point of view of the superclass.
#!usr/local/rune/bin/rune # # Program to demonstrate overloading vs refinement in a subclass # # import "sys" as self; import "stdio"; class NoisyInt { int x = 0; method void print1() { stdio.stdout->show("procedure #1 Value of X is:", this.x); } method void print2() { stdio.stdout->show("procedure #2 Value of X is:", this.x); } } subclass NoisyInt as ReallyNoisyInt { int x = 1; # NOTE, not refined refine method void print1() { stdio.stdout->show("Refined procedure #2 Value of X is:", this.x); } } void main() { NoisyInt a; ReallyNoisyInt b; stdio.stdout->show("Should print 0, 0, 0, 1, 1, 0"); a.print1(); # should print 0 a.print2(); # should print 0 b.print1(); # should print 1 b.print2(); # should print 0 because this is the super class's # function and we did not refine x. }
scope_qual* interface id [ "."id ]* [ id [ "."id ]* ] [ as id ] "{" declaration_stmt* "}"
An interface combines a subclass definition and instantiation (declaration) into a single entity. Elements within the subclass have contextual access to the object the interface is embedded within and the object may be passed to procedures or used in situations where the interface subclass is expected instead of the object. Use of typeof() on the object will match the object's type as well as the type of any interface within the object.
An interface is a form of multiple inheritance. For convenience, interfaces do not have to name the subclass or the object. Elements within the interface subclass definition may access the larger object context they were embedded in through the that identifier. Additionally, as another convenience, that may be used in the interface subclass's method procedures to access the larger object rather then the more correct this.that.
import "sys" as self; import "stdio"; class MyCustomClass { char c = 'x'; # Pretty simple, eh? Give MyCustomClass the ability to be shown through # the stdio show interface. # interface stdio.Showable { refine void showfunc(stdio.File @fp) { fp->fwrite(&that.c, 1); } } } void main() { MyCustomClass x; stdio.stdout->show("The char in x is: ", &x); }
Since Rune uses interfaces in core types to handle things like the stdio show() function, care must be taken to ensure that an interface does not make the physical object it is embedded within larger. In Rune, an interface subclass which itself requires no physical storage (i.e. contains only globals, typedefs, aliases, and procedures) does not need any additional storage to track the larger context it is called through, which is why all interfaces associated with core types have no physical components.
scope_qual* typedef declaration ";"
This works in a manner very similar to C except that in Rune you can do all sorts of other cool things like specify default values for the type (such as typedef int myint = 4;). typedef's main use is to shortcut semantic searches or to provide defaults.
Typedef may not be used to typedef a procedure, but it may be used to typedef a procedure pointer. Note: procedure pointers are not yet implemented.
Note that import itself can be used in a limited fashion to force a shortcut semantically using the self keyword. See 'import ... self' for further information. This feature is typically only used on the "sys" class and for collecting related source files in a library or project.
scope_qual* alias declaration ";"
An alias is very similar to a typedef but instead of defining a type an alias defines a value. Specifically, the declarative assignment operates like a macro. So, for example, in some class definition X you might have alias int c = a + b;, which a and b represents fields in the class and c represents the alias. Aliases take no space in the object and are evaluated every time they are referenced.
Aliases can be used to synthesize a shortcut out of an expression and label it. The declarative assignment in an alias declaration is not optional.
Aliases return rvalue quantities, so an aliases may also be used to create a read-only version of a field. Instead of exporting the actual (read-write) field publically, you would export a public alias to a private field.
scope_qual* declaration_stmt
Finally, you can have normal declarations at the top level of a program. These declarations have permanent storage, like globals in C. By default a declaration at the top level has library scope. You can override this by specifying a specific scope such as private or public.
Top level declarations do not have to be declared global, they are automatically global. Note that you can also declare global variables within a class definition, and in that case you must specify global so common storage is created rather then per-object storage.
declaration_stmt:
[scope_qual|stor_qual] declaration [ "," declaration ]* ";"
declaration "{" stmt* "}"
These are declarative statement elements that can appear inside procedures, class definitions, or at the top level.
A declaration_stmt is a sequence of one or more comma-delimited declarations ending with a semicolon, or a procedure declaration ending with the procedure body. Note that the first form may also represent a procedure reference (a procedure declaration without a procedure body), which is often used in an typedef to partially resolve or refine procedure arguments.
By disallowing certain expressions at the top level Rune can
distinguish between a declaration and an expression at parse time.
In particular, Rune assumes that a sequence like 'a * b;' is a
declaration, aka like 'int *b;', not an expression. Rune is capable
of distinguishing the following sequences as declarations:
prefix_sequence id
Where prefix_sequence is:
id [ "." id ]*
prefix_sequence "*"
prefix_sequence "@"
"(" anything ")"
If you aren't sure whether Rune can distinguish your declaration from an expression you can simply use the "auto" qualifier to prefix your declaration. The distinguishment code is only used in executable sections. The elements at the top level of a file or within a class or subclass definition are always declarations.
scope_qual* exception id "=" string ";"
An exception is basically a string constant. The declaration names the exception and must assign a constant string as the value. In addition, the string will have a unique address (even if multiple exceptions specify the same string).
Exception declarations are used to construct rendezvous addresses for the raise and catch statements, and may also be referenced as constant pointers to constant strings.
declaration_list:
[ declaration [ "," declaration ]* ]
A declaration_list is a comma-delimited list of declarations that usually appear as part of a compound type or a procedure's argument list.
The list can be empty. Note that a declaration_stmt is essentially a declaration_list with at least one element, and terminated by a semicolon.
declaration:
[ procedureClass ] type [ decl ]* [ "=" exp ]
A declaration consists of a type, a potentially complex set of declarators that may include an identifier, and an optional assigned expression. Note that assigned expressions are not allowed for procedure definitions (this is where the procedure body would normally go). A procedure definition's body is actually parsed as part of a declaration_stmt, not a declaration. A procedureClass sequence may only be used when declaring a procedure.
Assigned expressions represent different things depending on the context. In a procedure argument declaration an assigned expression represents a default value for the argument, making the argument optional. In a storage declaration the assigned expression represents the initial state of the storage. In a class or compound type definition the assigned expression represents the default initialization for an element in the class when the class is instantiated (and can be overriden by subclasses or when the class is instantiated).
Default assignment may reference other elements in the object directly. For example, you can have a class with fields A, B, and C and do something like: int C = A + B; for C. Defaults are assigned in the same order in which they were declared. If no default is specified, the type's default is used (a type can have a default as well, typically created via typedef). If no type default exists, the element will be zero'd. Pointer declarations will be set to NULL (in Rune pointers are ref-counted so we have to initialize a reference, even though the pointer value is NULL). Declarations are never left uninitialized. If no other defaults exist, the contents of an object will be zerod. This is holds true even for declarations made on the stack. Rune will attempt to optimize-away pre-zeroing when code analysis proves that a write occurs before a read.
Declarators modify the base type. See decl.
type:
stor_qual* "(" declaration_list ")"
A type may be compound, made up of a tuple of other types. The compound type is treated in a manner similar to a class.
stor_qual* id [ "." id ]*
A type may be specified by a sequence of scoped identifiers. Note that the sequence does not represent a hierarchical class path but instead represents a semantic search sequence. The first identifier is typically located in our current scope and we then recurse down semantically (for example, through import identifiers, class definition identifiers, and typedefs) until we reach the final element. This final element represents our type.
It is quite common for Rune programmers to use typedef to pull subclasses into their parent classes, making this sequence appear to be class-hierarchical, but it is only an illusion.
decl:
A decl specifies the semantic name and type qualifiers associated with a type. Declarations build the type up from left to right until you hit an identifier or procedure arguments, after which the type is built up from right to left. So, for example, something like const int * const fubar[2][10] is an array 2 of an array 10 of a constant pointer to a constant int, and int *fubar *(int a, int b) would be a pointer to a procedure taking two arguments and returning a pointer to an integer. Note that Rune does not use the C-style int (*ptr)(int, int) form to specify a procedure pointer. This release of the Rune language does not implement procedure pointers.
id [ "." id ]*
Specify the identifier for a declaration. Usually only a single identifier is specified. Multiple dot-separated identifiers are allowed for top-level declarations in order to move the declaration's semantic context into a class. This allows you to define a class and then place various complex declarations (like large procedures) outside of that class but within the same file. For example:
class fubar { } int fubar.func() { ... }
Such identifier sequences are resolved by the parser and may only be reverse-referenced. We do not search beyond PRIVATE scope (that is, the parser only looks within the current file to resolve the id prefix). This allows Rune to isolate the results of the main language parsing pass in order to be able to quickly generate and use pre-parsed intermediate file images.
At some future date I may extend this capability to LIBRARY scope.
"(" declaration_list ")"
A procedure taking the specified arguments and returning the type built up so far. For example, int fubar ( int a, int b );.
"[" exp "]"
Array N of some type. Note that when you reference an array in Rune you are referencing the entire object, not a pointer to the first element. In C when you reference an array you are generally, but not always, referencing a pointer to the first element.
"*"
A pointer to a type. A pointer points to an object of the instantiated type and cannot be bound to objects of any other instantiated type, not even to a subclass of the particular type.
"@"
A reference to a type. References are similar to pointers but can be dynamically rebound to subclasses of the type as well as to the type itself. The type, in this case, is usually refered to as the superclass. All operations performed through reference variables enforce the superclass's API. That is, you cannot directly access non-refined extensions made in the actual bound object but must instead access them indirectly through refined method calls whos APIs are specified in the superclass.
Rune distinguishes between subclass binding inferred by the class hierarchy and subclass bindings created through dynamic bindings to reference types. Rune will provide a guarantee to the programmer to allow the programmer to control bloating of the executable due to versioning.
Rune will guarantee the use of procedure versioning for procedures generated from class and subclass statements, and will guarantee the use of a far more compact indirect-call-through-type for code within a procedure which operates on a dynamically-bound pointer. This means that code acting on a dynamically bound type may actually be tacked onto the type meta-data as an indirect call instead of being inlined. This indirect call does not make the type itself any larger... it's added to the type meta-data (the description of the type), not to the data object.
stmt:
Executable statements appearing inside procedures
"{" stmt* "}"
A statement block or sub-block. Upon entering a sub-block the local declaration space will be re-initialized. Upon exiting a sub-block the local declaration space will be dereferenced (any referece-counted pointers will be cleared and dereferenced, for example).
exp ";"
An expression as a statement. The expression must return void.
declaration_stmt
A declaration as a statement may occur inside procedures at any point. Note the storage is allocated at the nearest semantic block but any assignment occurs at the point the statement is executed.
for "(" stmt exp ";" exp ")" stmt
while "(" exp ")" stmt
until "(" exp ")" stmt
do stmt while (" exp ")" ";"
do stmt until (" exp ")" ";"
Various loop statements. Do not be confused by the apparent missing semicolon in the for. Remember that stmt typically terminates with a semicolon. The format of these looping statements is approximately the same as in C.
Loop statements are contained within their own semantic block, which means that you can do things like for(int i = 0; i < 100; ++i) ...
break [ loop | switch | if | case | block | empty ] ";"
continue [ loop | switch | if | case | block | empty ] ";"
These statements behave as you would expect in C but have extensions that allow you to break or continue as if you were at a particular semantic level. For example, you can continue switch to re-evaluate and restart a switch statement.
Extended break and continue statements should be used only in situations where the logic is obvious. Rune does not implement multi-level breaks and continues on purpose. Anything more complex must use the exceptions (see raise).
Rune does not implement goto.
switch "(" exp ")" "{" case_stmt* "}"
switch "(" type ")" "{" case_stmt* "}"
This statement evaluates the expression and matches it against a case/default list, then jumps to the appropriate case or default.
You can also switch on a type, typically through the use of the typeof() construct. In this situation, case statements must also resolve to types, also typically through the same construct. This feature is most often used when processing a var-args procedure.
if "(" exp ")" stmt [ else stmt ]
The expression must return either a boolean or a type that can be cast to a boolean. Rune treats booleans as a separate numeric class from integers. While all numeric types have casts available to convert to a boolean, the functionality is strictly limited to common-sense cases. Pointers do not auto-cast to boolean and must be explicitly compared to NULL.
return [ "(" [ id ":" ] exp [ "," [ id ":" ] exp ]* ")" ] ";"
Return from procedure. If the procedure returns void you can use return; or return();. If the procedure is running as its own thread the thread will be terminated.
You may supply a compound expression to generate a multi-valued return. Compound expressions may include element identifiers.
result [ "(" [ id ":" ] exp [ "," [ id ":" ] exp ]* ")" ] ";"
Set the return value for the procedure and continue running.
result has special consequences if the procedure is threaded. When a call to a threaded procedure is made the caller stalls until the threaded procedure returns a result. The threaded procedure can return a result with result and continue running (so now both the caller and the threaded procedure are running). However, since the procedure arguments and return value are part of the caller's context, the threaded procedure cannot access them after calling result. You must copy any arguments you still wish to access prior to calling result.
You may supply a compound expression to generate a multi-valued return. Compound expressions may include element identifiers.
thread_schedule [ preempt | nopreempt | immediate ] ";"
Set the thread scheduling mode, force a thread switch, or allow a natural thread switch. If no arguments are specified Rune will do a natural thread switch, which means it will only switch threads if more then a certain number of cycles have elapsed running the current thread. If immediate is specified, Rune will do an immediate thread switch (which can be quite expensive). If preempt is specified, preemption is turned on. Preemption is essentially natural scheduling occuring at any time without additional prompting. nopreempt turns off preemption.
Turning on preemption is not recommended unless you are doing a large, completely self-contained calculation.
The preemption mode is adjusted on a procedure-by-procedure basis. Unless you specify preempt scope for a procedure, preemption is automatically turned off on entry to any procedure. The prior mode will be restored on return so, for example, a thread_schedule preempt; statement in a procedure will not change the scheduling mode of the caller of the procedure. What this means, in short, is that you have to worry about preemption related races only in those procedures for which preemption is explicitly enabled, and not in any procedure calls made from those procedures.
raise [ exp [ "," exp ] ] ";"
raise id ";"
Raise the specified exception along with an optional sub-code. Both the exception and the sub-code must generally refer to a exception declaration. These are special declarations which resolve to unique const string pointers. catch sequences compare the unique addresses and not the contents of the strings.
It is also acceptable to specify an ad-hoc identifier which matches against an accessible catch in the same procedure.
In its degenerate form, this statement chains an existing active exception and is typically used to allow catch bodies to chain semantically. Examples include, for example, sys.IOERROR, or perhaps something as simple as MYAPPIOERROR, or in an ad-hoc case perhaps something like raise failed;.
The exception raise propagates forward semantically looking for a matching catch, essentially falling through code blocks until it finds what it wants. When a procedural boundary is reached the procedural context is popped and the search continues in the caller. A normal procedural return does not occur! Exceptions which cross a thread boundary cause the thread to exit and the exception to be held for the reaper to interrogate. It will not cause an exception IN the reaper.
raise/catch sequences are typically used to propagate serious error conditions, such as a memory allocation failure or I/O error, but not nominal failures such as a short-read or EOF on a file.
These sequences can also be used as a poor-man's forward-goto when break; can't reach, such as if you desire to break out of multiple loop levels. The language grudgingly allows this behavior and at least forces the programmer to use this more explicit mechanism rather than an ephermal multi-level break or continue whos complexity could get lost in a large section of code. We respectfully suggest you only use it when you desire to unwind the procedure and return due to an error. Also note that exception handling is not considered a critical performance code path.
catch [ exp [ "," exp ]* ] "{"
Catch matching exception(s) which occurs prior to the catch
statement. If no identifiers are specified, all exceptions will
be caught. The body is only executed if a matching exception jumps
into it. Note that the expressions must resolve to exception
declarations. Simply specifying the same string will not work.
Exception handling compares string pointers directly, not their
contents.
If your code falls through the block the exception state is lost
and normal execution is resumed at that point. This mechanic is
most typically used for forward-failure processing using an ad-hoc
raise/catch sequence. Any normal statement which exits
the catch body, such as a break or continue, also
resumes normal execution. Your code can also elect to chain the
currently active exception by issuing a raise; inside the
catch body, or even by issuing a completely new raise.
If your exception code executes something which causes another
exception you can catch the secondary exception within your exception
code by emplacing additional catch statements within your
catch block. If the embedded catch falls through to the original
catch, the new exception is lost and the original exception is
reactivated. Note that an uncaught secondary exception will break
out of your catch and continue chaining up searching for a matching
catch. An exception occuring inside a catch body will not match
against or restart it.
Multiple matches against the exception code may be specified.
You must process any sub-code from within your catch, there is no
language mechanism to match the sub-code. You may access the
exception code and the sub-code from within the body using
.code and .subcode. Both are unique string pointers.
catch [ exp [ "," exp ]* ] ";"
case_stmt:
case exp ":" stmt*
case type ":" stmt*
A case inside of a switch. Case statement usually finishes up with a break statement. Without one it will fall through to the next case or default, or fall off the end of the switch.
Case statements are contained within their own semantic block, which means that you can do things like case blah: int i .... Declarations are valid only within the case and will be released when you leave the case.
default ":" stmt*
A default inside of a switch. Similar to case but this label takes the default if none of the cases match. The fall-through works the same way.
Default statements are contained within their own semantic block, which means that you can do things like case blah: int i .... Declarations are valid only within the case.
exp:
A variable identifier.
An integer, character, or floating point constant.
A quoted string. Quoted strings are classed as an array of const char but will auto-cast to a pointer to a const char if necessary.
The self identifier, when not used as a keyword, is the compound object representing the current procedure's arguments. This identifier is used almost exclusively to access extended (varargs) arguments, since other procedure arguments can be accessed directly by name.
The this identifier only exists in method procedures. It is the (typically invisible) first argument of the method procedure representing the object the procedure was called through. For global method procedures there is no object context and this represents the type the procedure was called through rather then the object it was called through, giving you access to globals within the type's class. this is a lvalue
The super identifier represents the method's object from the point of view of the superclass. You can access the previous version of a refined element this way, but your access to such elements is restricted to procedures only. There is no way to access the 'original' storage that you have refined because it does not exist in the object, only your refined storage exists. The same could be said for procedure declarations if not for the special case that Rune implements to give you access to the original procedure.
The difference between overloading an element (not using refine) in a subclass, and refining an element in a subclass that exists in the superclass, all comes down to how the object is viewed from the point of view of the superclass. This is the point of view that functions declared in the superclass (and not refined in the subclass) will have, as well as the point of view that an object accessed through reference type declaring the superclass will have.
If you overload a method or declaration rather then refine it then it does not exist from the point of view of the superclass... the original declaration will be accessed. If you refine the method instead, however, the superclass will see your refinements in the context of the superclass. Please refer to the subclass statement above for a more detailed explanation.
Converts an expression into a type or manipulates an existing type. This operator may not be used to instantiate the type, but it may be used to access global elements within the type via "." and the result of typeof() may be used in switch and case statements (when switching on varargs arguments, for example).
Note that in a switch/case statement Rune uses relaxed rules when doing type comparisons and will match even if there are differences in the lvalue, const, and volatile qualifiers to the types.
This construct may also be used to help cast intermediate elements of an expression, for example (typeof(a))(b + c).
Returns the physical storage required to hold the expression's type or the specified type.
Follows rules similar to typeof().
The expression or type must resolve into an array. Not a pointer, an array. The number of declared elements in the array will be returned.
Follows rules similar to typeof().
A parenthesized expression such as (1, 2) is a compound expression. Compound expressions may have several elements. Note that in Rune, there is no 'comma' operator. The operator instead denotes a compound expression.
A compound expression has an associated compound type (which is specified in a similar manner through comma delineation). Compound expressions are not vectorized in Rune. That is, you cannot use an operator defined for a normal non-compound type like int on a compound type made up of ints. You can, however, define and use operators designed to operate on specific compound types.
A normal parenthesized expression such as (23) is simply a degenerate case of a compound expression.
Cast the expression to the specified type.
Rune also allows casts of the typeof construct here. Although the use case can be somewhat confusing to read, there is merit to allowing the case.
Unlike C, Rune will not allow you to arbitrarily cast between types, nor will Rune automatically normalize integer arguments for operators. For convenience certain functions such as shift operators and pointer arithmatic can operate on mixed types, but most pure arithmatic operators require the same numeric type.
Type casts must be used when extracting an element from varargs, and must match the type of the element. This is typically combined with a switch / case sequence on the element type.
This construct is typically used to access global elements related to a particular type, but is generally only used to resolve pointer type fields (verses elements of the underlying class). For example (const char *).NULL gives you a NULL char pointer.
This method is not generally used to access globals in a base class because a more convenient direct construct exists for that. For example Ship.some_global_element.
Other methods of accessing NULL: typedef a pointer and access NULL via the typedef identifier, like typedef const char *string_t; ... string_t.NULL, or go through an object, like procedure (... const char *str) { ... if (str == str.NULL) ... }.
There is also a global NULL Which corresponds to the C equivalent of (void *)NULL, which you can use in any situation where Rune knows what pointer type to cast it to, including simple pointer comparisons against NULL.
Heap storage is allocated for the lvalue represented by the first exp. This is typically an initially NULL pointer variable or NULL reference variable. This can also be used to reallocate an object. Any prior object will be dereferenced and replaced with a new object.
The new method is defined in the Pointer class with an internal binding, hence why you use ".". You are not indirecting through the (likely NULL) pointer, but instead executing a class function ON the pointer object itself as an lvalue.
Heap objects are automatically freed when the last pointer reference to them goes away, typically by setting the pointer(s) to some other object or to NULL.
You can specify the number of objects to allocate (aka an array) as an argument to new. If not specified, one object is allocated.
Heap allocation works with both pointers and reference types. However, since array indexing and pointer arithmatic does not work with reference types, allocating more then one reference type is not generally useful.
This construct does not give you an array-of-objects type, it simply gives you an array of objects with the variable pointing at the first entry. You can manipulate the pointer with pointer arithmatic but a fatal error will occur if you attempt to indirect through an out-of-bounds pointer.
The actual class or subclass object allocated when you use new on a reference type or a pointer depends on what was last assigned to the reference type. If a generic NULL was assigned, the base class of the reference type will be allocated. This is not always useful. This function is almost exclusively used with pointers and not with reference types.
Pointer or reference indirection. exp's type must be a pointer to storage or a reference to storage. The operator will indirect through the pointer and return the object it was pointing to.
The returned object will be an lvalue.
Address of. The exp must be an lvalue. The address of the object is taken, returning a pointer object. The result is always an rvalue.
The "&" operator may also be combined with a procedure call expression to partially resolve procedural arguments, producing a new procedure. The procedure being partially resolved is not called in this case. For example, if you have a procedure int fubar(int a, int b, int c); then &fubar(a:23) results in a pointer to a procedure taking two arguments, b and c. Note that procedural function pointers have not yet been implemented, so this feature does not yet work.
Assignment. The right hand side is copied to the left hand side. The LHS must be an lvalue. A chain of assignments will execute right-to-left.
The internal implementation may wind up being somewhat more complex. If you are assigning a pointer previously associated with a heap object, the original heap object is dereferenced. If the new object is a pointer to a heap object then the new object's ref count will be bumped. If you are assigning whole objects, then the same thing happens to pointer elements emebedded in those objects (or embedded in elements embedded in those objects, etc...).
Structural indirection. The left hand side must resolve to a pointer to a class. The right hand side identifies an element in the class. The element does not necessarily have to represent storage. For example, it can represent a typedef'd type or a procedure.
If the element represents storage, then the result of this operator is the storage in question, an lvalue.
If the element represents a typedef, then the result of this operator is a type (for example that you might use in a declaration).
Structural selection. The left hand side must resolve to a grouped object, such as a compound type, a class, or self (representing the procedure's arguments and used almost solely to deal with var-args).
The right hand side identifies an element in the class. The element does not necessarily have to represent storage. For example, it can represent a typedef's type or a procedure.
If the element represents storage, then the result of this operator is the storage in question, an lvalue. Only globally scoped storage is available when selecting via a type. Local storage is available only when selecting via an object
If the element represents a typedef, then the result of this operator is a type (for example that you might use in a declaration).
A number of special identifiers may be used on the right hand side. These identifiers mostly apply to grouped objects like compound types, arguments (i.e. self), and classes. However, the NULL identifier applies to pointers.
NULL - When applied to a pointer this generates a NULL pointer of the specified type. Note that this is different from the global NULL pointer (which is a NULL pointer to a void type). A type-specific NULL pointer is a very different beast since you can access global data and non-method procedures specific to a type through it.
__count - An integer representing the number of elements in the object.
__data[exp] - The specified element in the object. You can do two things with this. First, you can access the element within a typeof() and switch on its type. Second, you can access the data itself but you must explicitly cast it to a compatible type. This is typically done inside a switch/case.
__varcount - same as __count but only counts extended arguments in varargs procedures.
__vardata[exp] - Same as __data but only applies to extended arguments in varargs procedures.
__typeid - (future) A unique integer identifying a type. Feature not yet implemented.
__typestr - (future) A string representing the resolved type. Feature not yet implemented.
A unary operator acting on an expression. Unary operators have a fixed precedence. Note that Rune supports only prefix operators such as ++x. Rune does not support postfix operators such as x++.
If the operator requires an lvalue, then exp must be an lvalue. If the operator returns an lvalue, then the result may be used in a larger expression that requires an lvalue.
A binary operator acting on an expression. Binary operators have specific precedences which are non-negotiable. This allows Rune to parse programs without needing knowledge outside the source file being operated on and also allows programmers to understand how expressions are evaluated across the board. Rune is complex enough as it is, we do not need dynamic operator precedence to muddy the waters further.
If the operator requires an lvalue, then exp must be an lvalue. If the operator returns an lvalue, then the result may be used in a larger expression that requires an lvalue.
Index into an array, returning an element of the array. The left hand side must be an array or a pointer. The right hand side will be cast to a 32 bit signed integer.
This operator returns an lvalue.
scope_qual:
Scope qualifiers are bound to the declaration itself rather then to the type(s) making up the declaration. A type does not have scope, but a declaration does. However, certain scope qualifiers may modify how an underlying type is created. Also note that when we specify that something is accessible, it must still fall within the semantic search of the entity attempting to access it. So, for example, a variable declaration in procedure A is not directly accessible by procedure B no matter how you scope it.
private
The declaration may only be accessed from the current source module and by modules imported by the current source module, so long as the imported modules are not self contained.
When used within a class definition, the declaration may only be accessed by elements within that class definition. Elements include procedures associated with the class and default assignments for other elements. Subclasses of a class will not have any access to the declaration and will not be able to refine it.
The declaration may be accessed from the current source module, from imports made by the current source module, and by sibling imports, so long as those imports are not self contained. For example, if a library's main.d imports two source modules able.d and charlie.d, then charlie.d will be able to access a library scoped variable declared in able.d.
When used within a class definition, the declaration may be accessed by elements within that class definition, by the current source module, from imports made by the current source module, and by sibling imports.
It is important to note that the barrier to library scope are modules marked as being self contained. Libraries that you are import are typically self contained so you would not have access to library-scoped elements declared in those imports nor would those imports have access to library-scoped elements that you declare.
Anyone can access this declaration if it is within the reach of a semantic search.
When used with a procedure argument lvalue requires that the caller supply an lvalue for that argument and forces the object to be passed by reference. When applied to a procedure's return type it requires that the procedure return an lvalue and this lvalue will also be passed by reference back to the caller.
When used in other circumstances lvalue is meaningless because a declaration is, by default, an lvalue.
In Rune, an lvalue is specified as a scope qualifier but is actually both a scope and storage qualifier. An lvalue type can be cast to a non-lvalue version of the same type, but not vise-versa. It should be noted that you operate on an lvalue as if it were a directly declared object. That is, you do not need to indirect through it. Here is an example:
#!usr/local/rune/bin/rune # # Program to demonstrate lvalue scope (a special kind of pass by # reference that is used in a pass-by-value manner). # import "sys" as self; import "stdio"; int main() { int x = 10; int y = 20; int z = 30; ++increment(increment(x,y),z); stdio.stdout->show("Should be 11, 21, and 31 result is:", x, y, z); } lvalue int increment(lvalue int x, lvalue int y) { ++x; return(y); }
As you can see, lvalue scope is an extremely powerful mechanism in Rune. It is used heavily in operator declarations and can be fully exploited by the programmer.
__nozero
Storage is not cleaned up (zerod). This may only be specified on objects which are not or do not contain pointers. This allows uninitalized elements to hold garbage instead of being zerod. This qualifier is really only applicable for stack-based or heap-based storage and has no effect on global or persist storage.
It recommended that this feature only be used when declaring large buffers on the stack.
Specify that the declaration represents global storage. You can embed global storage anywhere. It can be specified in a procedure, in a class, in a compound type, and as a procedure argument. Declarations made at the top level of a source file are automatically global by virtue of the import itself representing global storage.
Note that this qualifier does not effect visibility or scope. Do not confuse global with C's static/extern mechanism.
global may also be applied to non-storage entities such as procedure declarations, and implies that the procedure can be called through its class directly rather then through an instantiated object.
While a global procedure can also be a method procedure, it can also access global declarations within its class directly without using this. However, note that subclass refinement works differently between direct variable access and method access. Direct variable access will get the global in the class the procedure is defined in, whether or not the procedure is being called through a subclass. Method access using this, on the otherhand, will access properly refined globals found in the subclass even if the procedure is defined in the superclass, just as normal refined elements are accessed. This can be demonstrated with a program:
#!usr/local/rune/bin/rune # # Program to demonstrate the handling of a global procedure when refining # a global variable in a subclass. Each subclass gets its own set of # globals. import "sys" as self; import "stdio"; class fubar { public global int x = 1; public global method int test() { stdio.stdout->show(x, this.x); } } subclass fubar as fubar2 { refine global int x = 10; } int main() { stdio.stdout->show("results should be (1 1) and (1 10)"); fubar.test(); fubar2.test(); }
It is very typical to implement global and
global const elements in classes. These elements
may be referenced via the class type or pointer, even a NULL
pointer, because no object is needed to access the elements.
These elements are roughly similar to how constants are defined in
C using #define, but they operate within the namespace of the
class so collisions aren't possible. This frees the programmer
to use more simplified naming conventions.
persist
Persistent storage which survives program exit and will be resurrected when the program is restarted. Pointers do not survive and will be NULLed-out on program [re]start. Initializers are only executed to create the initial instance of the object and will not re-execute on restart.
The storage is memory-mapped with mmap() using a file path based on the import path and variable name. Flags are as follows:
Changing the topology or the size of the structure may break any persistent store if you are not careful. Each named object is separately mapped so we do not recommend this flag be used on many small individual variables. Objects may be mapped to a different location on each restart. Also note that all void constructors will be executed on program [re]start.
The storage is not embedded but is instead dynamically allocated.
Threaded procedure (applies to procedures only). When a threaded procedure is called a new thread is created to run the procedure in and the calling thread is suspended. The calling thread will be resumed with a result from the threaded procedure when the threaded procedure executes the result or return statements. Obviously there isn't much point to threading the procedure if all it does is run to completion and call return. The usual scenario is for the threaded procedure to call result and to then continue running.
Note that once a threaded procedure returns a result with result, the return value and procedure arguments will no longer be available to it (because these are part of the original caller's context and we just resumed the original caller, so the context goes poof). A threaded procedure must copy any arguments it wishes to access after calling result.
Generally applied to operators, casts, and procedures. If the arguments to the operator, cast, or procedure are pure or constants, then the result will also be considered pure and a constant. That is the result will be fixed given the same arguments, allowing the operation to be optimized out entirely.
This flag also implies that the operator, cast, or procedure has no side effects and that it can be executed by the interpreter as a pre-stage to compilation in addition to being optimized by the interpreter.
Note that normal const variable declarations are automatically considered to be pure and do not have be scoped pure.
This indicates that the class may NOT be directly instantiated as an object and instead may only be used as an API specification and (for a class) to provide default implementations for subclasses. Any subclasses will be realizable unless they to are specified as prototypes. Most typically this situation involves a class containing numerous generic types such as Integer, using a typedef, where realization makes no sense, and subclasses must refine the typedef into a subclass to specify the actual type.
A threaded procedure qualified as 'async' is run as its own machine thread, truely asynchronous from the caller's context once it posts its result. This is distinguished from the default threading mode which is always synchronous. A synchronous thread borrows the callers context and only switches when it blocks.
Not all threads should be asynchronous. For example, if implementing a GUI gadget library the potentially many gadgets can be implemented as synchronous threads instead a single asynchronous container, since only one gadget at a time is typically being manipulated.
This scope qualifier may only be used as part of a threaded procedure declaration.
Tells Rune that all object locks currently held upon entry into hard mode and any locks obtained while in hard mode may NOT be temporarily released due to the thread blocking until hard mode is exited. It is possible to deadlock your program if you use this mode incorrectly. Hard mode use can stack.
It is important to note that this also means that any locks that were obtained in prior soft modes will, during the hard mode operation, not be temporarily releaseable. They will return to being temporarily releaseable when you exit hard mode.
This is important for several reasons not the least of which being that it allows Rune to use a simple index field and save/restore to govern hard/soft operation, so the overhead is effectively O(1) regardless of how many locks are currently held.
This mode can be applied to a whole procedure or to a specific statement or statement block.
Tells Rune that any new object locks obtained after entry into soft mode MAY be temporarily released due to the thread blocking. This is the default mode of operation. Soft mode use can stack.
If you were in hard mode and then enter soft mode, only new locks obtained after entering soft mode can be temporarily released. More importantly, any recursive locking where the lock is already being held from hard mode will not cause the lock to suddenly be releaseable. It will remain non-releaseable.
This mode can be applied to a whole procedure or to a specific statement or statement block. soft-held locks cannot deadlock your application which is why Rune makes it the default operation.
Rune automatically locks the storage governing any objects while they are being accessed and automatically locks any object which is the target of a pointer while the pointer is active, including pointers passed as arguments (either directly or indirectly via an expression).
Call targets can assume that all arguments and any objects pointed to by arguments are locked. Pointer arguments will receive an extra lock reference allowing the target procedure to drop the current lock and acquire a new lock on reassignment (this can be optimized out if the procedure does not reassign the argument). The caller is responsible for destroying the argument space and dereferencing/unlocking any non-NULL pointer arguments upon return.
It is important to note how Rune optimizes locking operations across blocking conditions. Locks are on governing storage, typically on a per-dynamic-allocation basis, not on each individual object (so the size of an object is similar to what it is in C... as-expected by the programmer). Even so, there could be many locks held when a thread blocks. Rune handles this by leaving the locks intact and flagging the thread to allow other threads to steal the locks while it is blocked. When it wakes up it will recover any lost locks. This is handled optimally, so a thread could potentially have hundreds of held locks without incuring significant extra overhead when it blocks. Rune tracks lost/regained locks and supplies access methods to object storage to allow programs to detect if object locks were stolen across complex sections of code, or not.
This form of object locking is automatic and cannot be disabled by the programmer. It is necessary for proper handling of objects and to prevent SMP races in a multi-threaded environment.
The default soft locks do not prevent races in application code per-say, they simply ensure that Rune doesn't corrupt itself. The programmer can make assumptions on the atomicy of multiple statements with regards to objects whos pointers are being held (verses temporarily calculated with an expression), but must recognize that dereferencing any new pointer or making a procedure call into unknown code might cause held locks to be temporarily lost.
Rune programmers can ensure that held locks are not lost across a set of statements by using a hard code section around those statements. In fact, Rune programmers are expected to use hard code sections precisely for this reason for anything non-trivial.
Rune does NOT cache temporary object locks. That is, it explicitly does not retain the lock past the temporary objects use. The programmer can optimize-out unnecessary unlock/re-lock operations simply by assigning the pointer to a local pointer variable.
Indicate that this procedure is also a constructor. Constructors must take no arguments and must also be methods. Constructors are called when an object is instantiated, after any defaults have been set. Constructors will be called in the order in which they were declared. Refined constructors will be called in the order in which they were defined in their superclass. Extension constructors in the subclass are called last.
WARNING! All void constructors are always called when a persistent object is restarted. Initializers are only called on the initial creation of a persistent object.
Indicate that this procedure is also a destructor. Destructors must take no arguments and must also be methods. Destructors are called when an object looses its last reference. For example, the stdio library will flush and close an underlying file and the graphics library will cleanup XIDs.
Destructors will be called in the order in which they were declared. Refined destructors will be called in the order in which they were defined in their superclass. Extension destructors in the subclass are called last.
WARNING! Destructors are not typically called on persistent objects.
Force alignment of the storage to the specified number of bytes, regardless of the alignment that Rune would otherwise choose for this storage. Alignment must be a power of 2. This feature is used to create C-compatable prototypes and structures and is considered NONPORTABLE when used in general Rune programming.
NOTE: Rune by definition lays out structural elements using their natural (portable) alignment. That is, a 64-bit integer will be 8-byte aligned, and so forth. Including pointers and including larger integral and floating types.
Force C compatibility (non-inclusive of alignment issues). This feature is used to create C-compatible prototypes and structures and should not be used in general Rune programming. In particular, it will force pointers to be C-compatible. Pointers in Rune normally contain bounds and referential information along with the data pointer value.
This feature is meant to be used only by core system support. No application should ever use it and we will likely default the resolver to disallow it outside of core system elements.
stor_qual:
Constant storage. The object represents a constant, meaning that the object can only be initialized at the time it is instantiated and may not be changed afterwords. Because you can override constants when you instantiate an object, constants will take up literal space in the structure unless you declare them with global scope.
Normally Rune assumes that storage is semi-stable due to locking effects, depending on whether the execution context is hard or soft. When a hard section calls a soft section Rune assumes that object locks might be lost. If it does not, the Rune code generator is allowed to cache object data even across procedure calls if it can prove the case through analysis.
This storage qualifier forces Rune to assume that the contents of the storage can be modified at any time outside of Rune's control. Rune will always synchronize changes at the time they are made and will not cache the contents regardless of whether it is in a hard or soft code section.
procedureClass:
An operatic procedure associates an operator with a procedure. For example, operator "++" int (lvalue int v) { ... }. Operatic procedures may be unary or binary. You can act on an lvalue or an rvalue (the default is an rvalue). Note that lvalues require lvalue semantics and will be operated on by reference, whereas rvalues are passed by value.
operatic functions may be called as procedures as well as used via their operators. You may also use the lvalue qualifier for normal procedural arguments and for return values. However, note that you may not return stack-allocated storage as an lvalue (for obvious reasons) (XXX).
Non-lvalue return values are stored in temporary space and thus cannot be modified by the caller. This makes the 'const' storage qualifier redundant when applied to an operator's return value. Rune special-cases the use of const-qualified return values from operators to mean the following: if the arguments the procedure are constants, and the procedure's return type is const-qualified, then the return value is assumed to be a constant as well and Rune is free to cache and use it, potentially resulting in the procedure never being called again.
A cast allows an object of one type to be cast to an object of another type. The cast procedure may be defined in either object's class.
A method procedure is like a normal procedure except that it can only be called through an object (or the type if this is a global qualified method). The object the procedure is called through is silently passed to it as an lvalue with the identifier this. For example, the stdio library has a method call for FILE called setMode that looks like this:
public class FILE { ... public method void setMode(int mode) { this.mode = mode; } }
You would use the method call through a file pointer. For example you might say fi->setMode(FILE.M_FULL);. Note that this is not a keyword here, it is just an argument declaration that the parser quietly adds to the procedure's arguments.
You can also have global method calls. A global method call is like a normal method call except the first argument is a type rather then the object, so you can only access global elements within the class. Global method calls are often used to supply creation and destruction functions for objects.
Finally, it is possible for the programmer to override the this argument by explicitly declaring it as the first argument. This is only necessary when you wish to implement a method on a pointer or reference type rather then the object being pointed to. A good example of this is the Frame.createFrame() method in classes/gfx/window.d. The feature allows you to call a method through a NULL pointer and have the method allocate the object and assign it to the pointer. The feature can be used for other things as well.
Method calls provide extremely useful shortcuts in project design.
macro
A macro procedure is roughly equivalent to a GCC inline. In Rune, macro procedures (and normal procedures and operators for that matter) can be coupled with lvalue-qualified arguments to operate directly on the variables being passed and/or returned. Macro procedures are not yet implemented.
If no procedural scope is specified, a normal procedure is assumed.
compound_exp:
A parenthesized expression such as (1, 2) is a compound expression. Compound expressions may have several elements. Note that in Rune, there is no 'comma' operator. The operator instead denotes a compound expression occuring within parenthesis.
A compound expression has an associated compound type (which is specified in a similar manner through comma delineation). Compound expressions are not vectorized in Rune. That is, you cannot use an operator defined for a normal non-compound type like int on a compound type made up of ints. You can, however, define and use operators specifically designed to operate on compound expressions.
The elements in a compound expression may be named. The named elements correspond with similar named elements in the associated compound type. In Rune, procedure arguments are really just a compound expression. The ability to name procedure arguments is, in fact, nothing more then the ability to name elements in a compound expression.
In Rune a simple parenthesized expression such as (23) is not usually compound. Rune will, however, convert it to compound if it knows it has to be. You can always force simple expressions into compound expressions by naming the element. For example, (int a = 4, int b) v = (b:23); is reasonable.
When specifying unnamed elements in a compound expression, Rune will match your element up with an elment in the associated compound type by searching the compound type from its current position for the next non-global storage element. global and non-storage elements in a compound type are ignored unless specifically named. When you specify a named element, Rune will seek it's scan position to that element and any unnamed elements occuring afterwords will also occur after that element in the compound type. Reverse seeks are allowed... that is, specifying a named element occuring before the current element. Additionally, you can name the same element more then once, overwriting its previous contents, and you can name a global element, modifying the global storage.
Any elements without defaults which are not initialized will be initialized to zero unless the declaration is qualified with __nozero. Note that __nozero declarations may not be pointers and may not contain pointers.
Note that procedures may also return compound types.
As a degenerate case, () is equivalent to void.
id:
An identifier. Identifiers which begin with an upper-case character are types or classes. Identifiers which begin with a lower-case character are variables. Identifiers which are all-caps are constants. Also, identifiers which terminate in _t, _u, _p, or _m are types or classes regardless of how they start.
In general, these restrictions make the code more readable and also have the important effect of allowing the parser to distinguish between expressions, types, and declarations without needing to track down what the identifier is referencing... extremely important since the parser likely can't figure out what the identifier refers to until late in the resolver.
There are a few exceptions: void, bool, char, int, uint, long, ulong, float, double, and ldouble are still configured in the system class library but the identifiers are allowed to be types or classes.
NOTE: Upper-case and lower-case is defined by standard ascii, not uni-code.
constant:
Extensions are as follows:
B - 8 bit quantity
W - 16 bit quantity
(none) - 32 bit quantity (default)
L - 64 bit quantity
UB - unsigned 8 bit quantity (this is the 'char' type)
UW - unsigned 16 bit quantity
U - unsigned 32 bit quantity
UL - unsigned 64 bit quantity
F - float (32 bits)
D - double (64 bits) default for floating point)
X - long double (128 bits)
string:
Standard double quotes enclose a string. Rune supports ANSI string concatenation whereby several quoted strings strung together are parsed as a single string. Control characters (0x00-0x1F) may not be directly embedded in strings and must be properly escaped. Newlines in particular.
operator:
Note: certain operators are reserved and may not be defined by the user. All operators beginning with one or more '*'s are reserved (they are used for multiple-pointer indirection).
Reserved operators:
= - assignment
& - address-of
* - indirect through
Also note that Rune does not allow user-defined or system-defined unary postfix operators. Only unary prefix operators. Array accesses (which are post-unary) are explicitly parsed and not considered an operator for this purpose. This means, among other things, that only prefixed ++ and -- operators are supported.
It's just as well, a lot of programmers can't wrap their heads around post-increment and post-decrement (or, worse, use both forms willy nilly). More importantly, since Rune has no reverse-reference or deferred-reference limitations it must be able to definitively parse all operators at parse-time and that can't be done if we were to allow post-unary operators.
Finally, note that lvalue specifications are fully supported in the general syntax so you, the programmer, can implement your own lvalue-updating operators if you really want to. Assignments, unary operators, whatever. This is a very powerful feature of Rune.
character:
"\n" - a newline (ascii 10)
"\r" - a return (ascii 13)
"\t" - a tab character (ascii 9)
"\\" - a backslash
"\n[n[n]]" - an octal code with 1-3 digits
"\xNN" - a 8-bit hex code where NN is two hex digits.
NOTE: Programs which emit Rune should use the hex extension rather than the octal extension. If the octal extension is desired, encoding all three octal digits is recommended to avoid mis-parsing normal numeric characters that might occur after the escaped character.
specials:
"."
":"
";"
","
"{"
"}"
"["
"]"
"("
")"
"`"
"="
"->"
whitespace:
ascii 0x09 (tab)
ascii 0x0A (newline)
ascii 0x0C (return)
ascii 0x0D (formfeed)
comments:
"#" anything except newline "\n"
"/*" anything except '*/' "*/"
"//" anything except newline "\n"
Rune allows all three styles but the native Rune commenting form is to use '#'. The standard Rune coding for a comment is to leave a blank line before the comment and an empty line with just a # at the end of the comment, like this:
# Hey I'm setting x to 1 here, isn't this a dumb comment? # x = 1; # And b is being set to 2. I guess I didn't learn my lesson. # b = 2;For comments occuring after an open-brace a #-only line is typically used instead of a blank line.
for (i = 0; i < 10; ++i) { # # Oh this is not a good idea. # j = i; i = 1; # But don't sweat it, I can fix this snafu. # i = j; }Continuity is important, particularly when commenting inside conditionals. Using a #-only line maintains continuity when desired without crushing the comment text against the statement above or below it, which can make code (and comments) hard to read.
Operator Precedences:
Type | Operator | Precedence | |
binary | . | 60 | |
binary | -> | 60 | |
binary | closure | 60 | |
binary | call | 60 | |
binary | user-defined | 40 | Other operators not already covered |
binary | + | 35 | Any operator containing '-' or '+' |
binary | - | 35 | |
binary | compare | 33 | Any operator containing <, =, or > (overrides '-' and '+') |
binary | && | 30 | |
binary | || | 30 | |
binary | = | 20 | right-to-left |
binary | := | 20 | right-to-left |
Type | Operator | Precedence | |
unary | user-defined | 42 | Other operators not already covered |
unary | & | 42 | |
unary | ? | 42 | |
unary | * | 42 |