Skip to content

sympl language description

Stéphane Lozier edited this page Jan 18, 2021 · 1 revision

21 SymPL Language Description

The following sub sections contain very brief descriptions of language features and semantics. Some sections have more details, such as the description of Import, but most loosely describe the construct, such as try-catch.

21.1 High-level

Sympl is expression-based. The last expression executed in a function produces the return value of the function. All control flow constructs have result values. The executed branch of an If provides the value of If. If the break from a loop has a value, it is the result of the loop expression; otherwise, nil is.

There is support for top-level functions and lambda expressions. There is no flet, but you can get recursive lambdas by letting a variable be nil, then setting it to the result of a lambda expression, which can refer to the variable for recursive calls.

Sympl is a case-INsensitive language for identifiers.

Sympl does not demonstrate class definitions. See section for more information.

21.2 Lexical Aspects

Identifiers may contain any character except the following:

( ) " ; , ' @ \ .

Sympl should disallow backquote in case adding macro support later would be interesting. For now it allows backquote in identifiers since .NET raw type names allow that character. Like many languages, Sympl could provide a mapping from simpler names to those names that include backquote (for example, map List to List`1). Doing so didn't seem to add any teaching about the DLR, and Sympl identifiers are flexible enough to avoid the work now.

Due to the lack of infix operators (other than period), there's no issue with expressions like "a+b", which is an identifier in Sympl.

An identifier may begin with a backslash, when quoting a keyword for use as an identifier.

Sympl has integer and floats (.NET doubles) for numeric literals.

Doubles aren't in yet.

Strings are delimited by double quotes and use backslash as an escape character.

Apostrophes quote list literals, symbols, integers, and other literals. It is only necessary to quote lists and symbols (distinguishes them from identifiers).

Comments are line-oriented and begin with a semi-colon.

21.3 Built-in Types

Sympl has these built-in types:

  • Integers -- .NET int32 (no bignums)

  • Floats -- .NET doubles NOT ADDED YET

  • Strings -- .NET immutable strings

  • Boolean -- true and false literal keywords for .NET interop. Within Sympl, anything that is not nil or false is true.

  • Lists -- Cons cell based lists (with First and Rest instead of Car and Cdr :-))

  • Symbols -- interned names in a Sympl runtime instance

  • Lambdas -- .NET dynamic methods via Expression Trees v2 Lambda nodes

Lists and symbols were added as a nod to the name SymPL (Symbolic Programming Language), but they show having a language specific runtime object representation that might need to be handled specially with runtime helper functions or binding rules. They also provided a nice excuse for writing a library of functions that actually do something, as well as showing importing Sympl libraries.

21.4 Control Flow

Control flow in Sympl consists of function call, lexical exits, conditionals, loops, and try/catch.

21.4.1 Function Call

A function call has the following form (parentheses are literal, curlies group, asterisks are regular expression notation, and square brackets indicate optional syntax):

(expr [{.id | . invokemember}* .id] expr*)

Invokemember :: (id expr*)

A function call evaluates the first expression to get a value:

(foo 2 "three")

(bar)

((lambda (x) (print x)) 5)

The first expression may be a dotted expression. If an identifier follows the dot, it must be a member of the previously obtained value, evaluating left to right. If a period is followed by invoke member syntax, the identifier in the invoke member syntax must name a member of the previously obtained value, and the member must be callable.

These two expressions are equivalent, but the first is preferred for style:

(obj.foo.bar x y)

obj.foo.(bar x y)

The second should only be used in this sort of situation:

obj.(foo y).bar ;;bar is property

(obj.(foo y).bar ...) ;;bar is method or callable member

((obj.foo y) . bar ...) ;; also works but odd nested left parens

The first form, (obj.foo.bar.baz x y), has the following semantics when baz is a method (where tmp holds the implicit 'this'):

(let* ((tmp obj.foo.bar))

(tmp.baz x y))

The form has these semantics if baz is property with a callable value:

(let* ((tmp obj.foo.bar.baz))

(tmp x y)) ;; no implicit 'this'

Sympl prefers implicit this invocation over a callable value in that it first tries to invoke the member with an implicit this, and then tries to call with just the arguments passed.

21.4.2 Conditionals

If expressions have the following form (parentheses are literal and square brackets indicate optional syntax):

(if expr expr [expr])

The first expr is the test condition. If it is neither nil nor false, then the second (or consequent) expr executes to produce the value of the If. If the condition is false or nil, and there is no third expr, then the If returns false; otherwise, it executes the third (or alternative) expr to produce the value of the If.

21.4.3 Loops

There is one loop expression which has the form (plus is regular expression notation):

(loop expr+)

Loops may contain break expressions, which have the following forms (square brackets indicate optional syntax):

(break [expr])

Break expressions do not return. They transfer control to the end of the loop. Break exits the loop and produces a value for the loop if it has an argument. Otherwise, the loop returns nil.

Sympl may consider adding these too (with break and continue as well):

(for ([id init-expr [step-expr]]) ;;while is (for () (test) ...)

(test-expr [result-expr])

expr*)

(foreach (id seq-expr [result-expr]) expr*)

21.4.4 Try/Catch/Finally and Throw

Still need to add Try/Catch/Finally/Throw. These are pretty easy, direct translations like 'if' and 'loop' since Expression Trees v2 directly supports these expressions.

These have semantics as supported by DLR Expression Trees. A try has the following form (parentheses are literal, asterisks are regular expression notation, and square brackets indicate optional syntax):

(try <expr>

[(catch (<var> <type>) <body>)] *

[(finally <body>)] )

21.5 Built-in Operations

These are the built-in operations in Sympl, all coming in the form of keyword forms as the examples for each shows:

  • Function definition: defun keyword form. Sympl uses the defun keyword form to define functions. It takes a name as the first argument and a list of parameter names as the second. These are non-evaluated contexts in the Sympl code. The rest of a defun form is a series of expressions. The last expression to execute in a function is the return value of the function. Sympl does not currently support a return keyword form, but you'll see the implementation is plumbed for its support.

(defun name (param1 param2) (do-stuff param1) param2)

  • Assignment: set keyword form

(set x 5)

(set (elt arr 0) "bill") ; works for aggregate types, indexers, and Sympl lists

(set o.bar 3)

  • arithmetic: +, -, *, / keyword forms, where each requires two arguments

(* (+ x 5) (- y z))

  • Boolean: and, or, not keyword forms. For each operand, any value that is not nil or false, it is true. And is conditional, so it is equivalent to (if e1 e2). Or is conditional, so it is equivalent to (let* ((tmp1 e1)) (if tmp1 tmp1 (let* ((tmp2 e2)) (if tmp2 tmp2)))).

  • Comparisons: =, !=, <, >, eq keyword forms. All but eq have the semantics of Expression Trees v2 nodes. Eq returns true if two objects are reference equal, and for integers, returns true if they are numerically the same value.

  • Indexing: elt keyword form

(elt "bill" 2)

(elt '(a b c) 1)

(elt dot-net-dictionary "key")

  • Object instantiation: new keyword form

(new system.text.stringbuilder "hello world!")

(set types (system.array.createinstance system.type 1))

(set (elt types 0) system.int32)

(new (system.collections.generic.list`1.MakeGenericType types))

  • Object member access: uses infix dot/period syntax

o.foo

o.foo.bar

(set o.blah 5)

21.6 Globals, Scopes, and Import

The hosting object, Sympl, has a Globals dictionary. It holds globals the host makes available to an executing script. It also holds names for namespace and types added from assemblies when instantiating the Sympl hosting object. For example, if mscorlib.dll is passed to Sympl, then Sympl.Globals binds "system" to an ExpandoObject, which in turn binds "io" to an ExpandoObject, which in turn binds "textreader" to a model of the TextReader type.

21.6.1 File Scopes and Import

There is an implicit scope per file. Free references in expressions resolve to the file's implicit scope. Bindings are created in a file's scope by setting identifiers that do not resolve to a lexical scope. There also is an import expression that binds file scope variables to values brought into the file scope from the host's Sympl.Globals table or from loading other files.

Import has the following form (parentheses are literal, curlies group, asterisks are regular expression notation, and square brackets indicate optional syntax):

(import id[.id]* [{id | (id [id]*)} [{id | (id [id]*)}]] )

The first ID must be found in the Sympl.Globals table available to the executing code, or the ID must indicate a filename (with implicit extension .sympl) in the executing file's directory. If the first argument to import is a sequence of dotted IDs, then they must evaluate to an object via Sympl.Globals. The effect is that import creates a new file scope variable with the same name as the last dotted identifier and the value it had in the dotted expression.

If the second argument is supplied, it is a member of the result of the first expression, and the effect is that this member is imported and assigned to a file scope variable with the same name. If the second argument is a list of IDs, then each is a member of the first argument resulting in a new variable created for each one with the corresponding name.

If the third argument is supplied, it must match the count of IDs in the second argument. The third set of IDs specifies the names of the variables to create in the file's scope, setting each to the values of the corresponding members from the second list.

If the first ID is not found in Sympl.Globals, and it names a file in the executing file's directory, then that file is executed in its own file scope. Then in the file's scope that contains the import expression, import creates a variable with the name ID and the imported file's scope as the value. A file's scope is a dynamic object you can fetch members from. The second and third arguments have the same effect as specified above, but the member values come from the file's scope rather than an object fetched from Sympl.Globals.

The IDs may be keywords without any backslash quoting. Note, that if such an identifier is added to Globals, referencing locations in code will need to be quoted with the backslash.

Import returns nil.

21.6.2 Lexical Scoping

Sympl has strongly lexically scoped identifiers for referencing variables. Some variables have indefinite extent due to closures. Variables are introduced via function parameters or let* bindings. Function parameters can be referenced anywhere in a function where they are not shadowed by a let* binding. Let* variables can be referenced within the body of the let* expression. For example,

(defun foo (x)

(system.console.writeline x)

(let ((x 5))

(system.console.writeline x))

(set x 10))

(system.console.writeline x))

(system.console.writeline x))

(foo 3)

prints 3, then 5, then 10, then 3 .

Each time execution enters a let* scope, there are distinct bindings to new variables semantically. For example, if a let* were inside a loop, and you saved closures in each iteration of the loop, they would close over distinct variables.

21.6.3 Closures

Sympl has closure support. If you have a lambda expression in a function or within a let*, and you reference a parameter or let binding from within the lambda, Sympl closes over that binding. If a let* were inside a loop, and you saved closures in each iteration of the loop, they would close over distinct variables. The great thing about Expression Trees is this is free to the language implementer!

21.7 Why No Classes

Sympl does not demonstrate classes. Sympl could have showed using an ExpandoObject to describe class members, and used a derivation of DynamicObject to represent instances since DynamicObject can support invoke member. Sympl could have stayed simple by requiring static class members to be access via classname.staticmember. It also could require an explicit 'self' or 'this' parameter on any instance methods (and using self.instancemember). This may have been cute, but it wouldn't have demonstrated anything real on the path to implementing good .NET interop.

Real languages need to use .NET reflection to emit real classes into a dynamic assembly. You need to do this to derive from .NET types and to pass your class instances into .NET static libraries. Performance is also better when you can burn some members into class fields or properties for faster access.

21.8 Keywords

The following are keywords in Sympl:

  • Import

  • Defun, Lambda

  • Return (not currently used)

  • Let*, Block

  • Set

  • New

  • +, -, *, /

  • =, !=, <, >

  • Or, And, Not

  • If

  • Loop, Break, Continue (continue not currently used)

  • Try, Catch, Finally, Throw (not currently used)

  • Elt

  • List, Cons, First, Rest

  • Nil, True, False

21.9 Example Code (mostly from test.sympl)

(import system.windows.forms)

(defun nconc (lst1 lst2)

(if (eq lst2 nil)

lst1

(if (eq lst1 nil)

lst2

(block (if (eq lst1.Rest nil)

(set lst1.Rest lst2)

(nconc lst1.Rest lst2))

lst1))))

(defun reverse (l)

(let* ((reverse-aux nil))

(set reverse-aux

(lambda (remainder result)

(if remainder

(reverse-aux remainder.Rest

(cons remainder.First result))

result)))

(reverse-aux l nil)))

(import system)

(system.console.WriteLine "hey")

(defun print (x)

(if (eq x nil)

(system.console.writeline "nil")

(system.console.writeline x))

x)

(print nil)

(print 3)

(print (print "cool"))

(set blah 5)

(print blah)

(defun foo2 (x)

(print x)

(let* ((x "let x")

(y 7))

;; shadow binding local names

(print x)

(print y)

(set x 5)

(print x))

(print x)

(print blah)

;; shadow binding global names

(let* ((blah "let blah"))

(print blah)

(set blah "bill")

(print blah))

(print blah)

(set blah 17))

((lambda (z) (princ "non ID expr fun: ") (print z)) "yes")

(set closure (let* ((x 5))

(lambda (z) (princ z) (print x))))

(closure "closure: ")

(print nil)

(print true)

(print false)

(print (list x alist (list blah "bill" (list 'dev "martin") 10) 'todd))

(if (eq '(one).Rest nil) ;_getRest nil)

(print "tail was nil"))

(if (eq '(one two) nil)

(print "whatever")

(print "(one two) is not nil"))

;; Sympl library of list functions.

(import lists)

(set steve (cons 'steve 'grunt))

(set db (list (cons 'bill 'pm) (cons 'martin 'dev) (cons 'todd 'test)

steve))

(print (lists.assoc 'todd db))

(print (lists.member steve db))

(let* ((x '(2 6 8 9 4 10)))

(print

(loop

(if (eq x.First 9)

(break x.Rest)

(set x x.Rest)))))

(set x (new System.Text.StringBuilder "hello"))

(x.Append " world!")

(print (x.ToString))

(print (x.ToString 0 5))

(set y (new (x.GetType) (x.ToString)))

(print (y.ToString))

(print y.Length)

SymPL Implementation on the Dynamic Language Runtime

Frontmatter
1 Introduction
  1.1 Sources
  1.2 Walkthrough Organization
2 Quick Language Overview
3 Walkthrough of Hello World
  3.1 Quick Code Overview
  3.2 Hosting, Globals, and .NET Namespaces Access
    3.2.1 DLR Dynamic Binding and Interoperability -- a Very Quick Description
    3.2.2 DynamicObjectHelpers
    3.2.3 TypeModels and TypeModelMetaObjects
    3.2.4 TypeModelMetaObject's BindInvokeMember -- Finding a Binding
    3.2.5 TypeModelMetaObject.BindInvokeMember -- Restrictions and Conversions
  3.3 Import Code Generation and File Module Scopes
  3.4 Function Call and Dotted Expression Code Generation
    3.4.1 Analyzing Function and Member Invocations
    3.4.2 Analyzing Dotted Expressions
    3.4.3 What Hello World Needs
  3.5 Identifier and File Globals Code Generation
  3.6 Sympl.ExecuteFile and Finally Running Code
4 Assignment to Globals and Locals
5 Function Definition and Dynamic Invocations
  5.1 Defining Functions
  5.2 SymplInvokeBinder and Binding Function Calls
6 CreateThrow Runtime Binding Helper
7 A Few Easy, Direct Translations to Expression Trees
  7.1 Let* Binding
  7.2 Lambda Expressions and Closures
  7.3 Conditional (IF) Expressions
  7.4 Eq Expressions
  7.5 Loop Expressions
8 Literal Expressions
  8.1 Integers and Strings
  8.2 Keyword Constants
  8.3 Quoted Lists and Symbols
    8.3.1 AnalyzeQuoteExpr -- Code Generation
    8.3.2 Cons and List Keyword Forms and Runtime Support
9 Importing Sympl Libraries and Accessing and Invoking Their Globals
10 Type instantiation
  10.1 New Keyword Form Code Generation
  10.2 Binding CreateInstance Operations in TypeModelMetaObject
  10.3 Binding CreateInstance Operations in FallbackCreateInstance
  10.4 Instantiating Arrays and GetRuntimeTypeMoFromModel
11 SymplGetMemberBinder and Binding .NET Instance Members
12 ErrorSuggestion Arguments to Binder FallbackX Methods
13 SymplSetMemberBinder and Binding .NET Instance Members
14 SymplInvokeMemberBinder and Binding .NET Member Invocations
  14.1 FallbackInvokeMember
  14.2 FallbackInvoke
15 Indexing Expressions: GetIndex and SetIndex
  15.1 SymplGetIndexBinder's FallbackGetIndex
  15.2 GetIndexingExpression
  15.3 SymplSetIndexBinder's FallbackSetIndex
16 Generic Type Instantiation
17 Arithmetic, Comparison, and Boolean Operators
  17.1 Analysis and Code Generation for Binary Operations
  17.2 Analysis and Code Generation for Unary Operations
  17.3 SymplBinaryOperationBinder
  17.4 SymplUnaryOperationBinder
18 Canonical Binders or L2 Cache Sharing
19 Binding COM Objects
20 Using Defer When MetaObjects Have No Value
21 SymPL Language Description
  21.1 High-level
  21.2 Lexical Aspects
  21.3 Built-in Types
  21.4 Control Flow
    21.4.1 Function Call
    21.4.2 Conditionals
    21.4.3 Loops
    21.4.4 Try/Catch/Finally and Throw
  21.5 Built-in Operations
  21.6 Globals, Scopes, and Import
    21.6.1 File Scopes and Import
    21.6.2 Lexical Scoping
    21.6.3 Closures
  21.7 Why No Classes
  21.8 Keywords
  21.9 Example Code (mostly from test.sympl)
22 Runtime and Hosting
  22.1 Class Summary
23 Appendixes
  23.1 Supporting the DLR Hosting APIs
    23.1.1 Main and Example Host Consumer
    23.1.2 Runtime.cs Changes
    23.1.3 Sympl.cs Changes
    23.1.4 Why Not Show Using ScriptRuntime.Globals Namespace Reflection
    23.1.5 The New DlrHosting.cs File
  23.2 Using the Codeplex.com DefaultBinder for rich .NET interop
  23.3 Using Codeplex.com Namespace/Type Trackers instead of ExpandoObjects
  23.4 Using Codeplex.com GeneratorFunctionExpression


Other documents:

Dynamic Language Runtime
DLR Hostirng Spec
Expression Trees v2 Spec
Getting Started with the DLR as a Library Author
Sites, Binders, and Dynamic Object Interop Spec

Clone this wiki locally