wiki:ElementalInterpreter

Elemental Programming Guide

back

The 'elemental' language (originally GIRL) is lexically and grammatically a dialect of Python. Programs execute during virtual machine runtime emulation, and are symbolically bound to objects that interact with, among other things, the user input and output ('peer').

Peer output consists of object messaging methods that depend on user connectivity.

Classic canonical text sent as terminal data is done with the 'sendln' method:

session.peer.sendln('Hello, %s!' % peer.avatar.name)

Web page users receive content using the 'session' object and (indirectly) its postMessages method. This method sends interactivity commands to the browser application:

session.postMessages(['javascript', 'alert("Hello, user!")'],
                     ['output', 'A line of text.'])

session.messages += session.messages.javascript("alert('Hello, user!')")
session.messages += 'A line of text.'
session.messages += session.messages.panelHtml('<link rel="stylesheet" href="/page/identities/Itham/style.css">')

Because objects are symbolic in the virtual runtime, user interactivity with a given program is limited to the context which started the program. For instance, NPC mobile procedures have no peer associated directly with them, but they may reference the mobile entity:

# Send to all players in the mobile's room:
for e in mobile.location.people:
	if not e.npc and e.peer:
		e.peer.sendln('%s sends you a message.' % mobile.name)

A program is grammatically either: a functional expression; a single-line statement containing expressions and assignments; or a module containing expressions, assignments, control flow statements (structured programming), and subroutine (function) definitions.

Inline object data can be encoded using a statement syntax, specifying heredoc-style string constants, or other text-originating data formats. More on this later.

Comments are specified using the '#' character and continue to the end of line.

Expressions:

Constants (literals)

	Integers: any number of decimal digits (0-9)
	Float/decimal-point numerals: any number of digits . any number of digits
	Strings
		"Any printable character that is not a double apostrophe"
		'Any printable character that is not a single apostrophe'

		Non-printable characters can be encoded using a backslash \ followed
		by a character code.  This may include a double-or-single apostrophe.

	Lists: [ <expression-1>, ... <expression-N> ]

Variables (Identifiers)

	Symbols are resolved based on the scopes defined by the part of the runtime that calls
	the program.

	Symbolic variables can contain any number of:
		 Characters in the English upper or lower case (a-z, A-Z)
		 The '_' underscore character
		 The '$' dollar sign character
		 Decimal digits (0-9) that occur after at least one of the preceding characters.

Arithmetic

	Addition: <expression> + <expression>
	Subtraction: <expression> - <expression>
	Multiplication: <expression> * <expression>
	Division: <expression> / <expression>
	Exponentiation: <expression> ** <expression>
	Modulus: <expression> % <expression>

	Unary Minus: - <expression>
	Unary Plus: + <expression>
	Negation: not <expression>

Algebraic (Comparison)

	Equality: <expression> == <expression>
	Not-equal: <expression> != <expression>
	Less than: <expression> < <expression>
	Greater than: <expression> > <expression>
	Less than or equal to: <expression> <= <expression>
	Greater than or equal to: <expression> >= <expression>

Object Oriented

	Member Dereference: <expression> . <expression>

		Also known as an attribute or property dereference.

		A member name cannot begin with the underscore '_', because this is used to
		differentiate between private members that are only accessible from a native
		context.  This means that native objects build their encapsulated object API
		by hiding internal objects by naming them with a leading underscore, which
		means that for all other purposes, a python object is an object accessible
		by the interpreter.

	Subscript Dereference: <expression> [ <expression> ]

		Also known as an item dereference.

Functional

	Function Call: <expression> ( <expression-argument-1>, ... <expression-argument-N> )

Order of Operations

	Expressions can be explicitly grouped using parenthesis ( ).

Statements

Assignment

	<identifier> = <expression>
	<member-dereference> = <expression>
	<subscript-dereference> = <expression>

Augmented Assignment

	<identifier-or-member-or-subscript-dereference> += <expression>
	<identifier-or-member-or-subscript-dereference> -= <expression>
	<identifier-or-member-or-subscript-dereference> *= <expression>
	<identifier-or-member-or-subscript-dereference> /= <expression>
	<identifier-or-member-or-subscript-dereference> %= <expression>

Structured Flow Control (Branching)

	Conditional Execution

		if <expression>:
			<statements>
		elif <expression>:
			<statements>
		else:
			<statements>

	Iteration (Looping)

		for <identifier> in <expression>:
			<statements>
		else:
			<statements>


		while <expression>:
			<statements>


	Exception Handling

		try:
			<statements>
		except <exception-type-class: identifier> <variable-name: identifier>:
			<statements>
		except:
			<statements>
		finally:
			<statements>

	Flow Control

		break
		continue

		return
		return <expression>

                yield
                yield <expression>

Subroutine Definition

def <identifier>(<argument-identifier-1>, ..., <argument-identifier-N>):
	<statements>
inlinedef <identifier>(<argument-identifier-1>, ..., <argument-identifier-N>):
	<statements>

	Inline definitions are not compiled into instructions, but instead
	become Abstract Syntax Tree objects that can further be transformed,
	during execution.

        ** Inline definitions are replaced by custom syntax compilers declared
        with InlineText blocks.

Statement blocks that group structured statement sequences and subroutine scope are specified using Python INDENT/DEDENT lexicography.

What can it do that Python can do and can't do?

Inline Text

Text blocks can be declared inline to the source, which is also useful for declaring the structural interface for object data that conforms to the structural specification. This is one way to define rich document-originating formulas also supported by the application library, but is currently not entirely implemented.

When the compiler encounters a statement that is an identifier followed by two colons and a newline, the next indented block of text becomes an InlineText object. Interpreter instructions are emitted to call the bindInline$ native on this object before doing one of two things with that result:

  • If the identifier is 'return', the frame immediately returns with the returned InlineText object on the stack.
  • Otherwise, the InlineText result is bound to the local named by the identifier.

The InlineText object contains the native __bind__ method, which is called by bindInline$ and passed the frame as its first argument. This way, the InlineText object can bind to the frame properties like locals and environ, and used to reconstruct the attached execution environment when transforming the text later.

Backarrow Notation

The inline text feature can be extended to a syntax binding multiple variables to text blocks before piping to a custom evaluation expression.

.jsOut(render(code, wm(ctx))) \
  <- code:
    alert({{ message|safe }});
  <- ctx:
    message::
      prompt('Enter message:');

Flashpoint: Compiler Types

An inline text variable (or return statement) binding can be declared of a certain type which specifies a registered subordinate compiler implementation used for generating its custom VM instruction code.

code = compile(code, types = \
    mapping(dom = run$python(domCode))) \

<- domCode:
    from stuphos.kernel import WMBasedCompiler
    from xml.dom.minidom import parseString
    import pickle

    class domCompiler(WMBasedCompiler):
        def _compileInline(self, compiler, stmt):
            yield True # Override generated code

            o = parseString(stmt.text.value)
            yield (compiler.runtime.LOADOBJ,
                   [compiler.pushConstant(pickle.dumps(o))])

            yield from compiler._genInlineAssign \
                (stmt.name.value, stmt.lineno)

    __return = domCompiler()

<- code:
    return(dom):
        <Component />
return (backend = 'japplet:run', \
        path.strip(), content):

    void main(String [] argv)       {
        argv[1].write(argv[2]);     }

<- path:
    /home/itham/document.html

<- content:
    <html />
subcontract = imm(subcontract) <- subcontract:
    return wm(defn)

Native Functions

The execution of application programs is intended to be virtualized so that their resource access (including computing cycles) is restricted. When a normal python callable is called from the interpreter, the frame of the currently executing virtual task is passed as the first argument.

A full native interface tooling guide will be provided as part of the application configuration.

Upgraded Database

In LambdaMOO, each object had a number, and besides variable references, these numbers were the way you would address individual objects. Some might consider this tedious or too limiting.

In the Agent System, object prototype code and data is stored in a filesystem of alphanumerically-named nodes arranged in a tree. Exposed native subroutines are used to access the database programmatically, in order to submit or execute code. These native types take advantage of Python's capable typing system, but program (Activity) nodes can also be instantiated to make individual anonymous objects that inherit methods from library-backed modules.

Agent System Calling and Object Instantiation

A call into the library is divided into two categories: method invocation and instantiation.

Instantiation is a chance to create a memory-sensitive storage object, but it must be tied to a library module as its type. Having a type defines the activity and operation of the object (subroutine methods), organizes the runtime, and enables the power of a native interfacing layer. As a memory object, an instantiated ($) version of a library path can store properties and data according to user quota.

Accessing an attribute on the 'create' native allows for an attribute chaining method that builds a path to the target path, but the objects emitted for an attribute lookup are not themselves LibraryNodes. Rather, they are temporary create._index nodes that represent an attribute-based access layer to the library. Therefore, accessing the underlying LibraryNode is done with another call to 'create'.

For example, to access a method defined by the library activity at the path:

  $($.components.services.object).method(args)

This is because the attribute chain of create carries with it the intended operation of an instantiation:

  $.components.services.object(args) # Instantiate components/services/object

Dot syntax emits the underlying LibraryNode already, because the intended action of a dot expression is to access an object attribute, not perform instantiation:

  ...services.object.method(args) # Calls the method

But an instantiation can be performed on a LibraryNode object, allowing this syntax:

  $(...services.object, args) # Instantiate ../../services/object

The difference is that of one between a relative and an absolute import.

Similarly, the 'call' native object allows attribute-chaining of path components but the intended action is to perform a method invocation. In this case:

  call.components.services.object.method(args)

bypasses instantiation and attempts to execute the subroutine or package tool native. The same thing can be achieved by calling 'call':

  call('components/services/object/method', args)

String Value Notation

For convenience, string() values extend the base string type by adding methods, including the __call__ method which passes the string value instance and all arguments to the native call routine:

  'components/services/object/method'(args)

Generators

When a function uses a yield statement, it gets flagged as a generator. When called, it returns an iterator that generates values yielded from the function. For each yield, the stack is preserved for the calling task and restored when the generator continues.

Equivilant API:

def yield$():
    task = 'kernel/callObject$'('stuphos.kernel.vmCurrentTask')
    task.yieldFrame(task.frames[-2])

Commands and Subjective Verbs

The ultimate form of a verb is a method (procedure, subroutine or event) associated with an object that a user indirectly interacts with based on the access rules of the world that is composed of other programs. Text words and phrases and web page elements are an example of this indirect access.

Heartbeat Tasks

The MUD simulates a singular planet of active creatures and rich content, and is provided by the phaseware extension which runs in parallel to the game server and executes all operations as concurrent tasks in isolated processes. While programming this way might sound complex, the design is supposed to simplify concurrent programming by yielding to the MUD's timing pattern and the extension protocol so that all activities interoperate within clear resource boundaries.

Agents are Asynchronous

Programs in the agent system are effectively interrupted, periodically, allowing multiple user tasks to execute concurrently. In contrast, Python is a mandatorily synchronous language because even with async def/await it still relies on explicit program- pausing statements in the code in order to yield. Similarly, greenlets allow task switching but only with explicit calls in the python code.

back

Last modified 5 months ago Last modified on 07/16/23 22:55:18

Attachments (1)

Download all attachments as: .zip

Note: See TracWiki for help on using the wiki.