Initial port of Schemy code base to Github

Migrate Schemy code base from Microsoft internal repository to
Github.

Schemy is a lightweight, embeddable Scheme-like language interpreter. It
is open sourced under the MIT license (see LICENSE). For any
comment/issue, please contact author kefei.lu@microsoft.com.
pull/2/head
Kefei Lu 2018-04-02 00:39:16 -07:00
parent 155be4d717
commit 1e98c1b4c7
33 changed files with 2773 additions and 286 deletions

290
.gitignore vendored
View File

@ -1,288 +1,6 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
# Visual Studio 2015 cache/options directory
bin/
obj/
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUNIT
*.VisualState.xml
TestResult.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
**/Properties/launchSettings.json
*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding add-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# TODO: Comment the next line if you want to checkin your web deploy settings
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
**/packages/*
# except build/, which is used as an MSBuild target.
!**/packages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/packages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/
# Others
ClientBin/
~$*
*.swp
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Typescript v1 declaration files
typings/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# JetBrains Rider
.idea/
*.sln.iml
# CodeRush
.cr/
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
*.nupkg

201
README.md
View File

@ -1,3 +1,197 @@
# Schemy
Schemy is a lightweight Scheme-like scripting language interpreter for
embedded use in .NET applications. It's built from scratch without any
external dependency. Its primary goal is to serve as a highly flexible
configuration language. Example scenarios are to describe computational
graph, workflow, or to represent some complex configuration.
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.
* runs reasonably fast and low memory footprint
Non-goals:
* be highly optimized - it's designed to load configurations and not part of
any heavy computation, so being optimized is not the goal - e.g., there's no
JIT compiling, etc.
Schemy's implementation is inspired by Peter Norvig's [article on Lisp
interpreter][lispy], but is heavily adapted to .NET and engineered to be easily
extensible and embeddable in .NET applications.
## Scheme Features
It has most features that a language would support:
* number, boolean, string, list types
* varaible, function definition
* tail call optimization
* macro definition
* lexical scoping
Many Scheme features are not (yet) supported. Among those are:
* continuation (`call/cc`)
* use square brackets `[...]` in place of parenthesis `(...)`
## Embedding and Extending Schemy
Schemy is primarily designed to be embedded into a .NET application for
configuration or as a [shell-like interactive environment (REPL)](#repl). To
use Schemy, you can either:
1. Reference `schemy.dll`, or
2. Copy `src/schemy/*.cs` source code to include in your application. Since
Schemy code base is small. This approach is very feasible (don't forget to
also include the resource file `init.ss`).
The below sections describes how to embed and extend Schemy in .NET
applications and in Scheme scripts. For a comprehensive example, please refer
to [`src/examples/command_server`](src/examples/command_server).
### Extending Schemy in .NET
Schemy can be extended by feeding the interpreter symbols with predefined
.NET objects. Variables could be any .NET type. Procedures
must implement `ICallable`.
An example procedure implementation:
new NativeProcedure(args => args, "list");
This implements the Scheme procedure `list`, which converts its arguments
into a list:
schemy> (list 1 2 3 4)
(1 2 3 4)
To "register" extensions, one can pass them to the `Interpreter`'s
constructor:
```csharp
Interpreter.CreateSymbolTableDelegate extension = itpr => new Dictionary<Symbol, object>
{
{ Symbol.FromString("list"), new NativeProcedure(args => args, "list") },
};
var interpreter = new Interpreter(new[] { extension });
```
### Extending Schemy in Scheme
When launched, the interpreter tries to locate and load Scheme file `.init.ss`
in the same directory as the executing assembly. You can extend Schemy by
putting function, variable, macro definition inside this file.
#### Extending with functions
For example, this function implements the standard Scheme list reversion
function `reverse` (with proper tail call optimization):
```scheme
(define (reverse ls)
(define loop
(lambda (ls acc)
(if (null? ls) acc
(loop (cdr ls) (cons (car ls) acc)))))
(loop ls '()))
```
Use it like so:
```nohighlight
Schemy> (reverse '(1 2 "foo" "bar"))
("bar" "foo" 2 1)
```
#### Syntax augmentation in Scheme
For example, we want to augment Schemy with a new syntax for local variable
definition, [`let`][schemepl]. Here's what we want to achieve:
```nohighlight
Schemy> (let ((x 1) ; let x = 1
(y 2)) ; let y = 2
(+ x y)) ; evaluate x + y
3
```
The following macro implements the `let` form by using lambda invocation:
```scheme
(define-macro let
(lambda args
(define specs (car args)) ; ((var1 val1), ...)
(define bodies (cdr args)) ; (expr1 ...)
(if (null? specs)
`((lambda () ,@bodies))
(begin
(define spec1 (car specs)) ; (var1 val1)
(define spec_rest (cdr specs)) ; ((var2 val2) ...)
(define inner `((lambda ,(list (car spec1)) ,@bodies) ,(car (cdr spec1))))
`(let ,spec_rest ,inner)))))
```
<a id="repl"></a>
## Use Interactively (REPL)
The interpreter can be run interactively, when given a `TextReader` for input
and a `TextWriter` for output.
```csharp
/// <summary>Starts the Read-Eval-Print loop</summary>
/// <param name="input">the input source</param>
/// <param name="output">the output target</param>
/// <param name="prompt">a string prompt to be printed before each evaluation</param>
/// <param name="headers">a head text to be printed at the beginning of the REPL</param>
public void REPL(TextReader input, TextWriter output, string prompt = null, string[] headers = null)
```
This can be useful for expose a remote "shell" for the application, or as
debugging purposes (see how `src/examples/command_server/` uses the `--repl`
command line argument).
There is an example REPL application in
[`src/examples/repl/`](src/examples/repl/) that can be started as a REPL
interpreter:
$ schemy.repl.exe
-----------------------------------------------
| Schemy - Scheme as a Configuration Language |
| Press Ctrl-C to exit |
-----------------------------------------------
Schemy> (define (sum-to n acc)
(if (= n 0)
acc
(sum-to (- n 1) (+ acc n))))
Schemy> (sum-to 100 0)
5050
Schemy> (sum-to 10000 0) ; proper tail call optimization prevents stack overflow
50005000
Run a script:
$ schemy.repl.exe <some_file>
# Contributing
@ -12,3 +206,10 @@ provided by the bot. You will only need to do this once across all repos using o
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
[schemepl]: http://www.scheme.com/tspl4/start.html#./start:h4
[lispy]: http://norvig.com/lispy2.html
<!--- vim: set ft=markdown tw=78: -->

62
doc/example.ss Normal file
View File

@ -0,0 +1,62 @@
; --------------------
; Define a variable
; --------------------
(define str "foo bar")
str
; --------------------
; Define a function
; --------------------
(define (square x) (* x x))
(square 2) ; call the function
; --------------------
; Create a list of numbers
; --------------------
(define nums (range 0 10))
; --------------------
; Functional programming:
; Map the list into another list using a function
; --------------------
(map square nums)
; --------------------
; Tail call optimization
; Reverse a list recursively (without stack overflow)
; --------------------
(define (reverse ls)
(define loop
(lambda (ls acc)
(if (null? ls) acc
(loop (cdr ls) (cons (car ls) acc)))))
(loop ls '()))
(reverse '(1 2 "foo" "bar"))
(reverse (range 0 10000)) ; NO STACK OVERFLOW!
; --------------------
; Using LISP macros to extend the language syntax
; Here we define a `let` syntax that creates local variable for
; only the scope in the `let` block (usage below).
; --------------------
(define-macro let
(lambda args
(define specs (car args)) ; ((var1 val1), ...)
(define bodies (cdr args)) ; (expr1 ...)
(if (null? specs)
`((lambda () ,@bodies))
(begin
(define spec1 (car specs)) ; (var1 val1)
(define spec_rest (cdr specs)) ; ((var2 val2) ...)
(define inner `((lambda ,(list (car spec1)) ,@bodies) ,(car (cdr spec1))))
`(let ,spec_rest ,inner)))))
; --------------------
; Usage of the newly created `let` syntax
; --------------------
(let ((x 1) ; let x = 1
(y 2)) ; let y = 2
(+ x y)) ; evaluate x + y

View File

@ -0,0 +1,135 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Schemy;
namespace Examples.command_server
{
class Program
{
delegate object Function(object input);
static void Main(string[] args)
{
Interpreter.CreateSymbolTableDelegate extension = _ => new Dictionary<Symbol, object>()
{
{ Symbol.FromString("get-current-os"), NativeProcedure.Create(() => GetCurrentSystem()) },
{ Symbol.FromString("chain"), new NativeProcedure(funcs => new Function(input => funcs.Cast<Function>().Select(b => input = b(input)).Last())) },
{ Symbol.FromString("say-hi"), NativeProcedure.Create<Function>(() => name => $"Hello {name}!") },
{ Symbol.FromString("man-freebsd"), NativeProcedure.Create<Function>(() => cmd => GetUrl($"https://www.freebsd.org/cgi/man.cgi?query={cmd}&format=ascii")) },
{ Symbol.FromString("man-linux"), NativeProcedure.Create<Function>(() => cmd => GetUrl($"http://man7.org/linux/man-pages/man1/{cmd}.1.html")) },
{ Symbol.FromString("truncate-string"), NativeProcedure.Create<int, Function>(len => input => ((string)input).Substring(0, len)) },
};
var interpreter = new Interpreter(new[] { extension });
if (args.Contains("--repl")) // start the REPL with all implemented functions
{
interpreter.REPL(Console.In, Console.Out);
return;
}
else
{
// starts a TCP server that receives request (cmd <data>) and sends response back.
var engines = new Dictionary<string, Function>();
foreach (var fn in Directory.GetFiles(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), "*.ss"))
{
Console.WriteLine($"Loading file {fn}");
LoadScript(interpreter, fn);
engines[Path.GetFileNameWithoutExtension(fn)] = (Function)interpreter.Environment[Symbol.FromString("EXECUTE")];
}
string ip = "127.0.0.1"; int port = 8080;
var server = new TcpListener(IPAddress.Parse(ip), port);
server.Start();
Console.WriteLine($"Server started at {ip}:{port}");
try
{
using (var c = server.AcceptTcpClient())
using (var cs = c.GetStream())
using (var sr = new StreamReader(cs))
using (var sw = new StreamWriter(cs))
{
Console.WriteLine($"Client accepted at {c.Client.RemoteEndPoint}");
while (!sr.EndOfStream)
{
string line = sr.ReadLine();
string[] parsed = line.Split(new[] { ' ' }, 2);
if (parsed.Length != 2)
{
sw.WriteLine($"cannot parse {line}");
sw.Flush();
}
else
{
string engine = parsed[0], request = parsed[1];
if (!engines.ContainsKey(engine))
{
sw.WriteLine($"engine not found: {engine}");
sw.Flush();
}
else
{
string output = (string)(engines[engine](request));
sw.WriteLine(output);
sw.Flush();
}
}
}
}
}
catch (IOException) { }
}
}
static void LoadScript(Interpreter interpreter, string file)
{
using (Stream script = File.OpenRead(file))
using (TextReader reader = new StreamReader(script))
{
var res = interpreter.Evaluate(reader);
if (res.Error != null) throw res.Error;
}
}
// support: windows, freebsd, linux, unknown
static string GetCurrentSystem()
{
var os = System.Environment.OSVersion.Platform;
if (os.ToString().Contains("Win")) return "windows";
if (os == PlatformID.Unix)
{
Process proc = new Process() { StartInfo = new ProcessStartInfo("uname") { RedirectStandardOutput = true } };
proc.Start();
var output = proc.StandardOutput.ReadToEnd().Trim();
proc.WaitForExit();
foreach (var w in new[] { "freebsd", "linux" })
{
if (output.IndexOf(w, StringComparison.OrdinalIgnoreCase) >= 0) return w;
}
}
return "unknown";
}
static string GetUrl(string url)
{
using (var wc = new System.Net.WebClient())
{
return wc.DownloadString(url);
}
}
}
}

View File

@ -0,0 +1,74 @@
# EXAMPLE: A CONFIGURABLE COMMAND SERVER
This application is an example use of Schemy to load configurable command
processing pipelines and serve the loaded commands via TCP channel.
In this application, the server does the following things:
1. It extends an embedded Schemy interpreter with some functions implemented
in C#.
2. It finds `.ss` scripts which defines a command processing pipeline by using
those implemented functions.
3. The server finds and persists the composes pipeline from a script by
looking for the symbol `EXECUTE` which should be of type `Func<object,
object>`.
4. When a command request comes in, it simply invokes the corresponding
command processor (the one defined by `EXECUTE`), and responses with the
result.
A simple example is the [`say-hi.ss`](say-hi.ss) script:
```scheme
; This command processor would echo an input string `name` in the format:
;
; hello name!
(define EXECUTE (say-hi))
```
As a complex example, [`man.ss`](man.ss) defines a online man-page lookup:
```scheme
(define EXECUTE
(let ((os (get-current-os))
(max-length 500))
(chain ; chain functions together
(cond ; pick a manpage lookup based on OS
((equal? os "freebsd") (man-freebsd))
((equal? os "linux") (man-linux))
(else (man-freebsd)))
(truncate-string max-length)))) ; truncate output string to a max length
```
With these two scripts loaded the command server, a TCP client can issue commands
`man <unix_command>` and `sai-hi <name>` to the server:
```
$ ncat 127.0.0.1 8080
say-hi John Doe
Hello John Doe!
man ls
LS(1) FreeBSD General Commands Manual LS(1)
NAME
ls -- list directory contents
SYNOPSIS
ls [--libxo] [-ABCFGHILPRSTUWZabcdfghiklmnopqrstuwxy1,] [-D format]
[file ...]
DESCRIPTION
For each operand that names a file of a type other than directory, ls
displays its name as well as any requested, associated information. For
each operand that names a file of type directory, ls displays the names
of files contained within that directory, as well as any requested,
```

View File

@ -0,0 +1,65 @@
<?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>{88D7E5A3-0BA9-4155-B151-839FF5734F7C}</ProjectGuid>
<OutputType>Exe</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>command_server</RootNamespace>
<AssemblyName>command_server</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" />
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
<Copy SourceFiles="man.ss" DestinationFiles="$(TargetDir)man.ss" />
<Copy SourceFiles="say-hi.ss" DestinationFiles="$(TargetDir)say-hi.ss" />
</Target>
</Project>

View File

@ -0,0 +1,41 @@
; This script will be load by the server as command `man`. The command
; is consistent of the following functions chained together:
;
; 1. An online man-page look up - it detects the current operating system and
; decides to use either a linux or freebsd man page web API for the look up.
;
; 2. A string truncator `truncate-string` - it truncates the input string, in
; this case the output of the man-page lookup, to the specified number of
; characters.
;
; The client of the command server connects via raw RCP protocol, and can issue
; commands like:
;
; man ls
;
; and gets response like:
;
; LS(1) FreeBSD General Commands Manual LS(1)
;
; NAME
; ls -- list directory contents
;
; SYNOPSIS
; ls [--libxo] [-ABCFGHILPRSTUWZabcdfghiklmnopqrstuwxy1,] [-D format]
; [file ...]
;
; DESCRIPTION
; For each operand that names a file of a type other than directory, ls
; displays its name as well as any requested, associated information. For
; each operand that names a file of type directory, ls displays the names
; of files contained within that directory, as well as any requested,
(define EXECUTE
(let ((os (get-current-os))
(max-length 500))
(chain ; chain functions together
(cond ; pick a manpage lookup based on OS
((equal? os "freebsd") (man-freebsd))
((equal? os "linux") (man-linux))
(else (man-freebsd)))
(truncate-string max-length)))) ; truncate output string to a max length

View File

@ -0,0 +1,5 @@
; This command processor would echo an input string `name` in the format:
;
; hello name!
(define EXECUTE (say-hi))

View File

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

View File

@ -0,0 +1,41 @@
// 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

@ -0,0 +1,67 @@
<?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>

2
src/repl/.init.ss Normal file
View File

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

41
src/repl/Program.cs Normal file
View File

@ -0,0 +1,41 @@
// 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);
}
}
}
}

67
src/repl/repl.csproj Normal file
View File

@ -0,0 +1,67 @@
<?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>

40
src/schemy.sln Normal file
View File

@ -0,0 +1,40 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14
VisualStudioVersion = 14.0.25420.1
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "schemy", "schemy\schemy.csproj", "{E54139B7-CB81-4883-B8CD-40BAB5420EB8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "test", "test\test.csproj", "{4A62A84F-58AA-4D1C-AA7C-D3CDF0C3FFA6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "example.repl", "examples\repl\example.repl.csproj", "{56DADEFC-6B30-4C0F-AAEA-23D684BD817F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "example.command_server", "examples\command_server\example.command_server.csproj", "{88D7E5A3-0BA9-4155-B151-839FF5734F7C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{E54139B7-CB81-4883-B8CD-40BAB5420EB8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E54139B7-CB81-4883-B8CD-40BAB5420EB8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E54139B7-CB81-4883-B8CD-40BAB5420EB8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E54139B7-CB81-4883-B8CD-40BAB5420EB8}.Release|Any CPU.Build.0 = Release|Any CPU
{4A62A84F-58AA-4D1C-AA7C-D3CDF0C3FFA6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4A62A84F-58AA-4D1C-AA7C-D3CDF0C3FFA6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4A62A84F-58AA-4D1C-AA7C-D3CDF0C3FFA6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4A62A84F-58AA-4D1C-AA7C-D3CDF0C3FFA6}.Release|Any CPU.Build.0 = Release|Any CPU
{56DADEFC-6B30-4C0F-AAEA-23D684BD817F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{56DADEFC-6B30-4C0F-AAEA-23D684BD817F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{56DADEFC-6B30-4C0F-AAEA-23D684BD817F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{56DADEFC-6B30-4C0F-AAEA-23D684BD817F}.Release|Any CPU.Build.0 = Release|Any CPU
{88D7E5A3-0BA9-4155-B151-839FF5734F7C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{88D7E5A3-0BA9-4155-B151-839FF5734F7C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{88D7E5A3-0BA9-4155-B151-839FF5734F7C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{88D7E5A3-0BA9-4155-B151-839FF5734F7C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal

165
src/schemy/Builtins.cs Normal file
View File

@ -0,0 +1,165 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
namespace Schemy
{
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
/// <summary>
/// Extend the interpreter with essential builtin functionalities
/// </summary>
public class Builtins
{
public static IDictionary<Symbol, object> CreateBuiltins(Interpreter interpreter)
{
var builtins = new Dictionary<Symbol, object>();
builtins[Symbol.FromString("+")] = new NativeProcedure(Utils.MakeVariadic(Add), "+");
builtins[Symbol.FromString("-")] = new NativeProcedure(Utils.MakeVariadic(Minus), "-");
builtins[Symbol.FromString("*")] = new NativeProcedure(Utils.MakeVariadic(Multiply), "*");
builtins[Symbol.FromString("/")] = new NativeProcedure(Utils.MakeVariadic(Divide), "/");
builtins[Symbol.FromString("=")] = NativeProcedure.Create<double, double, bool>((x, y) => x == y, "=");
builtins[Symbol.FromString("<")] = NativeProcedure.Create<double, double, bool>((x, y) => x < y, "<");
builtins[Symbol.FromString("<=")] = NativeProcedure.Create<double, double, bool>((x, y) => x <= y, "<=");
builtins[Symbol.FromString(">")] = NativeProcedure.Create<double, double, bool>((x, y) => x > y, ">");
builtins[Symbol.FromString(">=")] = NativeProcedure.Create<double, double, bool>((x, y) => x >= y, ">=");
builtins[Symbol.FromString("eq?")] = NativeProcedure.Create<object, object, bool>((x, y) => object.ReferenceEquals(x, y), "eq?");
builtins[Symbol.FromString("equal?")] = NativeProcedure.Create<object, object, bool>(EqualImpl, "equal?");
builtins[Symbol.FromString("boolean?")] = NativeProcedure.Create<object, bool>(x => x is bool, "boolean?");
builtins[Symbol.FromString("num?")] = NativeProcedure.Create<object, bool>(x => x is int || x is double, "num?");
builtins[Symbol.FromString("string?")] = NativeProcedure.Create<object, bool>(x => x is string, "string?");
builtins[Symbol.FromString("symbol?")] = NativeProcedure.Create<object, bool>(x => x is Symbol, "symbol?");
builtins[Symbol.FromString("list?")] = NativeProcedure.Create<object, bool>(x => x is List<object>, "list?");
builtins[Symbol.FromString("map")] = NativeProcedure.Create<ICallable, List<object>, List<object>>((func, ls) => ls.Select(x => func.Call(new List<object> { x })).ToList());
builtins[Symbol.FromString("reverse")] = NativeProcedure.Create<List<object>, List<object>>(ls => ls.Reverse<object>().ToList());
builtins[Symbol.FromString("range")] = new NativeProcedure(RangeImpl, "range");
builtins[Symbol.FromString("apply")] = NativeProcedure.Create<ICallable, List<object>, object>((proc, args) => proc.Call(args), "apply");
builtins[Symbol.FromString("list")] = new NativeProcedure(args => args, "list");
builtins[Symbol.FromString("list-ref")] = NativeProcedure.Create<List<object>, int, object>((ls, idx) => ls[idx]);
builtins[Symbol.FromString("length")] = NativeProcedure.Create<List<object>, int>(list => list.Count, "length");
builtins[Symbol.FromString("car")] = NativeProcedure.Create<List<object>, object>(args => args[0], "car");
builtins[Symbol.FromString("cdr")] = NativeProcedure.Create<List<object>, List<object>>(args => args.Skip(1).ToList(), "cdr");
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");
builtins[Symbol.FromString("load")] = NativeProcedure.Create<string, None>(filename => LoadImpl(interpreter, filename), "load");
return builtins;
}
#region Builtin Implementations
private static List<object> RangeImpl(List<object> args)
{
Utils.CheckSyntax(args, args.Count >= 1 && args.Count <= 3);
foreach (var item in args)
{
Utils.CheckSyntax(args, item is int, "items must be integers");
}
int start, end, step;
if (args.Count == 1)
{
start = 0;
end = (int)args[0];
step = 1;
}
else if (args.Count == 2)
{
start = (int)args[0];
end = (int)args[1];
step = 1;
}
else
{
start = (int)args[0];
end = (int)args[1];
step = (int)args[2];
}
if (start < end) Utils.CheckSyntax(args, step > 0, "step must make the sequence end");
if (start > end) Utils.CheckSyntax(args, step < 0, "step must make the sequence end");
var res = new List<object>();
if (start <= end) for (int i = start; i < end; i += step) res.Add(i);
else for (int i = start; i > end; i += step) res.Add(i);
res.TrimExcess();
return res;
}
private static None AssertImpl(List<object> args)
{
Utils.CheckArity(args, 1, 2);
string msg = "Assertion failed";
msg += args.Count > 1 ? ": " + Utils.ConvertType<string>(args[1]) : string.Empty;
bool pred = Utils.ConvertType<bool>(args[0]);
if (!pred) throw new AssertionFailedError(msg);
return None.Instance;
}
private static None LoadImpl(Interpreter interpreter, string filename)
{
using (TextReader reader = new StreamReader(interpreter.FileSystemAccessor.OpenRead(filename)))
{
interpreter.Evaluate(reader);
}
return None.Instance;
}
public static bool EqualImpl(object x, object y)
{
if (object.Equals(x, y)) return true;
if (x == null || y == null) return false;
if (x is IList<object> && y is IList<object>)
{
var x2 = (IList<object>)x;
var y2 = (IList<object>)y;
if (x2.Count != y2.Count) return false;
return Enumerable.Zip(x2, y2, (a, b) => Tuple.Create(a, b))
.All(pair => EqualImpl(pair.Item1, pair.Item2));
}
return false;
}
private static object Add(object x, object y)
{
if (x is int && y is int) return (int)x + (int)y;
return (double)System.Convert.ChangeType(x, typeof(double)) + (double)System.Convert.ChangeType(y, typeof(double));
}
private static object Minus(object x, object y)
{
if (x is int && y is int) return (int)x - (int)y;
return (double)System.Convert.ChangeType(x, typeof(double)) - (double)System.Convert.ChangeType(y, typeof(double));
}
private static object Multiply(object x, object y)
{
if (x is int && y is int) return (int)x * (int)y;
return (double)System.Convert.ChangeType(x, typeof(double)) * (double)System.Convert.ChangeType(y, typeof(double));
}
private static object Divide(object x, object y)
{
if (x is int && y is int) return (int)x / (int)y;
return (double)System.Convert.ChangeType(x, typeof(double)) / (double)System.Convert.ChangeType(y, typeof(double));
}
#endregion Builtin Implementations
}
}

56
src/schemy/CommonTypes.cs Normal file
View File

@ -0,0 +1,56 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
namespace Schemy
{
using System;
using System.Collections.Generic;
public class None
{
public static readonly None Instance = new None();
}
class AssertionFailedError : Exception
{
public AssertionFailedError(string msg) : base(msg)
{
}
}
class SyntaxError : Exception
{
public SyntaxError(string msg) : base(msg)
{
}
}
/// <summary>
/// Poor man's discreminated union
/// </summary>
public class Union<T1, T2>
{
private readonly object data;
public Union(T1 data)
{
this.data = data;
}
public Union(T2 data)
{
this.data = data;
}
public TResult Use<TResult>(Func<T1, TResult> func1, Func<T2, TResult> func2)
{
if (this.data is T1)
{
return func1((T1)this.data);
}
else
{
return func2((T2)this.data);
}
}
}
}

107
src/schemy/Env.cs Normal file
View File

@ -0,0 +1,107 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
namespace Schemy
{
using System.Collections.Generic;
/// <summary>
/// Tracks the state of an interpreter or a procedure. It supports lexical scoping.
/// </summary>
public class Environment
{
private readonly IDictionary<Symbol, object> store;
/// <summary>
/// The enclosing environment. For top level env, this is null.
/// </summary>
private readonly Environment outer;
public Environment(IDictionary<Symbol, object> env, Environment outer)
{
this.store = env;
this.outer = outer;
}
public static Environment CreateEmpty()
{
return new Environment(new Dictionary<Symbol, object>(), null);
}
public static Environment FromVariablesAndValues(Union<Symbol, List<Symbol>> parameters, List<object> values, Environment outer)
{
return parameters.Use(
@params => new Environment(new Dictionary<Symbol, object>() { { @params, values } }, outer),
@params =>
{
if (values.Count != @params.Count)
{
throw new SyntaxError(string.Format("Unexpected number of arguments. Expecting {0}, Got {1}.", @params.Count, values.Count));
}
var dict = new Dictionary<Symbol, object>();
for (int i = 0; i < values.Count; i++)
{
dict[@params[i]] = values[i];
}
return new Environment(dict, outer);
});
}
/// <summary>
/// Attempts to get the value of the symbol. If it's not found in current env, recursively try the enclosing env.
/// </summary>
/// <param name="val">The value of the symbol to find</param>
/// <returns>if the symbol's value could be found</returns>
public bool TryGetValue(Symbol sym, out object val)
{
Environment env = this.TryFindContainingEnv(sym);
if (env != null)
{
val = env.store[sym];
return true;
}
else
{
val = null;
return false;
}
}
/// <summary>
/// Attempts to find the env that actually defines the symbol
/// </summary>
/// <param name="sym">The symbol to find</param>
/// <returns>the env that defines the symbol</returns>
public Environment TryFindContainingEnv(Symbol sym)
{
object val;
if (this.store.TryGetValue(sym, out val)) return this;
if (this.outer != null) return this.outer.TryFindContainingEnv(sym);
return null;
}
public object this[Symbol sym]
{
get
{
object val;
if (this.TryGetValue(sym, out val))
{
return val;
}
else
{
throw new KeyNotFoundException(string.Format("Symbol not defined: {0}", sym));
}
}
set
{
this.store[sym] = value;
}
}
}
}

View File

@ -0,0 +1,52 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
namespace Schemy
{
using System;
using System.IO;
/// <summary>
/// The only interface that the file system should be exposed within a Schemy interpreter.
/// </summary>
/// <remarks>
/// One could implement this interface in a way such that the interpreter can be used to access "files" in
/// any logical virtual file system. For security purposes, one could also choose to not implement, say,
/// <see cref="IFileSystemAccessor.OpenWrite"/> if the interpreter is used in a way that write does not need to supported.
/// The other (higher) level of protection would be to not expose any builtin function for writing to the file system.
/// </remarks>
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);
}
/// <summary>
/// An implementation of <see cref="IFileSystemAccessor"/> that grants readonly access to the host file system.
/// </summary>
/// <seealso cref="Schemy.IFileSystemAccessor" />
public class ReadOnlyFileSystemAccessor : IFileSystemAccessor
{
public Stream OpenRead(string path)
{
return File.OpenRead(path);
}
public Stream OpenWrite(string path)
{
throw new NotSupportedException("Writing to file system is not supported");
}
}
}

217
src/schemy/Procedure.cs Normal file
View File

@ -0,0 +1,217 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.Linq;
namespace Schemy
{
/// <summary>
/// Represents a procedure value in Scheme
/// </summary>
interface ICallable
{
/// <summary>
/// Invokes this procedure
/// </summary>
/// <param name="args">The arguments. These are the `cdr` of the s-expression for the procedure invocation.</param>
/// <returns>the result of the procedure invocation</returns>
object Call(List<object> args);
}
/// <summary>
/// A procedure implemented in Scheme
/// </summary>
/// <seealso cref="Schemy.ICallable" />
public class Procedure : ICallable
{
private readonly Union<Symbol, List<Symbol>> parameters;
private readonly object body;
private readonly Environment env;
public Procedure(Union<Symbol, List<Symbol>> parameters, object body, Environment env)
{
this.parameters = parameters;
this.body = body;
this.env = env;
}
public object Body
{
get { return this.body; }
}
public Union<Symbol, List<Symbol>> Parameters
{
get { return this.parameters; }
}
public Environment Env
{
get { return this.env; }
}
/// <summary>
/// Invokes this procedure
/// </summary>
/// <remarks>
/// Implementation note: under normal function invocation scenarios, this method is not used. Instead,
/// a tail call optimization is used in the interpreter evaluation phase that runs Scheme functions.
///
/// This method is useful however, in macro expansions, and any other occasions where the tail call optimization
/// is not (yet) implemented.
///
/// <see cref="Interpreter.EvaluateExpression(object, Environment)"/>
/// </remarks>
public object Call(List<object> args)
{
// NOTE: This is not needed for regular function invoke after the tail call optimization.
// a (non-native) procedure is now optimized into evaluating the body under the environment
// formed by the (params, args). So the `Call` method will never be used.
return Interpreter.EvaluateExpression(this.body, Environment.FromVariablesAndValues(this.parameters, args, this.env));
}
/// <summary>
/// Prints the implementation of the function.
/// </summary>
public override string ToString()
{
var form = new List<object> { Symbol.LAMBDA, this.parameters.Use(sym => (object)sym, syms => syms.Cast<object>().ToList()), this.body };
return Utils.PrintExpr(form);
}
}
/// <summary>
/// A procedure implemented in .NET
/// </summary>
/// <seealso cref="Schemy.ICallable" />
public class NativeProcedure : ICallable
{
private readonly Func<List<object>, object> func;
private readonly string name;
public NativeProcedure(Func<List<object>, object> func, string name = null)
{
this.func = func;
this.name = name;
}
public object Call(List<object> args)
{
return this.func(args);
}
/// <summary>
/// Convenient function method to create a native procedure and doing arity and type check for inputs. It makes the input function
/// implementation strongly typed.
/// </summary>
/// <see cref="Create{T1, T2}(Func{T1, T2}, string)"/>
public static NativeProcedure Create<T1, T2, T3, T4, T5, T6, T7, T8>(Func<T1, T2, T3, T4, T5, T6, T7, T8> func, string name = null)
{
return new NativeProcedure(args =>
{
Utils.CheckArity(args, 7);
return func(
Utils.ConvertType<T1>(args[0]),
Utils.ConvertType<T2>(args[1]),
Utils.ConvertType<T3>(args[2]),
Utils.ConvertType<T4>(args[3]),
Utils.ConvertType<T5>(args[4]),
Utils.ConvertType<T6>(args[5]),
Utils.ConvertType<T7>(args[6])
);
}, name);
}
/// <summary>
/// Convenient function method to create a native procedure and doing arity and type check for inputs. It makes the input function
/// implementation strongly typed.
/// </summary>
/// <see cref="Create{T1, T2}(Func{T1, T2}, string)"/>
public static NativeProcedure Create<T1, T2, T3, T4, T5>(Func<T1, T2, T3, T4, T5> func, string name = null)
{
return new NativeProcedure(args =>
{
Utils.CheckArity(args, 4);
return func(
Utils.ConvertType<T1>(args[0]),
Utils.ConvertType<T2>(args[1]),
Utils.ConvertType<T3>(args[2]),
Utils.ConvertType<T4>(args[3]));
}, name);
}
/// <summary>
/// Convenient function method to create a native procedure and doing arity and type check for inputs. It makes the input function
/// implementation strongly typed.
/// </summary>
/// <see cref="Create{T1, T2}(Func{T1, T2}, string)"/>
public static NativeProcedure Create<T1, T2, T3, T4>(Func<T1, T2, T3, T4> func, string name = null)
{
return new NativeProcedure(args =>
{
Utils.CheckArity(args, 3);
return func(
Utils.ConvertType<T1>(args[0]),
Utils.ConvertType<T2>(args[1]),
Utils.ConvertType<T3>(args[2]));
}, name);
}
/// <summary>
/// Convenient function method to create a native procedure and doing arity and type check for inputs. It makes the input function
/// implementation strongly typed.
/// </summary>
/// <see cref="Create{T1, T2}(Func{T1, T2}, string)"/>
public static NativeProcedure Create<T1, T2, T3>(Func<T1, T2, T3> func, string name = null)
{
return new NativeProcedure(args =>
{
Utils.CheckArity(args, 2);
return func(Utils.ConvertType<T1>(args[0]), Utils.ConvertType<T2>(args[1]));
}, name);
}
/// <summary>
/// Convenient function method to create a native procedure and doing arity and type check for inputs. It makes the input function
/// implementation strongly typed.
/// </summary>
/// <typeparam name="T1">The type of the 1st argument</typeparam>
/// <typeparam name="T2">The type of the 2nd argument</typeparam>
/// <param name="func">The function implementation</param>
/// <param name="name">The name of the function</param>
public static NativeProcedure Create<T1, T2>(Func<T1, T2> func, string name = null)
{
return new NativeProcedure(args =>
{
Utils.CheckArity(args, 1);
return func(Utils.ConvertType<T1>(args[0]));
}, name);
}
/// <summary>
/// Convenient function method to create a native procedure and doing arity and type check for inputs. It makes the input function
/// implementation strongly typed.
/// </summary>
/// <see cref="Create{T1, T2}(Func{T1, T2}, string)"/>
public static NativeProcedure Create<T1>(Func<T1> func, string name = null)
{
return new NativeProcedure(args =>
{
Utils.CheckArity(args, 0);
return func();
}, name);
}
/// <summary>
/// ToString implementation
/// </summary>
/// <returns>the string representation</returns>
public override string ToString()
{
return string.Format("#<NativeProcedure:{0}>", string.IsNullOrEmpty(this.name) ? "noname" : this.name);
}
}
}

69
src/schemy/Program.cs Normal file
View File

@ -0,0 +1,69 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.IO;
namespace Schemy
{
public static class Program
{
/// <summary>
/// Initializes the interpreter with a init script if present.
/// </summary>
static void Initialize(Interpreter interpreter)
{
string initFile = Path.Combine(Path.GetDirectoryName(typeof(Interpreter).Assembly.Location), ".init.ss");
if (File.Exists(initFile))
{
using (var reader = new StreamReader(initFile))
{
var res = interpreter.Evaluate(reader);
if (res.Error != null)
{
Console.WriteLine(string.Format("Error loading {0}: {1}{2}",
initFile,
System.Environment.NewLine,
res.Error));
}
else
{
Console.WriteLine("Loaded init file: " + initFile);
}
}
}
}
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();
Initialize(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();
Initialize(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

@ -0,0 +1,38 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("schemy")]
[assembly: AssemblyDescription("A lightweight, embeddable Scheme-like language interpreter")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("Microsoft")]
[assembly: AssemblyProduct("schemy")]
[assembly: AssemblyCopyright("Copyright © 2017")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("e54139b7-cb81-4883-b8cd-40bab5420eb8")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

583
src/schemy/Schemy.cs Normal file
View File

@ -0,0 +1,583 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
namespace Schemy
{
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Reflection;
public class Interpreter
{
private readonly Environment environment;
private readonly Dictionary<Symbol, Procedure> macroTable;
private readonly IFileSystemAccessor fsAccessor;
public delegate IDictionary<Symbol, object> CreateSymbolTableDelegate(Interpreter interpreter);
/// <summary>
/// Initializes a new instance of the <see cref="Interpreter"/> class.
/// </summary>
/// <param name="environmentInitializers">Array of environment initializers</param>
/// <param name="fsAccessor">The file system accessor</param>
public Interpreter(IEnumerable<CreateSymbolTableDelegate> environmentInitializers = null, IFileSystemAccessor fsAccessor = null)
{
this.fsAccessor = fsAccessor;
if (this.fsAccessor == null)
{
this.fsAccessor = new ReadOnlyFileSystemAccessor();
}
// populate an empty environment for the initializer to potentially work with
this.environment = Environment.CreateEmpty();
this.macroTable = new Dictionary<Symbol, Procedure>();
environmentInitializers = environmentInitializers ?? new List<CreateSymbolTableDelegate>();
environmentInitializers = new CreateSymbolTableDelegate[] { Builtins.CreateBuiltins }.Concat(environmentInitializers);
foreach (CreateSymbolTableDelegate initializer in environmentInitializers)
{
this.environment = new Environment(initializer(this), this.environment);
}
foreach (var iniReader in GetInitializeFiles())
{
this.Evaluate(iniReader);
}
}
private IEnumerable<TextReader> GetInitializeFiles()
{
using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("init.ss"))
using (StreamReader reader = new StreamReader(stream))
{
yield return reader;
}
string initFile = Path.Combine(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), ".init.ss");
if (File.Exists(initFile))
{
using (var reader = new StreamReader(initFile))
{
yield return reader;
}
}
}
public IFileSystemAccessor FileSystemAccessor { get { return this.fsAccessor; } }
public Environment Environment { get { return this.environment; } }
/// <summary>
/// Evaluate script from a input reader
/// </summary>
/// <param name="input">the input source</param>
/// <returns>the value of the last expression</returns>
public EvaluationResult Evaluate(TextReader input)
{
InPort port = new InPort(input);
object res = null;
while (true)
{
try
{
var expr = Expand(Read(port), environment, macroTable, true);
if (Symbol.EOF.Equals(expr))
{
return new EvaluationResult(null, res);
}
else
{
res = EvaluateExpression(expr, environment);
}
}
catch (Exception e)
{
return new EvaluationResult(e, null);
}
}
}
/// <summary>
/// Starts the Read-Eval-Print loop
/// </summary>
/// <param name="input">the input source</param>
/// <param name="output">the output target</param>
/// <param name="prompt">a string prompt to be printed before each evaluation</param>
/// <param name="headers">a head text to be printed at the beginning of the REPL</param>
public void REPL(TextReader input, TextWriter output, string prompt = null, string[] headers = null)
{
InPort port = new InPort(input);
if (headers != null)
{
foreach (var line in headers)
{
output.WriteLine(line);
}
}
object res = null;
while (true)
{
try
{
if (!string.IsNullOrEmpty(prompt) && output != null) output.Write(prompt);
var expr = Expand(Read(port), environment, macroTable, true);
if (Symbol.EOF.Equals(expr))
{
return;
}
else
{
res = EvaluateExpression(expr, environment);
if (output != null) output.WriteLine(Utils.PrintExpr(res));
}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
}
}
/// <summary>
/// Defines a global symbol
/// </summary>
/// <param name="sym">the symbol</param>
/// <param name="val">the associated value</param>
public void DefineGlobal(Symbol sym, object val)
{
this.environment[sym] = val;
}
/// <summary>
/// Reads an S-expression from the input source
/// </summary>
public static object Read(InPort port)
{
Func<object, object> readAhead = null;
readAhead = token =>
{
Symbol quote;
if (object.Equals(token, Symbol.EOF))
{
throw new SyntaxError("unexpected EOF");
}
else if (token is string)
{
string tokenStr = (string)token;
if (tokenStr == "(")
{
var L = new List<object>();
while (true)
{
token = port.NextToken();
if (token is string && (string)token == ")")
{
return L;
}
else
{
L.Add(readAhead(token));
}
}
}
else if (tokenStr == ")")
{
throw new SyntaxError("unexpected )");
}
else if (Symbol.QuotesMap.TryGetValue(tokenStr, out quote))
{
object quoted = Read(port);
return new List<object> { quote, quoted };
}
else
{
return ParseAtom(tokenStr);
}
}
else
{
throw new SyntaxError("unexpected token: " + token);
}
};
var token1 = port.NextToken();
return Symbol.EOF.Equals(token1) ? Symbol.EOF : readAhead(token1);
}
/// <summary>
/// Validates and expands the input s-expression
/// </summary>
/// <param name="expression">expression to expand</param>
/// <param name="env">env used to evaluate the macro procedures</param>
/// <param name="macroTable">the macro definition table</param>
/// <param name="isTopLevel">whether the current expansion is at the top level</param>
/// <returns>the s-expression after validation and expansion</returns>
public static object Expand(object expression, Environment env, Dictionary<Symbol, Procedure> macroTable, bool isTopLevel = true)
{
Procedure procedure = null;
Func<object, bool, object> expand = null;
expand = (x, topLevel) =>
{
if (!(x is List<object>))
{
return x;
}
List<object> xs = (List<object>)x;
Utils.CheckSyntax(xs, xs.Count > 0);
if (Symbol.QUOTE.Equals(xs[0]))
{
Utils.CheckSyntax(xs, xs.Count == 2);
return xs;
}
else if (Symbol.IF.Equals(xs[0]))
{
if (xs.Count == 3)
{
xs.Add(None.Instance);
}
Utils.CheckSyntax(xs, xs.Count == 4);
return xs.Select(expr => expand(expr, false)).ToList();
}
else if (Symbol.SET.Equals(xs[0]))
{
Utils.CheckSyntax(xs, xs.Count == 3);
Utils.CheckSyntax(xs, xs[1] is Symbol, "can only set! a symbol");
return new List<object> { Symbol.SET, xs[1], expand(xs[2], false) };
}
else if (Symbol.DEFINE.Equals(xs[0]) || Symbol.DEFINE_MACRO.Equals(xs[0]))
{
Utils.CheckSyntax(xs, xs.Count >= 3);
Symbol def = (Symbol)xs[0];
object v = xs[1]; // sym or (sym+)
List<object> body = xs.Skip(2).ToList(); // expr or expr+
if (v is List<object>)
{
// (define (f args) body)
var args = (List<object>)v;
Utils.CheckSyntax(xs, args.Count > 0);
var f = args[0];
var @params = args.Skip(1).ToList();
return expand(new List<object> { def, f, Enumerable.Concat(new object[] { Symbol.LAMBDA, @params }, body).ToList() }, false);
}
else
{
Utils.CheckSyntax(xs, xs.Count == 3); // (define x expr)
Utils.CheckSyntax(xs, v is Symbol);
var expr = expand(xs[2], false);
if (Symbol.DEFINE_MACRO.Equals(def))
{
Utils.CheckSyntax(xs, topLevel, "define-macro is only allowed at the top level");
var proc = EvaluateExpression(expr, env);
Utils.CheckSyntax(xs, proc is Procedure, "macro must be a procedure");
macroTable[(Symbol)v] = (Procedure)proc;
return None.Instance;
}
else
{
// `define v expr`
return new List<object> { Symbol.DEFINE, v, expr /* after expansion */ };
}
}
}
else if (Symbol.BEGIN.Equals(xs[0]))
{
if (xs.Count == 1) return None.Instance; // (begin) => None
// use the same topLevel so that `define-macro` is also allowed in a top-level `begin`.
return xs.Select(expr => expand(expr, topLevel)).ToList();
}
else if (Symbol.LAMBDA.Equals(xs[0]))
{
Utils.CheckSyntax(xs, xs.Count >= 3);
var vars = xs[1];
Utils.CheckSyntax(xs, vars is Symbol || (vars is List<object> && ((List<object>)vars).All(v => v is Symbol)), "illigal lambda argument");
object body;
if (xs.Count == 3)
{
// (lambda (...) expr)
body = xs[2];
}
else
{
// (lambda (...) expr+
body = Enumerable.Concat(new[] { Symbol.BEGIN }, xs.Skip(2)).ToList();
}
return new List<object> { Symbol.LAMBDA, vars, expand(body, false) };
}
else if (Symbol.QUASIQUOTE.Equals(xs[0]))
{
Utils.CheckSyntax(xs, xs.Count == 2);
return ExpandQuasiquote(xs[1]);
}
else if (xs[0] is Symbol && macroTable.TryGetValue((Symbol)xs[0], out procedure))
{
return expand(procedure.Call(xs.Skip(1).ToList()), topLevel);
}
else
{
return xs.Select(p => expand(p, false)).ToList();
}
};
return expand(expression, isTopLevel);
}
/// <summary>
/// Evaluates an s-expression
/// </summary>
/// <param name="expr">expression to be evaluated</param>
/// <param name="env">the environment in which the expression is evaluated</param>
/// <returns>the result of the evaluation</returns>
public static object EvaluateExpression(object expr, Environment env)
{
while (true)
{
if (expr is Symbol)
{
return env[(Symbol)expr];
}
else if (!(expr is List<object>))
{
return expr; // is a constant literal
}
else
{
List<object> exprList = (List<object>)expr;
if (Symbol.QUOTE.Equals(exprList[0]))
{
return exprList[1];
}
else if (Symbol.IF.Equals(exprList[0]))
{
var test = exprList[1];
var conseq = exprList[2];
var alt = exprList[3];
expr = ConvertToBool(EvaluateExpression(test, env)) ? conseq : alt;
}
else if (Symbol.DEFINE.Equals(exprList[0]))
{
var variable = (Symbol)exprList[1];
expr = exprList[2];
env[variable] = EvaluateExpression(expr, env);
return None.Instance; // TODO: what's the return type of define?
}
else if (Symbol.SET.Equals(exprList[0]))
{
var sym = (Symbol)exprList[1];
var containingEnv = env.TryFindContainingEnv(sym);
if (containingEnv == null)
{
throw new KeyNotFoundException("Symbol not defined: " + sym);
}
containingEnv[sym] = EvaluateExpression(exprList[2], env);
return None.Instance;
}
else if (Symbol.LAMBDA.Equals(exprList[0]))
{
Union<Symbol, List<Symbol>> parameters;
if (exprList[1] is Symbol)
{
parameters = new Union<Symbol, List<Symbol>>((Symbol)exprList[1]);
}
else
{
parameters = new Union<Symbol, List<Symbol>>(((List<object>)exprList[1]).Cast<Symbol>().ToList());
}
return new Procedure(parameters, exprList[2], env);
}
else if (Symbol.BEGIN.Equals(exprList[0]))
{
for (int i = 1; i < exprList.Count - 1 /* don't eval last expr yet */; i++)
{
EvaluateExpression(exprList[i], env);
}
expr = exprList[exprList.Count - 1]; // tail call optimization
}
else
{
// a procedure call
var rawProc = EvaluateExpression(exprList[0], env);
if (!(rawProc is ICallable))
{
throw new InvalidCastException(string.Format("Object is not callable: {0}", rawProc));
}
var args = exprList.Skip(1).Select(a => EvaluateExpression(a, env)).ToList();
if (rawProc is Procedure)
{
// Tail call optimization - instead of evaluating the procedure here which grows the
// stack by calling EvaluateExpression, we update the `expr` and `env` to be the
// body and the (params, args), and loop the evaluation from here.
var proc = (Procedure)rawProc;
expr = proc.Body;
env = Environment.FromVariablesAndValues(proc.Parameters, args, proc.Env);
}
else if (rawProc is NativeProcedure)
{
return ((NativeProcedure)rawProc).Call(args);
}
else
{
throw new InvalidOperationException("unexpected implementation of ICallable: " + rawProc.GetType().Name);
}
}
}
}
}
private static bool IsPair(object x)
{
return x is List<object> && ((List<object>)x).Count > 0;
}
private static object ExpandQuasiquote(object x)
{
if (!IsPair(x)) return new List<object> { Symbol.QUOTE, x };
var xs = (List<object>)x;
Utils.CheckSyntax(xs, !Symbol.UNQUOTE_SPLICING.Equals(xs[0]), "Cannot splice");
if (Symbol.UNQUOTE.Equals(xs[0]))
{
Utils.CheckSyntax(xs, xs.Count == 2);
return xs[1];
}
else if (IsPair(xs[0]) && Symbol.UNQUOTE_SPLICING.Equals(((List<object>)xs[0])[0]))
{
var x0 = (List<object>)xs[0];
Utils.CheckSyntax(x0, x0.Count == 2);
return new List<object> { Symbol.APPEND, x0[1], ExpandQuasiquote(xs.Skip(1).ToList()) };
}
else
{
return new List<object> { Symbol.CONS, ExpandQuasiquote(xs[0]), ExpandQuasiquote(xs.Skip(1).ToList()) };
}
}
private static object ParseAtom(string token)
{
int intVal;
double floatVal;
if (token == "#t")
{
return true;
}
else if (token == "#f")
{
return false;
}
else if (token[0] == '"')
{
return token.Substring(1, token.Length - 2);
}
else if (int.TryParse(token, out intVal))
{
return intVal;
}
else if (double.TryParse(token, out floatVal))
{
return floatVal;
}
else
{
return Symbol.FromString(token); // a symbol
}
}
private static bool ConvertToBool(object val)
{
if (val is bool) return (bool)val;
return true;
}
public struct EvaluationResult
{
private readonly Exception error;
private readonly object result;
public EvaluationResult(Exception error, object result) : this()
{
this.error = error;
this.result = result;
}
public Exception Error { get { return this.error; } }
public object Result { get { return this.result; } }
}
public class InPort
{
private const string tokenizer = @"^\s*(,@|[('`,)]|""(?:[\\].|[^\\""])*""|;.*|[^\s('""`,;)]*)(.*)";
private TextReader file;
private string line;
public InPort(TextReader file)
{
this.file = file;
this.line = string.Empty;
}
/// <summary>
/// Parses and returns the next token. Returns <see cref="Symbol.EOF"/> if there's no more content to read.
/// </summary>
public object NextToken()
{
while (true)
{
if (this.line == string.Empty)
{
this.line = this.file.ReadLine();
}
if (this.line == string.Empty)
{
continue;
}
else if (this.line == null)
{
return Symbol.EOF;
}
else
{
var res = Regex.Match(this.line, tokenizer);
var token = res.Groups[1].Value;
this.line = res.Groups[2].Value;
if (string.IsNullOrEmpty(token))
{
// 1st group is empty. All string falls into 2nd group. This usually means
// an error in the syntax, e.g., incomplete string "foo
var tmp = this.line;
this.line = string.Empty; // to continue reading next line
if (tmp.Trim() != string.Empty)
{
// this is a syntax error
Utils.CheckSyntax(tmp, false, "unexpected syntax");
}
}
if (!string.IsNullOrEmpty(token) && !token.StartsWith(";"))
{
return token;
}
}
}
}
}
}
}

107
src/schemy/Symbol.cs Normal file
View File

@ -0,0 +1,107 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
namespace Schemy
{
using System;
using System.Collections.Generic;
/// <summary>
/// Scheme symbol
/// </summary>
/// <remarks>
/// Symbols are interned so that symbols with the same name are actually of the same symbol object instance.
/// </remarks>
public class Symbol : IEquatable<Symbol>
{
private static readonly IDictionary<string, Symbol> table = new Dictionary<string, Symbol>();
public static readonly IReadOnlyDictionary<string, Symbol> QuotesMap = new Dictionary<string, Symbol>()
{
{ "'", Symbol.QUOTE },
{ "`", Symbol.QUASIQUOTE},
{ ",", Symbol.UNQUOTE},
{ ",@", Symbol.UNQUOTE_SPLICING},
};
private readonly string symbol;
/// <summary>
/// Initializes a new instance of the <see cref="Symbol"/> class.
/// </summary>
/// <param name="sym">The symbol</param>
/// <remarks>
/// This is private and the users should call <see cref="FromString"/> to instantiate a symbol object.
/// </remarks>
private Symbol(string sym)
{
this.symbol = sym;
}
public string AsString
{
get { return this.symbol; }
}
/// <summary>
/// Returns the interned symbol
/// </summary>
/// <param name="sym">The symbol name</param>
/// <returns>the symbol instance</returns>
public static Symbol FromString(string sym)
{
Symbol res;
if (!table.TryGetValue(sym, out res))
{
table[sym] = new Symbol(sym);
}
return table[sym];
}
#region wellknown symbols
public static Symbol IF { get { return Symbol.FromString("if"); } }
public static Symbol QUOTE { get { return Symbol.FromString("quote"); } }
public static Symbol SET { get { return Symbol.FromString("set!"); } }
public static Symbol DEFINE { get { return Symbol.FromString("define"); } }
public static Symbol LAMBDA { get { return Symbol.FromString("lambda"); } }
public static Symbol BEGIN { get { return Symbol.FromString("begin"); } }
public static Symbol DEFINE_MACRO { get { return Symbol.FromString("define-macro"); } }
public static Symbol QUASIQUOTE { get { return Symbol.FromString("quasiquote"); } }
public static Symbol UNQUOTE { get { return Symbol.FromString("unquote"); } }
public static Symbol UNQUOTE_SPLICING { get { return Symbol.FromString("unquote-splicing"); } }
public static Symbol EOF { get { return Symbol.FromString("#<eof-object>"); } }
public static Symbol APPEND { get { return Symbol.FromString("append"); } }
public static Symbol CONS { get { return Symbol.FromString("cons"); } }
#endregion wellknown symbols
#region object implementations
public override bool Equals(object obj)
{
if (obj == null) return false;
if (obj is Symbol)
{
return object.Equals(this.symbol, ((Symbol)obj).symbol);
}
else
{
return false;
}
}
public override string ToString()
{
return string.Format("'{0}", this.symbol);
}
public override int GetHashCode()
{
return this.symbol.GetHashCode();
}
public bool Equals(Symbol other)
{
return ((object)this).Equals(other);
}
#endregion object implementations
}
}

92
src/schemy/Utils.cs Normal file
View File

@ -0,0 +1,92 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
namespace Schemy
{
using System;
using System.Collections.Generic;
using System.Linq;
public static class Utils
{
/// <summary>
/// Checks the arity of input arguments of a procedure
/// </summary>
/// <param name="args">The arguments.</param>
/// <param name="acceptableArities">The acceptable arity.</param>
/// <exception cref="SyntaxError">thrown when that number of args doesn't match the expected arity.</exception>
public static void CheckArity(List<object> args, params int[] acceptableArities)
{
if (!acceptableArities.Contains(args.Count))
{
throw new SyntaxError(string.Format("Arity mismatch. Expecting {0}, Got {1}", string.Join(" or ", acceptableArities), args.Count));
}
}
/// <summary>
/// Throws <see cref="SyntaxError"/> if the syntax check is not successful, and prints the expression for diagnostics.
/// </summary>
/// <param name="expr">The expr that's being checked</param>
/// <param name="success">if the syntax check was successful</param>
/// <param name="msg">The error message</param>
/// <exception cref="SyntaxError">thrown when the syntax check was failed.</exception>
public static void CheckSyntax(object expr, bool success, string msg = null)
{
msg = msg ?? "Syntax error";
if (!success)
{
throw new SyntaxError(string.Format("{0}: {1}", msg, Utils.PrintExpr(expr)));
}
}
/// <summary>
/// Converts the type of the input to the desired type
/// </summary>
/// <typeparam name="T">desired target type</typeparam>
/// <param name="val">The input value.</param>
/// <returns>the object of the target type</returns>
/// <exception cref="InvalidOperationException">thrown when the conversion is not possible</exception>
/// <remarks>
/// This is needed because the regular casting can't handle some implicit convert when going through boxing/unboxing, e.g., int to object to double.
/// </remarks>
public static T ConvertType<T>(object val)
{
if (val is T) return (T)val;
// object x = 2;
// double y = (double)x; // <-- this would fail.
try
{
return (T)System.Convert.ChangeType(val, typeof(T));
}
catch
{
throw new InvalidOperationException(string.Format("Cannot convert {0} to type {1}", Utils.PrintExpr(val), typeof(T).Name));
}
}
public static string PrintExpr(object x)
{
if (x is bool)
{
return (bool)x ? "#t" : "#f";
}
else if (x is Symbol) return ((Symbol)x).AsString;
else if (x is string) return string.Format(@"""{0}""", x);
else if (x is List<object>) return string.Format("({0})", string.Join(" ", ((List<object>)x).Select(a => PrintExpr(a))));
else if (x == null) return string.Empty;
else return x.ToString();
}
/// <summary>
/// Converts a binary operator (function) to the variadic version.
/// </summary>
/// <remarks>
/// Given a summing function `sum(x, y) => result`. It creates a variadic version: `sum(x, y, ...) => result`.
/// </remarks>
public static Func<List<object>, object> MakeVariadic(Func<object, object, object> func)
{
return args => args.Aggregate(func);
}
}
}

22
src/schemy/init.ss Normal file
View File

@ -0,0 +1,22 @@
(define-macro let
(lambda args
(define specs (car args)) ; ( (var1 val1), ... )
(define bodies (cdr args)) ; (expr1 ...)
(if (null? specs)
`((lambda () ,@bodies))
(begin
(define spec1 (car specs)) ; (var1 val1)
(define spec_rest (cdr specs)) ; ((var2 val2) ...)
(define inner `((lambda ,(list (car spec1)) ,@bodies) ,(car (cdr spec1))))
`(let ,spec_rest ,inner)))))
(define-macro cond
(lambda args
(if (= 0 (length args)) ''()
(begin
(define first (car args))
(define rest (cdr args))
(define test1 (if (equal? (car first) 'else) '#t (car first)))
(define expr1 (car (cdr first)))
`(if ,test1 ,expr1
(cond ,@rest))))))

69
src/schemy/schemy.csproj Normal file
View File

@ -0,0 +1,69 @@
<?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>{E54139B7-CB81-4883-B8CD-40BAB5420EB8}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Schemy</RootNamespace>
<AssemblyName>schemy</AssemblyName>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
</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>
<PropertyGroup>
<StartupObject />
</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="Builtins.cs" />
<Compile Include="CommonTypes.cs" />
<Compile Include="Env.cs" />
<Compile Include="FileSystemAccessor.cs" />
<Compile Include="Procedure.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Schemy.cs" />
<Compile Include="Symbol.cs" />
<Compile Include="Utils.cs" />
<EmbeddedResource Include="init.ss"><LogicalName>init.ss</LogicalName></EmbeddedResource>
</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">
</Target>
-->
</Project>

27
src/test/Program.cs Normal file
View File

@ -0,0 +1,27 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
namespace test
{
using System;
using System.IO;
using Schemy;
class Program
{
static void Main(string[] args)
{
var interpreter = new Interpreter(fsAccessor: new ReadOnlyFileSystemAccessor());
using (var reader = new StreamReader(File.OpenRead("tests.ss")))
{
var result = interpreter.Evaluate(reader);
if (result.Error != null)
{
throw new InvalidOperationException(string.Format("Test Error: {0}", result.Error));
}
}
Console.WriteLine("Tests were successful");
}
}
}

95
src/test/test.csproj Normal file
View File

@ -0,0 +1,95 @@
<?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>{4A62A84F-58AA-4D1C-AA7C-D3CDF0C3FFA6}</ProjectGuid>
<OutputType>Exe</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>test</RootNamespace>
<AssemblyName>test</AssemblyName>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<PublishUrl>publish\</PublishUrl>
<Install>true</Install>
<InstallFrom>Disk</InstallFrom>
<UpdateEnabled>false</UpdateEnabled>
<UpdateMode>Foreground</UpdateMode>
<UpdateInterval>7</UpdateInterval>
<UpdateIntervalUnits>Days</UpdateIntervalUnits>
<UpdatePeriodically>false</UpdatePeriodically>
<UpdateRequired>false</UpdateRequired>
<MapFileExtensions>true</MapFileExtensions>
<ApplicationRevision>0</ApplicationRevision>
<ApplicationVersion>1.0.0.%2a</ApplicationVersion>
<IsWebBootstrapper>false</IsWebBootstrapper>
<UseApplicationTrust>false</UseApplicationTrust>
<BootstrapperEnabled>true</BootstrapperEnabled>
</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>
<BootstrapperPackage Include=".NETFramework,Version=v4.5.2">
<Visible>False</Visible>
<ProductName>Microsoft .NET Framework 4.5.2 %28x86 and x64%29</ProductName>
<Install>true</Install>
</BootstrapperPackage>
<BootstrapperPackage Include="Microsoft.Net.Framework.3.5.SP1">
<Visible>False</Visible>
<ProductName>.NET Framework 3.5 SP1</ProductName>
<Install>false</Install>
</BootstrapperPackage>
</ItemGroup>
<ItemGroup>
<Folder Include="Properties\" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<PropertyGroup>
<PostBuildEvent>copy $(ProjectDir)\tests.ss $(TargetDir)\tests.ss</PostBuildEvent>
</PropertyGroup>
<!-- 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">
</Target>
-->
</Project>

13
src/test/test.csproj.user Normal file
View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<PublishUrlHistory>publish\</PublishUrlHistory>
<InstallUrlHistory />
<SupportUrlHistory />
<UpdateUrlHistory />
<BootstrapperUrlHistory />
<ErrorReportUrlHistory />
<FallbackCulture>en-US</FallbackCulture>
<VerifyUploadedFiles>false</VerifyUploadedFiles>
</PropertyGroup>
</Project>

22
src/test/test.sln Normal file
View File

@ -0,0 +1,22 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14
VisualStudioVersion = 14.0.25420.1
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "test", "test.csproj", "{4A62A84F-58AA-4D1C-AA7C-D3CDF0C3FFA6}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{4A62A84F-58AA-4D1C-AA7C-D3CDF0C3FFA6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4A62A84F-58AA-4D1C-AA7C-D3CDF0C3FFA6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4A62A84F-58AA-4D1C-AA7C-D3CDF0C3FFA6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4A62A84F-58AA-4D1C-AA7C-D3CDF0C3FFA6}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal

147
src/test/tests.ss Normal file
View File

@ -0,0 +1,147 @@
;; ============
;; DEFINE TESTS
;; ============
;; ------------
;; Simple tests
;; ------------
(define simple-tests
(list
`(,(+ 1 2) 3)
`(,(- 2 1) 1)
`(,(* 2 3) 6)
`(,(/ 4 3) 1)
`(,(= 1 1) #t)
`(,(= 1 2) #f)
`(,(< 1 2) #t)
`(,(> 1 2) #f)
))
;; -----------
;; Test syntax
;; -----------
(define (test-syntax)
(define x 1)
(assert (= x 1))
(define f (lambda (x) (+ x 1)))
(assert (= 2 (f 1)))
;; Tests lambda definition and lexical scoping
;; `create-student` implements a minimum "struct" by using lexical variable
;; scoping. It is a function that returns a list of three functions:
;; 1. a function that returns the (name age)
;; 2. a function that sets the student's name
;; 3. a function that sets the student's age
(define (create-student name age)
(define (get-student) (list name age))
(define (set-name! v) (set! name v))
(define (set-age! v) (set! age v))
(list get-student set-name! set-age!))
(define john (create-student "john" 18))
(define mike (create-student "mike" 22))
(assert (equal? '("john" 18) ((list-ref john 0))))
((list-ref john 2) 19) ; set john's age to 19
(assert (equal? '("john" 19) ((list-ref john 0))))
(assert (equal? '("mike" 22) ((list-ref mike 0))))
;; Test proper tail recursion
(define (sum-up-to n acc)
(if (= n 0) acc
(sum-up-to (- n 1) (+ acc n))))
(assert (= 1250025000 (sum-up-to 50000 0)) "test proper tail recursion")
) ; test-syntax
;; ----------------------------
;; Test list related operations
;; ----------------------------
(define (test-list)
; test list is correctly constructed
; test `car` and `cdr`
(define ls (list 1 2 3 4))
(assert (list? ls))
(assert (not (list? 1)))
(assert (= 4 (length ls)))
(assert (= (car ls) 1))
(assert (= (car (cdr ls)) 2))
(assert (= (car (cdr (cdr ls))) 3))
(assert (= (car (cdr (cdr (cdr ls)))) 4))
; test list literal
(define ls2 '(1 2 3 4))
; test list operations
(assert (equal? ls ls2))
(assert (equal? ls (range 1 5)))
(assert (null? (list)))
(assert (not (null? (list 1))))
(assert (= 0 (length (list))))
; test list reversion
(define lsr '(4 3 2 1))
(assert (equal? (reverse ls) lsr))
; test `map`
(define (double x) (* x 2))
(assert (equal? `(2 4 6 8) (map double ls)))
) ; test-list
;; ----------
;; Test macro
;; ----------
(define-macro let
(lambda args
(define specs (car args)) ; ((var1 val1), ...)
(define bodies (cdr args)) ; (expr1 ...)
(if (null? specs)
`((lambda () ,@bodies))
(begin
(define spec1 (car specs)) ; (var1 val1)
(define spec_rest (cdr specs)) ; ((var2 val2) ...)
(define inner `((lambda ,(list (car spec1)) ,@bodies) ,(car (cdr spec1))))
`(let ,spec_rest ,inner)))))
(define (test-macro)
; test the `let` macro
(define x
(let ((a 4)
(b (+ 2 3)))
(* a b)))
(assert (= 20 x)))
;; =========
;; RUN TESTS
;; =========
;; run tests in ((actual, expected) ... )
(define (test specs)
(if (null? specs)
#t
(begin
(define head (car specs))
(assert (equal? (car head) (car (cdr head))))
(test (cdr specs)))))
(test simple-tests)
(test-list)
(test-syntax)
(test-macro)
;; =======================
;; Interpreter integration
;; =======================
; Test those global variables are accessible from interpreter environment table
; and that the interpreter can invoke the procedure to get the correct result.
(define ANSWER-TO-THE-ULTIMATE-QUESTION-OF-LIFE-UNIVERSE-AND-EVERYTHING 42)
(define (TIMES-TWO x) (* 2 x))
; Test that the last value is the return result of the interpreter
"good bye"

View File

@ -0,0 +1,45 @@
#|
This script evaluates a script file to transform input file content. The transformed output
is displayed to stdout.
This is currently broken because racket IO APIs doesn't strip BOM at the beginning of the file
|#
#lang at-exp racket
(require web-server/templates
racket/cmdline)
(define template-file (make-parameter ""))
(define input-file (make-parameter ""))
(command-line
#:once-each
[("-t" "--template") template
"template file to use. `FILENAME` and `INPUT` variables are available to the template"
(template-file template)]
#:args (input)
(input-file input))
#|
(define (read-content fn)
(define lines (port->lines (open-input-file #:mode 'text (input-file))))
(string-join lines "\n"))
|#
(define (read-content fn)
(port->string (open-input-file fn))
)
(define INPUT (read-content (input-file)))
(define FILENAME (path->string (file-name-from-path (input-file))))
(define ns (make-base-namespace))
(namespace-set-variable-value! 'INPUT INPUT #f ns)
(namespace-set-variable-value! 'FILENAME FILENAME #f ns)
(void
(write-string
(eval
(read
(open-input-file (template-file))) ns)))