Jump to content

Introspector/LanguageTools/SilverScheme/Doc

From Wikibooks, open books for an open world
-- Objects --

Everything in SilverScheme is an object. An object consists of two parts: a code
section and an field list. The code section may contain a lambda expression.
The field list is basically a hash with unique keys, the values refer to
other objects.

-- Scope --

Scope is complicated. SilverScheme, like any modern LISP has lexical scope (block
scope). To understand scope in SilverScheme a little knowledge of the syntax is
required.

Every definition is basically a (define), lambda list is (name value . forms).
The forms are evaluated in the scope of the (newly created) object. For example
(define a 10 (define b 20)) will create a new object a with value 10 and with an
attribute b with value 20. Thus a.b = 20 .

When you say (define a.b 30) you define in the current scope a new binding for
symbol b as field of object a. An example demonstrates some things:
(define a 10
 (define b 20))
(define-ref c a) ;c is an alias for a
(define d a) ;d is a copy of a
(define a.b 30)
a.b = 30
c.b = 30
d.b = 20

Thus (define a.b value) doesn't set a.b, but it sets the binding of symbol b to
object a.

Wow, I've finally managed to get scope working decently :-).

For your understanding here are a few simple facts:
* An object is in a box, a memory location.
* A field points to a box.
* The object id is actually the box id.
* The set! function changes the object in a box.
* The define function makes a field point to a different box.

-- Pass-by-reference, pass-by-value --

The general rule of SilverScheme is that function arguments are passed by
reference. However two basic functions are an exception on this. Arguments to
(define) are passed by value, (define-ref) is the normal reference one, thus
(define) can be seen as (define-ref), but with arg.copy done for each argument.

The let construct also needs pass-by-value to keep consistent with define (and
to avoid easily made hard to find bugs). To indicate reference one must put
:ref before the clause. For example:
(let ((a 10) ;a is by-value
      :ref (b 20)) ;b is by-ref
      __some_code__)

Granted, assigning by reference to a literal is stupid (literals are constant),
but this demonstrates the principle.

Copies in SilverScheme (pass-by-value creates copies) use copy-on-write. I guess
you already know how copy-on-write works.

-- Metadata --

SilverScheme is designed around the concept that: a) code should be free, in all
it's forms and b) that if you don't do that somebody else will ;-). All
information about an object is accessible through it's metadata interface. The
metadata interface consists of roughly two parts: documentation contributed by
the user and information gathered by the interpreter.

The meta field is also where all attributes are stored. An attribute in
SilverScheme is simply an object. The user can thus easily define attributes.

There are thus roughly three types of objects of metadata: the documentation,
the reflection info and custom attributes. The documentation is placed into the
obj.documentation field in hash form. Both reflection and custom attributes go
into obj.meta in normal object form.

SilverScheme supports documentation through the Argentum [1]
scheme. Argentum is the POD of SilverScheme. Documentation can be put in
doc comments of the form ;...\n and #!! ... !# (multi-line). Doc
comments use a syntax similar to YAML.

Attributes can be easily created using (define meta.attrname value).

-- Classes --

SilverScheme has a weird way of implementing classes. The instance of a class is
defined as a field of that class named 'prototype'. The constructor then does
(create-instance), (create-instance) adds the needed class information to a copy
of the prototype.

Note that classes can access private members of their prototypes and vice versa.

To access the class of an object you can use the obj.class field (or the
obj.meta.class field).

An example of a simple class:
(define-class Foo (Object Bar) ;;Foo inherits from Object
                               ;;and promises to implement
                               ;;the public interface of Bar
  (define-prototype ;;(define prototype) doesn't work!
    (define baz 10)))

-- Inheritance --

SilverScheme does not support multiple inheritance. Instead it has interface
inheritance and mixins (later more about that). Every class after the first is
interface inherited. Interface inheritance means that a class only inherits from
the parent in name, not in code (aka no code is copied).

Note that it's not possible to inherit from a non-class. This is to keep the
class mechanism simple.

-- Mixins --

SilverScheme uses mixins to do what other languages do with multiple inheritance. A
mixin is an object that has fields marked with the (mixin) attribute. The object
can also be marked with the (full-mixin) attribute, essentially giving all it's
fields the mixin attributes.

When an object has the (use-mixin __mixin_name__) attribute it copies the source
code of the mixin fields in the mixin and evaluates them in the current scope.
Kinda like how Ruby does it. This is used for example to give objects the <, <=,
=, etc functions for free if they have the <=> function and say (use-mixin
Comparable).

-- Domains --

SilverScheme provides some powerful tools to make programming without bugs easier.
The most important one is domains. A domain is a function that takes one
argument, and checks if that argument is in the domain. If so nothing happens,
if not an Domain-Violation-Error is thrown. Domains have three main uses:
checking variables, enforcing preconditions, enforcing postconditions.

To check a variable against a domain one simply uses the (domain) self-applying
attribute (most attributes are self-applying btw). For example:
(define a 5 ;domain: 0 < a < 5
  (domain (\ (value) (and (> value 0)
                          (< value 11)))))

The domain function accepts a few keywords after the lambda expression:
:read -- Check the domain whenever the variable contents are read.
:write -- Check the domain whenever the variable contents are written.

The keywords may be used both.

By default only :write is on. The domain-default-operation environment variable
is used to set the default to a different value.

To check a preconditions replace the argument name with (argument-name domain):
(\ ((a (\ (value) ;domain: a > 0
        (> a 0))))
    ...)

The domain may be in a variable (useful for often used domains).

Another way to do preconditions is to use the (pre-condition __arg_name__
__domain__) attribute. The advantage of the attribute is that you can use it
conditionally (for example in a testing situation, but not in production use)

To check a postcondition use the (post-condition) attribute. For example:
(define simple-add
 (\ (a b)
  (+ a b))
 (post-domain (\ (value) (> value 0))))

The Argentum documentation extractor (agdoc) also extracts domains, so using
them is a good way to show what the pre- and postconditions are.

-- owner, parent, super --

In a.b object defined as (define a 10 (define b 20)) a owns object b since b was
defined by object a. Object b has full access to the private members of object
a. For object b object a is known by as 'owner'. The variable 'owner' always
points to the owner of an object.

The parent of an object is the prototype of the superclass.

The super variable refers to parent.__name_of_this_function__ .

-- call/cc, call/tf, call/rf --

SilverScheme call/cc (call-with-current-continuation) is pretty much like the
standard Scheme call/cc. However it has some more argument passing power. When
you do (call/cc (\ (b) (b 10 20))) then the resulting continuation has a field
values which contains a list of the values passed to it. When you invoke the
continuation with the arguments the arguments are given as the result of the
break function, always as a list.

The call/tf (call-with-throw-function) function is like the Guile catch
function. It takes one argument, a lambda list with one parameter, the throw
function. When the throw function is invoked a non-local exit is done to the
call/tf, like with call/cc, however there is no return. The arguments passed to
the throw function are returned by (call/tf) as a list in the second element of
a list (the first element if #t if no non-local exit occurred and #f otherwise).

The call/rf function is similar. However it doesn't signal success or failure
and returns the list directly (not as a sublist).

-- Exceptions --

Exceptions are build upon call/tf and work roughly like this:

(try
 (begin
  __code_to_be_tried__)
 (catch
  (__exception_class__
   __code_to_be_executed_on_that_exception__)
  (__exception_class__
   __code_to_be_executed_on_that_exception__)
  ...)
 (finally
  __code_that_should_always_be_executed))

Pretty simple isn't it? The (throw) function is rigged to go to the nearest
exception handler at all times. It's not quite schemisch, but it looks pretty
good :-).

-- conditional evaluation of parameters --

Normally parameters are evaluated left to right. SilverScheme has an easy way to
create an exception on that however. For example:
(\ (`a b `(c (\ (val) (> val 10)))
  (if (b)
   a
   c))

Right, stupid example, but it demonstrates my point. If you put a backquote
before the argument in the lambda list the argument will not be evaluated until
needed.

-- macros --

A macro is a function that returns a list that is evaluated in the scope of the
caller. A macro is also an ideal way to get a virus in a SilverScheme system. A
macro is made by simply creating a function and then attaching the (macro)
attribute to it.

Macros can be dangerous, they can be helpful too however. It depends on the
situation what they are. That's why I'm introducing set-macro-safe. The
set-macro-safe function takes one argument which is either a number or a symbol
and sets the macro-safe level to that argument. The possible arguments are:
0 'allow-all -- Allow normal macro behaviour for all macros.
1 'explicit-deny -- Allow normal macro behaviour for all macro's except when
explicitly told not to do that.
2 'allow-base -- Allow normal macro behaviour for all macro's in the Base
hierarchy (the baselib). Disallow normal macro behaviour for all other macros
unless explicitly told otherwise.
3 'explicit-allow -- The same as 'allow-base, except that the Base macro's
aren't automatically allowed anymore.
4 'deny-all -- Deny all macro's.

Some primitive macro's are allowed regardless of what the macro-safe level says.

Macro's that can't be called with normal macro behaviour can still be called,
but they only take a single argument (a list) and produce a list that must be
explicitly evaluated (using '(eval list)').

The standard macro level is 0. Note that that's not a safe level for any
production code.

At the first deny command the macro level automatically gets set to 1, if it's
not already at that or a lower level. Thus level 0 can be regarded as level 1,
however it's faster since the macro deny checks are off.

-- Dynamic binding and environment variables --

Sometimes it's handy to pass arguments to functions without putting them in the
argument list but by defining a variable and having the function read that
variable. In the case (begin (define a 10) (foo)) foo can't read a, and it
shouldn't. For environment variables like macro-safe it's another situation
however. That's why I'm introducing environment variables.

Environment variables are dynamically bound. They can't leave the scope in which
they are defined however (like any other variable). To define an environment
variable you need a special function (define-environment-variable) aka (defenv).
This function simply behaves like a normal define, except that the variable is
automatically set read-only (you can override, but normally it's what you want).
The variable can then be accessed using env.variable-name, note that 'env' is a
special variable.

Almost all the special variables can be accessed using env too (env.super,
env.owner, etc), they only have names without env in them because they are used
so much that it's reasonable to shorten them. The macro-safe variable is however
only accessible with env.macro-safe (it's a read-write variable btw).

A word of warning is due here, don't define environment variables unless you
really need them, it's bad style to depend on them for stuff you should pass in
a parameter list.

-- compilation --

SilverScheme is like any Scheme compilable. However SilverScheme has multiple
compiling modes.

The standard mode is keep-all, which basically creates a resource section in the
executable and puts the literal code files in it. The keep-all mode has the
advantage that the result (obj.source) will have the same markup and comments as
the original. Since the data is put in resource files the actual code can be
executed pretty fast. Needless to say this actually increases file size.

A second mode is keep-source, this mode only keeps the actual source code, not
comments. It helps keep the file size down.

A third mode is keep-minimum, removing all the comments and sources except those
referred to in the program. Good for really small files, bad for readability.
Note that since documentation is put into the objects it can't be removed by
this mode.

The fourth and most drastic mode is keep-minimum-remove-docs, this mode also
kicks out the documentation. Only useful if you really really want a small program.

An implementation may compress resource files with tar and gzip at any
compression level. I recommend not or very weakly compressing the actual sources
but strongly compressing the documentation.

The extension of a SilverScheme source file is .dgs, the extension of a SilverScheme
compiled file is .dgsb (SilverScheme Bytecode).

Note that the SilverScheme bytecode is not IL, Parrot or machinecode though
compiling to these is allowed as an optional feature. The SilverScheme bytecode is
a special bytecode designed for SilverScheme (probably looking like other LISP
bytecodes).

-- Permissions --

The permission system is aimed at providing flexible authorization for certain
objects to do certain things. Permissions are given with tokens. Access tokens
give an object A the power to get/set/invoke object B even if this is forbidden
by the access system (access system: public SUBSET-OF {'read 'write 'invoke},
private SUBSET-OF {'read 'write 'invoke}). Here's an example of access tokens:

(define a 10
  (read-only) ;;(public 'read 'invoke) (private 'read 'write 'invoke)
  (define giver
    (\ ()
      (a.give-token-to-caller 'a.write))
    (a.give-bag-access)))

In this example 'a is first declared read-only (that's the default actually),
then a function a.giver is declared and made read-only implicitly. The giver
function is given bag-access by 'a. Any object can give bag access during it's
definition or at any time through it's code. The giver function now gives write
permission for a to any object that calls it. There is no caller object by the way
since I consider it a bad idea if code can manipulate things up the stack except
in such special cases.

There is a second kind of tokens called custom tokens. Basically a custom token
is a token that doesn't have an implicit action associated with it. An example
of a custom token system:

(define nice 0
  (define set ;accessor
    (\ (value)
      (if (< value 0)
        (if (caller-has-token? 'system.root-access)
          (set! nice value)
          (throw (Invalid-Rights-Exception "You don't have root permissions"))
        (set! nice value))))

Without the root-access token you can't use it.

To be honest, custom tokens don't feel right yet. I'd rather leave them out of
the language for now, letting them mature first, I included them only for
completeness sake.

-- Static --

The fields of an object can be divided into two parts: static and non-static.
Static fields are conceptually bound to the storage box of the object and stick
around when set! is used. However static and non-static fields are in the same
namespace, so if a non-static field is in the new value during set! it will
override the old value. Here's an example:

(define a 10
 (define set
  (\ ((value (\ (val) (> val 10))))
   (set! owner value))
  (static)))
(set! a 20) ;set! obj -> obj.set
(a.set.source) => (\ ((value (\ (val) (> val 10)))) (set! owner value))

-- Overload and override --

When defining a subclass and overloading a field one often easily uses define.
This is a bad idea however since define creates a whole new field that doesn't
share any of the attributes and stuff like that with the old field. Instead
SilverScheme has a method called (overload) which is used like this:
(define-class Foo ()
  (define bar
    (\ () __some_code__)
    (private)))

(define-class Fred (Foo)
  (overload bar
    ((\ () __some_other_code__))))

And voila, bar is copied. Here's a template for overload: (overload __name__
(__new_value__) __attributes__). If the new value part is empty the value isn't
changed (that's why I have it between parenthesises).

The (define) function will do an override. In some cases it might be helpful to
warn or throw an error when define is used. In such cases the (override)
function (which is simply define with another name) is useful.

The override error checking can be turned on and off using the environment
variable check-override-errors.

-- Temporary environment variables --

It's possible to create temporary environment variables that only exist in a
certain scope. One can use the (let-environment-variable) function for this, it
takes the same kinds of arguments as let, but puts them under env. The advantage
over a set..body..unset construction is that let-environment-variable
automatically unsets on error.

-- Primitives --

SilverScheme's version of intercalls are called primitives. Primitives are always
functions (RPC functions to be exact), but they are not objects. Primitives can
only be called from Base (baselib). Any attempt to do weird things with
primitives may be punished with unexpected behaviour (rationale: it's internal
stuff, only a few core hackers will touch it and they will know what to do).
There is one primitive handler which is defined as (call-primitive name . args)
and is private in the top level of the Base module. The primitive handler can
take and return complex objects, however mostly it will operate on simple things.

The advantage of a single primitive handler over for example a primitive
attribute is that there is just one entry point that's easily documented. After
all the accepted function names will probably be visible in a switch statement.

-- Relative modification --

The basic idea behind serialization in SilverScheme is 'relative modification'.
Instead of saying "send this object" the serialization mechanism says "I've got
an object with this identity, do you need it". Obviously you don't need the same
object twice, so the amount of data send over the network is decreased.

Another possibility with the serialization mechanism is sending in patches to
serialized libraries. This has a huge advantage, namely that you only need to
send the actual changes, instead of the textual changes (diff sometimes sends
over half the file for one line changed, SilverScheme would never do that).

The trick in serialization is the basic SilverScheme object itself. This is how
an object looks like:

    object                     field
+------------+              +-----------+
|  identity  |        +---> |  subject  |
+------------+        |     +-----------+
|  prototype |        |     |  name     |
+------------+        |     +---------+-+
|  changed   |        |     |  object |C|
+----------+-+        |     +---------+-+
|  ivalue  |C|        |
+----------+-+        |
|  code    |C|        |
+----------+-+        |
|  scope     | -------+
+------------+

The C subfields are change flags.

The identity of an object is to the outside world an uri like
'http://myproject/1003', internally it's a number. The identity of an object is
unique, no two different objects with the same identity can exist at any time.

The prototype of an object is the object that this object is a copy of. Often
this is the prototype of the class of the object, in case the object is copied
using for example define it's the original (the second arg to define). Objects
can have prototypes that are not in the current session (like a prototype from
the web).

The changed flag of an object is set on whenever the object is no longer a
perfect copy of the prototype. The changed flag is a kind of warning flag, it
doesn't tell what has changed only that something has changed.

The ivalue is the internal value of an object. It's were for example the value
of a String object is stored in the native representation of the string (or more
likely a pointer to the native representation of the string). It has a changed
flag to signal when the internal value has changed.

The code property is the code section of an object. It has a changed flag to
signal when the code has changed.

For each field in the scope there is a name, a subject reference and an object 
reference with a changed flag. If the field is bound to a new object (with a 
different identity) the change flag goes on.

To send over an object one first needs to ask the other party if he has the
prototype. If not the whole object is send over. If he has the prototype one
must look at the object's change flag, if it's off one can suffice with saying
"it's a perfect copy of the prototype", otherwise one has to look up which
change flags are on and send over the information in those properties.

-- The library system --

SilverScheme has two types of libraries: source libraries and static
state libraries (I'll call them state libraries from now on). A source
library is simply a bunch of SilverScheme source files. A state
library is a serialized library. Typically a state library is a
relative serialization patch against it's dependencies (to keep lib
size down) and some extra files (like pictures, should it be needed).

State libraries are usually published on the WWW with an URI like
this: 'http://domain/prefix/name/build/', for example
'http://somedomain/home/peter/myproject/12/'. The prefix is just the
path to the project directory. The build is a number that increases
when you create a new build of the state files, it runs independent of
the version number and is the primary means of identifying a version
of the library. Additionally there may be something like
'http://somedomain/home/peter/myproject/0.0.1/' where the version
number is just an alias for a certain build number.

-- Introspection --

Static state files in SilverScheme are similar to the output of the
introspector, indeed they are the output of the build-in SilverScheme
introspector. Thus they contain all information about a program (at
the time of serialization), except the stack and some other
information needed for running (non-static state files contain these,
but that stuff is out of scope here (I'm not even sure if there will
ever be non-static state files)). Thus the (static) state files
contain all the information needed for analyzation of the library.
I'll create an ontology for the state files similar to the
introspector ontology for the introspected IL files so that the state
files will be processable with the introspector toolchain, given a few
small adaptions to cope with the language differences. Unfortunately I
can't use the introspector ontology due to the differences between
SilverScheme and IL.

-- The SilverScheme Repository --

This is really wishful dreaming, but a nice dream :-). The
SilverScheme Repository will be something like CPAN, but more extended
toward introspection.  It will keep libraries as both compressed
source and as state files, it will keep programs (executable programs)
as compressed source (you can't really serialize those to static state
files since there is no natural idle point where the program is loaded
but not executing). Due to an intelligent toolchain coupled to a
webservice and web interface it's possible to see the structure of
libraries, the documentation of libraries, etc, generated on demand
out of the state files.