#Script Lisp

#Script is designed as a small, expressive and wrist-friendly dynamic scripting language that for maximum familiarity is modelled after the world's most popular and ubiquitous scripting Language, JavaScript. Its minimal syntax was inspired by other small but powerful languages which heavily utilizes functions instead of adopting a larger language grammar defining different bespoke syntax for language constructs.

Small Languages like Smalltalk, despite being one of the most influential languages in history, is famous for its minimal syntax that fits on a post card. A language with arguably better power to size ratio is Lisp which the inventor of Smalltalk, Alan Kay has credited it as being the greatest single programming language ever designed after realizing:

“the half page of code on the bottom of page 13… was Lisp in itself. These were “Maxwell’s Equations of Software!”

Lisp's unprecedented elegance and simplicity spawned a myriad of dialects, some noteworthy implementations illustrating the beauty of its small size and expressive power is lispy by by Peter Norvig (Director of Google Research) that implements a Lisp interpreter in just 117 lines of Python 3 code (inc. a REPL).

Another compact dialect is Zick Standard Lisp which @zick has implemented in 42 different languages including a recursive Lisp evaluator in Lisp implemented in only 66 lines of code.

A more complete Lisp implementation in C# is the elegant Nukata Lisp by SUZUKI Hisao which is a Common Lisp-like Lisp-1 dialect with tail call optimization and partially hygienic macros, although has some notable limitations including a small standard library, only uses the double numeric type and doesn't contain .NET Scripting support.

Script Lisp Overview

ScriptLisp is an enhanced version of Nukata Lisp with a number of new features that reuses #Script existing scripting capabilities to provide seamless integration with both the rest of #Script (see Language Blocks an Expressions) and .NET including Scripting of .NET Types, support for all .NET numeric types and access to its comprehensive library of over 1000+ Script Methods - optimally designed for accessing .NET functionality from a dynamic language.

To improve compatibility with existing Common Lisp source code it also implements most of the Simplified Common Lisp Reference as well as all missing functions required to implement C# LINQ 101 Examples in Lisp:

To improve readability and familiarity it also adopts a number of Clojure syntax for defining a data list and map literals, anonymous functions, syntax in Java Interop for .NET Interop, keyword syntax for indexing collections and accessing index accessors and Clojure's popular shorter aliases for fn, def, defn - improving source-code compatibility with Clojure.

Lisp REPL

In addition to being a 1st class language option in #Script, Lisp's dynamism and extensibility makes it particularly well suited for explanatory programming whose access via a REPL is now built into the latest web and app dotnet tools which can be quickly installed in any Windows, macOS or Linux OS (with .NET Core) with:

$ dotnet tool install -g web

Or if you have a previous version installed, update to the latest version with:

$ dotnet tool update -g web

Where you'll then be able to bring up an instant Lisp REPL with:

$ web lisp

The quick demo below shows the kind of exploratory programming available where you can query the scriptMethods available, query an objects props, query the Lisp interpreter's global symbols table containing all its global state including all defined lisp functions, macros and variables:

Annotated REPL Walk through

Here's an annotated version of the demo below which explains what each of the different expressions is doing.

Just like Sharp Scripts and Sharp Apps the Lisp REPL runs within the #Script Pages ScriptContext sandbox that when run from a Sharp App folder, starts a .NET Core App Server that simulates a fully configured .NET Core App. In this case it's running in the redis Sharp App directory where it was able to access its static web assets as well as its redis-server connection configured in its app.settings.

; quick lisp test!
(+ 1 2 3)

; List of ScriptMethodInfo that the ScriptContext running this Lisp Interpreter has access to
scriptMethods

; first script method
(:0 scriptMethods)

; show public properties of ScriptMethodInfo 
(props (:0 scriptMethods))

; show 1 property per line
(joinln (props (:0 scriptMethods)))

; show both Property Type and Name
(joinln (propTypes (:0 scriptMethods)))

; view the Names of all avaialble script methods
(joinln (map .Name scriptMethods))

; view all script methods starting with 'a'
(globln "a*" (map .Name scriptMethods))

; view all script methods starting with 'env'
(globln "env*" (map .Name scriptMethods))

; print environment info about this machine seperated by spaces
(printlns envOSVersion envMachineName envFrameworkDescription envLogicalDrives)

; expand logical drives
(printlns envOSVersion envMachineName envFrameworkDescription "- drives:" (join envLogicalDrives " "))

; view all current global symbols defined in this Lisp interpreter
symbols

; view all symbols starting with 'c'
(globln "c*" symbols)

; see how many symbols are defined in this interpreter
(count symbols)

; see how many script methods there are available
(count scriptMethods)

; view the method signature for all script methods starting with 'all'
(globln "all*" (map .Signature scriptMethods))

; count all files accessible from the configured ScriptContext
(count allFiles)

; view the public properties of the first IVirtualFile
(props (:0 allFiles))

; display the VirtualPath of all available files
(joinln (map .VirtualPath allFiles))

; display the method signature for all script methods starting with 'findFiles'
(globln "findFiles*" (map .Signature scriptMethods))

; see how many .html files are available to this App
(count (findFiles "*.html"))

; see how many .js files are available to this App
(count (findFiles "*.js"))

; show the VirtualPath of all .html files
(joinln (map .VirtualPath (findFiles "*.html")))

; view the VirtualPath's of the 1st and 2nd .html files
(:0 (map .VirtualPath (findFiles "*.html")))
(:1 (map .VirtualPath (findFiles "*.html")))

; view the text file contents of the 1st and 2nd .html files
(fileTextContents (:0 (map .VirtualPath (findFiles "*.html"))))
(fileTextContents (:1 (map .VirtualPath (findFiles "*.html"))))

; display the method signatures of all script methods starting with 'redis'
(globln "redis*" (map .Signature scriptMethods))

; search for all Redis Keys starting with 'urn:' in the redis-server instances this App is configured with
(redisSearchKeys "urn:*")

; display the first redis search entry
(:0 (redisSearchKeys "urn:*"))

; display the key names of all redis keys starting with 'urn:'
(joinln (map :id (redisSearchKeys "urn:*")))

; find out the redis-server data type of the 'urn:tags' key
(redisCall "TYPE urn:tags")

; view all tags in the 'urn:tags' sorted set
(redisCall "ZRANGE urn:tags 0 -1")

; view the string contents of the 'urn:question:1' key
(redisCall "GET urn:question:1")

; parse the json contents of question 1 and display its tag names
(:Tags (parseJson (redisCall "GET urn:question:1")))

; extract the 2nd tag of question 1
(:1 (:Tags (parseJson (redisCall "GET urn:question:1"))))

; clear the Console screen
clear

; exit the Lisp REPL
quit

Enable features and access resources with app.settings

You can configure the Lisp REPL with any of the resources and features that Sharp Apps and Gist Desktop Apps have access to, by creating a plain text app.settings file with all the features and resources you want the Lisp REPL to have access to, e.g. this Pure Cloud App app.settings allows the Lisp REPL to use Database Scripts against a AWS PostgreSQL RDS server and query remote S3 Virtual Files using Virtual File System APIs:

# Note: values prefixed with '$' are resolved from Environment Variables
name AWS S3 PostgreSQL Web App
db postgres
db.connection $AWS_RDS_POSTGRES
files s3
files.config {AccessKey:$AWS_S3_ACCESS_KEY,SecretKey:$AWS_S3_SECRET_KEY,Region:us-east-1,Bucket:rockwind}

See the plugins app.settings for examples of how to load and configure ServiceStack Plugins.

Lisp REPL TCP Server

In addition to launching a Lisp REPL from the Console above, you can also open a Lisp REPL into any ServiceStack App configured with the LispReplTcpServer ServiceStack plugin. This effectively opens a "programmable gateway" into any ServiceStack App where it's able to perform live queries, access IOC dependencies, invoke internal Server functions and query the state of a running Server which like the Debug Inspector can provide invaluable insight when diagnosing issues on a remote server.

To see it in action we'll enable it one of our production Apps techstacks.io which as it's a Vuetify SPA App is only configured with an empty SharpPagesFeature as it doesn't use any server-side scripting features.

We'll enable it in DebugMode where we can enable by setting DebugMode in our App's appsettings.Production.json which will launch a TCP Socket Server which by default is configured to listen to the loopback IP on port 5005.

if (Config.DebugMode)
{
    Plugins.Add(new LispReplTcpServer {
        ScriptMethods = {
            new DbScripts()
        },
        ScriptNamespaces = {
            nameof(TechStacks),
            $"{nameof(TechStacks)}.{nameof(ServiceInterface)}",
            $"{nameof(TechStacks)}.{nameof(ServiceModel)}",
        },
    });
}

ScriptNamespaces behaves like C#'s using Namespace; statement letting you reference Types by Name instead of its fully-qualified Namespace.

Whilst you can now connect to it with basic telnet, it's a much nicer experience to use it with the rlwrap readline wrap utility which provides an enhanced experience with line editing, persistent history and completion.

$ sudo apt-get install rlwrap

Then you can open a TCP Connection to connect to a new Lisp REPL with:

$ rlwrap telnet localhost 5005

Where you now have full scriptability of the running server as allowed by #Script Pages SharpPagesFeature which allows scripting of all .NET Types by default.

TechStacks TCP Lisp REPL Demo

In this demo we'll explore some of the possibilities of scripting the live techstacks.io Server where we can resolve IOC dependencies to send out tweets using its registered ITwitterUpdates dependency, view the source and load a remote parse-rss lisp function into the new Lisp interpreter attached to the TCP connection, use it to parse Hacker News RSS Feed into a .NET Collection where it can be more easily queried using its built-in functions which is used to construct an email body with HN's current Top 5 links.

It then uses DB Scripts to explore its configured AWS RDS PostgreSQL RDBMS, listing its DB tables and viewing its column names and definitions before retrieving the Email addresses of all Admin users, sending them each an email with HN's Top 5 Links by publishing 5x SendEmail Request DTOs using the publishMessage ServiceStack Script to where they're processed in the background by its configured MQ Server that uses it to execute the SendEmail ServiceStack Service where it uses its configured AWS SES SMTP Server to finally send out the Emails:

Password Protection

Since TCP Server effectively opens your remote Server up to being scripted you'll want to ensure the TCP Server is only accessible within a trusted network, effectively treating it the same as Redis Security Model.

A secure approach would be to leave the default of only binding to IPAddress.Loopback so only trusted users with SSH access will be able to access it, which they'll still be able to access remotely via Local PC > ssh > telnet 127.0.0.1 5005.

Just like Redis AUTH you can also add password protection for an additional layer of Security:

Plugins.Add(new LispReplTcpServer {
    RequireAuthSecret = true,
    ...
});

Which will only allow access to users with the configured AuthSecret:

SetConfig(new HostConfig { 
    AdminAuthSecret = "secretz" 
});

Annotated Lisp TCP REPL Transcript

; resolve `ITwitterUpdates` IOC dependency and assign it to `twitter`
(def twitter (resolve "ITwitterUpdates"))

; view its concrete Type Name
(typeName twitter)

; view its method names 
(joinln (methods twitter))

; view its method signatures 
(joinln (methodTypes twitter))

; use it to send tweet from its @webstacks account
(.Tweet twitter "Who's using #Script Lisp? https://sharpscript.net/lisp")

; view all available scripts in #Script Lisp Library Index gist.github.com/3624b0373904cfb2fc7bb3c2cb9dc1a3
(gistindex)

; view the source code of the `parse-rss` library
(load-src "index:parse-rss")

; assign the XML contents of HN's RSS feed to `xml`
(def xml (urlContents "https://news.ycombinator.com/rss"))

; preview its first 1000 chars
(subString xml 0 1000)

; use `parse-rss` to parse the RSS feed into a .NET Collection and assign it to `rss`
(def rss (parse-rss xml))

; view the `title`, `description` and the first `item` in the RSS feed:
(:title rss)
(:description rss)
(:0 (:items rss))

; view the links of all RSS feed items
(joinln (map :link (:items rss)))

; view the links and titles of the top 5 news items
(joinln (map :link (take 5 (:items rss))))
(joinln (map :title (take 5 (:items rss))))

; construct a plain-text numbered list of the top 5 HN Links and assign it to `body`
(joinln (map-index #(str %2 (:title %1)) (take 5 (:items rss))))
(joinln (map-index #(str (padLeft (1+ %2) 2) ". " (:title %1)) (take 5 (:items rss))))
(def body (joinln 
    (map-index #(str (padLeft (1+ %2) 2) ". " (:title %1) "\n" (:link %1) "\n") (take 5 (:items rss)))))

; view all TechStacks PostgreSQL AWS RDS tables
(dbTableNames)
(joinln dbTableNames)

; view the column names and definitions of the `technology` table
(joinln (dbColumnNames "technology"))
(joinln (dbColumns "technology"))

; search for all `user` tables
(globln "*user*" (dbTableNames))

; view how many Admin Users with Emails there are
(dbScalar "select count(email) from custom_user_auth where roles like '%Admin%'")

; assign the Admin Users email to the `emails` list
(def emails (map :email (dbSelect "select email from custom_user_auth where roles like '%Admin%'")))

; search for all `operation` script methods
(globln "*operation*" scriptMethods)

; search for all `email` Request DTOs
(globln "*email*" metaAllOperationNames)

; view the properties available on the `SendEmail` Request DTO
(props (SendEmail.))

; search for all `publish` script methods that can publish messages
(globln "publish*" scriptMethods)

; create and publish 5x `SendEmail` Request DTOs for processing by TechStacks configured MQ Server
(doseq (to emails) (publishMessage "SendEmail" { :To to :Subject "Top 5 HN Links" :Body body }))

Run and watch Lisp Scripts

The same Sharp Scripts functionality for #Script is also available to Lisp scripts where you can use the web and app dotnet tools to run and watch stand-alone Lisp scripts with the .l file extension, e.g:

$ web run lisp.l
$ web watch lisp.l

To clarify the behavioural differences between the Lisp REPL's above which uses the same Lisp interpreter to maintain state changes across each command, the watch Script is run with a new Lisp Interpreter which starts with a fresh copy of the Global symbols table so any state changes after each Ctrl+S save point is discarded.

Watch lisp scripts

This quick demo illustrates the same functionality in Sharp Scripts is also available in lisp scripts where it provides instant feedback whilst you develop in real-time:

Annotated Lisp watch script

;<!--
; db sqlite
; db.connection northwind.sqlite
; files s3
; files.config {AccessKey:$AWS_S3_ACCESS_KEY,SecretKey:$AWS_S3_SECRET_KEY,Region:us-east-1,Bucket:rockwind}
;-->

; delete remove.txt file
(sh (str (if isWin "del" "rm") " remove.txt"))

; View all `northwind.sqlite` RDBMS Tables
(textDump (dbTableNames) { :caption "Northwind" } )

; Display first `customer` row in Single Row View showing all Table Columns
(textDump (dbSelect "select * from customer limit 1"))

; Display all Customers in London
(def city "London")
(textDump (dbSelect "select Id, CompanyName, ContactName from customer where city = @city" { :city city } ))

; View all root files and folders in configured S3 Virtual File Provider
(joinln (map #(str (.Name %) "/") (allRootDirectories vfsContent)))
(joinln (map .Name (allRootFiles vfsContent)))

; Show first 10 *.png files in S3 VFS Provider
(def pattern (or (first ARGV) "*.png"))
(joinln (map .VirtualPath (take 10 (findFiles vfsContent pattern))))

Page Arguments

You can also use the same syntax for declaring any app.settings page arguments used in #Script and code Scripts:

<!--
 db sqlite
 db.connection northwind.sqlite
-->

But for compatibility with any Lisp syntax highlighters and code editors they can also be prefixed with a ; line comment as seen above.

Executing Lisp in .NET

Lisp like all #Script languages are executed within a ScriptContext that defines all functionality available to them, i.e:

var context = new ScriptContext {
    Args = { ... },              // Global Arguments available to all Scripts, Pages, Partials, etc
    ScriptMethods = { ... },     // Additional Methods
    ScriptBlocks = { ... },      // Additional Script Blocks 
    FilterTransformers = { .. }, // Additional Stream Transformers
    PageFormats = { ... },       // Additional Text Document Formats
    Plugins = { ... },           // Encapsulated Features e.g. Markdown, Protected or ServiceStack Features

    ScanTypes = { ... },         // Auto register Methods, Blocks and Code Page Types
    ScanAssemblies = { ... },    // Auto register all Methods, Blocks and Code Page Types in Assembly
}.Init();

Where you can customize the pure sandboxed ScriptContext your Script is executed within by extending it with:

To render lisp you'll first need to register the Lisp Language with the ScriptContext you're using:

var context = new ScriptContext {
    ScriptLanguages = { ScriptLisp.Language }
}.Init();

Then use RenderLisp (i.e. instead of RenderScript) to render Lisp code, e.g:

// render lisp
var output = context.RenderLisp("(dateFormat now \"HH:mm:ss\")"); 

// async
var output = await context.RenderLispAsync("(dateFormat now \"HH:mm:ss\")"); 

These APIs match the high-level APIs for rendering normal #Script:

var output = context.RenderScript("{{ now |> dateFormat('HH:mm:ss') }}"); 
var output = await context.RenderScriptAsync("{{ now |> dateFormat('HH:mm:ss') }}"); 

Finer grained control

The high-level APIs above wraps the finer-grained functionality below which works by rendering a SharpPage configured with the lisp language in a PageResult that all languages use:

var context = new ScriptContext {
    ScriptLanguages = { ScriptLisp.Language }
}.Init();
var dynamicPage = context.LispSharpPage("(dateFormat now \"HH:mm:ss\")");          // render lisp
//var dynamicPage = context.SharpScriptPage("{{ now |> dateFormat('HH:mm:ss') }}"); // render #Script
var output = new PageResult(dynamicPage).RenderScript();

//async
var output = await new PageResult(dynamicPage).RenderScriptAsync();

Evaluating Lisp Script Results

If you instead wanted to access return values instead of its rendered output, use the EvaluateLisp() APIs:

var result = context.EvaluateLisp("(return (+ 1 1))"); //= 2

The generic overloads below utilizes ServiceStack's Auto Mapping utils to convert the return value into your preferred type, e.g:

double result = context.EvaluateLisp<double>("(return (+ 1 1))"); //= 2.0
string result = context.EvaluateLisp<string>("(return (+ 1 1))"); //= "2"

Which can also be used for more powerful conversions like converting an Object Dictionary into your preferred POCO:

var result = context.EvaluateLisp<Customer>(
    "(return (dbSingle \"select * from customer where id=@id\" { :id id }))",
    new ObjectDictionary {
        ["id"] = 1
    });

.NET Interop

The syntax for .NET Interop is inspired directly from Clojure's syntax used for Java Interop. See Scripting .NET Type Resolution for how to configure Types and imported Namespaces you want your Lisp scripts to have access to.

Member Access

The '.' prefix if for accessing an instance members which can be used for retrieving a properties public properties, fields and invoking instance methods, e.g:

Indexer Access

Use ':' prefix for accessing a Types indexer or for indexing collections, e.g:

It can also be used to access an instance public Properties and Fields:

However for readability we recommend using '.' prefix above to convey instance member access.

Constructor Access

Use '.' suffix for creating instances of Types:

You can also create instances using the new script method, which as it accepts a string Type Name can be used to create generic classes with multiple generic args, e.g:

Static Member Access

Use the '/' separator to access a Type's static members or to invoke its static methods, e.g:

Use '.' dot notation for specifying the fully-qualified Type name or to reference its Inner classes, e.g:

Script Methods

Use '/' prefix to reference any Script Method registered in your ScriptContext:

Script Methods without arguments can be referenced as an argument binding that when referenced as an argument (i.e. without brackets) are implicitly evaluated, in-effect making them a calculated property:

While a '/' prefix indicates a reference to a script method, for readability it can be excluded as when there's no existing symbol defined in the Lisp interpreter's symbol table it will fallback to referencing a script method:

This does mean that when there exists a symbol of the same name defined you will need to use the '/' prefix to reference a script method.

Generic Types

All references above support referencing generic types and methods with a single generic Type argument, e.g:

As ',' is one of Lisp's few syntax tokens (unquoting) it prevents them from being able to use them to specify multiple generic arguments.

Instead you'll need to use the Constructor function for referencing constructors with multiple generic arguments where you'll also need to specify the types of the exact constructor you want to call, e.g:

The difference between the /C script method constructor function and Lisp's C function is that the script method only returns a reference to the constructor which you'll need to invoke with arguments to create an instance:

Whilst Lisp's C function will auto-invoke the constructor function with the supplied arguments in a single expression:

Likewise when needing to invoke generic methods with multiple generic args you'll need to use Function:

Or Script Lisp's F function for invoking a function reference in a single expression:

For more examples and information see Scripting .NET Types.

Property Setters

You can populate multiple properties on an instance using the set script method, e.g:

Alternatively properties can be set individually with:

Lisp Lists vs .NET Collections

A potential source of friction when interoperating with .NET is that Lisp Lists are Cons Cells so that a code or data list in Lisp, i.e:

'(1 2 3)
[1 2 3]

Is implemented as a Linked List of Cons cells:

(1 . (2 . (3 . null)))

Which is what Lisp's core functions expect to operate on, namely:

car cdr caar cadr cdar cddr caaar caadr cadar caddr cdaar cdadr cddar cdddr append mapcar consp cons?
listp list? memq member assq assoc nreverse last nconc dolist dotimes mapcan mapc nthcdr nbutlast 

These core Lisp functions can't be used against .NET collections directly, instead you can use (to-cons collection) to convert a .NET IEnumerable collection into a cons list, e.g:

(cdr (to-cons netEnumerable))

Should you need to do the inverse you can use (to-list cons-list) to convert a cons list to a .NET List, e.g:

(to-list (range 10))

We've made Script Lisp's cons Cell an IEnumerable so that all other built-in Lisp functions can operate on both cons cells and .NET Collections where instead of iterating a list with (do-list) you can use (do-seq) to iterate both .NET Collections and cons cells, e.g:

(do-seq (x collection) (println x) )

Annotated .NET Interop Example

To see what this looks like in action here's an annotated simple real-world example that heavily utilizes .NET interop:

; define function and assign to `parse-rss` value in Lisp interpreters symbols table
(defn parse-rss [xml]
    ; define local variables used within this scope
    (let ( (to) (doc) (channel) (items) (el) )
        ; use XDocument.Parse() to parse xml string argument containing xml and assign to `doc`
        (def doc (System.Xml.Linq.XDocument/Parse xml))
        ; create empty ObjectDictionary (wrapper for Dictionary<string,object>) and assign to `to`
        (def to  (ObjectDictionary.))
        ; create empty List of ObjectDictionary and assign to `items`
        (def items (List<ObjectDictionary>.))
        ; descend into first <channel> XML element and assign to `channel`
        (def channel (first (.Descendants doc "channel")))
        ; use `XLinqExtensions.FirstElement()` extension method to assign channels first XML element to `el`
        (def el  (XLinqExtensions/FirstElement channel))

        ; iterate through all elements up to the first <item> and add them as top-level entries in `to`
        (while (not= (.LocalName (.Name el)) "item")
            ; add current XML element name and value entry to `to`
            (.Add to (.LocalName (.Name el)) (.Value el))
            ; move to next element using `XLinqExtensions.NextElement()` extension method
            (def el (XLinqExtensions/NextElement el)))

        ; add all rss <item>'s to `items` list
        ; iterate through all `channel` child <item> XML elements
        (doseq (elItem (.Descendants channel "item"))
            ; create empty ObjectDictionary and assign to `item`
            (def item (ObjectDictionary.))
            
            ; use `XLinqExtensions.FirstElement()` to assign <item> first XML element to `el`
            (def el (XLinqExtensions/FirstElement elItem))
            (while el
                ; add current XML element name and value entry to `item`
                (.Add item (.LocalName (.Name el)) (.Value el))
                ; move to next element using `XLinqExtensions.NextElement()` extension method
                (def el (XLinqExtensions/NextElement el)))

            ; add `item` ObjectDictionary to `items` List
            (.Add items item))

        ; add `items` ObjectDictionary List to `to` at key `items`
        (.Add to "items" items)
        ; return `to` ObjectDictionary
        to
    )
)

For comparison, this would be the equivalent implementation in C#:

public static ObjectDictionary ParseRss(string xml)
{
    var to = new ObjectDictionary();
    var items = new List<ObjectDictionary>();

    var doc = XDocument.Parse(xml);
    var channel = doc.Descendants("channel").First();
    var el = channel.FirstElement();
    while (el.Name != "item")
    {
        to[el.Name.LocalName] = el.Value;
        el = el.NextElement();
    }

    var elItems = channel.Descendants("item");
    foreach (var elItem in elItems)
    {
        var item = new ObjectDictionary();
        el = elItem.FirstElement();
        while (el != null)
        {
            item[el.Name.LocalName] = el.Value;
            el = el.NextElement();
        }

        items.Add(item);
    }

    to["items"] = items;
    return to;
}

Importing Global Scripts

Importing scripts in Lisp is essentially a 2-stage process of parsing Lisp source code into an SExpression, (basically Lisp's AST of tokenized elements captured in a 2-field Cons Cell) then evaluating it in a Lisp interpreter where any defined symbols are captured in its Symbols table.

Lisp Script captures its "standard library" in a Global Interpreter which serves as the starting template for all other Lisp Interpreters which starts with a copy of the Global symbols table which you can further populate with your own common functions using Lisp.Import(), e.g:

Lisp.Import(@"
(defun fib (n)
    (if (< n 2)
        1
        (+ (fib (- n 1))
        (fib (- n 2)) )))");

Loading Scripts

Loading scripts within a Lisp script works similarly except they're only loaded into that Lisp interpreters symbol table, a new one of which is created in each new PageResult.

Scripts loaded locally are loaded from the ScriptContext configured Virtual Files Provider which for #Script Pages SharpPagesFeature is configured to use the App's cascading virtual file sources.

A new ScriptContext starts with an empty MemoryVirtualFiles which you can write files to with:

var context = new ScriptContext {
    ScriptLanguages = { ScriptLisp.Language },
    ScriptMethods = { new ProtectedScripts() },
};
context.VirtualFiles.WriteFile("lib1.l", "(defn lib-calc [a b] (+ a b))");
context.VirtualFiles.WriteFile("/dir/lib2.l", "(defn lib-calc [a b] (* a b))");
context.Init();

You can load these scripts by symbol name where it assumes a .l extension, by quoting the argument so Lisp doesn't try to evaluate it as an argument, e.g:

(load 'lib1)

(lib-calc 4 5) ;= 9

Alternatively you can specify the virtual path to the script. You can load multiple scripts with the same definitions, in Lisp this just updates the value assigned to the symbol name with the latest definition, e.g:

(load "lib1.l")

(lib-calc 4 5) ;= 9

(load "/dir/lib2.l")

(lib-calc 4 5) ;= 20

Import Scripts from URLs

Inspired by Deno you can also import remote scripts from URLs, e.g:

(load "https://example.org/lib.l")

Locally Cached

Like Deno all remote resources are cached after first use so after it's loaded once it only loads the locally cached version of the script (where it will still work in an airplane without an internet connection). This cache is maintained under a .lisp folder at your configured Virtual Files provider (that can be deleted to clear any caches).

For Sharp Scripts or Apps using the web or app dotnet tools it's stored in its own cache folder that can be cleared with:

$ web --clean

Import Scripts from Gists

There's also first-class support for gists which you can reference with gist:<gist-id>, e.g:

(load "gist:2f14d629ba1852ee55865607f1fa2c3e")

This will load all gist files in gist order, if you only to load a single file from a gist you can specify it with:

(load "gist:2f14d629ba1852ee55865607f1fa2c3e/lib1.l")

Script Lisp Library Index

To provide human readable names to remote Lisp Scripts and a discoverable catalog where anyone can share their own scripts, you reference gists by name listed in the #Script Lisp Library Index which is itself a self-documenting machine and human readable gist of named links to external gists maintained by their respective authors.

Index library references can be loaded using the format index:<name>, e.g:

(load "index:lib-calc")

Which also support being able to reference individual gist files:

(load "index:lib-calc/lib1.l")

If you'd like to share your own Lisp Scripts with everyone and publish your library to the index, just add a link to your gist with your preferred name in the Gist Index Comments.

Viewing Script Source Code

You can view the source code of any load script references with load-src, e.g:

(load-src 'lib)
(load-src "/dir/lib2.l")
(load-src "https://example.org/lib.l")
(load-src "gist:2f14d629ba1852ee55865607f1fa2c3e/lib1.l")
(load-src "index:lib-calc")

Disable Remote Imports

Should you wish, you can prevent anyone from loading remote scripts with:

Lisp.AllowLoadingRemoteScripts = false;

#Script Pages Integration

Whilst Lisp is a small, powerfully expressive functional dynamic language it's not great for use as a templating language. Whilst there have been several attempts to create a HTML DSL in Lisp, nothing is better than having no syntax which is the default Template mode for #Script where it will emit everything that's not in a Template or Language Expression as verbatim text.

A nice USP of Script Lisp is that you're never forced into going "full Lisp", you can utilize #Script template expressions and Script Blocks handlebars syntax that provides the ideal DSL for usage in a Template Language for generating HTML and utilize your preferred Lisp or Code Script Languages for any computational logic you want included in your page using Language Blocks and Expressions.

Implementation

Despite being implemented in different languages a #Script page containing multiple languages, e.g:

Still only produces a single page AST, where when first loaded #Script parses the page contents as a contiguous ReadOnlyMemory<char> where page slices of any Language Blocks and Expressions on the page are delegated to the ScriptContext registered ScriptLanguages for parsing which returns a fragment which is added to the pages AST:

When executing the page, each language is responsible for rendering its own fragments which all write directly to the pages OutputStream to generate the pages output.

The multi-languages support in #Script is designed to be extensible where everything about the language is encapsulated within its ScriptLanguage implementation so that if you omit its registration:

var context = new ScriptContext {
//    ScriptLanguages = { ScriptLisp.Language }
}.Init();

Any language expressions and language blocks referencing it become inert and its source code emitted as plain-text.

Lisp Argument Scopes

One differentiator between Lisp and Code languages is that code utilizes the containing page current scope for all its argument references where as Lisp stores all its definitions within the Lisp interpreter symbol table attached to the PageResult, so whilst Lisp scripts can access arguments within the pages scope, in order for the outer page to access any Lisp symbols they need to be exported, e.g:

Exporting Lisp Functions

Lisp functions can also be exported for usage in the rest of the page by calling (to-delegate lispFn) to convert it into a .NET Delegate, e.g:

Although an easier way to define functions in Lisp is to use the defn Script Block which wraps this in a convenient syntax:

Controlling Lisp output

One of Lisp's famous traits is everything is an expression which is typically desired within a language, but may not be what you want when generating output. E.g traditionally Lisp uses setq to set a variable which also returns its value that #Script will emit as it automatically emits all statement return values.

You could use def which is an alias for setq which returns null, other options include wrapping all statements within an empty let expression where only the last expression is returned, or you could use a Language Block Modifier to ignore the entire lisp block output and only export the result you want to be able to control precisely where to emit it:

can use either 'q', 'quiet' or 'mute' block modifier to ignore output

Another way to generate output from Lisp is to use its built-in print functions below:

Learn #Script Lisp

A great resource for learning Script Lisp is seeing it in action by seeing how to implement C#'s 101 LINQ Examples in Lisp:

Explore APIs in real-time

We can also take advantage of Lisp's dynamism and interactivity to explore APIs in real-time, a great way to do this is via a watched Lisp script on the side where it provides instant feedback after each Ctrl+S save point or a active Lisp REPL.

You can view the Scripts API Reference and Scripts Documentation on this website to interactively explore the available APIs, we'll work on providing further interactive documentation for the built-in Lisp functions, in the mean-time the best resource are their implementation.

For reference, here's are a quick list of all built-in Lisp symbols:

- % * *gensym-counter* *version* / /= _append _nreverse + < <= = > >= 1- 1+ 1st 2nd 3rd abs all? and any? 
append apply assoc assoc-key assoc-value assq atom atom? average butlast C caaar caadr caar cadar caddr 
cadr car cdaar cdadr cdar cddar cdddr cddr cdr ceiling cons cons? consp cos count debug dec decf def 
defmacro defn defun dispose dolist doseq doseq-while dotimes dump dump-inline elt empty? end? endp 
enumerator enumeratorCurrent enumeratorNext eq eql equal error even? every every? exit exp expt F f++ 
false filter filter-index first flatmap flatten floor gensym glob globln group-by htmldump identity if 
inc incf instance? intern isqrt it last length let letrec list list? listp load load-src logand logior 
logxor lower-case make-symbol map mapc mapcan mapcar map-index map-where max member memq min mod nbutlast 
nconc new new-map next not not= nreverse nth nthcdr null number? odd? or order-by pop pr prin1 princ 
print println printlns prn prs push push-end random range reduce remove remove-if rest return reverse 
round rplaca rplacd second seq? setcar setcdr set-difference sets sin skip skip-while some some? sort 
sort-by sqrt str string string? string-downcase string-upcase subseq sum symbol-name symbol-type t take 
take-while tan terpri textdump third to-array to-cons to-delegate to-dictionary to-list true truncate 
union unless upper-case when where where-index while zero? zerop zip zip-where

Common Lisp by convention uses a *p suffix for predicate functions but we prefer Clojure's (and Ruby's) more readable *? suffix convention, for source-code compatibility we include both for core Lisp predicts and just *? for others.

made with by ServiceStack