Update documents; Minor code fixes.

- Interpreter now defaults to use `DisablesFileSystemAccessor` - i.e.,
  file system IO is disabled by default.
- Provide motivation for using Schemy.
- Provide docs on building & virtual file system.
pull/2/head
Kefei Lu 2018-04-02 10:17:39 -07:00
parent 91ab6ed543
commit 8a5a33e1a7
10 changed files with 131 additions and 128 deletions

114
README.md
View File

@ -11,7 +11,7 @@ Its design goals are:
* easy to embed and extend in .NET
* extensible in Scheme via macro expansion
* safe without the need of complicated AppDomain sandboxing. It's safe because
IO functions are not implemented.
IO functions are not exposed by default
* runs reasonably fast and low memory footprint
Non-goals:
@ -26,12 +26,12 @@ interpreter][lispy], but is heavily adapted to .NET and engineered to be easily
extensible and embeddable in .NET applications.
## Scheme Features
## Language Features
It has most features that a language would support:
* number, boolean, string, list types
* varaible, function definition
* variable, function definition
* tail call optimization
* macro definition
* lexical scoping
@ -43,6 +43,62 @@ Many Scheme features are not (yet) supported. Among those are:
* use square brackets `[...]` in place of parenthesis `(...)`
## Why Schemy
Schemy was originally designed at Microsoft 365 to define complex machine
learning model workflows that handle web API requests. Since Schemy scripts
can be easier to develop, modify, and deploy than full fledged .NET
application or modules, the development and maintenance become more agile, and
concerns are better separated - request routing is handled by web server,
request handling logics are defined by Schemy scripts. The
[`command_server`](src/examples/command_server/) example application captures
this design in a very simplified way.
More generically, for applications that require reading configuration, the
usual resort is to configuration languages like JSON, XML, YAML, etc. While
they are simple and readable, they may not be suitable for more complex
configuration tasks, when dynamic conditioning, modularization, reusability
are desired. For example, when defining a computational graph whose components
depend on some runtime conditions, and when the graph is desirable to be
composed of reusable sub-graphs.
The other end of the spectrum is to use full fledged scripting languages like
Python (IronPhython), Lua (NLua), etc. While they are more flexible and
powerful, footprint for embedding them can be heavy, and they pull in
dependencies to the host application.
Schemy can be seen as sitting in the middle of the spectrum:
- It provides more languages features for conditioning,
modularization/reusability (functions, scripts), customization (macro),
then simple configuration languages.
- It's simpler in implementation (~1500 lines of code) and doesn't pull in
extra dependencies and have a smaller footprint to the host application.
- It can be safer in the sense that it doesn't provide access to file system
by default. Although we run it in fully trusted environment, this could be
useful as it doesn't need to be sandboxed. (IO is supported by [virtual
file system](#virtualfs).)
## Build
Simply run `msbuild` in `src/`, or use your favorite IDE.
The project structure looks like so:
├───doc
├───src
│ ├───schemy // the core schemy interpreter (schemy.dll)
│ ├───examples
│ │ ├───command_server // loading command handlers from schemy scripts
│ │ └───repl // an interactive interpreter (REPL)
│ └───test
└───tools
## Embedding and Extending Schemy
Schemy is primarily designed to be embedded into a .NET application for
@ -68,7 +124,9 @@ must implement `ICallable`.
An example procedure implementation:
new NativeProcedure(args => args, "list");
```csharp
new NativeProcedure(args => args, "list");
```
This implements the Scheme procedure `list`, which converts its arguments
into a list:
@ -76,6 +134,16 @@ into a list:
schemy> (list 1 2 3 4)
(1 2 3 4)
`NativeProcedure` has convenient factory methods that handles input type
checking and conversion, for example, `NativeProcedure.Create<double, double, bool>()`
takes a `Func<double, double bool>` as the implementation, and handles the
argument count checking (must be 2) and type conversion (`(double, double) -> bool`):
```csharp
builtins[Symbol.FromString("=")] = NativeProcedure.Create<double, double, bool>((x, y) => x == y, "=");
```
To "register" extensions, one can pass them to the `Interpreter`'s
constructor:
@ -151,7 +219,9 @@ The following macro implements the `let` form by using lambda invocation:
## Use Interactively (REPL)
The interpreter can be run interactively, when given a `TextReader` for input
and a `TextWriter` for output.
and a `TextWriter` for output. The flexibility of this interface means you can
not only expose the REPL via stdin/stdout, but also any streamable channels,
e.g., a socket, or web socket (please consider security!).
```csharp
/// <summary>Starts the Read-Eval-Print loop</summary>
@ -193,6 +263,40 @@ Run a script:
<a id="virtualfs"></a>
## Virtual File System
The interpreter's constructor takes a `IFileSystemAccessor`:
```csharp
public interface IFileSystemAccessor
{
/// <summary>
/// Opens the path for read
/// </summary>
/// <param name="path">The path</param>
/// <returns>the stream to read</returns>
Stream OpenRead(string path);
/// <summary>
/// Opens the path for write
/// </summary>
/// <param name="path">The path</param>
/// <returns>the stream to write</returns>
Stream OpenWrite(string path);
}
```
There're two builtin implementations: a `DisabledFileSystemAccessor`, which
blocks read/write, a `ReadOnlyFileSystemAccessor`, which provides readonly to
local file system. The default behavior for an interpreter is
`DisabledFileSystemAccessor`.
In addition to them, you can implement your own file system accessors. For
example, you could implement it to provide access into a Zip archive, treating
each zip archive entry as a file in a file system.
# Contributing
This project welcomes contributions and suggestions. Most contributions require you to agree to a

View File

@ -31,7 +31,7 @@ namespace Examples.command_server
{ Symbol.FromString("truncate-string"), NativeProcedure.Create<int, Function>(len => input => ((string)input).Substring(0, len)) },
};
var interpreter = new Interpreter(new[] { extension });
var interpreter = new Interpreter(new[] { extension }, new ReadOnlyFileSystemAccessor());
if (args.Contains("--repl")) // start the REPL with all implemented functions
{

View File

@ -14,7 +14,7 @@ namespace Schemy
{
// evaluate input file's content
var file = args[0];
var interpreter = new Interpreter();
var interpreter = new Interpreter(null, new ReadOnlyFileSystemAccessor());
using (TextReader reader = new StreamReader(file))
{
@ -25,7 +25,7 @@ namespace Schemy
else
{
// starts the REPL
var interpreter = new Interpreter();
var interpreter = new Interpreter(null, new ReadOnlyFileSystemAccessor());
var headers = new[]
{
"-----------------------------------------------",

View File

@ -1,2 +0,0 @@
; `.init.ss` is picked up by interpreter automatically
(define square (lambda (x) (* x x)))

View File

@ -1,41 +0,0 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
namespace Schemy
{
using System;
using System.IO;
public static class Program
{
public static void Main(string[] args)
{
if (args.Length > 0 && File.Exists(args[0]))
{
// evaluate input file's content
var file = args[0];
var interpreter = new Interpreter();
using (TextReader reader = new StreamReader(file))
{
object res = interpreter.Evaluate(reader);
Console.WriteLine(Utils.PrintExpr(res));
}
}
else
{
// starts the REPL
var interpreter = new Interpreter();
var headers = new[]
{
"-----------------------------------------------",
"| Schemy - Scheme as a Configuration Language |",
"| Press Ctrl-C to exit |",
"-----------------------------------------------",
};
interpreter.REPL(Console.In, Console.Out, "Schemy> ", headers);
}
}
}
}

View File

@ -1,67 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{56DADEFC-6B30-4C0F-AAEA-23D684BD817F}</ProjectGuid>
<OutputType>Exe</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Schemy</RootNamespace>
<AssemblyName>schemy.repl</AssemblyName>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<TargetFrameworkProfile />
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Program.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\schemy\schemy.csproj">
<Project>{e54139b7-cb81-4883-b8cd-40bab5420eb8}</Project>
<Name>schemy</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Folder Include="Properties\" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
-->
<Target Name="AfterBuild">
<Copy SourceFiles=".init.ss" DestinationFiles="$(TargetDir).init.ss" />
</Target>
</Project>

View File

@ -45,9 +45,6 @@ namespace Schemy
builtins[Symbol.CONS] = NativeProcedure.Create<object, List<object>, List<object>>((x, ys) => Enumerable.Concat(new[] { x }, ys).ToList(), "cons");
builtins[Symbol.FromString("not")] = NativeProcedure.Create<bool, bool>(x => !x, "not");
builtins[Symbol.APPEND] = NativeProcedure.Create<List<object>, List<object>, List<object>>((l1, l2) => Enumerable.Concat(l1, l2).ToList(), "append");
builtins[Symbol.FromString("null?")] = NativeProcedure.Create<object, bool>(x => x is List<object> && ((List<object>)x).Count == 0, "null?");
builtins[Symbol.FromString("assert")] = new NativeProcedure(AssertImpl, "assert");
builtins[Symbol.FromString("load")] = NativeProcedure.Create<string, None>(filename => LoadImpl(interpreter, filename), "load");
builtins[Symbol.FromString("null")] = NativeProcedure.Create<object>(() => (object)null, "null");
builtins[Symbol.FromString("null?")] = NativeProcedure.Create<object, bool>(x => x is List<object> && ((List<object>)x).Count == 0, "null?");
builtins[Symbol.FromString("assert")] = new NativeProcedure(AssertImpl, "assert");

View File

@ -32,10 +32,22 @@ namespace Schemy
Stream OpenWrite(string path);
}
/// <summary>
/// An implementation of <see cref="IFileSystemAccessor"/> that grants readonly access to the host file system.
/// </summary>
/// <seealso cref="Schemy.IFileSystemAccessor" />
/// <summary>An implementation of <see cref="IFileSystemAccessor"/> that blocks read/write.</summary>
/// <remarks>This is the default behavior for an interpreter.</remarks>
public class DisabledFileSystemAccessor : IFileSystemAccessor
{
public Stream OpenRead(string path)
{
throw new InvalidOperationException("File system access is blocked by DisabledFileSystemAccessor");
}
public Stream OpenWrite(string path)
{
throw new InvalidOperationException("File system access is blocked by DisabledFileSystemAccessor");
}
}
/// <summary>An implementation of <see cref="IFileSystemAccessor"/> that grants readonly access to the host file system.</summary>
public class ReadOnlyFileSystemAccessor : IFileSystemAccessor
{
public Stream OpenRead(string path)
@ -49,4 +61,3 @@ namespace Schemy
}
}
}

View File

@ -28,7 +28,7 @@ namespace Schemy
this.fsAccessor = fsAccessor;
if (this.fsAccessor == null)
{
this.fsAccessor = new ReadOnlyFileSystemAccessor();
this.fsAccessor = new DisabledFileSystemAccessor();
}
// populate an empty environment for the initializer to potentially work with

View File

@ -5,6 +5,7 @@
{
using System;
using System.IO;
using System.Reflection;
using Schemy;
class Program
@ -12,7 +13,7 @@
static void Main(string[] args)
{
var interpreter = new Interpreter(fsAccessor: new ReadOnlyFileSystemAccessor());
using (var reader = new StreamReader(File.OpenRead("tests.ss")))
using (var reader = new StreamReader(File.OpenRead(Path.Combine(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), "tests.ss"))))
{
var result = interpreter.Evaluate(reader);
if (result.Error != null)