mirror of https://github.com/microsoft/schemy.git
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
parent
91ab6ed543
commit
8a5a33e1a7
114
README.md
114
README.md
|
@ -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
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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[]
|
||||
{
|
||||
"-----------------------------------------------",
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
; `.init.ss` is picked up by interpreter automatically
|
||||
(define square (lambda (x) (* x x)))
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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");
|
||||
|
|
|
@ -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
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue