创建项目

This commit is contained in:
Sanchime 2022-08-11 12:13:13 +08:00
parent 65041d171f
commit 9edd41b329
180 changed files with 21002 additions and 0 deletions

252
.gitignore vendored Normal file
View File

@ -0,0 +1,252 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
# 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
.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
# DNX
project.lock.json
artifacts/
*_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
# 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 ignoreable 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/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.pfx
*.publishsettings
node_modules/
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
# 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
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# 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

9
LICENSE Normal file
View File

@ -0,0 +1,9 @@
MIT License
Copyright (c) 2011 Roman Ivantsov
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

49
Sanchime.Irony.sln Normal file
View File

@ -0,0 +1,49 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.2.32630.192
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{24CE2713-8206-4C06-9397-7AD757E7D002}"
ProjectSection(SolutionItems) = preProject
build\local_publish.ps1 = build\local_publish.ps1
build\subst_version.ps1 = build\subst_version.ps1
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sanchime.Irony", "src\Irony\Sanchime.Irony.csproj", "{0E7499D3-3692-4F0A-AF22-695AAFF46A5F}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sanchime.Irony.Interpreter", "src\Irony.Interpreter\Sanchime.Irony.Interpreter.csproj", "{A464E8CE-0EF5-41DB-AD71-6982F1BAE0D3}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sanchime.Irony.Tests", "src\Irony.Tests\Sanchime.Irony.Tests.csproj", "{9177CCEE-4279-4A3C-9967-E1B0E9272521}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sanchime.Irony.SampleApp", "src\Irony.SampleApp\Sanchime.Irony.SampleApp.csproj", "{184455FD-0D48-42E1-AB72-00E6A0A452EC}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{0E7499D3-3692-4F0A-AF22-695AAFF46A5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0E7499D3-3692-4F0A-AF22-695AAFF46A5F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0E7499D3-3692-4F0A-AF22-695AAFF46A5F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0E7499D3-3692-4F0A-AF22-695AAFF46A5F}.Release|Any CPU.Build.0 = Release|Any CPU
{A464E8CE-0EF5-41DB-AD71-6982F1BAE0D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A464E8CE-0EF5-41DB-AD71-6982F1BAE0D3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A464E8CE-0EF5-41DB-AD71-6982F1BAE0D3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A464E8CE-0EF5-41DB-AD71-6982F1BAE0D3}.Release|Any CPU.Build.0 = Release|Any CPU
{9177CCEE-4279-4A3C-9967-E1B0E9272521}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9177CCEE-4279-4A3C-9967-E1B0E9272521}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9177CCEE-4279-4A3C-9967-E1B0E9272521}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9177CCEE-4279-4A3C-9967-E1B0E9272521}.Release|Any CPU.Build.0 = Release|Any CPU
{184455FD-0D48-42E1-AB72-00E6A0A452EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{184455FD-0D48-42E1-AB72-00E6A0A452EC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{184455FD-0D48-42E1-AB72-00E6A0A452EC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{184455FD-0D48-42E1-AB72-00E6A0A452EC}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3FBB2DED-F7C5-4EA6-B8D6-50AD8F561D33}
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,19 @@
using Sanchime.Irony.Ast;
using Sanchime.Irony.Interpreter.Ast.PrimitiveNodes;
namespace Sanchime.Irony.Interpreter.Ast
{
//Extension of AstContext
public class InterpreterAstContext : AstContext
{
public readonly OperatorHandler OperatorHandler;
public InterpreterAstContext(LanguageData language, OperatorHandler operatorHandler = null) : base(language)
{
OperatorHandler = operatorHandler ?? new OperatorHandler(language.Grammar.CaseSensitive);
base.DefaultIdentifierNodeType = typeof(IdentifierNode);
base.DefaultLiteralNodeType = typeof(LiteralValueNode);
base.DefaultNodeType = null;
}
}//class
}//ns

View File

@ -0,0 +1,147 @@
using System.Linq.Expressions;
namespace Sanchime.Irony.Interpreter.Ast
{
public class OperatorInfo
{
public string Symbol;
public ExpressionType ExpressionType;
public int Precedence;
public Associativity Associativity;
}
public class OperatorInfoDictionary : Dictionary<string, OperatorInfo>
{
public OperatorInfoDictionary(bool caseSensitive) : base(caseSensitive ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase)
{
}
public void Add(string symbol, ExpressionType expressionType, int precedence, Associativity associativity = Associativity.Left)
{
var info = new OperatorInfo()
{
Symbol = symbol,
ExpressionType = expressionType,
Precedence = precedence,
Associativity = associativity
};
this[symbol] = info;
}
}//class
public class OperatorHandler
{
private OperatorInfoDictionary _registeredOperators;
public OperatorHandler(bool languageCaseSensitive)
{
_registeredOperators = new OperatorInfoDictionary(languageCaseSensitive);
BuildDefaultOperatorMappings();
}
public ExpressionType GetOperatorExpressionType(string symbol)
{
OperatorInfo opInfo;
if (_registeredOperators.TryGetValue(symbol, out opInfo))
return opInfo.ExpressionType;
return CustomExpressionTypes.NotAnExpression;
}
public virtual ExpressionType GetUnaryOperatorExpressionType(string symbol)
{
return symbol.ToLowerInvariant() switch
{
"+" => ExpressionType.UnaryPlus,
"-" => ExpressionType.Negate,
"!" or "not" or "~" => ExpressionType.Not,
_ => CustomExpressionTypes.NotAnExpression,
};
}
public virtual ExpressionType GetBinaryOperatorForAugmented(ExpressionType augmented)
{
return augmented switch
{
ExpressionType.AddAssign or ExpressionType.AddAssignChecked => ExpressionType.AddChecked,
ExpressionType.AndAssign => ExpressionType.And,
ExpressionType.Decrement => ExpressionType.SubtractChecked,
ExpressionType.DivideAssign => ExpressionType.Divide,
ExpressionType.ExclusiveOrAssign => ExpressionType.ExclusiveOr,
ExpressionType.LeftShiftAssign => ExpressionType.LeftShift,
ExpressionType.ModuloAssign => ExpressionType.Modulo,
ExpressionType.MultiplyAssign or ExpressionType.MultiplyAssignChecked => ExpressionType.MultiplyChecked,
ExpressionType.OrAssign => ExpressionType.Or,
ExpressionType.RightShiftAssign => ExpressionType.RightShift,
ExpressionType.SubtractAssign or ExpressionType.SubtractAssignChecked => ExpressionType.SubtractChecked,
_ => CustomExpressionTypes.NotAnExpression,
};
}
public virtual OperatorInfoDictionary BuildDefaultOperatorMappings()
{
var dict = _registeredOperators;
dict.Clear();
int p = 0; //precedence
p += 10;
dict.Add("=", ExpressionType.Assign, p);
dict.Add("+=", ExpressionType.AddAssignChecked, p);
dict.Add("-=", ExpressionType.SubtractAssignChecked, p);
dict.Add("*=", ExpressionType.MultiplyAssignChecked, p);
dict.Add("/=", ExpressionType.DivideAssign, p);
dict.Add("%=", ExpressionType.ModuloAssign, p);
dict.Add("|=", ExpressionType.OrAssign, p);
dict.Add("&=", ExpressionType.AndAssign, p);
dict.Add("^=", ExpressionType.ExclusiveOrAssign, p);
p += 10;
dict.Add("==", ExpressionType.Equal, p);
dict.Add("!=", ExpressionType.NotEqual, p);
dict.Add("<>", ExpressionType.NotEqual, p);
p += 10;
dict.Add("<", ExpressionType.LessThan, p);
dict.Add("<=", ExpressionType.LessThanOrEqual, p);
dict.Add(">", ExpressionType.GreaterThan, p);
dict.Add(">=", ExpressionType.GreaterThanOrEqual, p);
p += 10;
dict.Add("|", ExpressionType.Or, p);
dict.Add("or", ExpressionType.Or, p);
dict.Add("||", ExpressionType.OrElse, p);
dict.Add("orelse", ExpressionType.OrElse, p);
dict.Add("^", ExpressionType.ExclusiveOr, p);
dict.Add("xor", ExpressionType.ExclusiveOr, p);
p += 10;
dict.Add("&", ExpressionType.And, p);
dict.Add("and", ExpressionType.And, p);
dict.Add("&&", ExpressionType.AndAlso, p);
dict.Add("andalso", ExpressionType.AndAlso, p);
p += 10;
dict.Add("!", ExpressionType.Not, p);
dict.Add("not", ExpressionType.Not, p);
p += 10;
dict.Add("<<", ExpressionType.LeftShift, p);
dict.Add(">>", ExpressionType.RightShift, p);
p += 10;
dict.Add("+", ExpressionType.AddChecked, p);
dict.Add("-", ExpressionType.SubtractChecked, p);
p += 10;
dict.Add("*", ExpressionType.MultiplyChecked, p);
dict.Add("/", ExpressionType.Divide, p);
dict.Add("%", ExpressionType.Modulo, p);
dict.Add("**", ExpressionType.Power, p);
p += 10;
dict.Add("??", ExpressionType.Coalesce, p);
dict.Add("?", ExpressionType.Conditional, p);
return dict;
}//method
}
}

View File

@ -0,0 +1,44 @@
#region License
/* **********************************************************************************
* Copyright (c) Roman Ivantsov
* This source code is subject to terms and conditions of the MIT License
* for Irony. A copy of the license can be found in the License.txt file
* at the root of this distribution.
* By using this source code in any fashion, you are agreeing to be bound by the terms of the
* MIT License.
* You must not remove this notice from this software.
* **********************************************************************************/
#endregion
using System.Linq.Expressions;
namespace Sanchime.Irony.Interpreter.Ast
{
//This interface is expected by Irony's Gramamr Explorer.
public interface ICallTarget
{
object Call(ScriptThread thread, object[] parameters);
}
//Simple visitor interface
public interface IAstVisitor
{
void BeginVisit(IVisitableNode node);
void EndVisit(IVisitableNode node);
}
public interface IVisitableNode
{
void AcceptVisitor(IAstVisitor visitor);
}
public interface IOperatorHelper
{
ExpressionType GetOperatorExpressionType(string symbol);
ExpressionType GetUnaryOperatorExpressionType(string symbol);
}
}

View File

@ -0,0 +1,227 @@
#region License
/* **********************************************************************************
* Copyright (c) Roman Ivantsov
* This source code is subject to terms and conditions of the MIT License
* for Irony. A copy of the license can be found in the License.txt file
* at the root of this distribution.
* By using this source code in any fashion, you are agreeing to be bound by the terms of the
* MIT License.
* You must not remove this notice from this software.
* **********************************************************************************/
#endregion
using Sanchime.Irony.Ast;
using Sanchime.Irony.Interpreter.Ast.SpecialNodes;
using Sanchime.Irony.Interpreter.Scopes;
using System.Linq.Expressions;
namespace Sanchime.Irony.Interpreter.Ast
{
public static class CustomExpressionTypes
{
public const ExpressionType NotAnExpression = (ExpressionType)(-1);
}
public class AstNodeList : List<AstNode>
{ }
//Base AST node class
public partial class AstNode : IAstNodeInit, IBrowsableAstNode, IVisitableNode
{
public AstNode Parent;
public BnfTerm Term;
public SourceSpan Span { get; set; }
public AstNodeFlags Flags;
protected ExpressionType ExpressionType = CustomExpressionTypes.NotAnExpression;
protected object LockObject = new object();
//Used for pointing to error location. For most nodes it would be the location of the node itself.
// One exception is BinExprNode: when we get "Division by zero" error evaluating
// x = (5 + 3) / (2 - 2)
// it is better to point to "/" as error location, rather than the first "(" - which is the start
// location of binary expression.
public SourceLocation ErrorAnchor;
//UseType is set by parent
public NodeUseType UseType = NodeUseType.Unknown;
// Role is a free-form string used as prefix in ToString() representation of the node.
// Node's parent can set it to "property name" or role of the child node in parent's node currentFrame.Context.
public string Role;
// Default AstNode.ToString() returns 'Role: AsString', which is used for showing node in AST tree.
public virtual string AsString { get; protected set; }
public readonly AstNodeList ChildNodes = new AstNodeList(); //List of child nodes
//Reference to Evaluate method implementation. Initially set to DoEvaluate virtual method.
public EvaluateMethod Evaluate;
public ValueSetterMethod SetValue;
// Public default constructor
public AstNode()
{
Evaluate = DoEvaluate;
SetValue = DoSetValue;
}
public SourceLocation Location
{ get { return Span.Location; } }
#region IAstNodeInit Members
public virtual void Init(AstContext context, ParseTreeNode treeNode)
{
Term = treeNode.Term;
Span = treeNode.Span;
ErrorAnchor = Location;
treeNode.AstNode = this;
AsString = Term == null ? GetType().Name : Term.Name;
}
#endregion
//ModuleNode - computed on demand
public AstNode ModuleNode
{
get
{
if (_moduleNode == null)
{
_moduleNode = Parent == null ? this : Parent.ModuleNode;
}
return _moduleNode;
}
set { _moduleNode = value; }
}
private AstNode _moduleNode;
#region virtual methods: DoEvaluate, SetValue, IsConstant, SetIsTail, GetDependentScopeInfo
public virtual void Reset()
{
_moduleNode = null;
Evaluate = DoEvaluate;
foreach (var child in ChildNodes)
child.Reset();
}
//By default the Evaluate field points to this method.
protected virtual object DoEvaluate(ScriptThread thread)
{
//These 2 lines are standard prolog/epilog statements. Place them in every Evaluate and SetValue implementations.
thread.CurrentNode = this; //standard prolog
thread.CurrentNode = Parent; //standard epilog
return null;
}
public virtual void DoSetValue(ScriptThread thread, object value)
{
//Place the prolog/epilog lines in every implementation of SetValue method (see DoEvaluate above)
}
public virtual bool IsConstant()
{
return false;
}
/// <summary>
/// Sets a flag indicating that the node is in tail position. The value is propagated from parent to children.
/// Should propagate this call to appropriate children.
/// </summary>
public virtual void SetIsTail()
{
Flags |= AstNodeFlags.IsTail;
}
/// <summary>
/// Dependent scope is a scope produced by the node. For ex, FunctionDefNode defines a scope
/// </summary>
public virtual ScopeInfo DependentScopeInfo
{
get { return _dependentScope; }
set { _dependentScope = value; }
}
private ScopeInfo _dependentScope;
#endregion
#region IBrowsableAstNode Members
public virtual System.Collections.IEnumerable GetChildNodes()
{
return ChildNodes;
}
public int Position
{
get { return Span.Location.Position; }
}
#endregion
#region Visitors, Iterators
//the first primitive Visitor facility
public virtual void AcceptVisitor(IAstVisitor visitor)
{
visitor.BeginVisit(this);
if (ChildNodes.Count > 0)
foreach (AstNode node in ChildNodes)
node.AcceptVisitor(visitor);
visitor.EndVisit(this);
}
//Node traversal
public IEnumerable<AstNode> GetAll()
{
AstNodeList result = new AstNodeList();
AddAll(result);
return result;
}
private void AddAll(AstNodeList list)
{
list.Add(this);
foreach (AstNode child in ChildNodes)
if (child != null)
child.AddAll(list);
}
#endregion
#region overrides: ToString
public override string ToString()
{
return string.IsNullOrEmpty(Role) ? AsString : Role + ": " + AsString;
}
#endregion
#region Utility methods: AddChild, HandleError
protected AstNode AddChild(string role, ParseTreeNode childParseNode)
{
return AddChild(NodeUseType.Unknown, role, childParseNode);
}
protected AstNode AddChild(NodeUseType useType, string role, ParseTreeNode childParseNode)
{
var child = (AstNode)childParseNode.AstNode;
if (child == null)
child = new NullNode(childParseNode.Term); //put a stub to throw an exception with clear message on attempt to evaluate.
child.Role = role;
child.Parent = this;
ChildNodes.Add(child);
return child;
}
#endregion
}//class
}//namespace

View File

@ -0,0 +1,42 @@
#region License
/* **********************************************************************************
* Copyright (c) Roman Ivantsov
* This source code is subject to terms and conditions of the MIT License
* for Irony. A copy of the license can be found in the License.txt file
* at the root of this distribution.
* By using this source code in any fashion, you are agreeing to be bound by the terms of the
* MIT License.
* You must not remove this notice from this software.
* **********************************************************************************/
#endregion
namespace Sanchime.Irony.Interpreter.Ast
{
public delegate object EvaluateMethod(ScriptThread thread);
public delegate void ValueSetterMethod(ScriptThread thread, object value);
[Flags]
public enum AstNodeFlags
{
None = 0x0,
IsTail = 0x01, //the node is in tail position
//IsScope = 0x02, //node defines scope for local variables
}
[Flags]
public enum NodeUseType
{
Unknown,
Name, //identifier used as a Name container - system would not use it's Evaluate method directly
CallTarget,
ValueRead,
ValueWrite,
ValueReadWrite,
Parameter,
Keyword,
SpecialSymbol,
}
}

View File

@ -0,0 +1,138 @@
#region License
/* **********************************************************************************
* Copyright (c) Roman Ivantsov
* This source code is subject to terms and conditions of the MIT License
* for Irony. A copy of the license can be found in the License.txt file
* at the root of this distribution.
* By using this source code in any fashion, you are agreeing to be bound by the terms of the
* MIT License.
* You must not remove this notice from this software.
* **********************************************************************************/
#endregion
using Sanchime.Irony.Ast;
using System.Linq.Expressions;
namespace Sanchime.Irony.Interpreter.Ast.Expressions
{
public class BinaryOperationNode : AstNode
{
public AstNode Left, Right;
public string OpSymbol;
public ExpressionType Op;
private OperatorImplementation _lastUsed;
private object _constValue;
private int _failureCount;
public BinaryOperationNode()
{ }
public override void Init(AstContext context, ParseTreeNode treeNode)
{
base.Init(context, treeNode);
var nodes = treeNode.GetMappedChildNodes();
Left = AddChild("Arg", nodes[0]);
Right = AddChild("Arg", nodes[2]);
var opToken = nodes[1].FindToken();
OpSymbol = opToken.Text;
var ictxt = context as InterpreterAstContext;
Op = ictxt.OperatorHandler.GetOperatorExpressionType(OpSymbol);
// Set error anchor to operator, so on error (Division by zero) the explorer will point to
// operator node as location, not to the very beginning of the first operand.
ErrorAnchor = opToken.Location;
AsString = Op + "(operator)";
}
protected override object DoEvaluate(ScriptThread thread)
{
thread.CurrentNode = this; //standard prolog
//assign implementation method
Evaluate = Op switch
{
ExpressionType.AndAlso => EvaluateAndAlso,
ExpressionType.OrElse => EvaluateOrElse,
_ => DefaultEvaluateImplementation,
};
// actually evaluate and get the result.
var result = Evaluate(thread);
// Check if result is constant - if yes, save the value and switch to method that directly returns the result.
if (IsConstant())
{
_constValue = result;
AsString = Op + "(operator) Const=" + _constValue;
Evaluate = EvaluateConst;
}
thread.CurrentNode = Parent; //standard epilog
return result;
}
private object EvaluateAndAlso(ScriptThread thread)
{
var leftValue = Left.Evaluate(thread);
if (!thread.Runtime.IsTrue(leftValue)) return leftValue; //if false return immediately
return Right.Evaluate(thread);
}
private object EvaluateOrElse(ScriptThread thread)
{
var leftValue = Left.Evaluate(thread);
if (thread.Runtime.IsTrue(leftValue)) return leftValue;
return Right.Evaluate(thread);
}
protected object EvaluateFast(ScriptThread thread)
{
thread.CurrentNode = this; //standard prolog
var arg1 = Left.Evaluate(thread);
var arg2 = Right.Evaluate(thread);
//If we have _lastUsed, go straight for it; if types mismatch it will throw
if (_lastUsed != null)
{
try
{
var res = _lastUsed.EvaluateBinary(arg1, arg2);
thread.CurrentNode = Parent; //standard epilog
return res;
}
catch
{
_lastUsed = null;
_failureCount++;
// if failed 3 times, change to method without direct try
if (_failureCount > 3)
Evaluate = DefaultEvaluateImplementation;
} //catch
}// if _lastUsed
// go for normal evaluation
var result = thread.Runtime.ExecuteBinaryOperator(Op, arg1, arg2, ref _lastUsed);
thread.CurrentNode = Parent; //standard epilog
return result;
}//method
protected object DefaultEvaluateImplementation(ScriptThread thread)
{
thread.CurrentNode = this; //standard prolog
var arg1 = Left.Evaluate(thread);
var arg2 = Right.Evaluate(thread);
var result = thread.Runtime.ExecuteBinaryOperator(Op, arg1, arg2, ref _lastUsed);
thread.CurrentNode = Parent; //standard epilog
return result;
}//method
private object EvaluateConst(ScriptThread thread)
{
return _constValue;
}
public override bool IsConstant()
{
if (_isConstant) return true;
_isConstant = Left.IsConstant() && Right.IsConstant();
return _isConstant;
}
private bool _isConstant;
}//class
}//namespace

View File

@ -0,0 +1,44 @@
#region License
/* **********************************************************************************
* Copyright (c) Roman Ivantsov
* This source code is subject to terms and conditions of the MIT License
* for Irony. A copy of the license can be found in the License.txt file
* at the root of this distribution.
* By using this source code in any fashion, you are agreeing to be bound by the terms of the
* MIT License.
* You must not remove this notice from this software.
* **********************************************************************************/
#endregion
using Sanchime.Irony.Ast;
namespace Sanchime.Irony.Interpreter.Ast.Expressions
{
//A node representing expression list - for example, list of argument expressions in function call
public class ExpressionListNode : AstNode
{
public override void Init(AstContext context, ParseTreeNode treeNode)
{
base.Init(context, treeNode);
foreach (var child in treeNode.ChildNodes)
{
AddChild(NodeUseType.Parameter, "expr", child);
}
AsString = "Expression list";
}
protected override object DoEvaluate(ScriptThread thread)
{
thread.CurrentNode = this; //standard prolog
var values = new object[ChildNodes.Count];
for (int i = 0; i < values.Length; i++)
{
values[i] = ChildNodes[i].Evaluate(thread);
}
thread.CurrentNode = Parent; //standard epilog
return values;
}
}//class
}//namespace

View File

@ -0,0 +1,64 @@
#region License
/* **********************************************************************************
* Copyright (c) Roman Ivantsov
* This source code is subject to terms and conditions of the MIT License
* for Irony. A copy of the license can be found in the License.txt file
* at the root of this distribution.
* By using this source code in any fashion, you are agreeing to be bound by the terms of the
* MIT License.
* You must not remove this notice from this software.
* **********************************************************************************/
#endregion
using Sanchime.Irony.Ast;
namespace Sanchime.Irony.Interpreter.Ast.Expressions
{
public class IfNode : AstNode
{
public AstNode Test;
public AstNode IfTrue;
public AstNode IfFalse;
public override void Init(AstContext context, ParseTreeNode treeNode)
{
base.Init(context, treeNode);
var nodes = treeNode.GetMappedChildNodes();
Test = AddChild("Test", nodes[0]);
IfTrue = AddChild("IfTrue", nodes[1]);
if (nodes.Count > 2)
IfFalse = AddChild("IfFalse", nodes[2]);
}
protected override object DoEvaluate(ScriptThread thread)
{
thread.CurrentNode = this; //standard prolog
object result = null;
var test = Test.Evaluate(thread);
var isTrue = thread.Runtime.IsTrue(test);
if (isTrue)
{
if (IfTrue != null)
result = IfTrue.Evaluate(thread);
}
else
{
if (IfFalse != null)
result = IfFalse.Evaluate(thread);
}
thread.CurrentNode = Parent; //standard epilog
return result;
}
public override void SetIsTail()
{
base.SetIsTail();
if (IfTrue != null)
IfTrue.SetIsTail();
if (IfFalse != null)
IfFalse.SetIsTail();
}
}//class
}//namespace

View File

@ -0,0 +1,68 @@
#region License
/* **********************************************************************************
* Copyright (c) Roman Ivantsov
* This source code is subject to terms and conditions of the MIT License
* for Irony. A copy of the license can be found in the License.txt file
* at the root of this distribution.
* By using this source code in any fashion, you are agreeing to be bound by the terms of the
* MIT License.
* You must not remove this notice from this software.
* **********************************************************************************/
#endregion
using Sanchime.Irony.Ast;
using System.Linq.Expressions;
namespace Sanchime.Irony.Interpreter.Ast.Expressions
{
public class IncDecNode : AstNode
{
public bool IsPostfix;
public string OpSymbol;
public string BinaryOpSymbol; //corresponding binary operation: + for ++, - for --
public ExpressionType BinaryOp;
public AstNode Argument;
private OperatorImplementation _lastUsed;
public override void Init(AstContext context, ParseTreeNode treeNode)
{
base.Init(context, treeNode);
var nodes = treeNode.GetMappedChildNodes();
FindOpAndDetectPostfix(nodes);
int argIndex = IsPostfix ? 0 : 1;
Argument = AddChild(NodeUseType.ValueReadWrite, "Arg", nodes[argIndex]);
BinaryOpSymbol = OpSymbol[0].ToString(); //take a single char out of ++ or --
var interpContext = (InterpreterAstContext)context;
BinaryOp = interpContext.OperatorHandler.GetOperatorExpressionType(BinaryOpSymbol);
base.AsString = OpSymbol + (IsPostfix ? "(postfix)" : "(prefix)");
}
private void FindOpAndDetectPostfix(ParseTreeNodeList mappedNodes)
{
IsPostfix = false; //assume it
OpSymbol = mappedNodes[0].FindTokenAndGetText();
if (OpSymbol == "--" || OpSymbol == "++") return;
IsPostfix = true;
OpSymbol = mappedNodes[1].FindTokenAndGetText();
}
protected override object DoEvaluate(ScriptThread thread)
{
thread.CurrentNode = this; //standard prolog
var oldValue = Argument.Evaluate(thread);
var newValue = thread.Runtime.ExecuteBinaryOperator(BinaryOp, oldValue, 1, ref _lastUsed);
Argument.SetValue(thread, newValue);
var result = IsPostfix ? oldValue : newValue;
thread.CurrentNode = Parent; //standard epilog
return result;
}
public override void SetIsTail()
{
base.SetIsTail();
Argument.SetIsTail();
}
}//class
}

View File

@ -0,0 +1,91 @@
using Sanchime.Irony.Ast;
using System.Collections;
using System.Reflection;
namespace Sanchime.Irony.Interpreter.Ast.Expressions
{
public class IndexedAccessNode : AstNode
{
private AstNode _target, _index;
public override void Init(AstContext context, ParseTreeNode treeNode)
{
base.Init(context, treeNode);
var nodes = treeNode.GetMappedChildNodes();
_target = AddChild("Target", nodes.First());
_index = AddChild("Index", nodes.Last());
AsString = "[" + _index + "]";
}
protected override object DoEvaluate(ScriptThread thread)
{
thread.CurrentNode = this; //standard prolog
object result = null;
var targetValue = _target.Evaluate(thread);
if (targetValue == null)
thread.ThrowScriptError("Target object is null.");
var type = targetValue.GetType();
var indexValue = _index.Evaluate(thread);
//string and array are special cases
if (type == typeof(string))
{
var sTarget = targetValue as string;
var iIndex = Convert.ToInt32(indexValue);
result = sTarget[iIndex];
}
else if (type.IsArray)
{
var arr = targetValue as Array;
var iIndex = Convert.ToInt32(indexValue);
result = arr.GetValue(iIndex);
}
else if (targetValue is IDictionary dict)
{
result = dict[indexValue];
}
else
{
//const BindingFlags flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase | BindingFlags.InvokeMethod;
//result = type.InvokeMember("get_Item", flags, null, targetValue, new object[] { indexValue });
var methodInfo = type.GetTypeInfo().GetDeclaredMethod("get_Item");
methodInfo.Invoke(targetValue, new object[] { indexValue });
}
thread.CurrentNode = Parent; //standard epilog
return result;
}
public override void DoSetValue(ScriptThread thread, object value)
{
thread.CurrentNode = this; //standard prolog
var targetValue = _target.Evaluate(thread);
if (targetValue == null)
thread.ThrowScriptError("Target object is null.");
var type = targetValue.GetType();
var indexValue = _index.Evaluate(thread);
//string and array are special cases
if (type == typeof(string))
{
thread.ThrowScriptError("String is read-only.");
}
else if (type.IsArray)
{
var arr = targetValue as Array;
var iIndex = Convert.ToInt32(indexValue);
arr.SetValue(value, iIndex);
}
else if (targetValue is IDictionary dict)
{
dict[indexValue] = value;
}
else
{
//const BindingFlags flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase | BindingFlags.InvokeMethod;
var methodInfo = type.GetTypeInfo().GetDeclaredMethod("set_Item");
methodInfo.Invoke(targetValue, new object[] { indexValue, value });
//type.InvokeMember("set_Item", flags, null, targetValue, new object[] { indexValue, value });
}
thread.CurrentNode = Parent; //standard epilog
}//method
}//class
}//namespace

View File

@ -0,0 +1,91 @@
using Sanchime.Irony.Ast;
using System.Reflection;
namespace Sanchime.Irony.Interpreter.Ast.Expressions
{
//For now we do not support dotted namespace/type references like System.Collections or System.Collections.List.
// Only references to objects like 'objFoo.Name' or 'objFoo.DoStuff()'
public class MemberAccessNode : AstNode
{
private AstNode _left;
private string _memberName;
public override void Init(AstContext context, ParseTreeNode treeNode)
{
base.Init(context, treeNode);
var nodes = treeNode.GetMappedChildNodes();
_left = AddChild("Target", nodes[0]);
var right = nodes[nodes.Count - 1];
_memberName = right.FindTokenAndGetText();
ErrorAnchor = right.Span.Location;
AsString = "." + _memberName;
}
protected override object DoEvaluate(ScriptThread thread)
{
thread.CurrentNode = this; //standard prolog
object result = null;
var leftValue = _left.Evaluate(thread);
if (leftValue == null)
thread.ThrowScriptError("Target object is null.");
var type = leftValue.GetType();
var members = type.GetMember(_memberName);
if (members == null || members.Length == 0)
thread.ThrowScriptError("Member {0} not found in object of type {1}.", _memberName, type);
var member = members[0];
switch (member.MemberType)
{
case MemberTypes.Property:
var propInfo = member as PropertyInfo;
result = propInfo.GetValue(leftValue, null);
break;
case MemberTypes.Field:
var fieldInfo = member as FieldInfo;
result = fieldInfo.GetValue(leftValue);
break;
case MemberTypes.Method:
result = new ClrMethodBindingTargetInfo(type, _memberName, leftValue); //this bindingInfo works as a call target
break;
default:
thread.ThrowScriptError("Invalid member type ({0}) for member {1} of type {2}.", member.MemberType, _memberName, type);
result = null;
break;
}//switch
thread.CurrentNode = Parent; //standard epilog
return result;
}
public override void DoSetValue(ScriptThread thread, object value)
{
thread.CurrentNode = this; //standard prolog
var leftValue = _left.Evaluate(thread);
if (leftValue == null)
thread.ThrowScriptError("Target object is null.");
var type = leftValue.GetType();
var members = type.GetMember(_memberName);
if (members == null || members.Length == 0)
thread.ThrowScriptError("Member {0} not found in object of type {1}.", _memberName, type);
var member = members[0];
switch (member.MemberType)
{
case MemberTypes.Property:
var propInfo = member as PropertyInfo;
propInfo.SetValue(leftValue, value, null);
break;
case MemberTypes.Field:
var fieldInfo = member as FieldInfo;
fieldInfo.SetValue(leftValue, value);
break;
default:
thread.ThrowScriptError("Cannot assign to member {0} of type {1}.", _memberName, type);
break;
}//switch
thread.CurrentNode = Parent; //standard epilog
}//method
}//class
}//namespace

View File

@ -0,0 +1,51 @@
#region License
/* **********************************************************************************
* Copyright (c) Roman Ivantsov
* This source code is subject to terms and conditions of the MIT License
* for Irony. A copy of the license can be found in the License.txt file
* at the root of this distribution.
* By using this source code in any fashion, you are agreeing to be bound by the terms of the
* MIT License.
* You must not remove this notice from this software.
* **********************************************************************************/
#endregion
using Sanchime.Irony.Ast;
namespace Sanchime.Irony.Interpreter.Ast.Expressions
{
public class UnaryOperationNode : AstNode
{
public string OpSymbol;
public AstNode Argument;
private OperatorImplementation _lastUsed;
public override void Init(AstContext context, ParseTreeNode treeNode)
{
base.Init(context, treeNode);
var nodes = treeNode.GetMappedChildNodes();
OpSymbol = nodes[0].FindTokenAndGetText();
Argument = AddChild("Arg", nodes[1]);
base.AsString = OpSymbol + "(unary op)";
var interpContext = (InterpreterAstContext)context;
ExpressionType = interpContext.OperatorHandler.GetUnaryOperatorExpressionType(OpSymbol);
}
protected override object DoEvaluate(ScriptThread thread)
{
thread.CurrentNode = this; //standard prolog
var arg = Argument.Evaluate(thread);
var result = thread.Runtime.ExecuteUnaryOperator(ExpressionType, arg, ref _lastUsed);
thread.CurrentNode = Parent; //standard epilog
return result;
}
public override void SetIsTail()
{
base.SetIsTail();
Argument.SetIsTail();
}
}//class
}//namespace

View File

@ -0,0 +1,28 @@
using Sanchime.Irony.Interpreter.Scopes;
namespace Sanchime.Irony.Interpreter.Ast.Functions
{
public class Closure : ICallTarget
{
//The scope that created closure; is used to find Parents (enclosing scopes)
public Scope ParentScope;
public LambdaNode Lamda;
public Closure(Scope parentScope, LambdaNode targetNode)
{
ParentScope = parentScope;
Lamda = targetNode;
}
public object Call(ScriptThread thread, object[] parameters)
{
return Lamda.Call(ParentScope, thread, parameters);
}
public override string ToString()
{
return Lamda.ToString(); //returns nice string like "<function add>"
}
} //class
}

View File

@ -0,0 +1,139 @@
#region License
/* **********************************************************************************
* Copyright (c) Roman Ivantsov
* This source code is subject to terms and conditions of the MIT License
* for Irony. A copy of the license can be found in the License.txt file
* at the root of this distribution.
* By using this source code in any fashion, you are agreeing to be bound by the terms of the
* MIT License.
* You must not remove this notice from this software.
* **********************************************************************************/
#endregion
using Sanchime.Irony.Ast;
using Sanchime.Irony.Interpreter.Utilities;
namespace Sanchime.Irony.Interpreter.Ast.Functions
{
//A node representing function call. Also handles Special Forms
public class FunctionCallNode : AstNode
{
private AstNode TargetRef;
private AstNode Arguments;
private string _targetName;
private SpecialForm _specialForm;
private AstNode[] _specialFormArgs;
public override void Init(AstContext context, ParseTreeNode treeNode)
{
base.Init(context, treeNode);
var nodes = treeNode.GetMappedChildNodes();
TargetRef = AddChild("Target", nodes[0]);
TargetRef.UseType = NodeUseType.CallTarget;
_targetName = nodes[0].FindTokenAndGetText();
Arguments = AddChild("Args", nodes[1]);
AsString = "Call " + _targetName;
}
protected override object DoEvaluate(ScriptThread thread)
{
thread.CurrentNode = this; //standard prolog
SetupEvaluateMethod(thread);
var result = Evaluate(thread);
thread.CurrentNode = Parent; //standard epilog
return result;
}
private void SetupEvaluateMethod(ScriptThread thread)
{
var languageTailRecursive = thread.Runtime.Language.Grammar.LanguageFlags.IsSet(LanguageFlags.TailRecursive);
lock (LockObject)
{
var target = TargetRef.Evaluate(thread);
if (target is SpecialForm)
{
_specialForm = target as SpecialForm;
_specialFormArgs = Arguments.ChildNodes.ToArray();
Evaluate = EvaluateSpecialForm;
}
else
{
if (languageTailRecursive)
{
var isTail = Flags.IsSet(AstNodeFlags.IsTail);
if (isTail)
Evaluate = EvaluateTail;
else
Evaluate = EvaluateWithTailCheck;
}
else
Evaluate = EvaluateNoTail;
}
}//lock
}
// Evaluation for special forms
private object EvaluateSpecialForm(ScriptThread thread)
{
thread.CurrentNode = this; //standard prolog
var result = _specialForm(thread, _specialFormArgs);
thread.CurrentNode = Parent; //standard epilog
return result;
}
// Evaluation for non-tail languages
private object EvaluateNoTail(ScriptThread thread)
{
thread.CurrentNode = this; //standard prolog
var target = TargetRef.Evaluate(thread);
var iCall = target as ICallTarget;
if (iCall == null)
thread.ThrowScriptError(Resources.ErrVarIsNotCallable, _targetName);
var args = (object[])Arguments.Evaluate(thread);
object result = iCall.Call(thread, args);
thread.CurrentNode = Parent; //standard epilog
return result;
}
//Evaluation for tailed languages
private object EvaluateTail(ScriptThread thread)
{
thread.CurrentNode = this; //standard prolog
var target = TargetRef.Evaluate(thread);
var iCall = target as ICallTarget;
if (iCall == null)
thread.ThrowScriptError(Resources.ErrVarIsNotCallable, _targetName);
var args = (object[])Arguments.Evaluate(thread);
thread.Tail = iCall;
thread.TailArgs = args;
thread.CurrentNode = Parent; //standard epilog
return null;
}
private object EvaluateWithTailCheck(ScriptThread thread)
{
thread.CurrentNode = this; //standard prolog
var target = TargetRef.Evaluate(thread);
var iCall = target as ICallTarget;
if (iCall == null)
thread.ThrowScriptError(Resources.ErrVarIsNotCallable, _targetName);
var args = (object[])Arguments.Evaluate(thread);
object result = null;
result = iCall.Call(thread, args);
//Note that after invoking tail we can get another tail.
// So we need to keep calling tails while they are there.
while (thread.Tail != null)
{
var tail = thread.Tail;
var tailArgs = thread.TailArgs;
thread.Tail = null;
thread.TailArgs = null;
result = tail.Call(thread, tailArgs);
}
thread.CurrentNode = Parent; //standard epilog
return result;
}
}//class
}//namespace

View File

@ -0,0 +1,56 @@
#region License
/* **********************************************************************************
* Copyright (c) Roman Ivantsov
* This source code is subject to terms and conditions of the MIT License
* for Irony. A copy of the license can be found in the License.txt file
* at the root of this distribution.
* By using this source code in any fashion, you are agreeing to be bound by the terms of the
* MIT License.
* You must not remove this notice from this software.
* **********************************************************************************/
#endregion
using Sanchime.Irony.Ast;
namespace Sanchime.Irony.Interpreter.Ast.Functions
{
//A node representing function definition (named lambda)
public class FunctionDefNode : AstNode
{
public AstNode NameNode;
public LambdaNode Lambda;
public override void Init(AstContext context, ParseTreeNode treeNode)
{
base.Init(context, treeNode);
//child #0 is usually a keyword like "def"
var nodes = treeNode.GetMappedChildNodes();
NameNode = AddChild("Name", nodes[1]);
Lambda = new LambdaNode(context, treeNode, nodes[2], nodes[3])
{
Parent = this
}; //node, params, body
AsString = "<Function " + NameNode.AsString + ">";
//Lamda will set treeNode.AstNode to itself, we need to set it back to "this" here
treeNode.AstNode = this; //
}
public override void Reset()
{
DependentScopeInfo = null;
Lambda.Reset();
base.Reset();
}
protected override object DoEvaluate(ScriptThread thread)
{
thread.CurrentNode = this; //standard prolog
var closure = Lambda.Evaluate(thread); //returns closure
NameNode.SetValue(thread, closure);
thread.CurrentNode = Parent; //standard epilog
return closure;
}
}//class
}//namespace

View File

@ -0,0 +1,103 @@
#region License
/* **********************************************************************************
* Copyright (c) Roman Ivantsov
* This source code is subject to terms and conditions of the MIT License
* for Irony. A copy of the license can be found in the License.txt file
* at the root of this distribution.
* By using this source code in any fashion, you are agreeing to be bound by the terms of the
* MIT License.
* You must not remove this notice from this software.
* **********************************************************************************/
#endregion
using Sanchime.Irony.Ast;
using Sanchime.Irony.Interpreter.Scopes;
namespace Sanchime.Irony.Interpreter.Ast.Functions
{
//A node representing an anonymous function
public class LambdaNode : AstNode
{
public AstNode Parameters;
public AstNode Body;
public LambdaNode()
{ }
//Used by FunctionDefNode
public LambdaNode(AstContext context, ParseTreeNode node, ParseTreeNode parameters, ParseTreeNode body)
{
InitImpl(context, node, parameters, body);
}
public override void Init(AstContext context, ParseTreeNode parseNode)
{
var mappedNodes = parseNode.GetMappedChildNodes();
InitImpl(context, parseNode, mappedNodes[0], mappedNodes[1]);
}
private void InitImpl(AstContext context, ParseTreeNode parseNode, ParseTreeNode parametersNode, ParseTreeNode bodyNode)
{
base.Init(context, parseNode);
Parameters = AddChild("Parameters", parametersNode);
Body = AddChild("Body", bodyNode);
AsString = "Lambda[" + Parameters.ChildNodes.Count + "]";
Body.SetIsTail(); //this will be propagated to the last statement
}
public override void Reset()
{
DependentScopeInfo = null;
base.Reset();
}
protected override object DoEvaluate(ScriptThread thread)
{
thread.CurrentNode = this; //standard prolog
lock (LockObject)
{
if (DependentScopeInfo == null)
{
var langCaseSensitive = thread.App.Language.Grammar.CaseSensitive;
DependentScopeInfo = new ScopeInfo(this, langCaseSensitive);
}
// In the first evaluation the parameter list will add parameter's SlotInfo objects to Scope.ScopeInfo
thread.PushScope(DependentScopeInfo, null);
Parameters.Evaluate(thread);
thread.PopScope();
//Set Evaluate method and invoke it later
Evaluate = EvaluateAfter;
}
var result = Evaluate(thread);
thread.CurrentNode = Parent; //standard epilog
return result;
}
private object EvaluateAfter(ScriptThread thread)
{
thread.CurrentNode = this; //standard prolog
var closure = new Closure(thread.CurrentScope, this);
thread.CurrentNode = Parent; //standard epilog
return closure;
}
public object Call(Scope creatorScope, ScriptThread thread, object[] parameters)
{
var save = thread.CurrentNode; //prolog, not standard - the caller is NOT target node's parent
thread.CurrentNode = this;
thread.PushClosureScope(DependentScopeInfo, creatorScope, parameters);
Parameters.Evaluate(thread); // pre-process parameters
var result = Body.Evaluate(thread);
thread.PopScope();
thread.CurrentNode = save; //epilog, restoring caller
return result;
}
public override void SetIsTail()
{
//ignore this call, do not mark this node as tail, it is meaningless
}
}//class
}//namespace

View File

@ -0,0 +1,57 @@
#region License
/* **********************************************************************************
* Copyright (c) Roman Ivantsov
* This source code is subject to terms and conditions of the MIT License
* for Irony. A copy of the license can be found in the License.txt file
* at the root of this distribution.
* By using this source code in any fashion, you are agreeing to be bound by the terms of the
* MIT License.
* You must not remove this notice from this software.
* **********************************************************************************/
#endregion
using Sanchime.Irony.Ast;
using Sanchime.Irony.Interpreter.Ast.PrimitiveNodes;
using Sanchime.Irony.Interpreter.Scopes;
namespace Sanchime.Irony.Interpreter.Ast.Functions
{
public class ParamListNode : AstNode
{
public override void Init(AstContext context, ParseTreeNode treeNode)
{
base.Init(context, treeNode);
foreach (var child in treeNode.ChildNodes)
{
AddChild(NodeUseType.Parameter, "param", child);
}
AsString = "param_list[" + ChildNodes.Count + "]";
}
protected override object DoEvaluate(ScriptThread thread)
{
thread.CurrentNode = this; //standard prolog
// Is called once, at first evaluation of FunctionDefNode
// Creates parameter slots
foreach (var child in ChildNodes)
{
if (child is IdentifierNode idNode)
{
thread.CurrentScope.Info.AddSlot(idNode.Symbol, SlotType.Parameter);
}
}
Evaluate = EvaluateAfter;
thread.CurrentNode = Parent; //standard epilog
return null;
}//method
// TODO: implement handling list/dict parameter tails (Scheme, Python, etc)
private object EvaluateAfter(ScriptThread thread)
{
return null;
}
}//class
}//namespace

View File

@ -0,0 +1,56 @@
#region License
/* **********************************************************************************
* Copyright (c) Roman Ivantsov
* This source code is subject to terms and conditions of the MIT License
* for Irony. A copy of the license can be found in the License.txt file
* at the root of this distribution.
* By using this source code in any fashion, you are agreeing to be bound by the terms of the
* MIT License.
* You must not remove this notice from this software.
* **********************************************************************************/
#endregion
using Sanchime.Irony.Ast;
namespace Sanchime.Irony.Interpreter.Ast.PrimitiveNodes
{
public class IdentifierNode : AstNode
{
public string Symbol;
private Binding _accessor;
public IdentifierNode()
{ }
public override void Init(AstContext context, ParseTreeNode treeNode)
{
base.Init(context, treeNode);
Symbol = treeNode.Token.ValueString;
AsString = Symbol;
}
//Executed only once, on the first call
protected override object DoEvaluate(ScriptThread thread)
{
thread.CurrentNode = this; //standard prolog
_accessor = thread.Bind(Symbol, BindingRequestFlags.Read);
Evaluate = _accessor.GetValueRef; // Optimization - directly set method ref to accessor's method. EvaluateReader;
var result = Evaluate(thread);
thread.CurrentNode = Parent; //standard epilog
return result;
}
public override void DoSetValue(ScriptThread thread, object value)
{
thread.CurrentNode = this; //standard prolog
if (_accessor == null)
{
_accessor = thread.Bind(Symbol, BindingRequestFlags.Write | BindingRequestFlags.ExistingOrNew);
}
_accessor.SetValueRef(thread, value);
thread.CurrentNode = Parent; //standard epilog
}
}//class
}//namespace

View File

@ -0,0 +1,28 @@
using Sanchime.Irony.Ast;
namespace Sanchime.Irony.Interpreter.Ast.PrimitiveNodes
{
public class LiteralValueNode : AstNode
{
public object Value;
public override void Init(AstContext context, ParseTreeNode treeNode)
{
base.Init(context, treeNode);
Value = treeNode.Token.Value;
AsString = Value == null ? "null" : Value.ToString();
if (Value is string)
AsString = "\"" + AsString + "\"";
}
protected override object DoEvaluate(ScriptThread thread)
{
return Value;
}
public override bool IsConstant()
{
return true;
}
}//class
}

View File

@ -0,0 +1,184 @@
#region License
/* **********************************************************************************
* Copyright (c) Roman Ivantsov
* This source code is subject to terms and conditions of the MIT License
* for Irony. A copy of the license can be found in the License.txt file
* at the root of this distribution.
* By using this source code in any fashion, you are agreeing to be bound by the terms of the
* MIT License.
* You must not remove this notice from this software.
* **********************************************************************************/
#endregion
using Sanchime.Irony.Ast;
namespace Sanchime.Irony.Interpreter.Ast.PrimitiveNodes
{
// Implements Ruby-like active strings with embedded expressions
/* Example of use:
//String literal with embedded expressions ------------------------------------------------------------------
var stringLit = new StringLiteral("string", "\"", StringOptions.AllowsAllEscapes | StringOptions.IsTemplate);
stringLit.AstNodeType = typeof(StringTemplateNode);
var Expr = new NonTerminal("Expr");
var templateSettings = new StringTemplateSettings(); //by default set to Ruby-style settings
templateSettings.ExpressionRoot = Expr; //this defines how to evaluate expressions inside template
this.SnippetRoots.Add(Expr);
stringLit.AstNodeConfig = templateSettings;
//define Expr as an expression non-terminal in your grammar
*/
public class StringTemplateNode : AstNode
{
#region embedded classes
private enum SegmentType
{
Text,
Expression
}
private class TemplateSegment
{
public SegmentType Type;
public string Text;
public AstNode ExpressionNode;
public int Position; //Position in raw text of the token for error reporting
public TemplateSegment(string text, AstNode node, int position)
{
Type = node == null ? SegmentType.Text : SegmentType.Expression;
Text = text;
ExpressionNode = node;
Position = position;
}
}
private class SegmentList : List<TemplateSegment>
{ }
#endregion
private string _template;
private string _tokenText; //used for locating error
private StringTemplateSettings _templateSettings; //copied from Terminal.AstNodeConfig
private SegmentList _segments = new SegmentList();
public override void Init(AstContext context, ParseTreeNode treeNode)
{
base.Init(context, treeNode);
_template = treeNode.Token.ValueString;
_tokenText = treeNode.Token.Text;
_templateSettings = treeNode.Term.AstConfig.Data as StringTemplateSettings;
ParseSegments(context);
AsString = "\"" + _template + "\" (templated string)";
}
protected override object DoEvaluate(ScriptThread thread)
{
thread.CurrentNode = this; //standard prolog
var value = BuildString(thread);
thread.CurrentNode = Parent; //standard epilog
return value;
}
private void ParseSegments(AstContext context)
{
var exprParser = new Parser(context.Language, _templateSettings.ExpressionRoot);
// As we go along the "value text" (that has all escapes done), we track the position in raw token text in the variable exprPosInTokenText.
// This position is position in original text in source code, including original escaping sequences and open/close quotes.
// It will be passed to segment constructor, and maybe used later to compute the exact position of runtime error when it occurs.
int currentPos = 0, exprPosInTokenText = 0;
while (true)
{
var startTagPos = _template.IndexOf(_templateSettings.StartTag, currentPos);
if (startTagPos < 0) startTagPos = _template.Length;
var text = _template.Substring(currentPos, startTagPos - currentPos);
if (!string.IsNullOrEmpty(text))
_segments.Add(new TemplateSegment(text, null, 0)); //for text segments position is not used
if (startTagPos >= _template.Length)
break; //from while
//We have a real start tag, grab the expression
currentPos = startTagPos + _templateSettings.StartTag.Length;
var endTagPos = _template.IndexOf(_templateSettings.EndTag, currentPos);
if (endTagPos < 0)
{
//"No ending tag '{0}' found in embedded expression."
context.AddMessage(ErrorLevel.Error, Location, Resources.ErrNoEndTagInEmbExpr, _templateSettings.EndTag);
return;
}
var exprText = _template.Substring(currentPos, endTagPos - currentPos);
if (!string.IsNullOrEmpty(exprText))
{
//parse the expression
//_expressionParser.context.Reset();
var exprTree = exprParser.Parse(exprText);
if (exprTree.HasErrors())
{
//we use original search in token text instead of currentPos in template to avoid distortions caused by opening quote and escaped sequences
var baseLocation = Location + _tokenText.IndexOf(exprText);
CopyMessages(exprTree.ParserMessages, context.Messages, baseLocation, Resources.ErrInvalidEmbeddedPrefix);
return;
}
//add the expression segment
exprPosInTokenText = _tokenText.IndexOf(_templateSettings.StartTag, exprPosInTokenText) + _templateSettings.StartTag.Length;
var segmNode = exprTree.Root.AstNode as AstNode;
segmNode.Parent = this; //important to attach the segm node to current Module
_segments.Add(new TemplateSegment(null, segmNode, exprPosInTokenText));
//advance position beyond the expression
exprPosInTokenText += exprText.Length + _templateSettings.EndTag.Length;
}//if
currentPos = endTagPos + _templateSettings.EndTag.Length;
}//while
}
private void CopyMessages(LogMessageList fromList, LogMessageList toList, SourceLocation baseLocation, string messagePrefix)
{
foreach (var other in fromList)
toList.Add(new LogMessage(other.Level, baseLocation + other.Location, messagePrefix + other.Message, other.ParserState));
}//
private object BuildString(ScriptThread thread)
{
string[] values = new string[_segments.Count];
for (int i = 0; i < _segments.Count; i++)
{
var segment = _segments[i];
switch (segment.Type)
{
case SegmentType.Text:
values[i] = segment.Text;
break;
case SegmentType.Expression:
values[i] = EvaluateExpression(thread, segment);
break;
}//else
}//for i
var result = string.Join(string.Empty, values);
return result;
}//method
private string EvaluateExpression(ScriptThread thread, TemplateSegment segment)
{
try
{
var value = segment.ExpressionNode.Evaluate(thread);
return value == null ? string.Empty : value.ToString();
}
catch
{
//We need to catch here and set current node; ExpressionNode may have reset it, and location would be wrong
//TODO: fix this - set error location to exact location inside string.
thread.CurrentNode = this;
throw;
}
}
}//class
}

View File

@ -0,0 +1,7 @@
namespace Sanchime.Irony.Interpreter.Ast.SpecialNodes
{
//A statement that does nothing, like "pass" command in Python.
public class EmptyStatementNode : AstNode
{
}//class
}

View File

@ -0,0 +1,25 @@
using Sanchime.Irony.Ast;
namespace Sanchime.Irony.Interpreter.Ast.SpecialNodes
{
//A substitute node to use on constructs that are not yet supported by language implementation.
// The script would compile Ok but on attempt to evaluate the node would throw a runtime exception
public class NotSupportedNode : AstNode
{
private string Name;
public override void Init(AstContext context, ParseTreeNode treeNode)
{
base.Init(context, treeNode);
Name = treeNode.Term.ToString();
AsString = Name + " (not supported)";
}
protected override object DoEvaluate(ScriptThread thread)
{
thread.CurrentNode = this; //standard prolog
thread.ThrowScriptError(Resources.ErrConstructNotSupported, Name);
return null; //never happens
}
}//class
}

View File

@ -0,0 +1,19 @@
namespace Sanchime.Irony.Interpreter.Ast.SpecialNodes
{
//A stub to use when AST node was not created (type not specified on NonTerminal, or error on creation)
// The purpose of the stub is to throw a meaningful message when interpreter tries to evaluate null node.
public class NullNode : AstNode
{
public NullNode(BnfTerm term)
{
Term = term;
}
protected override object DoEvaluate(ScriptThread thread)
{
thread.CurrentNode = this; //standard prolog
thread.ThrowScriptError(Resources.ErrNullNodeEval, Term);
return null; //never happens
}
}//class
}

View File

@ -0,0 +1,114 @@
#region License
/* **********************************************************************************
* Copyright (c) Roman Ivantsov
* This source code is subject to terms and conditions of the MIT License
* for Irony. A copy of the license can be found in the License.txt file
* at the root of this distribution.
* By using this source code in any fashion, you are agreeing to be bound by the terms of the
* MIT License.
* You must not remove this notice from this software.
* **********************************************************************************/
#endregion
using Sanchime.Irony.Ast;
using System.Linq.Expressions;
namespace Sanchime.Irony.Interpreter.Ast.Statements
{
public class AssignmentNode : AstNode
{
public AstNode Target;
public string AssignmentOp;
public bool IsAugmented; // true if it is augmented operation like "+="
public ExpressionType BinaryExpressionType;
public AstNode Expression;
private OperatorImplementation _lastUsed;
private int _failureCount;
public override void Init(AstContext context, ParseTreeNode treeNode)
{
base.Init(context, treeNode);
var nodes = treeNode.GetMappedChildNodes();
Target = AddChild(NodeUseType.ValueWrite, "To", nodes[0]);
//Get Op and baseOp if it is combined assignment
AssignmentOp = nodes[1].FindTokenAndGetText();
if (string.IsNullOrEmpty(AssignmentOp))
AssignmentOp = "=";
BinaryExpressionType = CustomExpressionTypes.NotAnExpression;
//There maybe an "=" sign in the middle, or not - if it is marked as punctuation; so we just take the last node in child list
Expression = AddChild(NodeUseType.ValueRead, "Expr", nodes[nodes.Count - 1]);
AsString = AssignmentOp + " (assignment)";
// TODO: this is not always correct: in Pascal the assignment operator is :=.
IsAugmented = AssignmentOp.Length > 1;
if (IsAugmented)
{
var ictxt = context as InterpreterAstContext;
ExpressionType = ictxt.OperatorHandler.GetOperatorExpressionType(AssignmentOp);
BinaryExpressionType = ictxt.OperatorHandler.GetBinaryOperatorForAugmented(ExpressionType);
Target.UseType = NodeUseType.ValueReadWrite;
}
}
protected override object DoEvaluate(ScriptThread thread)
{
thread.CurrentNode = this; //standard prolog
if (IsAugmented)
Evaluate = EvaluateAugmentedFast;
else
Evaluate = EvaluateSimple; //non-augmented
//call self-evaluate again, now to call real methods
var result = Evaluate(thread);
thread.CurrentNode = Parent; //standard epilog
return result;
}
private object EvaluateSimple(ScriptThread thread)
{
thread.CurrentNode = this; //standard prolog
var value = Expression.Evaluate(thread);
Target.SetValue(thread, value);
thread.CurrentNode = Parent; //standard epilog
return value;
}
private object EvaluateAugmentedFast(ScriptThread thread)
{
thread.CurrentNode = this; //standard prolog
var value = Target.Evaluate(thread);
var exprValue = Expression.Evaluate(thread);
object result = null;
if (_lastUsed != null)
{
try
{
result = _lastUsed.EvaluateBinary(value, exprValue);
}
catch
{
_failureCount++;
// if failed 3 times, change to method without direct try
if (_failureCount > 3)
Evaluate = EvaluateAugmented;
} //catch
}// if _lastUsed
if (result == null)
result = thread.Runtime.ExecuteBinaryOperator(BinaryExpressionType, value, exprValue, ref _lastUsed);
Target.SetValue(thread, result);
thread.CurrentNode = Parent; //standard epilog
return result;
}
private object EvaluateAugmented(ScriptThread thread)
{
thread.CurrentNode = this; //standard prolog
var value = Target.Evaluate(thread);
var exprValue = Expression.Evaluate(thread);
var result = thread.Runtime.ExecuteBinaryOperator(BinaryExpressionType, value, exprValue, ref _lastUsed);
Target.SetValue(thread, result);
thread.CurrentNode = Parent; //standard epilog
return result;
}
}
}

View File

@ -0,0 +1,101 @@
#region License
/* **********************************************************************************
* Copyright (c) Roman Ivantsov
* This source code is subject to terms and conditions of the MIT License
* for Irony. A copy of the license can be found in the License.txt file
* at the root of this distribution.
* By using this source code in any fashion, you are agreeing to be bound by the terms of the
* MIT License.
* You must not remove this notice from this software.
* **********************************************************************************/
#endregion
using Sanchime.Irony.Ast;
namespace Sanchime.Irony.Interpreter.Ast.Statements
{
public class StatementListNode : AstNode
{
private AstNode _singleChild; //stores a single child when child count == 1, for fast access
public override void Init(AstContext context, ParseTreeNode treeNode)
{
base.Init(context, treeNode);
var nodes = treeNode.GetMappedChildNodes();
foreach (var child in nodes)
{
//don't add if it is null; it can happen that "statement" is a comment line and statement's node is null.
// So to make life easier for language creator, we just skip if it is null
if (child.AstNode != null)
AddChild(string.Empty, child);
}
AsString = "Statement List";
if (ChildNodes.Count == 0)
{
AsString += " (Empty)";
}
else
ChildNodes[ChildNodes.Count - 1].Flags |= AstNodeFlags.IsTail;
}
protected override object DoEvaluate(ScriptThread thread)
{
thread.CurrentNode = this; //standard prolog
lock (LockObject)
{
switch (ChildNodes.Count)
{
case 0:
Evaluate = EvaluateEmpty;
break;
case 1:
_singleChild = ChildNodes[0];
Evaluate = EvaluateOne;
break;
default:
Evaluate = EvaluateMultiple;
break;
}//switch
}//lock
var result = Evaluate(thread);
thread.CurrentNode = Parent; //standard epilog
return result;
}
private object EvaluateEmpty(ScriptThread thread)
{
return null;
}
private object EvaluateOne(ScriptThread thread)
{
thread.CurrentNode = this; //standard prolog
object result = _singleChild.Evaluate(thread);
thread.CurrentNode = Parent; //standard epilog
return result;
}
private object EvaluateMultiple(ScriptThread thread)
{
thread.CurrentNode = this; //standard prolog
object result = null;
for (int i = 0; i < ChildNodes.Count; i++)
{
result = ChildNodes[i].Evaluate(thread);
}
thread.CurrentNode = Parent; //standard epilog
return result; //return result of last statement
}
public override void SetIsTail()
{
base.SetIsTail();
if (ChildNodes.Count > 0)
ChildNodes[ChildNodes.Count - 1].SetIsTail();
}
}//class
}//namespace

View File

@ -0,0 +1,61 @@
#region License
/* **********************************************************************************
* Copyright (c) Roman Ivantsov
* This source code is subject to terms and conditions of the MIT License
* for Irony. A copy of the license can be found in the License.txt file
* at the root of this distribution.
* By using this source code in any fashion, you are agreeing to be bound by the terms of the
* MIT License.
* You must not remove this notice from this software.
* **********************************************************************************/
#endregion
namespace Sanchime.Irony.Interpreter.Bindings
{
// Binding is a link between a variable in the script (for ex, IdentifierNode) and a value storage -
// a slot in local or module-level Scope. Binding to internal variables is supported by SlotBinding class.
// Alternatively a symbol can be bound to external CLR entity in imported namespace - class, function, property, etc.
// Binding is produced by Runtime.Bind method and allows read/write operations through GetValueRef and SetValueRef methods.
public class Binding
{
public readonly BindingTargetInfo TargetInfo;
public EvaluateMethod GetValueRef; // ref to Getter method implementation
public ValueSetterMethod SetValueRef; // ref to Setter method implementation
public bool IsConstant { get; protected set; }
public Binding(BindingTargetInfo targetInfo)
{
TargetInfo = targetInfo;
}
public Binding(string symbol, BindingTargetType targetType)
{
TargetInfo = new BindingTargetInfo(symbol, targetType);
}
public override string ToString()
{
return "{Binding to + " + TargetInfo.ToString() + "}";
}
}//class
//Binding to a "fixed", constant value
public class ConstantBinding : Binding
{
public object Target;
public ConstantBinding(object target, BindingTargetInfo targetInfo) : base(targetInfo)
{
Target = target;
GetValueRef = GetValue;
IsConstant = true;
}
public object GetValue(ScriptThread thread)
{
return Target;
}
}
}

View File

@ -0,0 +1,52 @@
#region License
/* **********************************************************************************
* Copyright (c) Roman Ivantsov
* This source code is subject to terms and conditions of the MIT License
* for Irony. A copy of the license can be found in the License.txt file
* at the root of this distribution.
* By using this source code in any fashion, you are agreeing to be bound by the terms of the
* MIT License.
* You must not remove this notice from this software.
* **********************************************************************************/
#endregion
using Sanchime.Irony.Interpreter.Scopes;
namespace Sanchime.Irony.Interpreter.Bindings
{
[Flags]
public enum BindingRequestFlags
{
Read = 0x01,
Write = 0x02,
Invoke = 0x04,
ExistingOrNew = 0x10,
NewOnly = 0x20, // for new variable, for ex, in JavaScript "var x..." - introduces x as new variable
}
//Binding request is a container for information about requested binding. Binding request goes from an Ast node to language runtime.
// For example, identifier node would request a binding for an identifier.
public class BindingRequest
{
public ScriptThread Thread;
public AstNode FromNode;
public ModuleInfo FromModule;
public BindingRequestFlags Flags;
public string Symbol;
public ScopeInfo FromScopeInfo;
public bool IgnoreCase;
public BindingRequest(ScriptThread thread, AstNode fromNode, string symbol, BindingRequestFlags flags)
{
Thread = thread;
FromNode = fromNode;
FromModule = thread.App.DataMap.GetModule(fromNode.ModuleNode);
Symbol = symbol;
Flags = flags;
FromScopeInfo = thread.CurrentScope.Info;
IgnoreCase = !thread.Runtime.Language.Grammar.CaseSensitive;
}
}
}

View File

@ -0,0 +1,42 @@
#region License
/* **********************************************************************************
* Copyright (c) Roman Ivantsov
* This source code is subject to terms and conditions of the MIT License
* for Irony. A copy of the license can be found in the License.txt file
* at the root of this distribution.
* By using this source code in any fashion, you are agreeing to be bound by the terms of the
* MIT License.
* You must not remove this notice from this software.
* **********************************************************************************/
#endregion
namespace Sanchime.Irony.Interpreter.Bindings
{
public enum BindingTargetType
{
Slot,
BuiltInObject,
SpecialForm,
ClrInterop,
Custom, // any special non-standard type for specific language
}
public class BindingTargetInfo
{
public readonly string Symbol;
public readonly BindingTargetType Type;
public BindingTargetInfo(string symbol, BindingTargetType type)
{
Symbol = symbol;
Type = type;
}
public override string ToString()
{
return Symbol + "/" + Type.ToString();
}
}//class
}

View File

@ -0,0 +1,87 @@
#region License
/* **********************************************************************************
* Copyright (c) Roman Ivantsov
* This source code is subject to terms and conditions of the MIT License
* for Irony. A copy of the license can be found in the License.txt file
* at the root of this distribution.
* By using this source code in any fashion, you are agreeing to be bound by the terms of the
* MIT License.
* You must not remove this notice from this software.
* **********************************************************************************/
#endregion
namespace Sanchime.Irony.Interpreter
{
// A general delegate representing a built-in method implementation.
public delegate object BuiltInMethod(ScriptThread thread, object[] args);
//A wrapper to convert BuiltInMethod delegate (referencing some custom method in LanguageRuntime) into an ICallTarget instance (expected by FunctionCallNode)
public class BuiltInCallTarget : ICallTarget
{
public string Name;
public readonly BuiltInMethod Method;
public readonly int MinParamCount, MaxParamCount;
public string[] ParameterNames; //Just for information purpose
public BuiltInCallTarget(BuiltInMethod method, string name, int minParamCount = 0, int maxParamCount = 0, string parameterNames = null)
{
Method = method;
Name = name;
MinParamCount = minParamCount;
MaxParamCount = Math.Max(MinParamCount, maxParamCount);
if (!string.IsNullOrEmpty(parameterNames))
ParameterNames = parameterNames.Split(',');
}
#region ICallTarget Members
public object Call(ScriptThread thread, object[] parameters)
{
return Method(thread, parameters);
}
#endregion
}
// The class contains information about built-in function. It has double purpose.
// First, it is used as a BindingTargetInfo instance (meta-data) for a binding to a built-in function.
// Second, we use it as a reference to a custom built-in method that we store in LanguageRuntime.BuiltIns table.
// For this, we make it implement IBindingSource - we can add it to BuiltIns table of LanguageRuntime, which is a table of IBindingSource instances.
// Being IBindingSource, it can produce a binding object to the target method - singleton in fact;
// the same binding object is used for all calls to the method from all function-call AST nodes.
public class BuiltInCallableTargetInfo : BindingTargetInfo, IBindingSource
{
public Binding BindingInstance; //A singleton binding instance; we share it for all AST nodes (function call nodes) that call the method.
public BuiltInCallableTargetInfo(BuiltInMethod method, string methodName, int minParamCount = 0, int maxParamCount = 0, string parameterNames = null) :
this(new BuiltInCallTarget(method, methodName, minParamCount, maxParamCount, parameterNames))
{
}
public BuiltInCallableTargetInfo(BuiltInCallTarget target) : base(target.Name, BindingTargetType.BuiltInObject)
{
BindingInstance = new ConstantBinding(target, this);
}
//Implement IBindingSource.Bind
public Binding Bind(BindingRequest request)
{
return BindingInstance;
}
}//class
// Method for adding methods to BuiltIns table in Runtime
public static partial class BindingSourceTableExtensions
{
public static BindingTargetInfo AddMethod(this BindingSourceTable targets, BuiltInMethod method, string methodName,
int minParamCount = 0, int maxParamCount = 0, string parameterNames = null)
{
var callTarget = new BuiltInCallTarget(method, methodName, minParamCount, maxParamCount, parameterNames);
var targetInfo = new BuiltInCallableTargetInfo(callTarget);
targets.Add(methodName, targetInfo);
return targetInfo;
}
}
}//namespace

View File

@ -0,0 +1,210 @@
#region License
/* **********************************************************************************
* Copyright (c) Roman Ivantsov
* This source code is subject to terms and conditions of the MIT License
* for Irony. A copy of the license can be found in the License.txt file
* at the root of this distribution.
* By using this source code in any fashion, you are agreeing to be bound by the terms of the
* MIT License.
* You must not remove this notice from this software.
* **********************************************************************************/
#endregion
using System.Reflection;
namespace Sanchime.Irony.Interpreter.Bindings
{
//Unfinished, work in progress, file disabled for now
public enum ClrTargetType
{
Namespace,
Type,
Method,
Property,
Field,
}
public class ClrInteropBindingTargetInfo : BindingTargetInfo, IBindingSource
{
public ClrTargetType TargetSubType;
public ClrInteropBindingTargetInfo(string symbol, ClrTargetType targetSubType) : base(symbol, BindingTargetType.ClrInterop)
{
TargetSubType = targetSubType;
}
public virtual Binding Bind(BindingRequest request)
{
throw new NotImplementedException();
}
}//class
public class ClrNamespaceBindingTargetInfo : ClrInteropBindingTargetInfo
{
private ConstantBinding _binding;
public ClrNamespaceBindingTargetInfo(string ns) : base(ns, ClrTargetType.Namespace)
{
_binding = new ConstantBinding(ns, this);
}
public override Binding Bind(BindingRequest request)
{
return _binding;
}
}
public class ClrTypeBindingTargetInfo : ClrInteropBindingTargetInfo
{
private ConstantBinding _binding;
public ClrTypeBindingTargetInfo(Type type) : base(type.Name, ClrTargetType.Type)
{
_binding = new ConstantBinding(type, this);
}
public override Binding Bind(BindingRequest request)
{
return _binding;
}
}
public class ClrMethodBindingTargetInfo : ClrInteropBindingTargetInfo, ICallTarget
{ //The object works as ICallTarget itself
public object Instance;
public Type DeclaringType;
private BindingFlags _invokeFlags;
private Binding _binding;
public ClrMethodBindingTargetInfo(Type declaringType, string methodName, object instance = null) : base(methodName, ClrTargetType.Method)
{
DeclaringType = declaringType;
Instance = instance;
_invokeFlags = BindingFlags.InvokeMethod | BindingFlags.Public | BindingFlags.NonPublic;
if (Instance == null)
_invokeFlags |= BindingFlags.Static;
else
_invokeFlags |= BindingFlags.Instance;
_binding = new ConstantBinding(target: this as ICallTarget, targetInfo: this);
//The object works as CallTarget itself; the "as" conversion is not needed in fact, we do it just to underline the role
}
public override Binding Bind(BindingRequest request)
{
return _binding;
}
#region ICalllable.Call implementation
public object Call(ScriptThread thread, object[] args)
{
// TODO: fix this. Currently doing it slow but easy way, through reflection
if (args != null && args.Length == 0)
args = null;
var memberInfo = DeclaringType.GetTypeInfo().GetMethod(base.Symbol, _invokeFlags);
object result = null;
if (memberInfo != null)
{
result = memberInfo.Invoke(Instance, args);
}
//var result = DeclaringType.InvokeMember(base.Symbol, _invokeFlags, null, Instance, args);
return result;
}
#endregion
}
public class ClrPropertyBindingTargetInfo : ClrInteropBindingTargetInfo
{
public object Instance;
public PropertyInfo Property;
private Binding _binding;
public ClrPropertyBindingTargetInfo(PropertyInfo property, object instance) : base(property.Name, ClrTargetType.Property)
{
Property = property;
Instance = instance;
_binding = new Binding(this);
_binding.GetValueRef = GetPropertyValue;
_binding.SetValueRef = SetPropertyValue;
}
public override Binding Bind(BindingRequest request)
{
return _binding;
}
private object GetPropertyValue(ScriptThread thread)
{
var result = Property.GetValue(Instance, null);
return result;
}
private void SetPropertyValue(ScriptThread thread, object value)
{
Property.SetValue(Instance, value, null);
}
}
public class ClrFieldBindingTargetInfo : ClrInteropBindingTargetInfo
{
public object Instance;
public FieldInfo Field;
private Binding _binding;
public ClrFieldBindingTargetInfo(FieldInfo field, object instance) : base(field.Name, ClrTargetType.Field)
{
Field = field;
Instance = instance;
_binding = new Binding(this);
_binding.GetValueRef = GetPropertyValue;
_binding.SetValueRef = SetPropertyValue;
}
public override Binding Bind(BindingRequest request)
{
return _binding;
}
private object GetPropertyValue(ScriptThread thread)
{
var result = Field.GetValue(Instance);
return result;
}
private void SetPropertyValue(ScriptThread thread, object value)
{
Field.SetValue(Instance, value);
}
}
// Method for adding methods to BuiltIns table in Runtime
public static partial class BindingSourceTableExtensions
{
public static void ImportStaticMembers(this BindingSourceTable targets, Type fromType)
{
var members = fromType.GetMembers(BindingFlags.Public | BindingFlags.Static);
foreach (var member in members)
{
if (targets.ContainsKey(member.Name)) continue; //do not import overloaded methods several times
switch (member.MemberType)
{
case MemberTypes.Method:
targets.Add(member.Name, new ClrMethodBindingTargetInfo(fromType, member.Name));
break;
case MemberTypes.Property:
targets.Add(member.Name, new ClrPropertyBindingTargetInfo(member as PropertyInfo, null));
break;
case MemberTypes.Field:
targets.Add(member.Name, new ClrFieldBindingTargetInfo(member as FieldInfo, null));
break;
}//switch
}//foreach
}//method
}
}

View File

@ -0,0 +1,47 @@
#region License
/* **********************************************************************************
* Copyright (c) Roman Ivantsov
* This source code is subject to terms and conditions of the MIT License
* for Irony. A copy of the license can be found in the License.txt file
* at the root of this distribution.
* By using this source code in any fashion, you are agreeing to be bound by the terms of the
* MIT License.
* You must not remove this notice from this software.
* **********************************************************************************/
#endregion
namespace Sanchime.Irony.Interpreter.Bindings
{
public interface IBindingSource
{
Binding Bind(BindingRequest request);
}
public class BindingSourceList : List<IBindingSource>
{
}
public class BindingSourceTable : Dictionary<string, IBindingSource>, IBindingSource
{
public BindingSourceTable(bool caseSensitive)
: base(caseSensitive ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase)
{
}
//IBindingSource Members
public Binding Bind(BindingRequest request)
{
IBindingSource target;
if (TryGetValue(request.Symbol, out target))
return target.Bind(request);
return null;
}
}//class
// This class will be used to define extensions for BindingSourceTable
public static partial class BindingSourceTableExtensions
{
}
}

View File

@ -0,0 +1,35 @@
#region License
/* **********************************************************************************
* Copyright (c) Roman Ivantsov
* This source code is subject to terms and conditions of the MIT License
* for Irony. A copy of the license can be found in the License.txt file
* at the root of this distribution.
* By using this source code in any fashion, you are agreeing to be bound by the terms of the
* MIT License.
* You must not remove this notice from this software.
* **********************************************************************************/
#endregion
using Sanchime.Irony.Interpreter.Scopes;
namespace Sanchime.Irony.Interpreter.Bindings
{
// Module export, container for public, exported symbols from module
// Just a skeleton, to be completed
public class ModuleExport : IBindingSource
{
public ModuleInfo Module;
public ModuleExport(ModuleInfo module)
{
Module = module;
}
public Binding Bind(BindingRequest request)
{
return null;
}
}
}

View File

@ -0,0 +1,257 @@
#region License
/* **********************************************************************************
* Copyright (c) Roman Ivantsov
* This source code is subject to terms and conditions of the MIT License
* for Irony. A copy of the license can be found in the License.txt file
* at the root of this distribution.
* By using this source code in any fashion, you are agreeing to be bound by the terms of the
* MIT License.
* You must not remove this notice from this software.
* **********************************************************************************/
#endregion
using Sanchime.Irony.Interpreter.Scopes;
namespace Sanchime.Irony.Interpreter.Bindings
{
// Implements fast access to a variable (local/global var or parameter) in local scope or in any enclosing scope
// Important: the following code is very sensitive to even tiny changes - do not know exactly particular reasons.
public sealed class SlotBinding : Binding
{
public SlotInfo Slot;
public ScopeInfo FromScope;
public int SlotIndex;
public int StaticScopeIndex;
public AstNode FromNode;
public SlotBinding(SlotInfo slot, AstNode fromNode, ScopeInfo fromScope) : base(slot.Name, BindingTargetType.Slot)
{
Slot = slot;
FromNode = fromNode;
FromScope = fromScope;
SlotIndex = slot.Index;
StaticScopeIndex = Slot.ScopeInfo.StaticIndex;
SetupAccessorMethods();
}
private void SetupAccessorMethods()
{
// Check module scope
if (Slot.ScopeInfo.StaticIndex >= 0)
{
GetValueRef = FastGetStaticValue;
SetValueRef = SetStatic;
return;
}
var levelDiff = Slot.ScopeInfo.Level - FromScope.Level;
switch (levelDiff)
{
case 0: // local scope
if (Slot.Type == SlotType.Value)
{
GetValueRef = FastGetCurrentScopeValue;
SetValueRef = SetCurrentScopeValue;
}
else
{
GetValueRef = FastGetCurrentScopeParameter;
SetValueRef = SetCurrentScopeParameter;
}
return;
case 1: //direct parent
if (Slot.Type == SlotType.Value)
{
GetValueRef = GetImmediateParentScopeValue;
SetValueRef = SetImmediateParentScopeValue;
}
else
{
GetValueRef = GetImmediateParentScopeParameter;
SetValueRef = SetImmediateParentScopeParameter;
}
return;
default: // some enclosing scope
if (Slot.Type == SlotType.Value)
{
GetValueRef = GetParentScopeValue;
SetValueRef = SetParentScopeValue;
}
else
{
GetValueRef = GetParentScopeParameter;
SetValueRef = SetParentScopeParameter;
}
return;
}
}
// Specific method implementations =======================================================================================================
// Optimization: in most cases we go directly for Values array; if we fail, then we fallback to full method
// with proper exception handling. This fallback is expected to be extremely rare, so overall we have considerable perf gain
// Note that in we expect the methods to be used directly by identifier node (like: IdentifierNode.EvaluateRef = Binding.GetValueRef; } -
// to save a few processor cycles. Therefore, we need to provide a proper context (thread.CurrentNode) in case of exception.
// In all "full-method" implementations we set current node to FromNode, so exception correctly points
// to the owner Identifier node as a location of error.
// Current scope
private object FastGetCurrentScopeValue(ScriptThread thread)
{
try
{
//optimization: we go directly for values array; if we fail, then we fallback to regular "proper" method.
return thread.CurrentScope.Values[SlotIndex];
}
catch
{
return GetCurrentScopeValue(thread);
}
}
private object GetCurrentScopeValue(ScriptThread thread)
{
try
{
return thread.CurrentScope.GetValue(SlotIndex);
}
catch { thread.CurrentNode = FromNode; throw; }
}
private object FastGetCurrentScopeParameter(ScriptThread thread)
{
//optimization: we go directly for parameters array; if we fail, then we fallback to regular "proper" method.
try
{
return thread.CurrentScope.Parameters[SlotIndex];
}
catch
{
return GetCurrentScopeParameter(thread);
}
}
private object GetCurrentScopeParameter(ScriptThread thread)
{
try
{
return thread.CurrentScope.GetParameter(SlotIndex);
}
catch { thread.CurrentNode = FromNode; throw; }
}
private void SetCurrentScopeValue(ScriptThread thread, object value)
{
thread.CurrentScope.SetValue(SlotIndex, value);
}
private void SetCurrentScopeParameter(ScriptThread thread, object value)
{
thread.CurrentScope.SetParameter(SlotIndex, value);
}
// Static scope (module-level variables)
private object FastGetStaticValue(ScriptThread thread)
{
try
{
return thread.App.StaticScopes[StaticScopeIndex].Values[SlotIndex];
}
catch
{
return GetStaticValue(thread);
}
}
private object GetStaticValue(ScriptThread thread)
{
try
{
return thread.App.StaticScopes[StaticScopeIndex].GetValue(SlotIndex);
}
catch { thread.CurrentNode = FromNode; throw; }
}
private void SetStatic(ScriptThread thread, object value)
{
thread.App.StaticScopes[StaticScopeIndex].SetValue(SlotIndex, value);
}
// Direct parent
private object GetImmediateParentScopeValue(ScriptThread thread)
{
try
{
return thread.CurrentScope.Parent.Values[SlotIndex];
}
catch { }
//full method
try
{
return thread.CurrentScope.Parent.GetValue(SlotIndex);
}
catch { thread.CurrentNode = FromNode; throw; }
}
private object GetImmediateParentScopeParameter(ScriptThread thread)
{
try
{
return thread.CurrentScope.Parent.Parameters[SlotIndex];
}
catch { }
//full method
try
{
return thread.CurrentScope.Parent.GetParameter(SlotIndex);
}
catch { thread.CurrentNode = FromNode; throw; }
}
private void SetImmediateParentScopeValue(ScriptThread thread, object value)
{
thread.CurrentScope.Parent.SetValue(SlotIndex, value);
}
private void SetImmediateParentScopeParameter(ScriptThread thread, object value)
{
thread.CurrentScope.Parent.SetParameter(SlotIndex, value);
}
// Generic case
private object GetParentScopeValue(ScriptThread thread)
{
var targetScope = GetTargetScope(thread);
return targetScope.GetValue(SlotIndex);
}
private object GetParentScopeParameter(ScriptThread thread)
{
var targetScope = GetTargetScope(thread);
return targetScope.GetParameter(SlotIndex);
}
private void SetParentScopeValue(ScriptThread thread, object value)
{
var targetScope = GetTargetScope(thread);
targetScope.SetValue(SlotIndex, value);
}
private void SetParentScopeParameter(ScriptThread thread, object value)
{
var targetScope = GetTargetScope(thread);
targetScope.SetParameter(SlotIndex, value);
}
private Scope GetTargetScope(ScriptThread thread)
{
var targetLevel = Slot.ScopeInfo.Level;
var scope = thread.CurrentScope.Parent;
while (scope.Info.Level > targetLevel)
scope = scope.Parent;
return scope;
}
}//class SlotReader
}

View File

@ -0,0 +1,57 @@
#region License
/* **********************************************************************************
* Copyright (c) Roman Ivantsov
* This source code is subject to terms and conditions of the MIT License
* for Irony. A copy of the license can be found in the License.txt file
* at the root of this distribution.
* By using this source code in any fashion, you are agreeing to be bound by the terms of the
* MIT License.
* You must not remove this notice from this software.
* **********************************************************************************/
#endregion
namespace Sanchime.Irony.Interpreter.Bindings
{
public class SpecialFormBindingInfo : BindingTargetInfo, IBindingSource
{
public readonly ConstantBinding Binding;
public readonly int MinChildCount, MaxChildCount;
public string[] ChildRoles;
public SpecialFormBindingInfo(string symbol, SpecialForm form, int minChildCount = 0, int maxChildCount = 0, string childRoles = null)
: base(symbol, BindingTargetType.SpecialForm)
{
Binding = new ConstantBinding(form, this);
MinChildCount = minChildCount;
MaxChildCount = Math.Max(minChildCount, maxChildCount); //if maxParamCount=0 then set it equal to minParamCount
if (!string.IsNullOrEmpty(childRoles))
{
ChildRoles = childRoles.Split(',');
//TODO: add check that paramNames array is in accord with min/max param counts
}
}
#region IBindingSource Members
public Binding Bind(BindingRequest request)
{
return Binding;
}
#endregion
}//class
public static partial class BindingSourceTableExtensions
{
//Method for adding methods to BuiltIns table in Runtime
public static BindingTargetInfo AddSpecialForm(this BindingSourceTable targets, SpecialForm form, string formName,
int minChildCount = 0, int maxChildCount = 0, string parameterNames = null)
{
var formInfo = new SpecialFormBindingInfo(formName, form, minChildCount, maxChildCount, parameterNames);
targets.Add(formName, formInfo);
return formInfo;
}
}
}//namespace

View File

@ -0,0 +1,19 @@
Some vocabulary, to clarify the terms and class names:
Binding is an object that serves as a link between a symbol and its value. Binding has methods GetValue and SetValue, for setting/getting values into the current context of the app.
For example, symbol X in a script has a corresponding IdentifierNode (AST node). The code in this node, before it can read the value, must get a binding for a symbol "X". On the first execution the node calls a Bind method of the current thread, passing it a BindingRequest object (see below) and expecting back a Binding object that can be used to access the value.
Having a binding object , it can read the value:
var value = binding.GetValue(thread);
Binding Target Types - classification of bindings by a type of the target. One binding target is a Slot - a local or global variable. Other examples: built-in method; CLR method or object imported through interop.
BindingTargetInfo is a metadata for a binding; contains Symbol and BindingType. Each binding has TargetInfo property that describes it.
BindingRequest is a container for information about a desired binding when the code in AST node tries to get the binding from the executing script environment. It contains Symbol (name of the variable or function), and some other flags.
IBindingSource is an abstraction of a binding source - something that can produce a binding for a symbol. Simply speaking, binding source is asked "Do you have something named 'foo'?" it answers 'yes, here is a binding to foo thing'. Examples of binding sources: a table of built-in methods; a local frame with a set of variables; import specification in a module pointing to external module.
BindingSourceTable is a table of binding sources indexed by name. LanguageRuntime.BuiltIns field is such a table - it contains built-in methods and objects, stored as binding sources.
Scope - a set of variables in a programming scope. For ex: local scope, module scope, object scope. Scope consists of slots - locations where values are stored. Each slot has a SlotInfo meta data object. Each Scope has a ScopeInfo object (metadata) that contains a list of SlotInfo objects desribing slots in the scope.

View File

@ -0,0 +1,42 @@
#region License
/* **********************************************************************************
* Copyright (c) Roman Ivantsov
* This source code is subject to terms and conditions of the MIT License
* for Irony. A copy of the license can be found in the License.txt file
* at the root of this distribution.
* By using this source code in any fashion, you are agreeing to be bound by the terms of the
* MIT License.
* You must not remove this notice from this software.
* **********************************************************************************/
#endregion
namespace Sanchime.Irony.Interpreter.Diagnostics
{
public class ScriptException : Exception
{
public SourceLocation Location;
public ScriptStackTrace ScriptStackTrace;
public ScriptException(string message) : base(message)
{
}
public ScriptException(string message, Exception inner) : base(message, inner)
{
}
public ScriptException(string message, Exception inner, SourceLocation location, ScriptStackTrace stack)
: base(message, inner)
{
Location = location;
ScriptStackTrace = stack;
}
public override string ToString()
{
return Message + Environment.NewLine + ScriptStackTrace.ToString();
}
}//class
}

View File

@ -0,0 +1,20 @@
#region License
/* **********************************************************************************
* Copyright (c) Roman Ivantsov
* This source code is subject to terms and conditions of the MIT License
* for Irony. A copy of the license can be found in the License.txt file
* at the root of this distribution.
* By using this source code in any fashion, you are agreeing to be bound by the terms of the
* MIT License.
* You must not remove this notice from this software.
* **********************************************************************************/
#endregion
namespace Sanchime.Irony.Interpreter.Diagnostics
{
public class ScriptStackTrace
{
}
}

View File

@ -0,0 +1,11 @@
global using Sanchime.Irony.Interpreter.Ast;
global using Sanchime.Irony.Interpreter.Bindings;
global using Sanchime.Irony.Interpreter.Diagnostics;
global using Sanchime.Irony.Interpreter.SriptApplication;
global using Sanchime.Irony.Parsing.Data;
global using Sanchime.Irony.Parsing.Grammars;
global using Sanchime.Irony.Parsing.Parsers;
global using Sanchime.Irony.Parsing.Scanners;
global using Sanchime.Irony.Parsing.Terminals;
global using Sanchime.Irony.Utilities;
global using System.Text;

View File

@ -0,0 +1,66 @@
#region License
/* **********************************************************************************
* Copyright (c) Roman Ivantsov
* This source code is subject to terms and conditions of the MIT License
* for Irony. A copy of the license can be found in the License.txt file
* at the root of this distribution.
* By using this source code in any fashion, you are agreeing to be bound by the terms of the
* MIT License.
* You must not remove this notice from this software.
* **********************************************************************************/
#endregion
using Sanchime.Irony.Ast;
using Sanchime.Irony.Interpreter.Utilities;
namespace Sanchime.Irony.Interpreter
{
/// <summary> Base class for languages that use Irony Interpreter to execute scripts. </summary>
public abstract class InterpretedLanguageGrammar : Grammar, ICanRunSample
{
// making the class abstract so it won't load into Grammar Explorer
public InterpretedLanguageGrammar(bool caseSensitive)
: base(caseSensitive)
{
LanguageFlags = LanguageFlags.CreateAst;
}
// This method allows custom implementation of running a sample in Grammar Explorer
// By default it evaluates a parse tree using default interpreter.
// Irony's interpeter has one restriction: once a script (represented by AST node) is evaluated in ScriptApp,
// its internal fields in AST nodes become tied to this particular instance of ScriptApp (more precisely DataMap).
// If you want to evaluate the AST tree again, you have to do it in the context of the same DataMap.
// Grammar Explorer may call RunSample method repeatedly for evaluation of the same parsed script. So we keep ScriptApp instance in
// the field, and if we get the same script node, then we reuse the ScriptApp thus satisfying the requirement.
private ScriptApp _app;
private ParseTree _prevSample;
public virtual string RunSample(RunSampleArgs args)
{
if (_app == null || args.ParsedSample != _prevSample)
_app = new ScriptApp(args.Language);
_prevSample = args.ParsedSample;
//for (int i = 0; i < 1000; i++) //for perf measurements, to execute 1000 times
_app.Evaluate(args.ParsedSample);
return _app.OutputBuffer.ToString();
}
public virtual LanguageRuntime CreateRuntime(LanguageData language)
{
return new LanguageRuntime(language);
}
public override void BuildAst(LanguageData language, ParseTree parseTree)
{
var opHandler = new OperatorHandler(language.Grammar.CaseSensitive);
Util.Check(!parseTree.HasErrors(), "ParseTree has errors, cannot build AST.");
var astContext = new InterpreterAstContext(language, opHandler);
var astBuilder = new AstBuilder(astContext);
astBuilder.BuildAst(parseTree);
}
} //grammar class
}

View File

@ -0,0 +1,82 @@
#region License
/* **********************************************************************************
* Copyright (c) Roman Ivantsov
* This source code is subject to terms and conditions of the MIT License
* for Irony. A copy of the license can be found in the License.txt file
* at the root of this distribution.
* By using this source code in any fashion, you are agreeing to be bound by the terms of the
* MIT License.
* You must not remove this notice from this software.
* **********************************************************************************/
#endregion
namespace Sanchime.Irony.Interpreter
{
public class ConsoleWriteEventArgs : EventArgs
{
public string Text;
public ConsoleWriteEventArgs(string text)
{
Text = text;
}
}
//Note: mark the derived language-specific class as sealed - important for JIT optimizations
// details here: http://www.codeproject.com/KB/dotnet/JITOptimizations.aspx
public partial class LanguageRuntime
{
public readonly LanguageData Language;
public OperatorHandler OperatorHandler;
//Converter of the result for comparison operation; converts bool value to values
// specific for the language
public UnaryOperatorMethod BoolResultConverter = null;
//An unassigned reserved object for a language implementation
public NoneClass NoneValue { get; protected set; }
//Built-in binding sources
public BindingSourceTable BuiltIns;
public LanguageRuntime(LanguageData language)
{
Language = language;
NoneValue = NoneClass.Value;
BuiltIns = new BindingSourceTable(Language.Grammar.CaseSensitive);
Init();
}
public virtual void Init()
{
InitOperatorImplementations();
}
public virtual bool IsTrue(object value)
{
if (value is bool)
return (bool)value;
if (value is int)
return ((int)value != 0);
if (value == NoneValue)
return false;
return value != null;
}
protected internal void ThrowError(string message, params object[] args)
{
if (args != null && args.Length > 0)
message = string.Format(message, args);
throw new Exception(message);
}
protected internal void ThrowScriptError(string message, params object[] args)
{
if (args != null && args.Length > 0)
message = string.Format(message, args);
throw new ScriptException(message);
}
}//class
}//namespace

View File

@ -0,0 +1,116 @@
#region License
/* **********************************************************************************
* Copyright (c) Roman Ivantsov
* This source code is subject to terms and conditions of the MIT License
* for Irony. A copy of the license can be found in the License.txt file
* at the root of this distribution.
* By using this source code in any fashion, you are agreeing to be bound by the terms of the
* MIT License.
* You must not remove this notice from this software.
* **********************************************************************************/
#endregion
using Sanchime.Irony.Interpreter.Scopes;
using Sanchime.Irony.Interpreter.Utilities;
namespace Sanchime.Irony.Interpreter
{
public partial class LanguageRuntime : IBindingSource
{
//Binds to local variables, enclosing scopes, module scopes/globals and built-ins
public virtual Binding Bind(BindingRequest request)
{
var symbol = request.Symbol;
var mode = request.Flags;
if (mode.IsSet(BindingRequestFlags.Write))
return BindSymbolForWrite(request);
else if (mode.IsSet(BindingRequestFlags.Read))
return BindSymbolForRead(request);
else
{
//TODO: need to throw fatal error here
request.Thread.ThrowScriptError("Invalid binding request, access type (Read or Write) is not set in request Options.");
return null; // never happens
}
}//method
public virtual Binding BindSymbolForWrite(BindingRequest request)
{
var scope = request.Thread.CurrentScope;
var existingSlot = scope.Info.GetSlot(request.Symbol);
//1. If new only, check it does not exist yet, create and return it
if (request.Flags.IsSet(BindingRequestFlags.NewOnly))
{
if (existingSlot != null)
request.Thread.ThrowScriptError("Variable {0} already exists.", request.Symbol);
var newSlot = scope.AddSlot(request.Symbol);
return new SlotBinding(newSlot, request.FromNode, request.FromScopeInfo);
}
//2. If exists, then return it
if (existingSlot != null && request.Flags.IsSet(BindingRequestFlags.ExistingOrNew))
{
//TODO: For external client, check that slot is actually public or exported
return new SlotBinding(existingSlot, request.FromNode, request.FromScopeInfo);
}
//3. Check external module imports
foreach (var imp in request.FromModule.Imports)
{
var result = imp.Bind(request);
if (result != null)
return result;
}
//4. If nothing found, create new slot in current scope
if (request.Flags.IsSet(BindingRequestFlags.ExistingOrNew))
{
var newSlot = scope.AddSlot(request.Symbol);
return new SlotBinding(newSlot, request.FromNode, request.FromScopeInfo);
}
//5. Check built-in methods
var builtIn = BuiltIns.Bind(request);
if (builtIn != null) return builtIn;
//6. If still not found, return null.
return null;
}//method
public virtual Binding BindSymbolForRead(BindingRequest request)
{
var symbol = request.Symbol;
// First check current and enclosing scopes
var currScope = request.Thread.CurrentScope;
do
{
var existingSlot = currScope.Info.GetSlot(symbol);
if (existingSlot != null)
return new SlotBinding(existingSlot, request.FromNode, request.FromScopeInfo);
currScope = currScope.Parent;
} while (currScope != null);
// If not found, check imports
foreach (var imp in request.FromModule.Imports)
{
var result = imp.Bind(request);
if (result != null)
return result;
}
// Check built-in modules
var builtIn = BuiltIns.Bind(request);
if (builtIn != null) return builtIn;
// if not found, return null
return null;
}
//Binds symbol to a public member exported by a module.
public virtual Binding BindSymbol(BindingRequest request, ModuleInfo module)
{
return module.BindToExport(request);
}
}//class
}

View File

@ -0,0 +1,132 @@
#region License
/* **********************************************************************************
* Copyright (c) Roman Ivantsov
* This source code is subject to terms and conditions of the MIT License
* for Irony. A copy of the license can be found in the License.txt file
* at the root of this distribution.
* By using this source code in any fashion, you are agreeing to be bound by the terms of the
* MIT License.
* You must not remove this notice from this software.
* **********************************************************************************/
#endregion
using System.Linq.Expressions;
namespace Sanchime.Irony.Interpreter
{
public partial class LanguageRuntime
{
public readonly OperatorImplementationTable OperatorImplementations = new OperatorImplementationTable(2000);
public object ExecuteBinaryOperator(ExpressionType op, object arg1, object arg2, ref OperatorImplementation previousUsed)
{
// 1. Get arg types
Type arg1Type, arg2Type;
try
{
arg1Type = arg1.GetType();
arg2Type = arg2.GetType();
}
catch (NullReferenceException)
{
// arg1 or arg2 is null - which means never assigned.
CheckUnassigned(arg1);
CheckUnassigned(arg2);
throw;
}
// 2. If we had prev impl, check if current args types match it; first copy it into local variable
// Note: BinaryExpression node might already have tried it directly, without any checks, and
// apparently failed. At some point this attempt in BinaryExpressionNode can become disabled.
// But we might still try it here, with proper checks
var currentImpl = previousUsed;
if (currentImpl != null && (arg1Type != currentImpl.Key.Arg1Type || arg2Type != currentImpl.Key.Arg2Type))
currentImpl = null;
// 3. Find implementation for arg types
OperatorDispatchKey key;
if (currentImpl == null)
{
key = new OperatorDispatchKey(op, arg1Type, arg2Type);
if (!OperatorImplementations.TryGetValue(key, out currentImpl))
ThrowScriptError(Resources.ErrOpNotDefinedForTypes, op, arg1Type, arg2Type);
}
// 4. Actually call
try
{
previousUsed = currentImpl;
return currentImpl.EvaluateBinary(arg1, arg2);
}
catch (OverflowException)
{
if (currentImpl.OverflowHandler == null) throw;
previousUsed = currentImpl.OverflowHandler; //set previousUsed to overflowHandler, so it will be used next time
return ExecuteBinaryOperator(op, arg1, arg2, ref previousUsed); //call self recursively
}
catch (IndexOutOfRangeException)
{
//We can get here only if we use SmartBoxing - the result is out of range of pre-allocated boxes,
// so attempt to lookup a boxed value in _boxes dictionary fails with outOfRange exc
if (currentImpl.NoBoxImplementation == null) throw;
// If NoBoxImpl is not null, then it is implementation with auto-boxing.
// Auto-boxing failed - the result is outside the range of our boxes array. Let's call no-box version.
// we also set previousUsed to no-box implementation, so we use it in the future calls
previousUsed = currentImpl.NoBoxImplementation;
return ExecuteBinaryOperator(op, arg1, arg2, ref previousUsed); //call self recursively
}
}//method
public object ExecuteUnaryOperator(ExpressionType op, object arg1, ref OperatorImplementation previousUsed)
{
// 1. Get arg type
Type arg1Type;
try
{
arg1Type = arg1.GetType();
}
catch (NullReferenceException)
{
CheckUnassigned(arg1);
throw;
}
// 2. If we had prev impl, check if current args types match it; first copy it into local variable
OperatorDispatchKey key;
var currentImpl = previousUsed;
if (currentImpl != null && arg1Type != currentImpl.Key.Arg1Type)
currentImpl = null;
// 3. Find implementation for arg type
if (currentImpl == null)
{
key = new OperatorDispatchKey(op, arg1Type);
if (!OperatorImplementations.TryGetValue(key, out currentImpl))
ThrowError(Resources.ErrOpNotDefinedForType, op, arg1Type);
}
// 4. Actually call
try
{
previousUsed = currentImpl; //set previousUsed so next time we'll try this impl first
return currentImpl.Arg1Converter(arg1);
}
catch (OverflowException)
{
if (currentImpl.OverflowHandler == null)
throw;
previousUsed = currentImpl.OverflowHandler; //set previousUsed to overflowHandler, so it will be used next time
return ExecuteUnaryOperator(op, arg1, ref previousUsed); //call self recursively
}
}//method
//TODO: finish this
private void CheckUnassigned(object value)
{
if (value == null)
throw new Exception("Variable unassigned.");
}
}//class
}

View File

@ -0,0 +1,691 @@
#region License
/* **********************************************************************************
* Copyright (c) Roman Ivantsov
* This source code is subject to terms and conditions of the MIT License
* for Irony. A copy of the license can be found in the License.txt file
* at the root of this distribution.
* By using this source code in any fashion, you are agreeing to be bound by the terms of the
* MIT License.
* You must not remove this notice from this software.
* **********************************************************************************/
#endregion
using System.Linq.Expressions;
using System.Numerics;
namespace Sanchime.Irony.Interpreter
{
//Initialization of Runtime
public partial class LanguageRuntime
{
private static ExpressionType[] _overflowOperators = new ExpressionType[] {
ExpressionType.Add, ExpressionType.AddChecked, ExpressionType.Subtract, ExpressionType.SubtractChecked,
ExpressionType.Multiply, ExpressionType.MultiplyChecked, ExpressionType.Power};
// Smart boxing: boxes for a bunch of integers are preallocated
private object[] _boxes = new object[4096];
private const int _boxesMiddle = 2048;
// Note: ran some primitive tests, and it appears that use of smart boxing makes it slower
// by about 5-10%; so disabling it for now
public bool SmartBoxingEnabled = false;
private bool _supportsComplex;
private bool _supportsBigInt;
private bool _supportsRational;
protected virtual void InitOperatorImplementations()
{
_supportsComplex = this.Language.Grammar.LanguageFlags.IsSet(LanguageFlags.SupportsComplex);
_supportsBigInt = this.Language.Grammar.LanguageFlags.IsSet(LanguageFlags.SupportsBigInt);
_supportsRational = this.Language.Grammar.LanguageFlags.IsSet(LanguageFlags.SupportsRational);
// TODO: add support for Rational
if (SmartBoxingEnabled)
InitBoxes();
InitTypeConverters();
InitBinaryOperatorImplementationsForMatchedTypes();
InitUnaryOperatorImplementations();
CreateBinaryOperatorImplementationsForMismatchedTypes();
CreateOverflowHandlers();
}
//The value of smart boxing is questionable - so far did not see perf improvements, so currently it is disabled
private void InitBoxes()
{
for (int i = 0; i < _boxes.Length; i++)
_boxes[i] = i - _boxesMiddle;
}
#region Utility methods for adding converters and binary implementations
protected OperatorImplementation AddConverter(Type fromType, Type toType, UnaryOperatorMethod method)
{
var key = new OperatorDispatchKey(ExpressionType.ConvertChecked, fromType, toType);
var impl = new OperatorImplementation(key, toType, method);
OperatorImplementations[key] = impl;
return impl;
}
protected OperatorImplementation AddBinaryBoxed(ExpressionType op, Type baseType,
BinaryOperatorMethod boxedBinaryMethod, BinaryOperatorMethod noBoxMethod)
{
// first create implementation without boxing
var noBoxImpl = AddBinary(op, baseType, noBoxMethod);
if (!SmartBoxingEnabled)
return noBoxImpl;
//The boxedImpl will overwrite noBoxImpl in the dictionary
var boxedImpl = AddBinary(op, baseType, boxedBinaryMethod);
boxedImpl.NoBoxImplementation = noBoxImpl;
return boxedImpl;
}
protected OperatorImplementation AddBinary(ExpressionType op, Type baseType, BinaryOperatorMethod binaryMethod)
{
return AddBinary(op, baseType, binaryMethod, null);
}
protected OperatorImplementation AddBinary(ExpressionType op, Type commonType,
BinaryOperatorMethod binaryMethod, UnaryOperatorMethod resultConverter)
{
var key = new OperatorDispatchKey(op, commonType, commonType);
var impl = new OperatorImplementation(key, commonType, binaryMethod, null, null, resultConverter);
OperatorImplementations[key] = impl;
return impl;
}
protected OperatorImplementation AddUnary(ExpressionType op, Type commonType, UnaryOperatorMethod unaryMethod)
{
var key = new OperatorDispatchKey(op, commonType);
var impl = new OperatorImplementation(key, commonType, null, unaryMethod, null, null);
OperatorImplementations[key] = impl;
return impl;
}
#endregion
#region Initializing type converters
public virtual void InitTypeConverters()
{
Type targetType;
//->string
targetType = typeof(string);
AddConverter(typeof(char), targetType, ConvertAnyToString);
AddConverter(typeof(sbyte), targetType, ConvertAnyToString);
AddConverter(typeof(byte), targetType, ConvertAnyToString);
AddConverter(typeof(Int16), targetType, ConvertAnyToString);
AddConverter(typeof(UInt16), targetType, ConvertAnyToString);
AddConverter(typeof(Int32), targetType, ConvertAnyToString);
AddConverter(typeof(UInt32), targetType, ConvertAnyToString);
AddConverter(typeof(Int64), targetType, ConvertAnyToString);
AddConverter(typeof(UInt64), targetType, ConvertAnyToString);
AddConverter(typeof(Single), targetType, ConvertAnyToString);
if (_supportsBigInt)
AddConverter(typeof(BigInteger), targetType, ConvertAnyToString);
if (_supportsComplex)
AddConverter(typeof(Complex), targetType, ConvertAnyToString);
//->Complex
if (_supportsComplex)
{
targetType = typeof(Complex);
AddConverter(typeof(sbyte), targetType, ConvertAnyToComplex);
AddConverter(typeof(byte), targetType, ConvertAnyToComplex);
AddConverter(typeof(Int16), targetType, ConvertAnyToComplex);
AddConverter(typeof(UInt16), targetType, ConvertAnyToComplex);
AddConverter(typeof(Int32), targetType, ConvertAnyToComplex);
AddConverter(typeof(UInt32), targetType, ConvertAnyToComplex);
AddConverter(typeof(Int64), targetType, ConvertAnyToComplex);
AddConverter(typeof(UInt64), targetType, ConvertAnyToComplex);
AddConverter(typeof(Single), targetType, ConvertAnyToComplex);
if (_supportsBigInt)
AddConverter(typeof(BigInteger), targetType, ConvertBigIntToComplex);
}
//->BigInteger
if (_supportsBigInt)
{
targetType = typeof(BigInteger);
AddConverter(typeof(sbyte), targetType, ConvertAnyIntToBigInteger);
AddConverter(typeof(byte), targetType, ConvertAnyIntToBigInteger);
AddConverter(typeof(Int16), targetType, ConvertAnyIntToBigInteger);
AddConverter(typeof(UInt16), targetType, ConvertAnyIntToBigInteger);
AddConverter(typeof(Int32), targetType, ConvertAnyIntToBigInteger);
AddConverter(typeof(UInt32), targetType, ConvertAnyIntToBigInteger);
AddConverter(typeof(Int64), targetType, ConvertAnyIntToBigInteger);
AddConverter(typeof(UInt64), targetType, ConvertAnyIntToBigInteger);
}
//->Double
targetType = typeof(double);
AddConverter(typeof(sbyte), targetType, value => (double)(sbyte)value);
AddConverter(typeof(byte), targetType, value => (double)(byte)value);
AddConverter(typeof(Int16), targetType, value => (double)(Int16)value);
AddConverter(typeof(UInt16), targetType, value => (double)(UInt16)value);
AddConverter(typeof(Int32), targetType, value => (double)(Int32)value);
AddConverter(typeof(UInt32), targetType, value => (double)(UInt32)value);
AddConverter(typeof(Int64), targetType, value => (double)(Int64)value);
AddConverter(typeof(UInt64), targetType, value => (double)(UInt64)value);
AddConverter(typeof(Single), targetType, value => (double)(Single)value);
if (_supportsBigInt)
AddConverter(typeof(BigInteger), targetType, value => ((double)(BigInteger)value));
//->Single
targetType = typeof(Single);
AddConverter(typeof(sbyte), targetType, value => (Single)(sbyte)value);
AddConverter(typeof(byte), targetType, value => (Single)(byte)value);
AddConverter(typeof(Int16), targetType, value => (Single)(Int16)value);
AddConverter(typeof(UInt16), targetType, value => (Single)(UInt16)value);
AddConverter(typeof(Int32), targetType, value => (Single)(Int32)value);
AddConverter(typeof(UInt32), targetType, value => (Single)(UInt32)value);
AddConverter(typeof(Int64), targetType, value => (Single)(Int64)value);
AddConverter(typeof(UInt64), targetType, value => (Single)(UInt64)value);
if (_supportsBigInt)
AddConverter(typeof(BigInteger), targetType, value => (Single)(BigInteger)value);
//->UInt64
targetType = typeof(UInt64);
AddConverter(typeof(sbyte), targetType, value => (UInt64)(sbyte)value);
AddConverter(typeof(byte), targetType, value => (UInt64)(byte)value);
AddConverter(typeof(Int16), targetType, value => (UInt64)(Int16)value);
AddConverter(typeof(UInt16), targetType, value => (UInt64)(UInt16)value);
AddConverter(typeof(Int32), targetType, value => (UInt64)(Int32)value);
AddConverter(typeof(UInt32), targetType, value => (UInt64)(UInt32)value);
AddConverter(typeof(Int64), targetType, value => (UInt64)(Int64)value);
//->Int64
targetType = typeof(Int64);
AddConverter(typeof(sbyte), targetType, value => (Int64)(sbyte)value);
AddConverter(typeof(byte), targetType, value => (Int64)(byte)value);
AddConverter(typeof(Int16), targetType, value => (Int64)(Int16)value);
AddConverter(typeof(UInt16), targetType, value => (Int64)(UInt16)value);
AddConverter(typeof(Int32), targetType, value => (Int64)(Int32)value);
AddConverter(typeof(UInt32), targetType, value => (Int64)(UInt32)value);
//->UInt32
targetType = typeof(UInt32);
AddConverter(typeof(sbyte), targetType, value => (UInt32)(sbyte)value);
AddConverter(typeof(byte), targetType, value => (UInt32)(byte)value);
AddConverter(typeof(Int16), targetType, value => (UInt32)(Int16)value);
AddConverter(typeof(UInt16), targetType, value => (UInt32)(UInt16)value);
AddConverter(typeof(Int32), targetType, value => (UInt32)(Int32)value);
//->Int32
targetType = typeof(Int32);
AddConverter(typeof(sbyte), targetType, value => (Int32)(sbyte)value);
AddConverter(typeof(byte), targetType, value => (Int32)(byte)value);
AddConverter(typeof(Int16), targetType, value => (Int32)(Int16)value);
AddConverter(typeof(UInt16), targetType, value => (Int32)(UInt16)value);
//->UInt16
targetType = typeof(UInt16);
AddConverter(typeof(sbyte), targetType, value => (UInt16)(sbyte)value);
AddConverter(typeof(byte), targetType, value => (UInt16)(byte)value);
AddConverter(typeof(Int16), targetType, value => (UInt16)(Int16)value);
//->Int16
targetType = typeof(Int16);
AddConverter(typeof(sbyte), targetType, value => (Int16)(sbyte)value);
AddConverter(typeof(byte), targetType, value => (Int16)(byte)value);
//->byte
targetType = typeof(byte);
AddConverter(typeof(sbyte), targetType, value => (byte)(sbyte)value);
}
// Some specialized convert implementation methods
public static object ConvertAnyToString(object value)
{
return value == null ? string.Empty : value.ToString();
}
public static object ConvertBigIntToComplex(object value)
{
BigInteger bi = (BigInteger)value;
return new Complex((double)bi, 0);
}
public static object ConvertAnyToComplex(object value)
{
double d = Convert.ToDouble(value);
return new Complex(d, 0);
}
public static object ConvertAnyIntToBigInteger(object value)
{
long l = Convert.ToInt64(value);
return new BigInteger(l);
}
#endregion
#region Binary operators implementations
// Generates of binary implementations for matched argument types
public virtual void InitBinaryOperatorImplementationsForMatchedTypes()
{
// For each operator, we add a series of implementation methods for same-type operands. They are saved as OperatorImplementation
// records in OperatorImplementations table. This happens at initialization time.
// After this initialization (for same-type operands), system adds implementations for all type pairs (ex: int + double),
// using these same-type implementations and appropriate type converters.
// Note that arithmetics on byte, sbyte, int16, uint16 are performed in Int32 format (the way it's done in c# I guess)
// so the result is always Int32. We do not define operators for sbyte, byte, int16 and UInt16 types - they will
// be processed using Int32 implementation, with appropriate type converters.
ExpressionType op;
op = ExpressionType.AddChecked;
AddBinaryBoxed(op, typeof(Int32), (x, y) => _boxes[checked((Int32)x + (Int32)y) + _boxesMiddle],
(x, y) => checked((Int32)x + (Int32)y));
AddBinary(op, typeof(UInt32), (x, y) => checked((UInt32)x + (UInt32)y));
AddBinary(op, typeof(Int64), (x, y) => checked((Int64)x + (Int64)y));
AddBinary(op, typeof(UInt64), (x, y) => checked((UInt64)x + (UInt64)y));
AddBinary(op, typeof(Single), (x, y) => (Single)x + (Single)y);
AddBinary(op, typeof(double), (x, y) => (double)x + (double)y);
AddBinary(op, typeof(decimal), (x, y) => (decimal)x + (decimal)y);
if (_supportsBigInt)
AddBinary(op, typeof(BigInteger), (x, y) => (BigInteger)x + (BigInteger)y);
if (_supportsComplex)
AddBinary(op, typeof(Complex), (x, y) => (Complex)x + (Complex)y);
AddBinary(op, typeof(string), (x, y) => (string)x + (string)y);
AddBinary(op, typeof(char), (x, y) => ((char)x).ToString() + (char)y); //force to concatenate as strings
op = ExpressionType.SubtractChecked;
AddBinaryBoxed(op, typeof(Int32), (x, y) => _boxes[checked((Int32)x - (Int32)y) + _boxesMiddle],
(x, y) => checked((Int32)x - (Int32)y));
AddBinary(op, typeof(UInt32), (x, y) => checked((UInt32)x - (UInt32)y));
AddBinary(op, typeof(Int64), (x, y) => checked((Int64)x - (Int64)y));
AddBinary(op, typeof(UInt64), (x, y) => checked((UInt64)x - (UInt64)y));
AddBinary(op, typeof(Single), (x, y) => (Single)x - (Single)y);
AddBinary(op, typeof(double), (x, y) => (double)x - (double)y);
AddBinary(op, typeof(decimal), (x, y) => (decimal)x - (decimal)y);
if (_supportsBigInt)
AddBinary(op, typeof(BigInteger), (x, y) => (BigInteger)x - (BigInteger)y);
if (_supportsComplex)
AddBinary(op, typeof(Complex), (x, y) => (Complex)x - (Complex)y);
op = ExpressionType.MultiplyChecked;
AddBinaryBoxed(op, typeof(Int32), (x, y) => _boxes[checked((Int32)x * (Int32)y) + _boxesMiddle],
(x, y) => checked((Int32)x * (Int32)y));
AddBinary(op, typeof(UInt32), (x, y) => checked((UInt32)x * (UInt32)y));
AddBinary(op, typeof(Int64), (x, y) => checked((Int64)x * (Int64)y));
AddBinary(op, typeof(UInt64), (x, y) => checked((UInt64)x * (UInt64)y));
AddBinary(op, typeof(Single), (x, y) => (Single)x * (Single)y);
AddBinary(op, typeof(double), (x, y) => (double)x * (double)y);
AddBinary(op, typeof(decimal), (x, y) => (decimal)x * (decimal)y);
if (_supportsBigInt)
AddBinary(op, typeof(BigInteger), (x, y) => (BigInteger)x * (BigInteger)y);
if (_supportsComplex)
AddBinary(op, typeof(Complex), (x, y) => (Complex)x * (Complex)y);
op = ExpressionType.Divide;
AddBinary(op, typeof(Int32), (x, y) => checked((Int32)x / (Int32)y));
AddBinary(op, typeof(UInt32), (x, y) => checked((UInt32)x / (UInt32)y));
AddBinary(op, typeof(Int64), (x, y) => checked((Int64)x / (Int64)y));
AddBinary(op, typeof(UInt64), (x, y) => checked((UInt64)x / (UInt64)y));
AddBinary(op, typeof(Single), (x, y) => (Single)x / (Single)y);
AddBinary(op, typeof(double), (x, y) => (double)x / (double)y);
AddBinary(op, typeof(decimal), (x, y) => (decimal)x / (decimal)y);
if (_supportsBigInt)
AddBinary(op, typeof(BigInteger), (x, y) => (BigInteger)x / (BigInteger)y);
if (_supportsComplex)
AddBinary(op, typeof(Complex), (x, y) => (Complex)x / (Complex)y);
op = ExpressionType.Modulo;
AddBinary(op, typeof(Int32), (x, y) => checked((Int32)x % (Int32)y));
AddBinary(op, typeof(UInt32), (x, y) => checked((UInt32)x % (UInt32)y));
AddBinary(op, typeof(Int64), (x, y) => checked((Int64)x % (Int64)y));
AddBinary(op, typeof(UInt64), (x, y) => checked((UInt64)x % (UInt64)y));
AddBinary(op, typeof(Single), (x, y) => (Single)x % (Single)y);
AddBinary(op, typeof(double), (x, y) => (double)x % (double)y);
AddBinary(op, typeof(decimal), (x, y) => (decimal)x % (decimal)y);
if (_supportsBigInt)
AddBinary(op, typeof(BigInteger), (x, y) => (BigInteger)x % (BigInteger)y);
// For bitwise operator, we provide explicit implementations for "small" integer types
op = ExpressionType.And;
AddBinary(op, typeof(bool), (x, y) => (bool)x & (bool)y);
AddBinary(op, typeof(sbyte), (x, y) => (sbyte)x & (sbyte)y);
AddBinary(op, typeof(byte), (x, y) => (byte)x & (byte)y);
AddBinary(op, typeof(Int16), (x, y) => (Int16)x & (Int16)y);
AddBinary(op, typeof(UInt16), (x, y) => (UInt16)x & (UInt16)y);
AddBinary(op, typeof(Int32), (x, y) => (Int32)x & (Int32)y);
AddBinary(op, typeof(UInt32), (x, y) => (UInt32)x & (UInt32)y);
AddBinary(op, typeof(Int64), (x, y) => (Int64)x & (Int64)y);
AddBinary(op, typeof(UInt64), (x, y) => (UInt64)x & (UInt64)y);
op = ExpressionType.Or;
AddBinary(op, typeof(bool), (x, y) => (bool)x | (bool)y);
AddBinary(op, typeof(sbyte), (x, y) => (sbyte)x | (sbyte)y);
AddBinary(op, typeof(byte), (x, y) => (byte)x | (byte)y);
AddBinary(op, typeof(Int16), (x, y) => (Int16)x | (Int16)y);
AddBinary(op, typeof(UInt16), (x, y) => (UInt16)x | (UInt16)y);
AddBinary(op, typeof(Int32), (x, y) => (Int32)x | (Int32)y);
AddBinary(op, typeof(UInt32), (x, y) => (UInt32)x | (UInt32)y);
AddBinary(op, typeof(Int64), (x, y) => (Int64)x | (Int64)y);
AddBinary(op, typeof(UInt64), (x, y) => (UInt64)x | (UInt64)y);
op = ExpressionType.ExclusiveOr;
AddBinary(op, typeof(bool), (x, y) => (bool)x ^ (bool)y);
AddBinary(op, typeof(sbyte), (x, y) => (sbyte)x ^ (sbyte)y);
AddBinary(op, typeof(byte), (x, y) => (byte)x ^ (byte)y);
AddBinary(op, typeof(Int16), (x, y) => (Int16)x ^ (Int16)y);
AddBinary(op, typeof(UInt16), (x, y) => (UInt16)x ^ (UInt16)y);
AddBinary(op, typeof(Int32), (x, y) => (Int32)x ^ (Int32)y);
AddBinary(op, typeof(UInt32), (x, y) => (UInt32)x ^ (UInt32)y);
AddBinary(op, typeof(Int64), (x, y) => (Int64)x ^ (Int64)y);
AddBinary(op, typeof(UInt64), (x, y) => (UInt64)x ^ (UInt64)y);
op = ExpressionType.LessThan;
AddBinary(op, typeof(Int32), (x, y) => checked((Int32)x < (Int32)y), BoolResultConverter);
AddBinary(op, typeof(UInt32), (x, y) => checked((UInt32)x < (UInt32)y), BoolResultConverter);
AddBinary(op, typeof(Int64), (x, y) => checked((Int64)x < (Int64)y), BoolResultConverter);
AddBinary(op, typeof(UInt64), (x, y) => checked((UInt64)x < (UInt64)y), BoolResultConverter);
AddBinary(op, typeof(Single), (x, y) => (Single)x < (Single)y, BoolResultConverter);
AddBinary(op, typeof(double), (x, y) => (double)x < (double)y, BoolResultConverter);
AddBinary(op, typeof(decimal), (x, y) => (decimal)x < (decimal)y);
if (_supportsBigInt)
AddBinary(op, typeof(BigInteger), (x, y) => (BigInteger)x < (BigInteger)y, BoolResultConverter);
op = ExpressionType.GreaterThan;
AddBinary(op, typeof(Int32), (x, y) => checked((Int32)x > (Int32)y), BoolResultConverter);
AddBinary(op, typeof(UInt32), (x, y) => checked((UInt32)x > (UInt32)y), BoolResultConverter);
AddBinary(op, typeof(Int64), (x, y) => checked((Int64)x > (Int64)y), BoolResultConverter);
AddBinary(op, typeof(UInt64), (x, y) => checked((UInt64)x > (UInt64)y), BoolResultConverter);
AddBinary(op, typeof(Single), (x, y) => (Single)x > (Single)y, BoolResultConverter);
AddBinary(op, typeof(double), (x, y) => (double)x > (double)y, BoolResultConverter);
AddBinary(op, typeof(decimal), (x, y) => (decimal)x > (decimal)y);
if (_supportsBigInt)
AddBinary(op, typeof(BigInteger), (x, y) => (BigInteger)x > (BigInteger)y, BoolResultConverter);
op = ExpressionType.LessThanOrEqual;
AddBinary(op, typeof(Int32), (x, y) => checked((Int32)x <= (Int32)y), BoolResultConverter);
AddBinary(op, typeof(UInt32), (x, y) => checked((UInt32)x <= (UInt32)y), BoolResultConverter);
AddBinary(op, typeof(Int64), (x, y) => checked((Int64)x <= (Int64)y), BoolResultConverter);
AddBinary(op, typeof(UInt64), (x, y) => checked((UInt64)x <= (UInt64)y), BoolResultConverter);
AddBinary(op, typeof(Single), (x, y) => (Single)x <= (Single)y, BoolResultConverter);
AddBinary(op, typeof(double), (x, y) => (double)x <= (double)y, BoolResultConverter);
AddBinary(op, typeof(decimal), (x, y) => (decimal)x <= (decimal)y);
if (_supportsBigInt)
AddBinary(op, typeof(BigInteger), (x, y) => (BigInteger)x <= (BigInteger)y, BoolResultConverter);
op = ExpressionType.GreaterThanOrEqual;
AddBinary(op, typeof(Int32), (x, y) => checked((Int32)x >= (Int32)y), BoolResultConverter);
AddBinary(op, typeof(UInt32), (x, y) => checked((UInt32)x >= (UInt32)y), BoolResultConverter);
AddBinary(op, typeof(Int64), (x, y) => checked((Int64)x >= (Int64)y), BoolResultConverter);
AddBinary(op, typeof(UInt64), (x, y) => checked((UInt64)x >= (UInt64)y), BoolResultConverter);
AddBinary(op, typeof(Single), (x, y) => (Single)x >= (Single)y, BoolResultConverter);
AddBinary(op, typeof(double), (x, y) => (double)x >= (double)y, BoolResultConverter);
AddBinary(op, typeof(decimal), (x, y) => (decimal)x >= (decimal)y);
if (_supportsBigInt)
AddBinary(op, typeof(BigInteger), (x, y) => (BigInteger)x >= (BigInteger)y, BoolResultConverter);
op = ExpressionType.Equal;
AddBinary(op, typeof(Int32), (x, y) => checked((Int32)x == (Int32)y), BoolResultConverter);
AddBinary(op, typeof(UInt32), (x, y) => checked((UInt32)x == (UInt32)y), BoolResultConverter);
AddBinary(op, typeof(Int64), (x, y) => checked((Int64)x == (Int64)y), BoolResultConverter);
AddBinary(op, typeof(UInt64), (x, y) => checked((UInt64)x == (UInt64)y), BoolResultConverter);
AddBinary(op, typeof(Single), (x, y) => (Single)x == (Single)y, BoolResultConverter);
AddBinary(op, typeof(double), (x, y) => (double)x == (double)y, BoolResultConverter);
AddBinary(op, typeof(decimal), (x, y) => (decimal)x == (decimal)y);
if (_supportsBigInt)
AddBinary(op, typeof(BigInteger), (x, y) => (BigInteger)x == (BigInteger)y, BoolResultConverter);
op = ExpressionType.NotEqual;
AddBinary(op, typeof(Int32), (x, y) => checked((Int32)x != (Int32)y), BoolResultConverter);
AddBinary(op, typeof(UInt32), (x, y) => checked((UInt32)x != (UInt32)y), BoolResultConverter);
AddBinary(op, typeof(Int64), (x, y) => checked((Int64)x != (Int64)y), BoolResultConverter);
AddBinary(op, typeof(UInt64), (x, y) => checked((UInt64)x != (UInt64)y), BoolResultConverter);
AddBinary(op, typeof(Single), (x, y) => (Single)x != (Single)y, BoolResultConverter);
AddBinary(op, typeof(double), (x, y) => (double)x != (double)y, BoolResultConverter);
AddBinary(op, typeof(decimal), (x, y) => (decimal)x != (decimal)y);
if (_supportsBigInt)
AddBinary(op, typeof(BigInteger), (x, y) => (BigInteger)x != (BigInteger)y, BoolResultConverter);
}//method
public virtual void InitUnaryOperatorImplementations()
{
var op = ExpressionType.UnaryPlus;
AddUnary(op, typeof(sbyte), x => +(sbyte)x);
AddUnary(op, typeof(byte), x => +(byte)x);
AddUnary(op, typeof(Int16), x => +(Int16)x);
AddUnary(op, typeof(UInt16), x => +(UInt16)x);
AddUnary(op, typeof(Int32), x => +(Int32)x);
AddUnary(op, typeof(UInt32), x => +(UInt32)x);
AddUnary(op, typeof(Int64), x => +(Int64)x);
AddUnary(op, typeof(UInt64), x => +(UInt64)x);
AddUnary(op, typeof(Single), x => +(Single)x);
AddUnary(op, typeof(double), x => +(double)x);
AddUnary(op, typeof(decimal), x => +(decimal)x);
if (_supportsBigInt)
AddUnary(op, typeof(BigInteger), x => +(BigInteger)x);
op = ExpressionType.Negate;
AddUnary(op, typeof(sbyte), x => -(sbyte)x);
AddUnary(op, typeof(byte), x => -(byte)x);
AddUnary(op, typeof(Int16), x => -(Int16)x);
AddUnary(op, typeof(UInt16), x => -(UInt16)x);
AddUnary(op, typeof(Int32), x => -(Int32)x);
AddUnary(op, typeof(UInt32), x => -(UInt32)x);
AddUnary(op, typeof(Int64), x => -(Int64)x);
AddUnary(op, typeof(Single), x => -(Single)x);
AddUnary(op, typeof(double), x => -(double)x);
AddUnary(op, typeof(decimal), x => -(decimal)x);
if (_supportsBigInt)
AddUnary(op, typeof(BigInteger), x => -(BigInteger)x);
if (_supportsComplex)
AddUnary(op, typeof(Complex), x => -(Complex)x);
op = ExpressionType.Not;
AddUnary(op, typeof(bool), x => !(bool)x);
AddUnary(op, typeof(sbyte), x => ~(sbyte)x);
AddUnary(op, typeof(byte), x => ~(byte)x);
AddUnary(op, typeof(Int16), x => ~(Int16)x);
AddUnary(op, typeof(UInt16), x => ~(UInt16)x);
AddUnary(op, typeof(Int32), x => ~(Int32)x);
AddUnary(op, typeof(UInt32), x => ~(UInt32)x);
AddUnary(op, typeof(Int64), x => ~(Int64)x);
}
// Generates binary implementations for mismatched argument types
public virtual void CreateBinaryOperatorImplementationsForMismatchedTypes()
{
// find all data types are there
var allTypes = new HashSet<Type>();
var allBinOps = new HashSet<ExpressionType>();
foreach (var kv in OperatorImplementations)
{
allTypes.Add(kv.Key.Arg1Type);
if (kv.Value.BaseBinaryMethod != null)
allBinOps.Add(kv.Key.Op);
}
foreach (var arg1Type in allTypes)
foreach (var arg2Type in allTypes)
if (arg1Type != arg2Type)
foreach (ExpressionType op in allBinOps)
CreateBinaryOperatorImplementation(op, arg1Type, arg2Type);
}//method
// Creates a binary implementations for an operator with mismatched argument types.
// Determines common type, retrieves implementation for operator with both args of common type, then creates
// implementation for mismatched types using type converters (by converting to common type)
public OperatorImplementation CreateBinaryOperatorImplementation(ExpressionType op, Type arg1Type, Type arg2Type)
{
Type commonType = GetCommonTypeForOperator(op, arg1Type, arg2Type);
if (commonType == null)
return null;
//Get base method for the operator and common type
var baseImpl = FindBaseImplementation(op, commonType);
if (baseImpl == null)
{ //Try up-type
commonType = GetUpType(commonType);
if (commonType == null)
return null;
baseImpl = FindBaseImplementation(op, commonType);
}
if (baseImpl == null)
return null;
//Create implementation and save it in implementations table
var impl = CreateBinaryOperatorImplementation(op, arg1Type, arg2Type, commonType, baseImpl.BaseBinaryMethod, baseImpl.ResultConverter);
OperatorImplementations[impl.Key] = impl;
return impl;
}
protected virtual OperatorImplementation CreateBinaryOperatorImplementation(ExpressionType op, Type arg1Type, Type arg2Type,
Type commonType, BinaryOperatorMethod method, UnaryOperatorMethod resultConverter)
{
OperatorDispatchKey key = new OperatorDispatchKey(op, arg1Type, arg2Type);
UnaryOperatorMethod arg1Converter = arg1Type == commonType ? null : GetConverter(arg1Type, commonType);
UnaryOperatorMethod arg2Converter = arg2Type == commonType ? null : GetConverter(arg2Type, commonType);
var impl = new OperatorImplementation(
key, commonType, method, arg1Converter, arg2Converter, resultConverter);
return impl;
}
// Creates overflow handlers. For each implementation, checks if operator can overflow;
// if yes, creates and sets an overflow handler - another implementation that performs
// operation using "upper" type that wouldn't overflow. For ex: (int * int) has overflow handler (int64 * int64)
protected virtual void CreateOverflowHandlers()
{
foreach (var impl in OperatorImplementations.Values)
{
if (!CanOverflow(impl))
continue;
var key = impl.Key;
var upType = GetUpType(impl.CommonType);
if (upType == null)
continue;
var upBaseImpl = FindBaseImplementation(key.Op, upType);
if (upBaseImpl == null)
continue;
impl.OverflowHandler = CreateBinaryOperatorImplementation(key.Op, key.Arg1Type, key.Arg2Type, upType,
upBaseImpl.BaseBinaryMethod, upBaseImpl.ResultConverter);
// Do not put OverflowHandler into OperatoImplementations table! - it will override some other, non-overflow impl
}
}
private OperatorImplementation FindBaseImplementation(ExpressionType op, Type commonType)
{
var baseKey = new OperatorDispatchKey(op, commonType, commonType);
OperatorImplementation baseImpl;
OperatorImplementations.TryGetValue(baseKey, out baseImpl);
return baseImpl;
}
// Important: returns null if fromType == toType
public virtual UnaryOperatorMethod GetConverter(Type fromType, Type toType)
{
if (fromType == toType)
return (x => x);
var key = new OperatorDispatchKey(ExpressionType.ConvertChecked, fromType, toType);
OperatorImplementation impl;
if (!OperatorImplementations.TryGetValue(key, out impl))
return null;
return impl.Arg1Converter;
}
#endregion
#region Utilities
private static bool CanOverflow(OperatorImplementation impl)
{
if (!CanOverflow(impl.Key.Op))
return false;
if (impl.CommonType == typeof(Int32) && IsSmallInt(impl.Key.Arg1Type) && IsSmallInt(impl.Key.Arg2Type))
return false;
if (impl.CommonType == typeof(double) || impl.CommonType == typeof(Single))
return false;
if (impl.CommonType == typeof(BigInteger))
return false;
return true;
}
private static bool CanOverflow(ExpressionType expression)
{
return _overflowOperators.Contains(expression);
}
private static bool IsSmallInt(Type type)
{
return type == typeof(byte) || type == typeof(sbyte) || type == typeof(Int16) || type == typeof(UInt16);
}
/// <summary>
/// Returns the type to which arguments should be converted to perform the operation
/// for a given operator and arguments types.
/// </summary>
/// <param name="op">Operator.</param>
/// <param name="argType1">The type of the first argument.</param>
/// <param name="argType2">The type of the second argument</param>
/// <returns>A common type for operation.</returns>
protected virtual Type GetCommonTypeForOperator(ExpressionType op, Type argType1, Type argType2)
{
if (argType1 == argType2)
return argType1;
//TODO: see how to handle properly null/NoneValue in expressions
// var noneType = typeof(NoneClass);
// if (argType1 == noneType || argType2 == noneType) return noneType;
// Check for unsigned types and convert to signed versions
var t1 = GetSignedTypeForUnsigned(argType1);
var t2 = GetSignedTypeForUnsigned(argType2);
// The type with higher index in _typesSequence is the commont type
var index1 = _typesSequence.IndexOf(t1);
var index2 = _typesSequence.IndexOf(t2);
if (index1 >= 0 && index2 >= 0)
return _typesSequence[Math.Max(index1, index2)];
//If we have some custom type,
return null;
}//method
// If a type is one of "unsigned" int types, returns next bigger signed type
protected virtual Type GetSignedTypeForUnsigned(Type type)
{
if (!_unsignedTypes.Contains(type)) return type;
if (type == typeof(byte) || type == typeof(UInt16)) return typeof(int);
if (type == typeof(UInt32)) return typeof(Int64);
if (type == typeof(UInt64)) return typeof(Int64); //let's remain in Int64
return typeof(BigInteger);
}
/// <summary>
/// Returns the "up-type" to use in operation instead of the type that caused overflow.
/// </summary>
/// <param name="type">The base type for operation that caused overflow.</param>
/// <returns>The type to use for operation.</returns>
/// <remarks>
/// Can be overwritten in language implementation to implement different type-conversion policy.
/// </remarks>
protected virtual Type GetUpType(Type type)
{
// In fact we do not need to care about unsigned types - they are eliminated from common types for operations,
// so "type" parameter can never be unsigned type. But just in case...
if (_unsignedTypes.Contains(type))
return GetSignedTypeForUnsigned(type); //it will return "upped" type in fact
if (type == typeof(byte) || type == typeof(sbyte) || type == typeof(UInt16) || type == typeof(Int16))
return typeof(int);
if (type == typeof(Int32))
return typeof(Int64);
if (type == typeof(Int64))
return typeof(BigInteger);
return null;
}
//Note bool type at the end - if any of operands is of bool type, convert the other to bool as well
private static TypeList _typesSequence = new TypeList(
typeof(sbyte), typeof(Int16), typeof(Int32), typeof(Int64), typeof(BigInteger), // typeof(Rational)
typeof(Single), typeof(Double), typeof(Complex),
typeof(bool), typeof(char), typeof(string)
);
private static TypeList _unsignedTypes = new TypeList(
typeof(byte), typeof(UInt16), typeof(UInt32), typeof(UInt64)
);
#endregion
}//class
}//namespace

View File

@ -0,0 +1,39 @@
#region License
/* **********************************************************************************
* Copyright (c) Roman Ivantsov
* This source code is subject to terms and conditions of the MIT License
* for Irony. A copy of the license can be found in the License.txt file
* at the root of this distribution.
* By using this source code in any fashion, you are agreeing to be bound by the terms of the
* MIT License.
* You must not remove this notice from this software.
* **********************************************************************************/
#endregion
namespace Sanchime.Irony.Interpreter
{
// A class for special reserved None value used in many scripting languages.
public class NoneClass
{
private string _toString;
private NoneClass()
{
_toString = Resources.LabelNone;
}
public NoneClass(string toString)
{
_toString = toString;
}
public override string ToString()
{
return _toString;
}
public static NoneClass Value = new NoneClass();
}
}

View File

@ -0,0 +1,244 @@
#region License
/* **********************************************************************************
* Copyright (c) Roman Ivantsov
* This source code is subject to terms and conditions of the MIT License
* for Irony. A copy of the license can be found in the License.txt file
* at the root of this distribution.
* By using this source code in any fashion, you are agreeing to be bound by the terms of the
* MIT License.
* You must not remove this notice from this software.
* **********************************************************************************/
#endregion
using System.Linq.Expressions;
namespace Sanchime.Irony.Interpreter
{
public delegate object UnaryOperatorMethod(object arg);
public delegate object BinaryOperatorMethod(object arg1, object arg2);
#region OperatorDispatchKey class
/// <summary>
/// The struct is used as a key for the dictionary of operator implementations.
/// Contains types of arguments for a method or operator implementation.
/// </summary>
public struct OperatorDispatchKey
{
public static readonly OperatorDispatchKeyComparer Comparer = new OperatorDispatchKeyComparer();
public readonly ExpressionType Op;
public readonly Type Arg1Type;
public readonly Type Arg2Type;
public readonly int HashCode;
//For binary operators
public OperatorDispatchKey(ExpressionType op, Type arg1Type, Type arg2Type)
{
Op = op;
Arg1Type = arg1Type;
Arg2Type = arg2Type;
int h0 = (int)Op;
int h1 = Arg1Type.GetHashCode();
int h2 = Arg2Type.GetHashCode();
HashCode = unchecked(h0 << 8 ^ h1 << 4 ^ h2);
}
//For unary operators
public OperatorDispatchKey(ExpressionType op, Type arg1Type)
{
Op = op;
Arg1Type = arg1Type;
Arg2Type = null;
int h0 = (int)Op;
int h1 = Arg1Type.GetHashCode();
int h2 = 0;
HashCode = unchecked(h0 << 8 ^ h1 << 4 ^ h2);
}
public override int GetHashCode()
{
return HashCode;
}
public override string ToString()
{
return Op + "(" + Arg1Type + ", " + Arg2Type + ")";
}
}//class
#endregion
#region OperatorDispatchKeyComparer class
// Note: I believe (guess) that a custom Comparer provided to a Dictionary is a bit more efficient
// than implementing IComparable on the key itself
public class OperatorDispatchKeyComparer : IEqualityComparer<OperatorDispatchKey>
{
public bool Equals(OperatorDispatchKey x, OperatorDispatchKey y)
{
return x.HashCode == y.HashCode && x.Op == y.Op && x.Arg1Type == y.Arg1Type && x.Arg2Type == y.Arg2Type;
}
public int GetHashCode(OperatorDispatchKey obj)
{
return obj.HashCode;
}
}//class
#endregion
public class TypeConverterTable : Dictionary<OperatorDispatchKey, UnaryOperatorMethod>
{
public TypeConverterTable(int capacity) : base(capacity, OperatorDispatchKey.Comparer)
{
}
}//class
public class OperatorImplementationTable : Dictionary<OperatorDispatchKey, OperatorImplementation>
{
public OperatorImplementationTable(int capacity) : base(capacity, OperatorDispatchKey.Comparer)
{
}
}
///<summary>
///The OperatorImplementation class represents an implementation of an operator for specific argument types.
///</summary>
///<remarks>
/// The OperatorImplementation is used for holding implementation for binary operators, unary operators,
/// and type converters (special case of unary operators)
/// it holds 4 method references for binary operators:
/// converters for both arguments, implementation method and converter for the result.
/// For unary operators (and type converters) the implementation is in Arg1Converter
/// operator (arg1 is used); the converter method is stored in Arg1Converter; the target type is in CommonType
///</remarks>
public sealed class OperatorImplementation
{
public readonly OperatorDispatchKey Key;
// The type to which arguments are converted and no-conversion method for this type.
public readonly Type CommonType;
public readonly BinaryOperatorMethod BaseBinaryMethod;
//converters
internal UnaryOperatorMethod Arg1Converter;
internal UnaryOperatorMethod Arg2Converter;
internal UnaryOperatorMethod ResultConverter;
//A reference to the actual binary evaluator method - one of EvaluateConvXXX
public BinaryOperatorMethod EvaluateBinary;
// An overflow handler - the implementation to handle arithmetic overflow
public OperatorImplementation OverflowHandler;
// No-box counterpart for implementations with auto-boxed output. If this field <> null, then this is
// implementation with auto-boxed output
public OperatorImplementation NoBoxImplementation;
//Constructor for binary operators
public OperatorImplementation(OperatorDispatchKey key, Type resultType, BinaryOperatorMethod baseBinaryMethod,
UnaryOperatorMethod arg1Converter, UnaryOperatorMethod arg2Converter, UnaryOperatorMethod resultConverter)
{
Key = key;
CommonType = resultType;
Arg1Converter = arg1Converter;
Arg2Converter = arg2Converter;
ResultConverter = resultConverter;
BaseBinaryMethod = baseBinaryMethod;
SetupEvaluationMethod();
}
//Constructor for unary operators and type converters
public OperatorImplementation(OperatorDispatchKey key, Type type, UnaryOperatorMethod method)
{
Key = key;
CommonType = type;
Arg1Converter = method;
Arg2Converter = null;
ResultConverter = null;
BaseBinaryMethod = null;
}
public override string ToString()
{
return "[OpImpl for " + Key.ToString() + "]";
}
public void SetupEvaluationMethod()
{
if (BaseBinaryMethod == null)
//special case - it is unary method, the method itself in Arg1Converter; LanguageRuntime.ExecuteUnaryOperator will handle this properly
return;
// Binary operator
if (ResultConverter == null)
{
//without ResultConverter
if (Arg1Converter == null && Arg2Converter == null)
EvaluateBinary = EvaluateConvNone;
else if (Arg1Converter != null && Arg2Converter == null)
EvaluateBinary = EvaluateConvLeft;
else if (Arg1Converter == null && Arg2Converter != null)
EvaluateBinary = EvaluateConvRight;
else // if (Arg1Converter != null && arg2Converter != null)
EvaluateBinary = EvaluateConvBoth;
}
else
{
//with result converter
if (Arg1Converter == null && Arg2Converter == null)
EvaluateBinary = EvaluateConvNoneConvResult;
else if (Arg1Converter != null && Arg2Converter == null)
EvaluateBinary = EvaluateConvLeftConvResult;
else if (Arg1Converter == null && Arg2Converter != null)
EvaluateBinary = EvaluateConvRightConvResult;
else // if (Arg1Converter != null && Arg2Converter != null)
EvaluateBinary = EvaluateConvBothConvResult;
}
}
private object EvaluateConvNone(object arg1, object arg2)
{
return BaseBinaryMethod(arg1, arg2);
}
private object EvaluateConvLeft(object arg1, object arg2)
{
return BaseBinaryMethod(Arg1Converter(arg1), arg2);
}
private object EvaluateConvRight(object arg1, object arg2)
{
return BaseBinaryMethod(arg1, Arg2Converter(arg2));
}
private object EvaluateConvBoth(object arg1, object arg2)
{
return BaseBinaryMethod(Arg1Converter(arg1), Arg2Converter(arg2));
}
private object EvaluateConvNoneConvResult(object arg1, object arg2)
{
return ResultConverter(BaseBinaryMethod(arg1, arg2));
}
private object EvaluateConvLeftConvResult(object arg1, object arg2)
{
return ResultConverter(BaseBinaryMethod(Arg1Converter(arg1), arg2));
}
private object EvaluateConvRightConvResult(object arg1, object arg2)
{
return ResultConverter(BaseBinaryMethod(arg1, Arg2Converter(arg2)));
}
private object EvaluateConvBothConvResult(object arg1, object arg2)
{
return ResultConverter(BaseBinaryMethod(Arg1Converter(arg1), Arg2Converter(arg2)));
}
}//class
}//namespace

View File

@ -0,0 +1,14 @@
namespace Sanchime.Irony.Interpreter
{
public delegate object SpecialForm(ScriptThread thread, AstNode[] childNodes);
public static class SpecialFormsLibrary
{
public static object Iif(ScriptThread thread, AstNode[] childNodes)
{
var testValue = childNodes[0].Evaluate(thread);
object result = thread.Runtime.IsTrue(testValue) ? childNodes[1].Evaluate(thread) : childNodes[2].Evaluate(thread);
return result;
}
}//class
}

View File

@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<PackageId>Sanchime.Irony.Interpreter.NetCore</PackageId>
<LangVersion>10.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Description>Irony.NetCore is a .NET Core compatible version of the Irony framework initially developed and maintained by Roman Ivantsov. Irony is a development kit for implementing languages on .NET platform. In Irony the target language grammar is coded directly in c# using operator overloading to express grammar constructs. Irony's scanner and parser modules use the grammar encoded as c# class to control the parsing process.</Description>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageTags>irony;parser</PackageTags>
<RepositoryType>Github</RepositoryType>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Irony\Sanchime.Irony.csproj" />
</ItemGroup>
<ItemGroup>
<None Include="..\..\LICENSE">
<Pack>True</Pack>
<PackagePath></PackagePath>
</None>
</ItemGroup>
</Project>

View File

@ -0,0 +1,45 @@
#region License
/* **********************************************************************************
* Copyright (c) Roman Ivantsov
* This source code is subject to terms and conditions of the MIT License
* for Irony. A copy of the license can be found in the License.txt file
* at the root of this distribution.
* By using this source code in any fashion, you are agreeing to be bound by the terms of the
* MIT License.
* You must not remove this notice from this software.
* **********************************************************************************/
#endregion
namespace Sanchime.Irony.Interpreter.Scopes
{
/// <summary> Represents a set of all of static scopes/modules in the application. </summary>
public class AppDataMap
{
public AstNode ProgramRoot; //artificial root associated with MainModule
public ScopeInfoList StaticScopeInfos = new ScopeInfoList();
public ModuleInfoList Modules = new ModuleInfoList();
public ModuleInfo MainModule;
public readonly bool LanguageCaseSensitive;
public AppDataMap(bool languageCaseSensitive, AstNode programRoot = null)
{
LanguageCaseSensitive = languageCaseSensitive;
ProgramRoot = programRoot ?? new AstNode();
var mainScopeInfo = new ScopeInfo(ProgramRoot, LanguageCaseSensitive);
StaticScopeInfos.Add(mainScopeInfo);
mainScopeInfo.StaticIndex = 0;
MainModule = new ModuleInfo("main", "main", mainScopeInfo);
Modules.Add(MainModule);
}
public ModuleInfo GetModule(AstNode moduleNode)
{
foreach (var m in Modules)
if (m.ScopeInfo == moduleNode.DependentScopeInfo)
return m;
return null;
}
}//class
}

View File

@ -0,0 +1,40 @@
#region License
/* **********************************************************************************
* Copyright (c) Roman Ivantsov
* This source code is subject to terms and conditions of the MIT License
* for Irony. A copy of the license can be found in the License.txt file
* at the root of this distribution.
* By using this source code in any fashion, you are agreeing to be bound by the terms of the
* MIT License.
* You must not remove this notice from this software.
* **********************************************************************************/
#endregion
namespace Sanchime.Irony.Interpreter.Scopes
{
public class ModuleInfoList : List<ModuleInfo>
{ }
public class ModuleInfo
{
public readonly string Name;
public readonly string FileName;
public readonly ScopeInfo ScopeInfo; //scope for module variables
public readonly BindingSourceList Imports = new BindingSourceList();
public ModuleInfo(string name, string fileName, ScopeInfo scopeInfo)
{
Name = name;
FileName = fileName;
ScopeInfo = scopeInfo;
}
//Used for imported modules
public Binding BindToExport(BindingRequest request)
{
return null;
}
}
}

View File

@ -0,0 +1,74 @@
#region License
/* **********************************************************************************
* Copyright (c) Roman Ivantsov
* This source code is subject to terms and conditions of the MIT License
* for Irony. A copy of the license can be found in the License.txt file
* at the root of this distribution.
* By using this source code in any fashion, you are agreeing to be bound by the terms of the
* MIT License.
* You must not remove this notice from this software.
* **********************************************************************************/
#endregion
namespace Sanchime.Irony.Interpreter.Scopes
{
public class Scope : ScopeBase
{
public object[] Parameters;
public Scope Caller;
public Scope Creator; //either caller or closure parent
private Scope _parent; //computed on demand
public Scope(ScopeInfo scopeInfo, Scope caller, Scope creator, object[] parameters) : base(scopeInfo)
{
Caller = caller;
Creator = creator;
Parameters = parameters;
}
public object[] GetParameters()
{
return Parameters;
}
public object GetParameter(int index)
{
return Parameters[index];
}
public void SetParameter(int index, object value)
{
Parameters[index] = value;
}
// Lexical parent, computed on demand
public Scope Parent
{
get
{
if (_parent == null)
_parent = GetParent();
return _parent;
}
set { _parent = value; }
}
protected Scope GetParent()
{
// Walk along creators chain and find a scope with ScopeInfo matching this.ScopeInfo.Parent
var parentScopeInfo = Info.Parent;
if (parentScopeInfo == null)
return null;
var current = Creator;
while (current != null)
{
if (current.Info == parentScopeInfo)
return current;
current = current.Creator;
}
return null;
}// method
}//class
}//namespace

View File

@ -0,0 +1,121 @@
#region License
/* **********************************************************************************
* Copyright (c) Roman Ivantsov
* This source code is subject to terms and conditions of the MIT License
* for Irony. A copy of the license can be found in the License.txt file
* at the root of this distribution.
* By using this source code in any fashion, you are agreeing to be bound by the terms of the
* MIT License.
* You must not remove this notice from this software.
* **********************************************************************************/
#endregion
namespace Sanchime.Irony.Interpreter.Scopes
{
public class ScopeBase
{
public ScopeInfo Info;
public volatile object[] Values;
public ScopeBase(ScopeInfo scopeInfo) : this(scopeInfo, null)
{
}
public ScopeBase(ScopeInfo scopeInfo, object[] values)
{
Info = scopeInfo;
Values = values;
if (Values == null)
Values = new object[scopeInfo.ValuesCount];
}
public SlotInfo AddSlot(string name)
{
var slot = Info.AddSlot(name, SlotType.Value);
if (slot.Index >= Values.Length)
Resize(Values.Length + 4);
return slot;
}
public object[] GetValues()
{
return Values;
}
public object GetValue(int index)
{
try
{
var tmp = Values;
// The following line may throw null-reference exception (tmp==null), if resizing is happening at the same time
// It may also throw IndexOutOfRange exception if new variable was added by another thread in another frame(scope)
// but this scope and Values array were created before that, so Values is shorter than #slots in SlotInfo.
// But in this case, it does not matter, result value is null (unassigned)
return tmp[index];
}
catch (NullReferenceException)
{
Thread.Sleep(0); // Silverlight does not have Thread.Yield;
// Thread.Yield(); // maybe SpinWait.SpinOnce?
return GetValue(index); //repeat attempt
}
catch (IndexOutOfRangeException)
{
return null; //we do not resize here, value is unassigned anyway.
}
}//method
public void SetValue(int index, object value)
{
try
{
var tmp = Values;
// The following line may throw null-reference exception (tmp==null), if resizing is happening at the same time
// It may also throw IndexOutOfRange exception if new variable was added by another thread in another frame(scope)
// but this scope and Values array were created before that, so Values is shorter than #slots in SlotInfo
tmp[index] = value;
//Now check that tmp is the same as Values - if not, then resizing happened in the middle,
// so repeat assignment to make sure the value is in resized array.
if (tmp != Values)
SetValue(index, value); // do it again
}
catch (NullReferenceException)
{
Thread.Sleep(0); // it's OK to Sleep intead of SpinWait - it is really rare event, so we don't care losing a few more cycles here.
SetValue(index, value); //repeat it again
}
catch (IndexOutOfRangeException)
{
Resize(Info.GetSlotCount());
SetValue(index, value); //repeat it again
}
}//method
// Disabling warning: 'Values: a reference to a volatile field will not be treated as volatile'
// According to MSDN for CS0420 warning (see http://msdn.microsoft.com/en-us/library/4bw5ewxy.aspx),
// this does NOT apply to Interlocked API - which we use here.
protected void Resize(int newSize)
{
lock (Info.LockObject)
{
if (Values.Length >= newSize) return;
object[] tmp = Interlocked.Exchange(ref Values, null);
Array.Resize(ref tmp, newSize);
Interlocked.Exchange(ref Values, tmp);
}
}
public IDictionary<string, object> AsDictionary()
{
return new ScopeValuesDictionary(this);
}
public override string ToString()
{
return Info.ToString();
}
}//class
}

View File

@ -0,0 +1,127 @@
#region License
/* **********************************************************************************
* Copyright (c) Roman Ivantsov
* This source code is subject to terms and conditions of the MIT License
* for Irony. A copy of the license can be found in the License.txt file
* at the root of this distribution.
* By using this source code in any fashion, you are agreeing to be bound by the terms of the
* MIT License.
* You must not remove this notice from this software.
* **********************************************************************************/
#endregion
namespace Sanchime.Irony.Interpreter.Scopes
{
public class ScopeInfoList : List<ScopeInfo>
{ }
/// <summary>Describes all variables (locals and parameters) defined in a scope of a function or module. </summary>
/// <remarks>ScopeInfo is metadata, it does not contain variable values. The Scope object (described by ScopeInfo) is a container for values.</remarks>
// Note that all access to SlotTable is done through "lock" operator, so it's thread safe
public class ScopeInfo
{
public int ValuesCount, ParametersCount;
public AstNode OwnerNode; //might be null
// Static/singleton scopes only; for ex, modules are singletons. Index in App.StaticScopes array
public int StaticIndex = -1;
public int Level;
public readonly string AsString;
public Scope ScopeInstance; //Experiment: reusable scope instance; see ScriptThread.cs class
private SlotInfoDictionary _slots;
protected internal object LockObject = new object();
public ScopeInfo(AstNode ownerNode, bool caseSensitive)
{
OwnerNode = ownerNode ?? throw new Exception("ScopeInfo owner node may not be null.");
_slots = new SlotInfoDictionary(caseSensitive);
Level = Parent == null ? 0 : Parent.Level + 1;
var sLevel = "level=" + Level;
AsString = OwnerNode == null ? sLevel : OwnerNode.AsString + ", " + sLevel;
}
//Lexical parent
public ScopeInfo Parent
{
get
{
if (_parent == null)
_parent = GetParent();
return _parent;
}
}
private ScopeInfo _parent;
public ScopeInfo GetParent()
{
if (OwnerNode == null) return null;
var currentParent = OwnerNode.Parent;
while (currentParent != null)
{
var result = currentParent.DependentScopeInfo;
if (result != null) return result;
currentParent = currentParent.Parent;
}
return null; //should never happen
}
#region Slot operations
public SlotInfo AddSlot(string name, SlotType type)
{
lock (LockObject)
{
var index = type == SlotType.Value ? ValuesCount++ : ParametersCount++;
var slot = new SlotInfo(this, type, name, index);
_slots.Add(name, slot);
return slot;
}
}
//Returns null if slot not found.
public SlotInfo GetSlot(string name)
{
lock (LockObject)
{
_slots.TryGetValue(name, out var slot);
return slot;
}
}
public IList<SlotInfo> GetSlots()
{
lock (LockObject)
{
return new List<SlotInfo>(_slots.Values);
}
}
public IList<string> GetNames()
{
lock (LockObject)
{
return new List<string>(_slots.Keys);
}
}
public int GetSlotCount()
{
lock (LockObject)
{
return _slots.Count;
}
}
#endregion
public override string ToString()
{
return AsString;
}
}//class
} //namespace

View File

@ -0,0 +1,132 @@
#region License
/* **********************************************************************************
* Copyright (c) Roman Ivantsov
* This source code is subject to terms and conditions of the MIT License
* for Irony. A copy of the license can be found in the License.txt file
* at the root of this distribution.
* By using this source code in any fashion, you are agreeing to be bound by the terms of the
* MIT License.
* You must not remove this notice from this software.
* **********************************************************************************/
#endregion
namespace Sanchime.Irony.Interpreter.Scopes
{
/// <summary>
/// A wrapper around Scope exposing it as a string-object dictionary. Used to expose Globals dictionary from Main scope
/// </summary>
public class ScopeValuesDictionary : IDictionary<string, object>
{
private ScopeBase _scope;
internal ScopeValuesDictionary(ScopeBase scope)
{
_scope = scope;
}
public void Add(string key, object value)
{
var slot = _scope.Info.GetSlot(key);
if (slot == null)
slot = _scope.AddSlot(key);
_scope.SetValue(slot.Index, value);
}
public bool ContainsKey(string key)
{
return _scope.Info.GetSlot(key) != null;
}
public ICollection<string> Keys
{
get { return _scope.Info.GetNames(); }
}
//We do not remove the slotInfo (you can't do that, slot set can only grow); instead we set the value to null
// to indicate "unassigned"
public bool Remove(string key)
{
this[key] = null;
return true;
}
public bool TryGetValue(string key, out object value)
{
value = null;
SlotInfo slot = _scope.Info.GetSlot(key);
if (slot == null)
return false;
value = _scope.GetValue(slot.Index);
return true;
}
public ICollection<object> Values
{
get { return _scope.GetValues(); }
}
public object this[string key]
{
get
{
TryGetValue(key, out object value);
return value;
}
set
{
Add(key, value);
}
}
public void Add(KeyValuePair<string, object> item)
{
Add(item.Key, item.Value);
}
public void Clear()
{
var values = _scope.GetValues();
for (int i = 0; i < values.Length; i++)
values[i] = null;
}
public bool Contains(KeyValuePair<string, object> item)
{
return _scope.Info.GetSlot(item.Key) != null;
}
public void CopyTo(KeyValuePair<string, object>[] array, int arrayIndex)
{
throw new NotImplementedException();
}
public int Count
{
get { return _scope.Info.GetSlotCount(); }
}
public bool IsReadOnly
{
get { return true; }
}
public bool Remove(KeyValuePair<string, object> item)
{
return Remove(item.Key);
}
public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
{
var slots = _scope.Info.GetSlots(); //make local copy
foreach (var slot in slots)
yield return new KeyValuePair<string, object>(slot.Name, _scope.GetValue(slot.Index));
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}

View File

@ -0,0 +1,48 @@
#region License
/* **********************************************************************************
* Copyright (c) Roman Ivantsov
* This source code is subject to terms and conditions of the MIT License
* for Irony. A copy of the license can be found in the License.txt file
* at the root of this distribution.
* By using this source code in any fashion, you are agreeing to be bound by the terms of the
* MIT License.
* You must not remove this notice from this software.
* **********************************************************************************/
#endregion
namespace Sanchime.Irony.Interpreter.Scopes
{
public enum SlotType
{
Value, //local or property value
Parameter, //function parameter
Function,
Closure,
}
/// <summary> Describes a variable. </summary>
public class SlotInfo
{
public readonly ScopeInfo ScopeInfo;
public readonly SlotType Type;
public readonly string Name;
public readonly int Index;
public bool IsPublic = true; //for module-level slots, indicator that symbol is "exported" and visible by code that imports the module
internal SlotInfo(ScopeInfo scopeInfo, SlotType type, string name, int index)
{
ScopeInfo = scopeInfo;
Type = type;
Name = name;
Index = index;
}
}
public class SlotInfoDictionary : Dictionary<string, SlotInfo>
{
public SlotInfoDictionary(bool caseSensitive)
: base(32, caseSensitive ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase) { }
}
}

View File

@ -0,0 +1,84 @@
Variables/values storage - some traditonal approaches
1. In scripting languages, the data elements (fields, properties, local variables) are created on the fly, on the first write. There's no pre-allocation at compile time. The set of variables is unknown in advance. Traditional, straightforward solution is to use a dictionary (string=>object) to store local variables of a function, or module-level variables.
2. In free-threaded environment the variables may be accessed from different threads. This means that the access to containing dictionaries should be performed using thread-locking mechanism, guaanteeing that only a single thread is accessing a dictionary.
So the script statement like:
x = 5 (1)
is translated into something like this:
lock(scope) { (2)
scope.Values["x"] = 5;
}
Two important and unfortunate observations.
1. Dictionary access is slow. At least if we mean .NET Dictionary<TKey, TValue> generic class. Simple tests coupled with source code inspection show that the cost is in the range of hundreds of processor instructions.
2. Thread locking is slow. The cost is also in the range of hundreds of instructions.
The result is that implementation (2) is quite slow. Really slow, especially considering the fact that actual thread collisions on the same dictionary objects are quite rare, while we have to incur the extra cost of lock every time we read or write a value.
What can be done better?
Our interpreter stores data in linear arrays, and does NOT use thread locking when reading/writing the data. There is an explicit locking when we "create" a variable for the first time - we lock meta-data dictionary containing "descriptions" of data slots; but then all subsequent accesses to the value are done performed using the variable index.
But before we explain how it works, we need to state one explicit assumption we rely on:
Assumption:
Assignment of an object reference to a variable (ex: x = someObj) is an atomic operation and is "thread-safe".
So the assignment can be safely done without thread locking. if one thread makes an assignment, and the other thread reads the reference, this other thread would see either old or new value, but never any "corrupted middle". This assumption mostly concerns safety of Reading from another thread. A special case is writing or replacing the value (when we resize the Values array, we replace it with new resized copy) - see more on this below.
Back to Irony's data storage implementation: arrays with no-lock read/write access. The data is stored in linear array of objects: Values[] field (see ScopeBase class). The field is marked with "volatile" keyword. All access is done by index.
Let's look at an example and explain what happens. Suppose we have an AST node that represents a variable "x" with READ access. When interpreter evaluates this node for the first time, it looks up a variable metadata (SlotInfo) in current scope metadata (ScopeInfo). The result is linear index of the data value in Values array. It then reads the value using the index:
vx = scope.Values[xSlotIndex];
All later evaluations will do the same - lookup by index but without looking up the SlotInfo: the xSlotIndex is cached in the node (more accurately, in SlotBinding object). Writing the value works the same way - the array element is assigned by index.
The problems comes when we need to resize the Values array because we are adding some local variable - for example, our script runs into new assignment statement in the local scope:
y = 5
We need to add "y" to the list of slots (metadata), but then we also need to "extend" the Values[] array and add an extra element for "y". The question now is: how to resize Values array in such a way that if some other thread(s) is reading or writing other values in the same scope, it does it correctly even if it happens exactly at the moment when we resize the array?
Here's how we do it. First let's look at the ScopeBase.Resize method:
#pragma warning disable 0420
protected void Resize(int newSize) {
lock (this) {
if (Values.Length >= newSize) return;
object[] tmp = Interlocked.Exchange(ref Values, null);
Array.Resize(ref tmp, newSize);
Interlocked.Exchange(ref Values, tmp);
}
}
We use Interlocked.Exchange to replace Values field with null as an atomic operation. We do it to force any concurrent reads/writes to fail, if they happen at exactly this time. Note that we disable a compiler warning stating that volatile field Values will not be treated as volatile in a call to Interlocked.Exchange. According to MSDN, this is usually the case with "by-ref" arguments, but Interlocked API is an exception, so we're OK here.
Now, in GetValue and SetValue methods, we expect this failure, and have a try/catch block to handle the null reference exception and retry the operation. Here's SetValues method:
public void SetValue(int index, object value) {
try {
var tmp = Values;
tmp[index] = value;
//Now check that tmp is the same as Values - if not, then resizing happened in the middle,
// so repeat assignment to make sure the value is in resized array.
if (tmp != Values)
SetValue(index, value); // do it again
} catch (NullReferenceException) {
Thread.Sleep(0);
SetValue(index, value); //repeat it again
} .....
}//method
The "catch" block for NullReferenceException is for handling the situation when Values was null while other thread was resizing it. Remember that try/catch block is free, it does not add any executable commands if we run without exception.
There is additional twist when writing a value. It might happen that after we copied the Values reference into tmp variable, some other thread resized the Values array - replacing it with a new extended array. As a result, we will be writing the value into a "dead" old array. To check against this, after we do the value change we check that "tmp" and "Values" reference the same object. If not, we got concurrent resize, so we repeat SetValue to make sure we set it in the new Values instance.
To sum it up: all variables are stored in object arrays, values are accessed by index, and accessed without explicit thread locks. The net result of this technique (and some other improvements) is approximate 5-fold performance gain - compared to old interpreter.
NOW 5 TIMES FASTER!
References:
Very illuminating article about low-lock memory access:
http://msdn.microsoft.com/en-us/magazine/cc163715.aspx
Another article about spin locks and interlocked operations:
http://msdn.microsoft.com/en-us/magazine/cc163715.aspx

View File

@ -0,0 +1,196 @@
#region License
/* **********************************************************************************
* Copyright (c) Roman Ivantsov
* This source code is subject to terms and conditions of the MIT License
* for Irony. A copy of the license can be found in the License.txt file
* at the root of this distribution.
* By using this source code in any fashion, you are agreeing to be bound by the terms of the
* MIT License.
* You must not remove this notice from this software.
* **********************************************************************************/
#endregion
namespace Sanchime.Irony.Interpreter.SriptApplication
{
//An abstraction of a Console.
public interface IConsoleAdaptor
{
bool Canceled { get; set; }
void Write(string text);
void WriteLine(string text);
void SetTextStyle(ConsoleTextStyle style);
int Read(); //reads a key
string ReadLine(); //reads a line; returns null if Ctrl-C is pressed
void SetTitle(string title);
}
//WARNING: Ctrl-C for aborting running script does NOT work when you run console app from Visual Studio 2010.
// Run executable directly from bin folder.
//public class CommandLine {
// #region Fields and properties
// public readonly LanguageRuntime Runtime;
// public readonly IConsoleAdaptor _console;
// //Initialized from grammar
// public string Title;
// public string Greeting;
// public string Prompt; //default prompt
// public string PromptMoreInput; //prompt to show when more input is expected
// public readonly ScriptApp App;
// Thread _workerThread;
// public bool IsEvaluating { get; private set; }
// #endregion
// public CommandLine(LanguageRuntime runtime, IConsoleAdaptor console = null) {
// Runtime = runtime;
// _console = console ?? new ConsoleAdapter();
// var grammar = runtime.Language.Grammar;
// Title = grammar.ConsoleTitle;
// Greeting = grammar.ConsoleGreeting;
// Prompt = grammar.ConsolePrompt;
// PromptMoreInput = grammar.ConsolePromptMoreInput;
// App = new ScriptApp(Runtime);
// App.ParserMode = ParseMode.CommandLine;
// // App.PrintParseErrors = false;
// App.RethrowExceptions = false;
// }
// public void Run() {
// try {
// RunImpl();
// } catch (Exception ex) {
// _console.SetTextStyle(ConsoleTextStyle.Error);
// _console.WriteLine(Resources.ErrConsoleFatalError);
// _console.WriteLine(ex.ToString());
// _console.SetTextStyle(ConsoleTextStyle.Normal);
// _console.WriteLine(Resources.MsgPressAnyKeyToExit);
// _console.Read();
// }
// }
// private void RunImpl() {
// _console.SetTitle(Title);
// _console.WriteLine(Greeting);
// string input;
// while (true) {
// _console.Canceled = false;
// _console.SetTextStyle(ConsoleTextStyle.Normal);
// string prompt = (App.Status == AppStatus.WaitingMoreInput ? PromptMoreInput : Prompt);
// //Write prompt, read input, check for Ctrl-C
// _console.Write(prompt);
// input = _console.ReadLine();
// if (_console.Canceled)
// if (Confirm(Resources.MsgExitConsoleYN))
// return;
// else
// continue; //from the start of the loop
// //Execute
// App.ClearOutputBuffer();
// EvaluateAsync(input);
// //Evaluate(input);
// WaitForScriptComplete();
// switch (App.Status) {
// case AppStatus.Ready: //success
// _console.WriteLine(App.GetOutput());
// break;
// case AppStatus.SyntaxError:
// _console.WriteLine(App.GetOutput()); //write all output we have
// _console.SetTextStyle(ConsoleTextStyle.Error);
// foreach (var err in App.GetParserMessages()) {
// _console.WriteLine(string.Empty.PadRight(prompt.Length + err.Location.Column) + "^"); //show err location
// _console.WriteLine(err.Message); //print message
// }
// break;
// case AppStatus.Crash:
// case AppStatus.RuntimeError:
// ReportException();
// break;
// default: break;
// }//switch
// }
// }//Run method
// private void WaitForScriptComplete() {
// _console.Canceled = false;
// while(true) {
// Thread.Sleep(50);
// if(!IsEvaluating) return;
// if(_console.Canceled) {
// _console.Canceled = false;
// if (Confirm(Resources.MsgAbortScriptYN))
// WorkerThreadAbort();
// }//if Canceled
// }
// }
// private void Evaluate(string script) {
// try {
// IsEvaluating = true;
// App.Evaluate(script);
// } finally {
// IsEvaluating = false;
// }
// }
// private void EvaluateAsync(string script) {
// IsEvaluating = true;
// _workerThread = new Thread(WorkerThreadStart);
// _workerThread.Start(script);
// }
// private void WorkerThreadStart(object data) {
// try {
// var script = data as string;
// App.Evaluate(script);
// } finally {
// IsEvaluating = false;
// }
// }
// private void WorkerThreadAbort() {
// try {
// _workerThread.Abort();
// _workerThread.Join(50);
// } finally {
// IsEvaluating = false;
// }
// }
// private bool Confirm(string message) {
// _console.WriteLine(string.Empty);
// _console.Write(message);
// var input = _console.ReadLine();
// return Resources.ConsoleYesChars.Contains(input);
// }
// private void ReportException() {
// _console.SetTextStyle(ConsoleTextStyle.Error);
// var ex = App.LastException;
// var scriptEx = ex as ScriptException;
// if (scriptEx != null)
// _console.WriteLine(scriptEx.Message + " " + Resources.LabelLocation + " " + scriptEx.Location.ToUiString());
// else {
// if (App.Status == AppStatus.Crash)
// _console.WriteLine(ex.ToString()); //Unexpected interpreter crash: the full stack when debugging your language
// else
// _console.WriteLine(ex.Message);
// }
// //
// }
//}//class
}

View File

@ -0,0 +1,83 @@
#region License
/* **********************************************************************************
* Copyright (c) Roman Ivantsov
* This source code is subject to terms and conditions of the MIT License
* for Irony. A copy of the license can be found in the License.txt file
* at the root of this distribution.
* By using this source code in any fashion, you are agreeing to be bound by the terms of the
* MIT License.
* You must not remove this notice from this software.
* **********************************************************************************/
#endregion
namespace Sanchime.Irony.Interpreter.SriptApplication
{
//WARNING: Ctrl-C for aborting running script does NOT work when you run console app from Visual Studio 2010.
// Run executable directly from bin folder.
public enum ConsoleTextStyle
{
Normal,
Error,
}
// Default implementation of IConsoleAdaptor with System Console as input/output.
public class ConsoleAdapter : IConsoleAdaptor
{
public ConsoleAdapter()
{
Console.CancelKeyPress += Console_CancelKeyPress;
}
private void Console_CancelKeyPress(object sender, ConsoleCancelEventArgs e)
{
e.Cancel = true; //do not kill the app yet
Canceled = true;
}
public bool Canceled { get; set; }
public void Write(string text)
{
Console.Write(text);
}
public void WriteLine(string text)
{
Console.WriteLine(text);
}
public void SetTextStyle(ConsoleTextStyle style)
{
switch (style)
{
case ConsoleTextStyle.Normal:
Console.ForegroundColor = ConsoleColor.White;
break;
case ConsoleTextStyle.Error:
Console.ForegroundColor = ConsoleColor.Red;
break;
}
}
public int Read()
{
return Console.Read();
}
public string ReadLine()
{
var input = Console.ReadLine();
Canceled = input == null; // Windows console method ReadLine returns null if Ctrl-C was pressed.
return input;
}
public void SetTitle(string title)
{
Console.Title = title;
}
}
}

View File

@ -0,0 +1,260 @@
#region License
/* **********************************************************************************
* Copyright (c) Roman Ivantsov
* This source code is subject to terms and conditions of the MIT License
* for Irony. A copy of the license can be found in the License.txt file
* at the root of this distribution.
* By using this source code in any fashion, you are agreeing to be bound by the terms of the
* MIT License.
* You must not remove this notice from this software.
* **********************************************************************************/
#endregion
using Sanchime.Irony.Interpreter.Scopes;
using Sanchime.Irony.Interpreter.Utilities;
using System.Reflection;
using System.Security;
namespace Sanchime.Irony.Interpreter.SriptApplication
{
public enum AppStatus
{
Ready,
Evaluating,
WaitingMoreInput, //command line only
SyntaxError,
RuntimeError,
Crash, //interpreter crash
Aborted
}
/// <summary> Represents a running instance of a script application. </summary>
public sealed class ScriptApp
{
public readonly LanguageData Language;
public readonly LanguageRuntime Runtime;
public Parser Parser { get; private set; }
public AppDataMap DataMap;
public Scope[] StaticScopes;
public Scope MainScope;
public IDictionary<string, object> Globals { get; private set; }
private IList<Assembly> ImportedAssemblies = new List<Assembly>();
public StringBuilder OutputBuffer = new StringBuilder();
private object _lockObject = new object();
// Current mode/status variables
public AppStatus Status;
public long EvaluationTime;
public Exception LastException;
public bool RethrowExceptions = true;
public ParseTree LastScript { get; private set; } //the root node of the last executed script
#region Constructors
public ScriptApp(LanguageData language)
{
Language = language;
var grammar = language.Grammar as InterpretedLanguageGrammar;
Runtime = grammar.CreateRuntime(language);
DataMap = new AppDataMap(Language.Grammar.CaseSensitive);
Init();
}
public ScriptApp(LanguageRuntime runtime)
{
Runtime = runtime;
Language = Runtime.Language;
DataMap = new AppDataMap(Language.Grammar.CaseSensitive);
Init();
}
public ScriptApp(AppDataMap dataMap)
{
DataMap = dataMap;
Init();
}
[SecuritySafeCritical]
private void Init()
{
Parser = new Parser(Language);
//Create static scopes
MainScope = new Scope(DataMap.MainModule.ScopeInfo, null, null, null);
StaticScopes = new Scope[DataMap.StaticScopeInfos.Count];
StaticScopes[0] = MainScope;
Globals = MainScope.AsDictionary();
}
#endregion
public LogMessageList GetParserMessages()
{
return Parser.Context.CurrentParseTree.ParserMessages;
}
public ParseMode ParserMode
{
get { return Parser.Context.Mode; }
set { Parser.Context.Mode = value; }
}
#region Evaluation
public object Evaluate(string script)
{
try
{
var parsedScript = Parser.Parse(script);
if (parsedScript.HasErrors())
{
Status = AppStatus.SyntaxError;
if (RethrowExceptions)
throw new ScriptException("Syntax errors found.");
return null;
}
if (ParserMode == ParseMode.CommandLine && Parser.Context.Status == ParserStatus.AcceptedPartial)
{
Status = AppStatus.WaitingMoreInput;
return null;
}
LastScript = parsedScript;
var result = EvaluateParsedScript();
return result;
}
catch (ScriptException)
{
throw;
}
catch (Exception ex)
{
LastException = ex;
Status = AppStatus.Crash;
return null;
}
}
// Irony interpreter requires that once a script is executed in a ScriptApp, it is bound to AppDataMap object,
// and all later script executions should be performed only in the context of the same app (or at least by an App with the same DataMap).
// The reason is because the first execution sets up a data-binding fields, like slots, scopes, etc, which are bound to ScopeInfo objects,
// which in turn is part of DataMap.
public object Evaluate(ParseTree parsedScript)
{
Util.Check(parsedScript.Root.AstNode != null, "Root AST node is null, cannot evaluate script. Create AST tree first.");
var root = parsedScript.Root.AstNode as AstNode;
Util.Check(root != null,
"Root AST node {0} is not a subclass of Irony.Interpreter.AstNode. ScriptApp cannot evaluate this script.", root.GetType());
Util.Check(root.Parent == null || root.Parent == DataMap.ProgramRoot,
"Cannot evaluate parsed script. It had been already evaluated in a different application.");
LastScript = parsedScript;
return EvaluateParsedScript();
}
public object Evaluate()
{
Util.Check(LastScript != null, "No previously parsed/evaluated script.");
return EvaluateParsedScript();
}
//Actual implementation
private object EvaluateParsedScript()
{
LastScript.Tag = DataMap;
var root = LastScript.Root.AstNode as AstNode;
root.DependentScopeInfo = MainScope.Info;
Status = AppStatus.Evaluating;
ScriptThread thread = null;
try
{
thread = new ScriptThread(this);
var result = root.Evaluate(thread);
if (result != null)
thread.App.WriteLine(result.ToString());
Status = AppStatus.Ready;
return result;
}
catch (ScriptException se)
{
Status = AppStatus.RuntimeError;
se.Location = thread.CurrentNode.Location;
se.ScriptStackTrace = thread.GetStackTrace();
LastException = se;
if (RethrowExceptions)
throw;
return null;
}
catch (Exception ex)
{
Status = AppStatus.RuntimeError;
var se = new ScriptException(ex.Message, ex, thread.CurrentNode.Location, thread.GetStackTrace());
LastException = se;
if (RethrowExceptions)
throw se;
return null;
}//catch
}
#endregion
#region Output writing
#region ConsoleWrite event
public event EventHandler<ConsoleWriteEventArgs> ConsoleWrite;
private void OnConsoleWrite(string text)
{
if (ConsoleWrite != null)
{
ConsoleWriteEventArgs args = new ConsoleWriteEventArgs(text);
ConsoleWrite(this, args);
}
}
#endregion
public void Write(string text)
{
lock (_lockObject)
{
OnConsoleWrite(text);
OutputBuffer.Append(text);
}
}
public void WriteLine(string text)
{
lock (_lockObject)
{
OnConsoleWrite(text + Environment.NewLine);
OutputBuffer.AppendLine(text);
}
}
public void ClearOutputBuffer()
{
lock (_lockObject)
{
OutputBuffer.Clear();
}
}
public string GetOutput()
{
lock (_lockObject)
{
return OutputBuffer.ToString();
}
}
#endregion
}//class
}

View File

@ -0,0 +1,107 @@
#region License
/* **********************************************************************************
* Copyright (c) Roman Ivantsov
* This source code is subject to terms and conditions of the MIT License
* for Irony. A copy of the license can be found in the License.txt file
* at the root of this distribution.
* By using this source code in any fashion, you are agreeing to be bound by the terms of the
* MIT License.
* You must not remove this notice from this software.
* **********************************************************************************/
#endregion
using Sanchime.Irony.Interpreter.Scopes;
namespace Sanchime.Irony.Interpreter.SriptApplication
{
/// <summary> Represents a running thread in script application. </summary>
public sealed class ScriptThread : IBindingSource
{
public readonly ScriptApp App;
public readonly LanguageRuntime Runtime;
public Scope CurrentScope;
public AstNode CurrentNode;
// Tail call parameters
public ICallTarget Tail;
public object[] TailArgs;
public ScriptThread(ScriptApp app)
{
App = app;
Runtime = App.Runtime;
CurrentScope = app.MainScope;
}
public void PushScope(ScopeInfo scopeInfo, object[] parameters)
{
CurrentScope = new Scope(scopeInfo, CurrentScope, CurrentScope, parameters);
}
public void PushClosureScope(ScopeInfo scopeInfo, Scope closureParent, object[] parameters)
{
CurrentScope = new Scope(scopeInfo, CurrentScope, closureParent, parameters);
}
public void PopScope()
{
CurrentScope = CurrentScope.Caller;
}
public Binding Bind(string symbol, BindingRequestFlags options)
{
var request = new BindingRequest(this, CurrentNode, symbol, options);
var binding = Bind(request);
if (binding == null)
ThrowScriptError("Unknown symbol '{0}'.", symbol);
return binding;
}
#region Exception handling
public object HandleError(Exception exception)
{
if (exception is ScriptException)
throw exception;
var stack = GetStackTrace();
var rex = new ScriptException(exception.Message, exception, CurrentNode.ErrorAnchor, stack);
throw rex;
}
// Throws ScriptException exception.
public void ThrowScriptError(string message, params object[] args)
{
if (args != null && args.Length > 0)
message = string.Format(message, args);
var loc = GetCurrentLocation();
var stack = GetStackTrace();
throw new ScriptException(message, null, loc, stack);
}
//TODO: add construction of Script Call stack
public ScriptStackTrace GetStackTrace()
{
return new ScriptStackTrace();
}
private SourceLocation GetCurrentLocation()
{
return CurrentNode == null ? new SourceLocation() : CurrentNode.Location;
}
#endregion
#region IBindingSource Members
public Binding Bind(BindingRequest request)
{
return Runtime.Bind(request);
}
#endregion
}//class
}

View File

@ -0,0 +1,15 @@
namespace Sanchime.Irony.Interpreter.Utilities
{
public static class InterpreterEnumExtensions
{
public static bool IsSet(this BindingRequestFlags enumValue, BindingRequestFlags flag)
{
return (enumValue & flag) != 0;
}
public static bool IsSet(this AstNodeFlags enumValue, AstNodeFlags flag)
{
return (enumValue & flag) != 0;
}
}
}

View File

@ -0,0 +1,25 @@
namespace Sanchime.Irony.Interpreter.Utilities
{
public static class Util
{
public static string SafeFormat(this string template, params object[] args)
{
if (args == null || args.Length == 0) return template;
try
{
template = string.Format(template, args);
}
catch (Exception ex)
{
template = template + "(message formatting failed: " + ex.Message + " Args: " + string.Join(",", args) + ")";
}
return template;
}//method
public static void Check(bool condition, string messageTemplate, params object[] args)
{
if (condition) return;
throw new Exception(messageTemplate.SafeFormat(args));
}
}//class
}

View File

@ -0,0 +1,73 @@
#region License
/* **********************************************************************************
* Copyright (c) Roman Ivantsov
* This source code is subject to terms and conditions of the MIT License
* for Irony. A copy of the license can be found in the License.txt file
* at the root of this distribution.
* By using this source code in any fashion, you are agreeing to be bound by the terms of the
* MIT License.
* You must not remove this notice from this software.
* **********************************************************************************/
#endregion
namespace Sanchime.Irony.Interpreter._Evaluator
{
public class ExpressionEvaluator
{
public ExpressionEvaluatorGrammar Grammar { get; private set; }
public Parser Parser { get; private set; }
public LanguageData Language { get; private set; }
public LanguageRuntime Runtime { get; private set; }
public ScriptApp App { get; private set; }
public IDictionary<string, object> Globals
{
get { return App.Globals; }
}
//Default constructor, creates default evaluator
public ExpressionEvaluator() : this(new ExpressionEvaluatorGrammar())
{
}
//Default constructor, creates default evaluator
public ExpressionEvaluator(ExpressionEvaluatorGrammar grammar)
{
Grammar = grammar;
Language = new LanguageData(Grammar);
Parser = new Parser(Language);
Runtime = Grammar.CreateRuntime(Language);
App = new ScriptApp(Runtime);
}
public object Evaluate(string script)
{
var result = App.Evaluate(script);
return result;
}
public object Evaluate(ParseTree parsedScript)
{
var result = App.Evaluate(parsedScript);
return result;
}
//Evaluates again the previously parsed/evaluated script
public object Evaluate()
{
return App.Evaluate();
}
public void ClearOutput()
{
App.ClearOutputBuffer();
}
public string GetOutput()
{
return App.GetOutput();
}
}//class
}

View File

@ -0,0 +1,184 @@
#region License
/* **********************************************************************************
* Copyright (c) Roman Ivantsov
* This source code is subject to terms and conditions of the MIT License
* for Irony. A copy of the license can be found in the License.txt file
* at the root of this distribution.
* By using this source code in any fashion, you are agreeing to be bound by the terms of the
* MIT License.
* You must not remove this notice from this software.
* **********************************************************************************/
#endregion
using Sanchime.Irony.Interpreter.Ast.Expressions;
using Sanchime.Irony.Interpreter.Ast.Functions;
using Sanchime.Irony.Interpreter.Ast.PrimitiveNodes;
using Sanchime.Irony.Interpreter.Ast.Statements;
namespace Sanchime.Irony.Interpreter._Evaluator
{
// A ready-to-use evaluator implementation.
// This grammar describes programs that consist of simple expressions and assignments
// for ex:
// x = 3
// y = -x + 5
// the result of calculation is the result of last expression or assignment.
// Irony's default runtime provides expression evaluation.
// supports inc/dec operators (++,--), both prefix and postfix, and combined assignment operators like +=, -=, etc.
// supports bool operators &, |, and short-circuit versions &&, ||
// supports ternary ?: operator
[Language("ExpressionEvaluator", "1.0", "Multi-line expression evaluator")]
public class ExpressionEvaluatorGrammar : InterpretedLanguageGrammar
{
public ExpressionEvaluatorGrammar() : base(caseSensitive: false)
{
GrammarComments =
@"Irony expression evaluator. Case-insensitive. Supports big integers, float data types, variables, assignments,
arithmetic operations, augmented assignments (+=, -=), inc/dec (++,--), strings with embedded expressions;
bool operations &,&&, |, ||; ternary '?:' operator.";
// 1. Terminals
var number = new NumberLiteral("number");
//Let's allow big integers (with unlimited number of digits):
number.DefaultIntTypes = new TypeCode[] { TypeCode.Int32, TypeCode.Int64, NumberLiteral.TypeCodeBigInt };
var identifier = new IdentifierTerminal("identifier");
var comment = new CommentTerminal("comment", "#", "\n", "\r");
//comment must be added to NonGrammarTerminals list; it is not used directly in grammar rules,
// so we add it to this list to let Scanner know that it is also a valid terminal.
NonGrammarTerminals.Add(comment);
var comma = ToTerm(",");
//String literal with embedded expressions ------------------------------------------------------------------
var stringLit = new StringLiteral("string", "\"", StringOptions.AllowsAllEscapes | StringOptions.IsTemplate);
stringLit.AddStartEnd("'", StringOptions.AllowsAllEscapes | StringOptions.IsTemplate);
stringLit.AstConfig.NodeType = typeof(StringTemplateNode);
var Expr = new NonTerminal("Expr"); //declare it here to use in template definition
var templateSettings = new StringTemplateSettings(); //by default set to Ruby-style settings
templateSettings.ExpressionRoot = Expr; //this defines how to evaluate expressions inside template
SnippetRoots.Add(Expr);
stringLit.AstConfig.Data = templateSettings;
//--------------------------------------------------------------------------------------------------------
// 2. Non-terminals
var Term = new NonTerminal("Term");
var BinExpr = new NonTerminal("BinExpr", typeof(BinaryOperationNode));
var ParExpr = new NonTerminal("ParExpr");
var UnExpr = new NonTerminal("UnExpr", typeof(UnaryOperationNode));
var TernaryIfExpr = new NonTerminal("TernaryIf", typeof(IfNode));
var ArgList = new NonTerminal("ArgList", typeof(ExpressionListNode));
var FunctionCall = new NonTerminal("FunctionCall", typeof(FunctionCallNode));
var MemberAccess = new NonTerminal("MemberAccess", typeof(MemberAccessNode));
var IndexedAccess = new NonTerminal("IndexedAccess", typeof(IndexedAccessNode));
var ObjectRef = new NonTerminal("ObjectRef"); // foo, foo.bar or f['bar']
var UnOp = new NonTerminal("UnOp");
var BinOp = new NonTerminal("BinOp", "operator");
var PrefixIncDec = new NonTerminal("PrefixIncDec", typeof(IncDecNode));
var PostfixIncDec = new NonTerminal("PostfixIncDec", typeof(IncDecNode));
var IncDecOp = new NonTerminal("IncDecOp");
var AssignmentStmt = new NonTerminal("AssignmentStmt", typeof(AssignmentNode));
var AssignmentOp = new NonTerminal("AssignmentOp", "assignment operator");
var Statement = new NonTerminal("Statement");
var Program = new NonTerminal("Program", typeof(StatementListNode));
// 3. BNF rules
Expr.Rule = Term | UnExpr | BinExpr | PrefixIncDec | PostfixIncDec | TernaryIfExpr;
Term.Rule = number | ParExpr | stringLit | FunctionCall | identifier | MemberAccess | IndexedAccess;
ParExpr.Rule = "(" + Expr + ")";
UnExpr.Rule = UnOp + Term + ReduceHere();
UnOp.Rule = ToTerm("+") | "-" | "!";
BinExpr.Rule = Expr + BinOp + Expr;
BinOp.Rule = ToTerm("+") | "-" | "*" | "/" | "**" | "==" | "<" | "<=" | ">" | ">=" | "!=" | "&&" | "||" | "&" | "|";
PrefixIncDec.Rule = IncDecOp + identifier;
PostfixIncDec.Rule = identifier + PreferShiftHere() + IncDecOp;
IncDecOp.Rule = ToTerm("++") | "--";
TernaryIfExpr.Rule = Expr + "?" + Expr + ":" + Expr;
MemberAccess.Rule = Expr + PreferShiftHere() + "." + identifier;
AssignmentStmt.Rule = ObjectRef + AssignmentOp + Expr;
AssignmentOp.Rule = ToTerm("=") | "+=" | "-=" | "*=" | "/=";
Statement.Rule = AssignmentStmt | Expr | Empty;
ArgList.Rule = MakeStarRule(ArgList, comma, Expr);
FunctionCall.Rule = Expr + PreferShiftHere() + "(" + ArgList + ")";
FunctionCall.NodeCaptionTemplate = "call #{0}(...)";
ObjectRef.Rule = identifier | MemberAccess | IndexedAccess;
IndexedAccess.Rule = Expr + PreferShiftHere() + "[" + Expr + "]";
Program.Rule = MakePlusRule(Program, NewLine, Statement);
Root = Program; // Set grammar root
// 4. Operators precedence
RegisterOperators(10, "?");
RegisterOperators(15, "&", "&&", "|", "||");
RegisterOperators(20, "==", "<", "<=", ">", ">=", "!=");
RegisterOperators(30, "+", "-");
RegisterOperators(40, "*", "/");
RegisterOperators(50, Associativity.Right, "**");
RegisterOperators(60, "!");
// For precedence to work, we need to take care of one more thing: BinOp.
//For BinOp which is or-combination of binary operators, we need to either
// 1) mark it transient or 2) set flag TermFlags.InheritPrecedence
// We use first option, making it Transient.
// 5. Punctuation and transient terms
MarkPunctuation("(", ")", "?", ":", "[", "]");
RegisterBracePair("(", ")");
RegisterBracePair("[", "]");
MarkTransient(Term, Expr, Statement, BinOp, UnOp, IncDecOp, AssignmentOp, ParExpr, ObjectRef);
// 7. Syntax error reporting
MarkNotReported("++", "--");
AddToNoReportGroup("(", "++", "--");
AddToNoReportGroup(NewLine);
AddOperatorReportGroup("operator");
AddTermsReportGroup("assignment operator", "=", "+=", "-=", "*=", "/=");
//8. Console
ConsoleTitle = "Irony Expression Evaluator";
ConsoleGreeting =
@"Irony Expression Evaluator
Supports variable assignments, arithmetic operators (+, -, *, /),
augmented assignments (+=, -=, etc), prefix/postfix operators ++,--, string operations.
Supports big integer arithmetics, string operations.
Supports strings with embedded expressions : ""name: #{name}""
Press Ctrl-C to exit the program at any time.
";
ConsolePrompt = "?";
ConsolePromptMoreInput = "?";
//9. Language flags.
// Automatically add NewLine before EOF so that our BNF rules work correctly when there's no final line break in source
LanguageFlags = LanguageFlags.NewLineBeforeEOF | LanguageFlags.CreateAst | LanguageFlags.SupportsBigInt;
}
public override LanguageRuntime CreateRuntime(LanguageData language)
{
return new ExpressionEvaluatorRuntime(language);
}
#region Running in Grammar Explorer
private static ExpressionEvaluator _evaluator;
public override string RunSample(RunSampleArgs args)
{
if (_evaluator == null)
{
_evaluator = new ExpressionEvaluator(this);
_evaluator.Globals.Add("null", _evaluator.Runtime.NoneValue);
_evaluator.Globals.Add("true", true);
_evaluator.Globals.Add("false", false);
}
_evaluator.ClearOutput();
//for (int i = 0; i < 1000; i++) //for perf measurements, to execute 1000 times
_evaluator.Evaluate(args.ParsedSample);
return _evaluator.GetOutput();
}
#endregion
}//class
}//namespace

View File

@ -0,0 +1,54 @@
namespace Sanchime.Irony.Interpreter._Evaluator
{
public class ExpressionEvaluatorRuntime : LanguageRuntime
{
public ExpressionEvaluatorRuntime(LanguageData language) : base(language)
{
}
public override void Init()
{
base.Init();
//add built-in methods, special form IIF, import Math and Environment methods
BuiltIns.AddMethod(BuiltInPrintMethod, "print");
BuiltIns.AddMethod(BuiltInFormatMethod, "format");
BuiltIns.AddSpecialForm(SpecialFormsLibrary.Iif, "iif", 3, 3);
BuiltIns.ImportStaticMembers(typeof(Math));
BuiltIns.ImportStaticMembers(typeof(Environment));
}
//Built-in methods
private object BuiltInPrintMethod(ScriptThread thread, object[] args)
{
string text = string.Empty;
switch (args.Length)
{
case 1:
text = string.Empty + args[0]; //compact and safe conversion ToString()
break;
case 0:
break;
default:
text = string.Join(" ", args);
break;
}
thread.App.WriteLine(text);
return null;
}
private object BuiltInFormatMethod(ScriptThread thread, object[] args)
{
if (args == null || args.Length == 0) return null;
var template = args[0] as string;
if (template == null)
this.ThrowScriptError("Format template must be a string.");
if (args.Length == 1) return template;
//create formatting args array
var formatArgs = args.Skip(1).ToArray();
var text = string.Format(template, formatArgs);
return text;
}
}
}

View File

@ -0,0 +1,84 @@
using Sanchime.Irony.Parsing.Grammars;
using Sanchime.Irony.Parsing.Terminals;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Sanchime.Irony.SampleApp
{
[Language("大头", "0.0.1", "测试")]
public class DatouGrammar : Grammar
{
public DatouGrammar(): base(true)
{
#region
var globals = ToTerm("globals");
var end = ToTerm("end");
var @return = ToTerm("return");
var func = ToTerm("func");
var local = ToTerm("local");
#endregion
// 定义数字类型
var number = new NumberLiteral("Number");
number.DefaultIntTypes = new TypeCode[] { TypeCode.Int16, TypeCode.Int32, TypeCode.Int64, TypeCode.Decimal, TypeCode.Single, TypeCode.Double };
// 默认Decimal类型
number.DefaultFloatType = TypeCode.Decimal;
var Identifier = new IdentifierTerminal("Identifier");
CommentTerminal blockComment = new CommentTerminal("block-comment", "/*", "*/");
CommentTerminal lineComment = new CommentTerminal("line-comment", "//",
"\r", "\n", "\u2085", "\u2028", "\u2029");
NonGrammarTerminals.Add(blockComment);
NonGrammarTerminals.Add(lineComment);
// 变量声明
var Declaration = new NonTerminal("Declaration");
Declaration.Rule = MakeStarRule(Declaration, Declaration);
Declaration.Rule = Identifier;
var Parameters = new NonTerminal("Parameters");
// a, b, c
Parameters.Rule = Parameters + "," + Identifier | Identifier;
// 全局变量
var Globals = new NonTerminal("Globals");
Globals.Rule = "globals" + Declaration + "end";
// 定义函数
var Function = new NonTerminal("Function");
Function.Rule = "func" + Identifier + "end";
#region
// 定义表达式
var Expression = new NonTerminal("Expression");
// 小括号表达式
var ParenthesesExpression = new NonTerminal("ParenthesesExpression");
ParenthesesExpression.Rule = "(" + Expression + ")";
// 定义一元表达式
var UnaryExpression = new NonTerminal("UnaryExpression");
var UnarOperation = new NonTerminal("UnaryOperation", "operator");
UnarOperation.Rule = ToTerm("+") | "-" | "++" | "--";
// 定义二元表达式
var BinaryExpression = new NonTerminal("BinaryExpression");
var BinaryOperation = new NonTerminal("BinaryOperation", "operator");
BinaryOperation.Rule = ToTerm("+") | "-" | "*" | "/";
BinaryExpression.Rule = Expression + BinaryOperation + Expression;
Expression.Rule = UnaryExpression | BinaryExpression | ParenthesesExpression;
#endregion
// 赋值
var Assignment = new NonTerminal("Assignment");
#region
RegisterOperators(1, "+", "-");
RegisterOperators(2, "-", "*");
#endregion
// 大头语言主体
var Program = new NonTerminal("Program");
}
}
}

View File

@ -0,0 +1,46 @@
using System;
namespace Sanchime.Irony.SampleApp.Evaluations
{
internal sealed class BinaryEvaluation : Evaluation
{
private readonly Evaluation left;
private readonly Evaluation right;
private readonly BinaryOperation oper;
public BinaryEvaluation(Evaluation left, Evaluation right, BinaryOperation oper)
{
this.left = left;
this.right = right;
this.oper = oper;
}
public override object Value
{
get
{
if (left.Value == null || right.Value == null)
{
throw new InvalidOperationException("Either left or right value of the binary evaluation has been evaluated to null.");
}
if (!float.TryParse(left.Value.ToString(), out float leftValue) ||
!float.TryParse(right.Value.ToString(), out float rightValue))
{
throw new InvalidOperationException("Either left or right value of the binary evaluation cannot be evaluated as a float value.");
}
return oper switch
{
BinaryOperation.Add => leftValue + rightValue,
BinaryOperation.Sub => leftValue - rightValue,
BinaryOperation.Mul => leftValue * rightValue,
BinaryOperation.Div => leftValue / rightValue,
_ => throw new InvalidOperationException("无效的二元运算符")
};
}
}
public override string ToString() => $"{left?.ToString()} {oper} {right?.ToString()}";
}
}

View File

@ -0,0 +1,10 @@
namespace Sanchime.Irony.SampleApp.Evaluations
{
internal enum BinaryOperation
{
Add,
Sub,
Mul,
Div
}
}

View File

@ -0,0 +1,14 @@
namespace Sanchime.Irony.SampleApp.Evaluations
{
internal sealed class ConstantEvaluation : Evaluation
{
private readonly object value;
public ConstantEvaluation(object value)
{
this.value = value;
}
public override object Value => value;
}
}

View File

@ -0,0 +1,9 @@
namespace Sanchime.Irony.SampleApp.Evaluations
{
internal abstract class Evaluation
{
public abstract object Value { get; }
public override string ToString() => Value?.ToString();
}
}

View File

@ -0,0 +1,73 @@
using Sanchime.Irony.Parsing.Data;
using Sanchime.Irony.Parsing.Parsers;
using Sanchime.Irony.Utilities;
using System;
using System.Globalization;
using System.Text;
namespace Sanchime.Irony.SampleApp.Evaluations
{
internal sealed class Evaluator
{
public Evaluation Evaluate(string input)
{
var language = new LanguageData(new ExpressionGrammar());
var parser = new Parser(language);
var syntaxTree = parser.Parse(input);
if (syntaxTree.HasErrors())
{
throw new InvalidOperationException(BuildParsingErrorMessage(syntaxTree.ParserMessages));
}
return PerformEvaluate(syntaxTree.Root);
}
private Evaluation PerformEvaluate(ParseTreeNode node)
{
switch (node.Term.Name)
{
case "BinaryExpression":
var leftNode = node.ChildNodes[0];
var opNode = node.ChildNodes[1];
var rightNode = node.ChildNodes[2];
Evaluation left = PerformEvaluate(leftNode);
Evaluation right = PerformEvaluate(rightNode);
BinaryOperation op = BinaryOperation.Add;
switch (opNode.Term.Name)
{
case "+":
op = BinaryOperation.Add;
break;
case "-":
op = BinaryOperation.Sub;
break;
case "*":
op = BinaryOperation.Mul;
break;
case "/":
op = BinaryOperation.Div;
break;
}
return new BinaryEvaluation(left, right, op);
case "Number":
var value = Convert.ToSingle(node.Token.Text, CultureInfo.InvariantCulture.NumberFormat);
return new ConstantEvaluation(value);
}
throw new InvalidOperationException($"Unrecognizable term {node.Term.Name}.");
}
private static string BuildParsingErrorMessage(LogMessageList messages)
{
var sb = new StringBuilder();
sb.AppendLine("Parsing failed with the following errors:");
messages.ForEach(msg => sb.AppendLine($"\t{msg.Message}"));
return sb.ToString();
}
}
}

View File

@ -0,0 +1,53 @@
using Sanchime.Irony.Interpreter.Ast.Expressions;
using Sanchime.Irony.Interpreter.Ast.Statements;
using Sanchime.Irony.Parsing.Grammars;
using Sanchime.Irony.Parsing.Terminals;
using System;
namespace Sanchime.Irony.SampleApp
{
/// <summary>
/// Represents the grammar of a custom expression.
/// </summary>
/// <seealso cref="Grammar" />
[Language("Expression Grammar", "1.0", "A simple arithmetic expression grammar.")]
public class ExpressionGrammar : Grammar
{
/// <summary>
/// Initializes a new instance of the <see cref="ExpressionGrammar"/> class.
/// </summary>
public ExpressionGrammar() : base(false)
{
var number = new NumberLiteral("Number");
number.DefaultIntTypes = new TypeCode[] { TypeCode.Int16, TypeCode.Int32, TypeCode.Int64 };
number.DefaultFloatType = TypeCode.Single;
var identifier = new IdentifierTerminal("Identifier");
var comma = ToTerm(",");
var BinOp = new NonTerminal("BinaryOperator", "operator");
var ParExpr = new NonTerminal("ParenthesisExpression");
var BinExpr = new NonTerminal("BinaryExpression", typeof(BinaryOperationNode));
var Expr = new NonTerminal("Expression");
var Term = new NonTerminal("Term");
var Program = new NonTerminal("Program", typeof(StatementListNode));
Expr.Rule = Term | ParExpr | BinExpr;
Term.Rule = number | identifier;
ParExpr.Rule = "(" + Expr + ")";
BinExpr.Rule = Expr + BinOp + Expr;
BinOp.Rule = ToTerm("+") | "-" | "*" | "/";
RegisterOperators(10, "+", "-");
RegisterOperators(20, "*", "/");
MarkPunctuation("(", ")");
RegisterBracePair("(", ")");
MarkTransient(Expr, Term, BinOp, ParExpr);
Root = Expr;
}
}
}

View File

@ -0,0 +1,31 @@
using Sanchime.Irony.Parsing.Data;
using Sanchime.Irony.Parsing.Parsers;
using System;
namespace Sanchime.Irony.SampleApp
{
internal class Program
{
private static void Main(string[] args)
{
string sql = "SELECT Id, Name, Age, Gender FROM Student WHERE Age > 10 ORDER BY CreateDate";
Console.WriteLine("SQL:");
Console.WriteLine(sql);
SqlParser(sql);
}
private static void SqlParser(string sql)
{
var language = new LanguageData(new SqlGrammar());
var parser = new Parser(language);
var syntaxTree = parser.Parse(sql);
}
private static void Tree(ParseTree root)
{
}
}
}

View File

@ -0,0 +1,260 @@
! -----------------------------------------------------------------------------------
! SQL '89
!
! SQL (Structured Query Language)
! SQL (结构化查询语言)
!
! The SQL programming language was developed as a uniform means of modifying and
! querying relational databases. By using a single abstract language to interact
! with the database, programs can be written that are independent of the vender and
! format of the database itself. Variations are used by Oracle, Microsoft and most
! other developers
!
! In 1992, a new version SQL as released but has yet to be implemented by any major
! developer. The reason to this lies in the sheer complexity of the grammar. SQL 92
! contains over 300 rules and a myraid of new features. For instance, in SQL 92, the
! developer can create types using COBOL syntax rather than the normal data types of
! SQL 89. This reason combined with the fact that SQL 89 is a time-tested and
! ample tool maintains it as the standard of the database industry.
!
! Update:
! 02/17/2005
! Added "NULL" to the <Value> rule, I also added more comments to the grammar
!
! Note: This is an ad hoc version of the language. If there are any flaws, please
! visit www.devincook.com/goldparser
! -----------------------------------------------------------------------------------
"Name" = 'SQL 89'
"Version" = '1989'
"About" = 'This is the ANSI 89 version of SQL. Variations are used by'
| 'Oracle, Microsoft and most other database developers'
"Start Symbol" = <Query>
! =============================================================================
! 注释
! =============================================================================
Comment Start = '/*'
Comment End = '*/'
Comment Line = '--'
! =============================================================================
! 终结符
! =============================================================================
{String Ch 1} = {Printable} - ["]
{String Ch 2} = {Printable} - ['']
{Id Ch Standard} = {Alphanumeric} + [_]
{Id Ch Extended} = {Printable} - ['['] - [']']
StringLiteral = '"'{String Ch 1}*'"' | ''{String Ch 2}*''
IntegerLiteral = {Digit}+
RealLiteral = {Digit}+'.'{Digit}+
!----- SQL标识符
Id = ({Letter}{Id Ch Standard}* | '['{Id Ch Extended}+']') ('.'({Letter}{Id Ch Standard}* | '['{Id Ch Extended}+']'))?
! =============================================================================
! 语法规则
! =============================================================================
<Query> ::= <Alter Stm>
| <Create Stm>
| <Delete Stm>
| <Drop Stm>
| <Insert Stm>
| <Select Stm>
| <Update Stm>
! =============================================================================
! 数据表修改语句
! =============================================================================
<Alter Stm> ::= ALTER TABLE Id ADD COLUMN <Field Def List> <Constraint Opt>
| ALTER TABLE Id ADD <Constraint>
| ALTER TABLE Id DROP COLUMN Id
| ALTER TABLE Id DROP CONSTRAINT Id
<Create Stm> ::= CREATE <Unique> INDEX IntegerLiteral ON Id '(' <Order List> ')' <With Clause>
| CREATE TABLE Id '(' <ID List> ')' <Constraint Opt>
<Unique> ::= UNIQUE
|
<With Clause> ::= WITH PRIMARY
| WITH DISALLOW NULL
| WITH IGNORE NULL
|
<Field Def> ::= Id <Type> NOT NULL
| Id <Type>
<Field Def List> ::= <Field Def> ',' <Field Def List>
| <Field Def>
<Type> ::= BIT
| DATE
| TIME
| TIMESTAMP
| DECIMAL
| REAL
| FLOAT
| SMALLINT
| INTEGER
| INTERVAL
| CHARACTER
<Constraint Opt> ::= <Constraint>
|
<Constraint> ::= CONSTRAINT Id <Constraint Type>
| CONSTRAINT Id
<Constraint Type> ::= PRIMARY KEY '(' <Id List> ')'
| UNIQUE '(' <Id List> ')'
| NOT NULL '(' <Id List> ')'
| FOREIGN KEY '(' <Id List> ')' REFERENCES Id '(' <Id List> ')'
<Drop Stm> ::= DROP TABLE Id
| DROP INDEX Id ON Id
! =============================================================================
! 更新数据库内容
! =============================================================================
<Insert Stm> ::= INSERT INTO Id '(' <Id List> ')' <Select Stm>
| INSERT INTO Id '(' <Id List> ')' VALUES '(' <Expr List> ')'
<Update Stm> ::= UPDATE Id SET <Assign List> <Where Clause>
<Assign List> ::= Id '=' <Expression> ',' <Assign List>
| Id '=' <Expression>
<Delete Stm> ::= DELETE FROM Id <Where Clause>
! =============================================================================
! 查询语句
! =============================================================================
<Select Stm> ::= SELECT <Columns> <Into Clause> <From Clause> <Where Clause> <Group Clause> <Having Clause> <Order Clause>
<Columns> ::= <Restriction> '*'
| <Restriction> <Column List>
<Column List> ::= <Column Item> ',' <Column List>
| <Column Item>
<Column Item> ::= <Column Source>
| <Column Source> Id !ALIAS
<Column Source> ::= <Aggregate>
| Id
<Restriction> ::= ALL
| DISTINCT
|
<Aggregate> ::= Count '(' '*' ')'
| Count '(' <Expression> ')'
| Avg '(' <Expression> ')'
| Min '(' <Expression> ')'
| Max '(' <Expression> ')'
| StDev '(' <Expression> ')'
| StDevP '(' <Expression> ')'
| Sum '(' <Expression> ')'
| Var '(' <Expression> ')'
| VarP '(' <Expression> ')'
<Into Clause> ::= INTO Id
|
<From Clause> ::= FROM <Id List> <Join Chain>
<Join Chain> ::= <Join> <Join Chain>
|
<Join> ::= INNER JOIN <Id List> ON Id '=' Id
| LEFT JOIN <Id List> ON Id '=' Id
| RIGHT JOIN <Id List> ON Id '=' Id
| JOIN <Id List> ON Id '=' Id
<Where Clause> ::= WHERE <Expression>
|
<Group Clause> ::= GROUP BY <Id List>
|
<Order Clause> ::= ORDER BY <Order List>
|
<Order List> ::= ID <Order Type> ',' <Order List>
| ID <Order Type>
<Order Type> ::= ASC
| DESC
|
<Having Clause> ::= HAVING <Expression>
|
! =============================================================================
! 表达式
! =============================================================================
<Expression> ::= <And Exp> OR <Expression>
| <And Exp>
<And Exp> ::= <Not Exp> AND <And Exp>
| <Not Exp>
<Not Exp> ::= NOT <Pred Exp>
| <Pred Exp>
<Pred Exp> ::= <Add Exp> BETWEEN <Add Exp> AND <Add Exp>
| <Add Exp> NOT BETWEEN <Add Exp> AND <Add Exp>
| <Value> IS NOT NULL
| <Value> IS NULL
| <Add Exp> LIKE StringLiteral
| <Add Exp> IN <Tuple>
| <Add Exp> '=' <Add Exp>
| <Add Exp> '<>' <Add Exp>
| <Add Exp> '!=' <Add Exp>
| <Add Exp> '>' <Add Exp>
| <Add Exp> '>=' <Add Exp>
| <Add Exp> '<' <Add Exp>
| <Add Exp> '<=' <Add Exp>
| <Add Exp>
<Add Exp> ::= <Add Exp> '+' <Mult Exp>
| <Add Exp> '-' <Mult Exp>
| <Mult Exp>
<Mult Exp> ::= <Mult Exp> '*' <Negate Exp>
| <Mult Exp> '/' <Negate Exp>
| <Negate Exp>
<Negate Exp> ::= '-' <Value>
| <Value>
<Value> ::= <Tuple>
| ID
| IntegerLiteral
| RealLiteral
| StringLiteral
| NULL
<Tuple> ::= '(' <Select Stm> ')'
| '(' <Expr List> ')'
<Expr List> ::= <Expression> ',' <Expr List>
| <Expression>
<Id List> ::= <Id Member> ',' <Id List>
| <Id Member>
<Id Member> ::= Id
| Id Id

View File

@ -0,0 +1,243 @@
using Sanchime.Irony.Parsing.Grammars;
using Sanchime.Irony.Parsing.Terminals;
namespace Sanchime.Irony.SampleApp
{
[Language("SQL", "89", "SQL 89 语法")]
public class SqlGrammar : Grammar
{
public SqlGrammar() : base(false)
{ //SQL is case insensitive
//Terminals
var comment = new CommentTerminal("comment", "/*", "*/");
var lineComment = new CommentTerminal("line_comment", "--", "\n", "\r\n");
NonGrammarTerminals.Add(comment);
NonGrammarTerminals.Add(lineComment);
var number = new NumberLiteral("number");
var string_literal = new StringLiteral("string", "'", StringOptions.AllowsDoubledQuote);
var Id_simple = TerminalFactory.CreateSqlExtIdentifier(this, "id_simple"); //covers normal identifiers (abc) and quoted id's ([abc d], "abc d")
var comma = ToTerm(",");
var dot = ToTerm(".");
var CREATE = ToTerm("CREATE");
var NULL = ToTerm("NULL");
var NOT = ToTerm("NOT");
var UNIQUE = ToTerm("UNIQUE");
var WITH = ToTerm("WITH");
var TABLE = ToTerm("TABLE");
var ALTER = ToTerm("ALTER");
var ADD = ToTerm("ADD");
var COLUMN = ToTerm("COLUMN");
var DROP = ToTerm("DROP");
var CONSTRAINT = ToTerm("CONSTRAINT");
var INDEX = ToTerm("INDEX");
var ON = ToTerm("ON");
var KEY = ToTerm("KEY");
var PRIMARY = ToTerm("PRIMARY");
var INSERT = ToTerm("INSERT");
var INTO = ToTerm("INTO");
var UPDATE = ToTerm("UPDATE");
var SET = ToTerm("SET");
var VALUES = ToTerm("VALUES");
var DELETE = ToTerm("DELETE");
var SELECT = ToTerm("SELECT");
var FROM = ToTerm("FROM");
var AS = ToTerm("AS");
var COUNT = ToTerm("COUNT");
var JOIN = ToTerm("JOIN");
var BY = ToTerm("BY");
// 非终结符
var Id = new NonTerminal("Id");
var stmt = new NonTerminal("stmt");
var createTableStmt = new NonTerminal("createTableStmt");
var createIndexStmt = new NonTerminal("createIndexStmt");
var alterStmt = new NonTerminal("alterStmt");
var dropTableStmt = new NonTerminal("dropTableStmt");
var dropIndexStmt = new NonTerminal("dropIndexStmt");
var selectStmt = new NonTerminal("selectStmt");
var insertStmt = new NonTerminal("insertStmt");
var updateStmt = new NonTerminal("updateStmt");
var deleteStmt = new NonTerminal("deleteStmt");
var fieldDef = new NonTerminal("fieldDef");
var fieldDefList = new NonTerminal("fieldDefList");
var nullSpecOpt = new NonTerminal("nullSpecOpt");
var typeName = new NonTerminal("typeName");
var typeSpec = new NonTerminal("typeSpec");
var typeParamsOpt = new NonTerminal("typeParams");
var constraintDef = new NonTerminal("constraintDef");
var constraintListOpt = new NonTerminal("constraintListOpt");
var constraintTypeOpt = new NonTerminal("constraintTypeOpt");
var idlist = new NonTerminal("idlist");
var idlistPar = new NonTerminal("idlistPar");
var uniqueOpt = new NonTerminal("uniqueOpt");
var orderList = new NonTerminal("orderList");
var orderMember = new NonTerminal("orderMember");
var orderDirOpt = new NonTerminal("orderDirOpt");
var withClauseOpt = new NonTerminal("withClauseOpt");
var alterCmd = new NonTerminal("alterCmd");
var insertData = new NonTerminal("insertData");
var intoOpt = new NonTerminal("intoOpt");
var assignList = new NonTerminal("assignList");
var whereClauseOpt = new NonTerminal("whereClauseOpt");
var assignment = new NonTerminal("assignment");
var expression = new NonTerminal("expression");
var exprList = new NonTerminal("exprList");
var selRestrOpt = new NonTerminal("selRestrOpt");
var selList = new NonTerminal("selList");
var intoClauseOpt = new NonTerminal("intoClauseOpt");
var fromClauseOpt = new NonTerminal("fromClauseOpt");
var groupClauseOpt = new NonTerminal("groupClauseOpt");
var havingClauseOpt = new NonTerminal("havingClauseOpt");
var orderClauseOpt = new NonTerminal("orderClauseOpt");
var columnItemList = new NonTerminal("columnItemList");
var columnItem = new NonTerminal("columnItem");
var columnSource = new NonTerminal("columnSource");
var asOpt = new NonTerminal("asOpt");
var aliasOpt = new NonTerminal("aliasOpt");
var aggregate = new NonTerminal("aggregate");
var aggregateArg = new NonTerminal("aggregateArg");
var aggregateName = new NonTerminal("aggregateName");
var tuple = new NonTerminal("tuple");
var joinChainOpt = new NonTerminal("joinChainOpt");
var joinKindOpt = new NonTerminal("joinKindOpt");
var term = new NonTerminal("term");
var unExpr = new NonTerminal("unExpr");
var unOp = new NonTerminal("unOp");
var binExpr = new NonTerminal("binExpr");
var binOp = new NonTerminal("binOp");
var betweenExpr = new NonTerminal("betweenExpr");
var inExpr = new NonTerminal("inExpr");
var parSelectStmt = new NonTerminal("parSelectStmt");
var notOpt = new NonTerminal("notOpt");
var funCall = new NonTerminal("funCall");
var stmtLine = new NonTerminal("stmtLine");
var semiOpt = new NonTerminal("semiOpt");
var stmtList = new NonTerminal("stmtList");
var funArgs = new NonTerminal("funArgs");
var inStmt = new NonTerminal("inStmt");
//BNF Rules
this.Root = stmtList;
stmtLine.Rule = stmt + semiOpt;
semiOpt.Rule = Empty | ";";
stmtList.Rule = MakePlusRule(stmtList, stmtLine);
// 标识符语法规则
Id.Rule = MakePlusRule(Id, dot, Id_simple);
stmt.Rule = createTableStmt | createIndexStmt | alterStmt
| dropTableStmt | dropIndexStmt
| selectStmt | insertStmt | updateStmt | deleteStmt
| "GO";
// Create Table
createTableStmt.Rule = CREATE + TABLE + Id + "(" + fieldDefList + ")" + constraintListOpt;
fieldDefList.Rule = MakePlusRule(fieldDefList, comma, fieldDef);
fieldDef.Rule = Id + typeName + typeParamsOpt + nullSpecOpt;
nullSpecOpt.Rule = NULL | NOT + NULL | Empty;
typeName.Rule = ToTerm("BIT") | "DATE" | "TIME" | "TIMESTAMP" | "DECIMAL" | "REAL" | "FLOAT" | "SMALLINT" | "INTEGER"
| "INTERVAL" | "CHARACTER"
// MS SQL types:
| "DATETIME" | "INT" | "DOUBLE" | "CHAR" | "NCHAR" | "VARCHAR" | "NVARCHAR"
| "IMAGE" | "TEXT" | "NTEXT";
typeParamsOpt.Rule = "(" + number + ")" | "(" + number + comma + number + ")" | Empty;
constraintDef.Rule = CONSTRAINT + Id + constraintTypeOpt;
constraintListOpt.Rule = MakeStarRule(constraintListOpt, constraintDef);
constraintTypeOpt.Rule = PRIMARY + KEY + idlistPar | UNIQUE + idlistPar | NOT + NULL + idlistPar
| "Foreign" + KEY + idlistPar + "References" + Id + idlistPar;
idlistPar.Rule = "(" + idlist + ")";
idlist.Rule = MakePlusRule(idlist, comma, Id);
//Create Index
createIndexStmt.Rule = CREATE + uniqueOpt + INDEX + Id + ON + Id + orderList + withClauseOpt;
uniqueOpt.Rule = Empty | UNIQUE;
orderList.Rule = MakePlusRule(orderList, comma, orderMember);
orderMember.Rule = Id + orderDirOpt;
orderDirOpt.Rule = Empty | "ASC" | "DESC";
withClauseOpt.Rule = Empty | WITH + PRIMARY | WITH + "Disallow" + NULL | WITH + "Ignore" + NULL;
//Alter
alterStmt.Rule = ALTER + TABLE + Id + alterCmd;
alterCmd.Rule = ADD + COLUMN + fieldDefList + constraintListOpt
| ADD + constraintDef
| DROP + COLUMN + Id
| DROP + CONSTRAINT + Id;
//Drop stmts
dropTableStmt.Rule = DROP + TABLE + Id;
dropIndexStmt.Rule = DROP + INDEX + Id + ON + Id;
// 插入语法规则
insertStmt.Rule = INSERT + intoOpt + Id + idlistPar + insertData;
insertData.Rule = selectStmt | VALUES + "(" + exprList + ")";
intoOpt.Rule = Empty | INTO; //Into is optional in MSSQL
// 更新语法规则
updateStmt.Rule = UPDATE + Id + SET + assignList + whereClauseOpt;
assignList.Rule = MakePlusRule(assignList, comma, assignment);
assignment.Rule = Id + "=" + expression;
// 删除语法规则
deleteStmt.Rule = DELETE + FROM + Id + whereClauseOpt;
// 查询语法规则
selectStmt.Rule = SELECT + selRestrOpt + selList + intoClauseOpt + fromClauseOpt + whereClauseOpt +
groupClauseOpt + havingClauseOpt + orderClauseOpt;
selRestrOpt.Rule = Empty | "ALL" | "DISTINCT";
selList.Rule = columnItemList | "*";
columnItemList.Rule = MakePlusRule(columnItemList, comma, columnItem);
columnItem.Rule = columnSource + aliasOpt;
aliasOpt.Rule = Empty | asOpt + Id;
asOpt.Rule = Empty | AS;
columnSource.Rule = aggregate | Id;
aggregate.Rule = aggregateName + "(" + aggregateArg + ")";
aggregateArg.Rule = expression | "*";
aggregateName.Rule = COUNT | "Avg" | "Min" | "Max" | "StDev" | "StDevP" | "Sum" | "Var" | "VarP";
intoClauseOpt.Rule = Empty | INTO + Id;
fromClauseOpt.Rule = Empty | FROM + idlist + joinChainOpt;
joinChainOpt.Rule = Empty | joinKindOpt + JOIN + idlist + ON + Id + "=" + Id;
joinKindOpt.Rule = Empty | "INNER" | "LEFT" | "RIGHT";
whereClauseOpt.Rule = Empty | "WHERE" + expression;
groupClauseOpt.Rule = Empty | "GROUP" + BY + idlist;
havingClauseOpt.Rule = Empty | "HAVING" + expression;
orderClauseOpt.Rule = Empty | "ORDER" + BY + orderList;
//Expression
exprList.Rule = MakePlusRule(exprList, comma, expression);
expression.Rule = term | unExpr | binExpr;// | betweenExpr; //-- BETWEEN doesn't work - yet; brings a few parsing conflicts
term.Rule = Id | string_literal | number | funCall | tuple | parSelectStmt;// | inStmt;
tuple.Rule = "(" + exprList + ")";
parSelectStmt.Rule = "(" + selectStmt + ")";
unExpr.Rule = unOp + term;
unOp.Rule = NOT | "+" | "-" | "~";
binExpr.Rule = expression + binOp + expression;
binOp.Rule = ToTerm("+") | "-" | "*" | "/" | "%" //arithmetic
| "&" | "|" | "^" //bit
| "=" | ">" | "<" | ">=" | "<=" | "<>" | "!=" | "!<" | "!>"
| "AND" | "OR" | "LIKE" | NOT + "LIKE" | "IN" | NOT + "IN";
betweenExpr.Rule = expression + notOpt + "BETWEEN" + expression + "AND" + expression;
notOpt.Rule = Empty | NOT;
//funCall covers some psedo-operators and special forms like ANY(...), SOME(...), ALL(...), EXISTS(...), IN(...)
funCall.Rule = Id + "(" + funArgs + ")";
funArgs.Rule = selectStmt | exprList;
inStmt.Rule = expression + "IN" + "(" + exprList + ")";
//Operators
RegisterOperators(10, "*", "/", "%");
RegisterOperators(9, "+", "-");
RegisterOperators(8, "=", ">", "<", ">=", "<=", "<>", "!=", "!<", "!>", "LIKE", "IN");
RegisterOperators(7, "^", "&", "|");
RegisterOperators(6, NOT);
RegisterOperators(5, "AND");
RegisterOperators(4, "OR");
MarkPunctuation(",", "(", ")");
MarkPunctuation(asOpt, semiOpt);
//Note: we cannot declare binOp as transient because it includes operators "NOT LIKE", "NOT IN" consisting of two tokens.
// Transient non-terminals cannot have more than one non-punctuation child nodes.
// Instead, we set flag InheritPrecedence on binOp , so that it inherits precedence value from it's children, and this precedence is used
// in conflict resolution when binOp node is sitting on the stack
base.MarkTransient(stmt, term, asOpt, aliasOpt, stmtLine, expression, unOp, tuple);
binOp.SetFlag(TermFlags.InheritPrecedence);
}//constructor
}//class
}

View File

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Irony.Interpreter\Sanchime.Irony.Interpreter.csproj" />
<ProjectReference Include="..\Irony\Sanchime.Irony.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,24 @@
using Sanchime.Irony.Parsing.Parsers;
using Sanchime.Irony.Parsing.Scanners;
using Sanchime.Irony.Parsing.Terminals;
using Xunit;
namespace Sanchime.Irony.Tests
{
public class CommentTerminalTests
{
[Fact]
public void TestCommentTerminal()
{
Parser parser; Token token;
parser = TestHelper.CreateParser(new CommentTerminal("Comment", "/*", "*/"));
token = parser.ParseInput("/* abc */");
Assert.True(token.Category == TokenCategory.Comment, "Failed to read comment");
parser = TestHelper.CreateParser(new CommentTerminal("Comment", "//", "\n"));
token = parser.ParseInput("// abc \n ");
Assert.True(token.Category == TokenCategory.Comment, "Failed to read line comment");
}//method
}//class
}//namespace

View File

@ -0,0 +1,52 @@
using Sanchime.Irony.Parsing.Parsers;
using Sanchime.Irony.Parsing.Scanners;
using Sanchime.Irony.Parsing.Terminals;
using System;
using Xunit;
namespace Sanchime.Irony.Tests
{
public class DataLiteralsTests
{
[Fact]
public void TestDataLiterals()
{
Parser parser; Token token;
Terminal term;
// FixedLengthLiteral ---------------------------------------------------------
term = new FixedLengthLiteral("fixedLengthInteger", 2, TypeCode.Int32);
parser = TestHelper.CreateParser(term, null);
token = parser.ParseInput("1200");
Assert.True(token.Value != null, "Failed to parse fixed-length integer.");
Assert.True((int)token.Value == 12, "Failed to parse fixed-length integer - result value does not match.");
term = new FixedLengthLiteral("fixedLengthString", 2, TypeCode.String);
parser = TestHelper.CreateParser(term);
token = parser.ParseInput("abcd", useTerminator: false);
Assert.True(token != null && token.Value != null, "Failed to parse fixed-length string.");
Assert.True((string)token.Value == "ab", "Failed to parse fixed-length string - result value does not match");
// DsvLiteral ----------------------------------------------------------------
term = new DsvLiteral("DsvInteger", TypeCode.Int32, ",");
parser = TestHelper.CreateParser(term);
token = parser.ParseInput("12,");
Assert.True(token != null && token.Value != null, "Failed to parse CSV integer.");
Assert.True((int)token.Value == 12, "Failed to parse CSV integer - result value does not match.");
term = new DsvLiteral("DsvInteger", TypeCode.String, ",");
parser = TestHelper.CreateParser(term);
token = parser.ParseInput("ab,");
Assert.True(token != null && token.Value != null, "Failed to parse CSV string.");
Assert.True((string)token.Value == "ab", "Failed to parse CSV string - result value does not match.");
// QuotedValueLiteral ----------------------------------------------------------------
term = new QuotedValueLiteral("QVDate", "#", TypeCode.DateTime);
parser = TestHelper.CreateParser(term);
token = parser.ParseInput("#11/15/2009#");
Assert.True(token != null && token.Value != null, "Failed to parse quoted date.");
Assert.True((DateTime)token.Value == new DateTime(2009, 11, 15), "Failed to parse quoted date - result value does not match.");
}//method
}//class
}//namespace

View File

@ -0,0 +1,47 @@
using Sanchime.Irony.Parsing.Grammars;
using Sanchime.Irony.Parsing.Parsers;
using Sanchime.Irony.Parsing.Terminals;
using Xunit;
namespace Sanchime.Irony.Tests
{
public class ErrorRecoveryTests
{
#region Grammars
//A simple grammar for language consisting of simple assignment statements: x=y + z; z= t + m;
public class ErrorRecoveryGrammar : Grammar
{
public ErrorRecoveryGrammar()
{
var id = new IdentifierTerminal("id");
var expr = new NonTerminal("expr");
var stmt = new NonTerminal("stmt");
var stmtList = new NonTerminal("stmt");
Root = stmtList;
stmtList.Rule = MakeStarRule(stmtList, stmt);
stmt.Rule = id + "=" + expr + ";";
stmt.ErrorRule = SyntaxError + ";";
expr.Rule = id | id + "+" + id;
}
}// class
#endregion Grammars
[Fact]
public void TestErrorRecovery()
{
var grammar = new ErrorRecoveryGrammar();
var parser = new Parser(grammar);
TestHelper.CheckGrammarErrors(parser);
//correct sample
var parseTree = parser.Parse("x = y; y = z + m; m = n;");
Assert.False(parseTree.HasErrors(), "Unexpected parse errors in correct source sample.");
parseTree = parser.Parse("x = y; m = = d ; y = z + m; x = z z; m = n;");
Assert.True(2 == parseTree.ParserMessages.Count, "Invalid # of errors.");
}
}//class
}//namespace

View File

@ -0,0 +1,274 @@
using Sanchime.Irony.Interpreter._Evaluator;
using Sanchime.Irony.Interpreter.SriptApplication;
using System;
using System.Collections.Generic;
using Xunit;
namespace Sanchime.Irony.Tests
{
public class EvaluatorTests
{
[Fact]
public void TestEvaluator_Ops()
{
var eval = new ExpressionEvaluator();
string script;
object result;
//Simple computation
script = "2*3";
result = eval.Evaluate(script);
Assert.Equal(6, result);
//Using variables
script = @"
x=2
y=4
x * y
";
result = eval.Evaluate(script);
Assert.Equal(8, result);
//Operator precedence
script = @"
x=2
y=3
x + y * 5
";
result = eval.Evaluate(script);
Assert.Equal(17, result);
//parenthesis
script = @"
x=3
y=2
1 + (x - y) * 5
";
result = eval.Evaluate(script);
Assert.Equal(6, result);
//strings
script = @"
x='2'
y='3'
x + y + 4
";
result = eval.Evaluate(script);
Assert.Equal("234", result);
//string with embedded expressions
script = @"
x = 4
y = 7
'#{x} * #{y} = #{x * y}'
";
result = eval.Evaluate(script);
Assert.Equal("4 * 7 = 28", result);
//various operators
script = @"
x = 1 + 2 * 3 # =7
y = --x # = 6
z = x * 1.5 # = 9
z -= y # = 3
";
result = eval.Evaluate(script);
Assert.InRange(3.0 - (double)result, -0.0001, 0.0001);
//&&, || operators
script = @"x = (1 > 0) || (1/0)";
result = eval.Evaluate(script);
Assert.Equal(true, result);
//Operator precedence test
script = @"2+3*3*3";
result = eval.Evaluate(script);
Assert.Equal(29, result);
script = @"x = (1 < 0) && (1/0)";
result = eval.Evaluate(script);
Assert.Equal(false, result);
}
[Fact]
public void TestEvaluator_BuiltIns()
{
var eval = new ExpressionEvaluator();
string script;
object result;
//Using methods imported from System.Math class
//TODO this generates System.Reflection.AmbiguousMatchException
script = @"abs(-1.0) + Log10(100.0) + sqrt(9) + floor(4.5) + sin(PI/2)";
result = eval.Evaluate(script);
Assert.True(result is double, "Result is not double.");
Assert.InRange(11.0 - (double)result, -0.001, 0.001);
//Using methods imported from System.Environment
script = @"report = '#{MachineName}-#{ProcessorCount}'";
result = eval.Evaluate(script);
var expected = string.Format("{0}-{1}", Environment.MachineName, Environment.ProcessorCount);
Assert.Equal(expected, result);
//Using special built-in methods print and format
eval.ClearOutput();
script = @"print(format('{0} * {1} = {2}', 3, 4, 3 * 4))";
eval.Evaluate(script);
result = eval.GetOutput();
Assert.Equal("3 * 4 = 12\r\n", result);
//Add custom built-in method SayHello and test it
//eval.Runtime.BuiltIns.AddMethod(SayHello, "SayHello", 1, 1, "name");
script = @"SayHello('John')";
result = eval.Evaluate(script);
Assert.Equal("Hello, John!", result);
}
//custom built-in method added to evaluator in Built-in tests
public static string SayHello(ScriptThread thread, object[] args)
{
return "Hello, " + args[0] + "!";
}
[Fact]
public void TestEvaluator_Iif()
{
var eval = new ExpressionEvaluator();
string script;
object result;
//Test '? :' operator
script = @"1 < 0 ? 1/0 : 'false' "; // Notice that (1/0) is not evaluated
result = eval.Evaluate(script);
Assert.Equal("false", result);
//Test iif special form
script = @"iif(1 > 0, 'true', 1/0) "; //Notice that (1/0) is not evaluated
result = eval.Evaluate(script);
Assert.Equal("true", result);
}
[Fact]
public void TestEvaluator_MemberAccess()
{
var eval = new ExpressionEvaluator();
eval.Globals["foo"] = new Foo();
string script;
object result;
//Test access to field, prop, calling a method
script = @"foo.Field + ',' + foo.Prop + ',' + foo.GetStuff()";
result = eval.Evaluate(script);
Assert.Equal("F,P,S", result);
script = @"
foo.Field = 'FF'
foo.Prop = 'PP'
R = foo.Field + foo.Prop ";
result = eval.Evaluate(script);
Assert.Equal("FFPP", result);
//Test access to indexed properties
//TODO this generates System.Reflection.AmbiguousMatchException
script = @"foo[3]";
result = eval.Evaluate(script);
Assert.Equal("#3", result);
//TODO this generates System.Reflection.AmbiguousMatchException
script = @"foo['a']";
result = eval.Evaluate(script);
Assert.Equal("V-a", result);
// Test with string literal
script = @" '0123'.Substring(1) + 'abcd'.Length ";
result = eval.Evaluate(script);
Assert.Equal("1234", result);
}
//A class used for member access testing
public class Foo
{
public string Field = "F";
public string Prop { get; set; }
public Foo()
{
Prop = "P";
}
public string GetStuff()
{
return "S";
}
public string this[int i]
{
get { return "#" + i; }
set { }
}
public string this[string key]
{
get { return "V-" + key; }
set { }
}
}
[Fact]
public void TestEvaluator_ArrayDictDataRow()
{
var eval = new ExpressionEvaluator();
//Create an array, a dictionary and a data row and add them to Globals
eval.Globals["primes"] = new int[] { 3, 5, 7, 11, 13 };
var nums = new Dictionary<string, string>(StringComparer.CurrentCultureIgnoreCase);
nums["one"] = "1";
nums["two"] = "2";
nums["three"] = "3";
eval.Globals["nums"] = nums;
//var t = new System.Data.DataTable();
//t.Columns.Add("Name", typeof(string));
//t.Columns.Add("Age", typeof(int));
//var row = t.NewRow();
var row = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
row["Name"] = "John";
row["Age"] = 30;
eval.Globals["row"] = row;
string script;
object result;
//Test array
script = @"primes[3]";
result = eval.Evaluate(script);
Assert.Equal(11, result);
script = @"
primes[3] = 12345
primes[3]";
result = eval.Evaluate(script);
Assert.Equal(12345, result);
//Test dict
script = @"nums['three'] + nums['two'] + nums['one']";
result = eval.Evaluate(script);
Assert.Equal("321", result);
script = @"
nums['two'] = '22'
nums['three'] + nums['two'] + nums['one']
";
result = eval.Evaluate(script);
Assert.Equal("3221", result);
//Test data row
script = @"row['Name'] + ', ' + row['age']";
result = eval.Evaluate(script);
Assert.Equal("John, 30", result);
script = @"
row['Name'] = 'Jon'
row['Name'] + ', ' + row['age']";
result = eval.Evaluate(script);
Assert.Equal("Jon, 30", result);
}
}//class
}//namespace

View File

@ -0,0 +1,90 @@
using Sanchime.Irony.Parsing.Grammars;
using Sanchime.Irony.Parsing.Parsers;
using Sanchime.Irony.Parsing.Scanners;
using Sanchime.Irony.Parsing.Terminals;
using Xunit;
namespace Sanchime.Irony.Tests
{
public class FreeTextLiteralTests
{
//A special grammar that does not skip whitespace
private class FreeTextLiteralTestGrammar : Grammar
{
public string Terminator = "END";
public FreeTextLiteralTestGrammar(Terminal terminal)
: base(caseSensitive: true)
{
var rule = new BnfExpression(terminal);
MarkReservedWords(Terminator);
rule += Terminator;
Root = new NonTerminal("Root");
Root.Rule = rule;
}
//Overrides base method, effectively suppressing skipping whitespaces
public override void SkipWhitespace(ISourceStream source)
{
return;
}
}//class
private Parser CreateParser(Terminal terminal)
{
var grammar = new FreeTextLiteralTestGrammar(terminal);
return new Parser(grammar);
}
private Token GetFirst(ParseTree tree)
{
return tree.Tokens[0];
}
//The following test method and a fix are contributed by ashmind codeplex user
[Fact]
public void TestFreeTextLiteral_Escapes()
{
Parser parser; Token token;
//Escapes test
var term = new FreeTextLiteral("FreeText", ",", ")");
term.Escapes.Add(@"\\", @"\");
term.Escapes.Add(@"\,", @",");
term.Escapes.Add(@"\)", @")");
parser = CreateParser(term);
token = GetFirst(parser.Parse(@"abc\\de\,\)fg,"));
Assert.False(token == null, "Failed to produce a token on valid string.");
Assert.True(term == token.Terminal, "Failed to scan a string - invalid Terminal in the returned token.");
Assert.True(token.Value.ToString() == @"abc\de,)fg", "Failed to scan a string");
term = new FreeTextLiteral("FreeText", FreeTextOptions.AllowEof, ";");
parser = CreateParser(term);
token = GetFirst(parser.Parse(@"abcdefg"));
Assert.False(token == null, "Failed to produce a token for free text ending at EOF.");
Assert.True(term == token.Terminal, "Failed to scan a free text ending at EOF - invalid Terminal in the returned token.");
Assert.True(token.Value.ToString() == @"abcdefg", "Failed to scan a free text ending at EOF");
//The following test method and a fix are contributed by ashmind codeplex user
//VAR
//MESSAGE:STRING80;
//(*_ORError Message*)
//END_VAR
term = new FreeTextLiteral("varContent", "END_VAR");
term.Firsts.Add("VAR");
parser = CreateParser(term);
token = GetFirst(parser.Parse("VAR\r\nMESSAGE:STRING80;\r\n(*_ORError Message*)\r\nEND_VAR"));
Assert.False(token == null, "Failed to produce a token on valid string.");
Assert.True(term == token.Terminal, "Failed to scan a string - invalid Terminal in the returned token.");
Assert.True(token.ValueString == "\r\nMESSAGE:STRING80;\r\n(*_ORError Message*)\r\n", "Failed to scan a string");
term = new FreeTextLiteral("freeText", FreeTextOptions.AllowEof);
parser = CreateParser(term);
token = GetFirst(parser.Parse(" "));
Assert.False(token == null, "Failed to produce a token on valid string.");
Assert.True(term == token.Terminal, "Failed to scan a string - invalid Terminal in the returned token.");
Assert.True(token.ValueString == " ", "Failed to scan a string");
}
}//class
}//namespace

View File

@ -0,0 +1,76 @@
using Sanchime.Irony.Parsing.Parsers;
using Sanchime.Irony.Parsing.Scanners;
using Sanchime.Irony.Parsing.Terminals;
using Xunit;
namespace Sanchime.Irony.Tests
{
public class IdentifierTerminalTests
{
[Fact]
public void TestIdentifier_CSharp()
{
Parser parser; Token token;
parser = TestHelper.CreateParser(TerminalFactory.CreateCSharpIdentifier("Identifier"));
token = parser.ParseInput("x ");
Assert.True(token.Terminal.Name == "Identifier", "Failed to parse identifier");
Assert.True((string)token.Value == "x", "Failed to parse identifier");
token = parser.ParseInput("_a01 ");
Assert.True(token.Terminal.Name == "Identifier", "Failed to parse identifier starting with _");
Assert.True((string)token.Value == "_a01", "Failed to parse identifier starting with _");
token = parser.ParseInput("0abc ");
Assert.True(token.IsError(), "Erroneously recognized an identifier.");
token = parser.ParseInput(@"_\u0061bc ");
Assert.True(token.Terminal.Name == "Identifier", "Failed to parse identifier starting with _");
Assert.True((string)token.Value == "_abc", "Failed to parse identifier containing escape sequence \\u");
token = parser.ParseInput(@"a\U00000062c_ ");
Assert.True(token.Terminal.Name == "Identifier", "Failed to parse identifier starting with _");
Assert.True((string)token.Value == "abc_", "Failed to parse identifier containing escape sequence \\U");
}//method
[Fact]
public void TestIdentifier_CaseRestrictions()
{
Parser parser; Token token;
var id = new IdentifierTerminal("identifier");
id.CaseRestriction = CaseRestriction.None;
parser = TestHelper.CreateParser(id);
token = parser.ParseInput("aAbB");
Assert.True(token != null, "Failed to scan an identifier aAbB.");
id.CaseRestriction = CaseRestriction.FirstLower;
parser = TestHelper.CreateParser(id);
token = parser.ParseInput("BCD");
Assert.True(token.IsError(), "Erroneously recognized an identifier BCD with FirstLower restriction.");
token = parser.ParseInput("bCd ");
Assert.True(token != null && token.ValueString == "bCd", "Failed to scan identifier bCd with FirstLower restriction.");
id.CaseRestriction = CaseRestriction.FirstUpper;
parser = TestHelper.CreateParser(id);
token = parser.ParseInput("cDE");
Assert.True(TokenCategory.Error == token.Category, "Erroneously recognized an identifier cDE with FirstUpper restriction.");
token = parser.ParseInput("CdE");
Assert.True(token != null && token.ValueString == "CdE", "Failed to scan identifier CdE with FirstUpper restriction.");
id.CaseRestriction = CaseRestriction.AllLower;
parser = TestHelper.CreateParser(id);
token = parser.ParseInput("DeF");
Assert.True(token.IsError(), "Erroneously recognized an identifier DeF with AllLower restriction.");
token = parser.ParseInput("def");
Assert.True(token != null && token.ValueString == "def", "Failed to scan identifier def with AllLower restriction.");
id.CaseRestriction = CaseRestriction.AllUpper;
parser = TestHelper.CreateParser(id);
token = parser.ParseInput("EFg ");
Assert.True(token.IsError(), "Erroneously recognized an identifier EFg with AllUpper restriction.");
token = parser.ParseInput("EFG");
Assert.True(token != null && token.ValueString == "EFG", "Failed to scan identifier EFG with AllUpper restriction.");
}//method
}//class
}//namespace

View File

@ -0,0 +1,104 @@
using Sanchime.Irony.Parsing.Data;
using Sanchime.Irony.Parsing.Grammars;
using Sanchime.Irony.Parsing.Parsers;
using Sanchime.Irony.Parsing.Scanners;
using Sanchime.Irony.Parsing.Terminals;
using Xunit;
//Tests of Visual Studio integration functionality
namespace Sanchime.Irony.Tests
{
public class IntegrationTestGrammar : Grammar
{
public IntegrationTestGrammar()
{
var comment = new CommentTerminal("comment", "/*", "*/");
NonGrammarTerminals.Add(comment);
var str = new StringLiteral("str", "'", StringOptions.AllowsLineBreak);
var stmt = new NonTerminal("stmt");
stmt.Rule = str | Empty;
Root = stmt;
}
}//class
public class IntegrationTests
{
private Grammar _grammar;
private LanguageData _language;
private Scanner _scanner;
private ParsingContext _context;
private int _state;
private void Init(Grammar grammar)
{
_grammar = grammar;
_language = new LanguageData(_grammar);
var parser = new Parser(_language);
_scanner = parser.Scanner;
_context = parser.Context;
_context.Mode = ParseMode.VsLineScan;
}
private void SetSource(string text)
{
_scanner.VsSetSource(text, 0);
}
private Token Read()
{
Token token = _scanner.VsReadToken(ref _state);
return token;
}
[Fact]
public void TestIntegration_VsScanningComment()
{
Init(new IntegrationTestGrammar());
SetSource(" /* ");
Token token = Read();
Assert.True(token.IsSet(TokenFlags.IsIncomplete), "Expected incomplete token (line 1)");
token = Read();
Assert.True(token == null, "NULL expected");
SetSource(" comment ");
token = Read();
Assert.True(token.IsSet(TokenFlags.IsIncomplete), "Expected incomplete token (line 2)");
token = Read();
Assert.True(token == null, "NULL expected");
SetSource(" */ /*x*/");
token = Read();
Assert.False(token.IsSet(TokenFlags.IsIncomplete), "Expected complete token (line 3)");
token = Read();
Assert.False(token.IsSet(TokenFlags.IsIncomplete), "Expected complete token (line 3)");
token = Read();
Assert.True(token == null, "Null expected.");
}
[Fact]
public void TestIntegration_VsScanningString()
{
Init(new IntegrationTestGrammar());
SetSource(" 'abc");
Token token = Read();
Assert.True(token.ValueString == "abc", "Expected incomplete token 'abc' (line 1)");
Assert.True(token.IsSet(TokenFlags.IsIncomplete), "Expected incomplete token (line 1)");
token = Read();
Assert.True(token == null, "NULL expected");
SetSource(" def ");
token = Read();
Assert.True(token.ValueString == " def ", "Expected incomplete token ' def ' (line 2)");
Assert.True(token.IsSet(TokenFlags.IsIncomplete), "Expected incomplete token (line 2)");
token = Read();
Assert.True(token == null, "NULL expected");
SetSource("ghi' 'x'");
token = Read();
Assert.True(token.ValueString == "ghi", "Expected token 'ghi' (line 3)");
Assert.False(token.IsSet(TokenFlags.IsIncomplete), "Expected complete token (line 3)");
token = Read();
Assert.True(token.ValueString == "x", "Expected token 'x' (line 3)");
Assert.False(token.IsSet(TokenFlags.IsIncomplete), "Expected complete token (line 3)");
token = Read();
Assert.True(token == null, "Null expected.");
}
}//class
}//namespace

View File

@ -0,0 +1,60 @@
using Sanchime.Irony.Parsing.Parsers;
using Sanchime.Irony.Parsing.Scanners;
using Sanchime.Irony.Parsing.Terminals;
using Xunit;
namespace Sanchime.Irony.Tests
{
public class LineContinuationTests
{
[Fact]
public void TestContinuationTerminal_Simple()
{
Parser parser; Token token;
parser = TestHelper.CreateParser(new LineContinuationTerminal("LineContinuation", "\\"));
token = parser.ParseInput("\\\r\t");
Assert.True(token.Category == TokenCategory.Outline, "Failed to read simple line continuation terminal");
}
[Fact]
public void TestContinuationTerminal_Default()
{
Parser parser; Token token;
parser = TestHelper.CreateParser(new LineContinuationTerminal("LineContinuation"));
token = parser.ParseInput("_\r\n\t");
Assert.True(token.Category == TokenCategory.Outline, "Failed to read default line continuation terminal");
token = parser.ParseInput("\\\v ");
Assert.True(token.Category == TokenCategory.Outline, "Failed to read default line continuation terminal");
}
[Fact]
public void TestContinuationTerminal_Complex()
{
Parser parser; Token token;
parser = TestHelper.CreateParser(new LineContinuationTerminal("LineContinuation", @"\continue", @"\cont", "++CONTINUE++"));
token = parser.ParseInput("\\cont \r\n ");
Assert.True(token.Category == TokenCategory.Outline, "Failed to read complex line continuation terminal");
token = parser.ParseInput("++CONTINUE++\t\v");
Assert.True(token.Category == TokenCategory.Outline, "Failed to read complex line continuation terminal");
}
[Fact]
public void TestContinuationTerminal_Incomplete()
{
Parser parser; Token token;
parser = TestHelper.CreateParser(new LineContinuationTerminal("LineContinuation"));
token = parser.ParseInput("\\ garbage");
Assert.True(token.Category == TokenCategory.Error, "Failed to read incomplete line continuation terminal");
parser = TestHelper.CreateParser(new LineContinuationTerminal("LineContinuation"));
token = parser.ParseInput("_");
Assert.True(token.Category == TokenCategory.Error, "Failed to read incomplete line continuation terminal");
}
}
}

View File

@ -0,0 +1,400 @@
//Authors: Roman Ivantsov, Philipp Serr
using Sanchime.Irony.Parsing.Parsers;
using Sanchime.Irony.Parsing.Scanners;
using Sanchime.Irony.Parsing.Terminals;
using System;
using Xunit;
namespace Sanchime.Irony.Tests
{
public class NumberLiteralTests
{
[Fact]
public void TestNumber_General()
{
Parser parser; Token token;
NumberLiteral number = new NumberLiteral("Number");
number.DefaultIntTypes = new TypeCode[] { TypeCode.Int32, TypeCode.Int64, NumberLiteral.TypeCodeBigInt };
parser = TestHelper.CreateParser(number);
token = parser.ParseInput("123");
CheckType(token, typeof(int));
Assert.True((int)token.Value == 123, "Failed to read int value");
token = parser.ParseInput("123.4");
Assert.True(Math.Abs(Convert.ToDouble(token.Value) - 123.4) < 0.000001, "Failed to read float value");
//100 digits
string sbig = "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890";
token = parser.ParseInput(sbig);
Assert.True(token.Value.ToString() == sbig, "Failed to read big integer value");
}//method
//The following "sign" test methods and a fix are contributed by ashmind codeplex user
[Fact]
public void TestNumber_SignedDoesNotMatchSingleMinus()
{
Parser parser; Token token;
var number = new NumberLiteral("number", NumberOptions.AllowSign);
parser = TestHelper.CreateParser(number);
token = parser.ParseInput("-");
Assert.True(token.IsError(), "Parsed single '-' as a number value.");
}
[Fact]
public void TestNumber_SignedDoesNotMatchSinglePlus()
{
Parser parser; Token token;
var number = new NumberLiteral("number", NumberOptions.AllowSign);
parser = TestHelper.CreateParser(number);
token = parser.ParseInput("+");
Assert.True(token.IsError(), "Parsed single '+' as a number value.");
}
[Fact]
public void TestNumber_SignedMatchesNegativeCorrectly()
{
Parser parser; Token token;
var number = new NumberLiteral("number", NumberOptions.AllowSign);
parser = TestHelper.CreateParser(number);
token = parser.ParseInput("-500");
Assert.Equal(-500, token.Value);
}
[Fact]
public void TestNumber_CSharp()
{
Parser parser; Token token;
double eps = 0.0001;
parser = TestHelper.CreateParser(TerminalFactory.CreateCSharpNumber("Number"));
//Simple integers and suffixes
token = parser.ParseInput("123 ");
CheckType(token, typeof(int));
Assert.True(token.Details != null, "ScanDetails object not found in token.");
Assert.True((int)token.Value == 123, "Failed to read int value");
token = parser.ParseInput(int.MaxValue.ToString());
CheckType(token, typeof(int));
Assert.True((int)token.Value == int.MaxValue, "Failed to read Int32.MaxValue.");
token = parser.ParseInput(ulong.MaxValue.ToString());
CheckType(token, typeof(ulong));
Assert.True((ulong)token.Value == ulong.MaxValue, "Failed to read uint64.MaxValue value");
token = parser.ParseInput("123U ");
CheckType(token, typeof(uint));
Assert.True((uint)token.Value == 123, "Failed to read uint value");
token = parser.ParseInput("123L ");
CheckType(token, typeof(long));
Assert.True((long)token.Value == 123, "Failed to read long value");
token = parser.ParseInput("123uL ");
CheckType(token, typeof(ulong));
Assert.True((ulong)token.Value == 123, "Failed to read ulong value");
//Hex representation
token = parser.ParseInput("0x012 ");
CheckType(token, typeof(int));
Assert.True((int)token.Value == 0x012, "Failed to read hex int value");
token = parser.ParseInput("0x12U ");
CheckType(token, typeof(uint));
Assert.True((uint)token.Value == 0x012, "Failed to read hex uint value");
token = parser.ParseInput("0x012L ");
CheckType(token, typeof(long));
Assert.True((long)token.Value == 0x012, "Failed to read hex long value");
token = parser.ParseInput("0x012uL ");
CheckType(token, typeof(ulong));
Assert.True((ulong)token.Value == 0x012, "Failed to read hex ulong value");
//Floating point types
token = parser.ParseInput("123.4 ");
CheckType(token, typeof(double));
Assert.True(Math.Abs((double)token.Value - 123.4) < eps, "Failed to read double value #1");
token = parser.ParseInput("1234e-1 ");
CheckType(token, typeof(double));
Assert.True(Math.Abs((double)token.Value - 1234e-1) < eps, "Failed to read double value #2");
token = parser.ParseInput("12.34e+01 ");
CheckType(token, typeof(double));
Assert.True(Math.Abs((double)token.Value - 123.4) < eps, "Failed to read double value #3");
token = parser.ParseInput("0.1234E3 ");
CheckType(token, typeof(double));
Assert.True(Math.Abs((double)token.Value - 123.4) < eps, "Failed to read double value #4");
token = parser.ParseInput("123.4f ");
CheckType(token, typeof(float));
Assert.True(Math.Abs((float)token.Value - 123.4) < eps, "Failed to read float(single) value");
token = parser.ParseInput("123.4m ");
CheckType(token, typeof(decimal));
Assert.True(Math.Abs((decimal)token.Value - 123.4m) < Convert.ToDecimal(eps), "Failed to read decimal value");
token = parser.ParseInput("123. ", useTerminator: false); //should ignore dot and read number as int. compare it to python numbers - see below
CheckType(token, typeof(int));
Assert.True((int)token.Value == 123, "Failed to read int value with trailing dot");
//Quick parse
token = parser.ParseInput("1 ");
CheckType(token, typeof(int));
//When going through quick parse path (for one-digit numbers), the NumberScanInfo record is not created and hence is absent in Attributes
Assert.True(token.Details == null, "Quick parse test failed: ScanDetails object is found in token - quick parse path should not produce this object.");
Assert.True((int)token.Value == 1, "Failed to read quick-parse value");
}
[Fact]
public void TestNumber_VB()
{
Parser parser; Token token;
double eps = 0.0001;
parser = TestHelper.CreateParser(TerminalFactory.CreateVbNumber("Number"));
//Simple integer
token = parser.ParseInput("123 ");
CheckType(token, typeof(int));
Assert.True(token.Details != null, "ScanDetails object not found in token.");
Assert.True((int)token.Value == 123, "Failed to read int value");
//Test all suffixes
token = parser.ParseInput("123S ");
CheckType(token, typeof(short));
Assert.True((short)token.Value == 123, "Failed to read short value");
token = parser.ParseInput("123I ");
CheckType(token, typeof(int));
Assert.True((int)token.Value == 123, "Failed to read int value");
token = parser.ParseInput("123% ");
CheckType(token, typeof(int));
Assert.True((int)token.Value == 123, "Failed to read int value");
token = parser.ParseInput("123L ");
CheckType(token, typeof(long));
Assert.True((long)token.Value == 123, "Failed to read long value");
token = parser.ParseInput("123& ");
CheckType(token, typeof(long));
Assert.True((long)token.Value == 123, "Failed to read long value");
token = parser.ParseInput("123us ");
CheckType(token, typeof(ushort));
Assert.True((ushort)token.Value == 123, "Failed to read ushort value");
token = parser.ParseInput("123ui ");
CheckType(token, typeof(uint));
Assert.True((uint)token.Value == 123, "Failed to read uint value");
token = parser.ParseInput("123ul ");
CheckType(token, typeof(ulong));
Assert.True((ulong)token.Value == 123, "Failed to read ulong value");
//Hex and octal
token = parser.ParseInput("&H012 ");
CheckType(token, typeof(int));
Assert.True((int)token.Value == 0x012, "Failed to read hex int value");
token = parser.ParseInput("&H012L ");
CheckType(token, typeof(long));
Assert.True((long)token.Value == 0x012, "Failed to read hex long value");
token = parser.ParseInput("&O012 ");
CheckType(token, typeof(int));
Assert.True((int)token.Value == 10, "Failed to read octal int value"); //12(oct) = 10(dec)
token = parser.ParseInput("&o012L ");
CheckType(token, typeof(long));
Assert.True((long)token.Value == 10, "Failed to read octal long value");
//Floating point types
token = parser.ParseInput("123.4 ");
CheckType(token, typeof(double));
Assert.True(Math.Abs((double)token.Value - 123.4) < eps, "Failed to read double value #1");
token = parser.ParseInput("1234e-1 ");
CheckType(token, typeof(double));
Assert.True(Math.Abs((double)token.Value - 1234e-1) < eps, "Failed to read double value #2");
token = parser.ParseInput("12.34e+01 ");
CheckType(token, typeof(double));
Assert.True(Math.Abs((double)token.Value - 123.4) < eps, "Failed to read double value #3");
token = parser.ParseInput("0.1234E3 ");
CheckType(token, typeof(double));
Assert.True(Math.Abs((double)token.Value - 123.4) < eps, "Failed to read double value #4");
token = parser.ParseInput("123.4R ");
CheckType(token, typeof(double));
Assert.True(Math.Abs((double)token.Value - 123.4) < eps, "Failed to read double value #5");
token = parser.ParseInput("123.4# ");
CheckType(token, typeof(double));
Assert.True(Math.Abs((double)token.Value - 123.4) < eps, "Failed to read double value #6");
token = parser.ParseInput("123.4f ");
CheckType(token, typeof(float));
Assert.True(Math.Abs((float)token.Value - 123.4) < eps, "Failed to read float(single) value");
token = parser.ParseInput("123.4! ");
CheckType(token, typeof(float));
Assert.True(Math.Abs((float)token.Value - 123.4) < eps, "Failed to read float(single) value");
token = parser.ParseInput("123.4D ");
CheckType(token, typeof(decimal));
Assert.True(Math.Abs((decimal)token.Value - 123.4m) < Convert.ToDecimal(eps), "Failed to read decimal value");
token = parser.ParseInput("123.4@ ");
CheckType(token, typeof(decimal));
Assert.True(Math.Abs((decimal)token.Value - 123.4m) < Convert.ToDecimal(eps), "Failed to read decimal value");
//Quick parse
token = parser.ParseInput("1 ");
CheckType(token, typeof(int));
//When going through quick parse path (for one-digit numbers), the NumberScanInfo record is not created and hence is absent in Attributes
Assert.True(token.Details == null, "Quick parse test failed: ScanDetails object is found in token - quick parse path should not produce this object.");
Assert.True((int)token.Value == 1, "Failed to read quick-parse value");
}
[Fact]
public void TestNumber_Python()
{
Parser parser; Token token;
double eps = 0.0001;
parser = TestHelper.CreateParser(TerminalFactory.CreatePythonNumber("Number"));
//Simple integers and suffixes
token = parser.ParseInput("123 ");
CheckType(token, typeof(int));
Assert.True(token.Details != null, "ScanDetails object not found in token.");
Assert.True((int)token.Value == 123, "Failed to read int value");
token = parser.ParseInput("123L ");
CheckType(token, typeof(long));
Assert.True((long)token.Value == 123, "Failed to read long value");
//Hex representation
token = parser.ParseInput("0x012 ");
CheckType(token, typeof(int));
Assert.True((int)token.Value == 0x012, "Failed to read hex int value");
token = parser.ParseInput("0x012l "); //with small "L"
CheckType(token, typeof(long));
Assert.True((long)token.Value == 0x012, "Failed to read hex long value");
//Floating point types
token = parser.ParseInput("123.4 ");
CheckType(token, typeof(double));
Assert.True(Math.Abs((double)token.Value - 123.4) < eps, "Failed to read double value #1");
token = parser.ParseInput("1234e-1 ");
CheckType(token, typeof(double));
Assert.True(Math.Abs((double)token.Value - 1234e-1) < eps, "Failed to read double value #2");
token = parser.ParseInput("12.34e+01 ");
CheckType(token, typeof(double));
Assert.True(Math.Abs((double)token.Value - 123.4) < eps, "Failed to read double value #3");
token = parser.ParseInput("0.1234E3 ");
CheckType(token, typeof(double));
Assert.True(Math.Abs((double)token.Value - 123.4) < eps, "Failed to read double value #4");
token = parser.ParseInput(".1234 ");
CheckType(token, typeof(double));
Assert.True(Math.Abs((double)token.Value - 0.1234) < eps, "Failed to read double value with leading dot");
token = parser.ParseInput("123. ");
CheckType(token, typeof(double));
Assert.True(Math.Abs((double)token.Value - 123.0) < eps, "Failed to read double value with trailing dot");
//Big integer
string sbig = "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"; //100 digits
token = parser.ParseInput(sbig);
Assert.True(token.Value.ToString() == sbig, "Failed to read big integer value");
//Quick parse
token = parser.ParseInput("1 ");
CheckType(token, typeof(int));
Assert.True(token.Details == null, "Quick parse test failed: ScanDetails object is found in token - quick parse path should produce this object.");
Assert.True((int)token.Value == 1, "Failed to read quick-parse value");
}
[Fact]
public void TestNumber_Scheme()
{
Parser parser; Token token;
double eps = 0.0001;
parser = TestHelper.CreateParser(TerminalFactory.CreateSchemeNumber("Number"));
//Just test default float value (double), and exp symbols (e->double, s->single, d -> double)
token = parser.ParseInput("123.4 ");
CheckType(token, typeof(double));
Assert.True(Math.Abs((double)token.Value - 123.4) < eps, "Failed to read double value #1");
token = parser.ParseInput("1234e-1 ");
CheckType(token, typeof(double));
Assert.True(Math.Abs((double)token.Value - 1234e-1) < eps, "Failed to read single value #2");
token = parser.ParseInput("1234s-1 ");
CheckType(token, typeof(float));
Assert.True(Math.Abs((float)token.Value - 1234e-1) < eps, "Failed to read single value #3");
token = parser.ParseInput("12.34d+01 ");
CheckType(token, typeof(double));
Assert.True(Math.Abs((double)token.Value - 123.4) < eps, "Failed to read double value #4");
}//method
[Fact]
public void TestNumber_WithUnderscore()
{
Parser parser; Token token;
var number = new NumberLiteral("number", NumberOptions.AllowUnderscore);
parser = TestHelper.CreateParser(number);
//Simple integers and suffixes
token = parser.ParseInput("1_234_567");
CheckType(token, typeof(int));
Assert.True((int)token.Value == 1234567, "Failed to read int value with underscores.");
}//method
//There was a bug discovered in NumberLiteral - it cannot parse appropriately the int.MinValue value.
// This test ensures that the issue is fixed.
[Fact]
public void TestNumber_MinMaxValues()
{
Parser parser; Token token;
var number = new NumberLiteral("number", NumberOptions.AllowSign);
number.DefaultIntTypes = new TypeCode[] { TypeCode.Int32 };
parser = TestHelper.CreateParser(number);
var s = int.MinValue.ToString();
token = parser.ParseInput(s);
Assert.False(token.IsError(), "Failed to scan int.MinValue, scanner returned an error.");
CheckType(token, typeof(int));
Assert.True((int)token.Value == int.MinValue, "Failed to scan int.MinValue, scanned value does not match.");
s = int.MaxValue.ToString();
token = parser.ParseInput(s);
Assert.False(token.IsError(), "Failed to scan int.MaxValue, scanner returned an error.");
CheckType(token, typeof(int));
Assert.True((int)token.Value == int.MaxValue, "Failed to read int.MaxValue");
}//method
private void CheckType(Token token, Type type)
{
Assert.False(token == null, "TryMatch returned null, while token was expected.");
Type vtype = token.Value.GetType();
Assert.True(vtype == type, "Invalid target type, expected " + type.ToString() + ", found: " + vtype);
}
}//class
}//namespace

View File

@ -0,0 +1,151 @@
using Sanchime.Irony.Parsing.Grammars;
using Sanchime.Irony.Parsing.Parsers;
using Sanchime.Irony.Parsing.Terminals;
using Xunit;
namespace Sanchime.Irony.Tests
{
public class OperatorTests
{
#region Grammars
public class OperatorGrammar : Grammar
{
public OperatorGrammar()
{
var id = new IdentifierTerminal("id");
var binOp = new NonTerminal("binOp");
var unOp = new NonTerminal("unOp");
var expr = new NonTerminal("expr");
var binExpr = new NonTerminal("binExpr");
var unExpr = new NonTerminal("unExpr");
Root = expr;
expr.Rule = id | binExpr | unExpr;
binExpr.Rule = expr + binOp + expr;
binOp.Rule = ToTerm("+") | "-" | "*" | "/";
unExpr.Rule = unOp + expr;
unOp.Rule = ToTerm("+") | "-";
RegisterOperators(10, "+", "-");
RegisterOperators(20, "*", "/");
MarkTransient(expr, binOp, unOp);
}
}//operator grammar class
public class OperatorGrammarHintsOnTerms : Grammar
{
public OperatorGrammarHintsOnTerms()
{
var id = new IdentifierTerminal("id");
var binOp = new NonTerminal("binOp");
var unOp = new NonTerminal("unOp");
var expr = new NonTerminal("expr");
var binExpr = new NonTerminal("binExpr");
var unExpr = new NonTerminal("unExpr");
Root = expr;
expr.Rule = id | binExpr | unExpr;
binExpr.Rule = expr + binOp + expr;
binOp.Rule = ToTerm("+") | "-" | "*" | "/";
unExpr.Rule = unOp + expr;
var unOpHint = ImplyPrecedenceHere(30); // Force higher precedence than multiplication precedence
unOp.Rule = unOpHint + "+" | unOpHint + "-";
RegisterOperators(10, "+", "-");
RegisterOperators(20, "*", "/");
MarkTransient(expr, binOp, unOp);
}
}//operator grammar class
public class OperatorGrammarHintsOnNonTerms : Grammar
{
public OperatorGrammarHintsOnNonTerms()
{
var id = new IdentifierTerminal("id");
var binOp = new NonTerminal("binOp");
var unOp = new NonTerminal("unOp");
var expr = new NonTerminal("expr");
var binExpr = new NonTerminal("binExpr");
var unExpr = new NonTerminal("unExpr");
Root = expr;
expr.Rule = id | binExpr | unExpr;
binExpr.Rule = expr + binOp + expr;
binOp.Rule = ToTerm("+") | "-" | "*" | "/";
var unOpHint = ImplyPrecedenceHere(30); // Force higher precedence than multiplication precedence
unExpr.Rule = unOpHint + unOp + expr;
unOp.Rule = ToTerm("+") | "-";
RegisterOperators(10, "+", "-");
RegisterOperators(20, "*", "/");
MarkTransient(expr, binOp, unOp);
}
}//operator grammar class
#endregion Grammars
[Fact]
public void TestOperatorPrecedence()
{
var grammar = new OperatorGrammar();
var parser = new Parser(grammar);
TestHelper.CheckGrammarErrors(parser);
var parseTree = parser.Parse("x + y * z");
TestHelper.CheckParseErrors(parseTree);
Assert.True(parseTree.Root != null, "Root not found.");
Assert.True(parseTree.Root.Term.Name == "binExpr", "Expected binExpr.");
Assert.True(parseTree.Root.ChildNodes[1].Term.Name == "+", "Expected + operator."); //check that top operator is "+", not "*"
parseTree = parser.Parse("x * y + z");
TestHelper.CheckParseErrors(parseTree);
Assert.True(parseTree.Root != null, "Root not found.");
Assert.True(parseTree.Root.Term.Name == "binExpr", "Expected binExpr.");
Assert.True(parseTree.Root.ChildNodes[1].Term.Name == "+", "Expected + operator."); //check that top operator is "+", not "*"
parseTree = parser.Parse("-x * y"); //should be interpreted as -(x*y), so top operator should be -
TestHelper.CheckParseErrors(parseTree);
Assert.True(parseTree.Root != null, "Root not found.");
Assert.True(parseTree.Root.Term.Name == "unExpr", "Expected unExpr.");
Assert.True(parseTree.Root.ChildNodes[0].Term.Name == "-", "Expected - operator."); //check that top operator is "+", not "*"
}
//These tests check how implied precedence work. We use ImpliedPrecedenceHint to set precedence on unary +,- operators and make it
// higher than binary +,-. We make it even higher than * precedence, so that -x*y is interpreted as '(-x)*y', not like '-(x*y)'
// the second interpretation is chosen when there are no hints.
[Fact]
public void TestOperatorPrecedenceWithHints()
{
var grammar = new OperatorGrammarHintsOnTerms();
var parser = new Parser(grammar);
TestHelper.CheckGrammarErrors(parser);
var parseTree = parser.Parse("x + y * z");
TestHelper.CheckParseErrors(parseTree);
Assert.True(parseTree.Root != null, "Root not found.");
Assert.True(parseTree.Root.Term.Name == "binExpr", "Expected binExpr.");
Assert.True(parseTree.Root.ChildNodes[1].Term.Name == "+", "Expected + operator."); //check that top operator is "+", not "*"
parseTree = parser.Parse("-x * y"); //should be interpreted as (-x)*y, so top operator should be *
TestHelper.CheckParseErrors(parseTree);
Assert.True(parseTree.Root != null, "Root not found.");
Assert.True(parseTree.Root.Term.Name == "binExpr", "Expected binExpr.");
Assert.True(parseTree.Root.ChildNodes[1].Term.Name == "*", "Expected * operator."); //check that top operator is "+", not "*"
var grammar2 = new OperatorGrammarHintsOnNonTerms();
parser = new Parser(grammar2);
TestHelper.CheckGrammarErrors(parser);
parseTree = parser.Parse("x + y * z");
TestHelper.CheckParseErrors(parseTree);
Assert.True(parseTree.Root != null, "Root not found.");
Assert.True(parseTree.Root.Term.Name == "binExpr", "Expected binExpr.");
Assert.True(parseTree.Root.ChildNodes[1].Term.Name == "+", "Expected + operator."); //check that top operator is "+", not "*"
parseTree = parser.Parse("-x*y"); //should be interpreted as (-x)*y, so top operator should be *
TestHelper.CheckParseErrors(parseTree);
Assert.True(parseTree.Root != null, "Root not found.");
Assert.True(parseTree.Root.Term.Name == "binExpr", "Expected binExpr.");
Assert.True(parseTree.Root.ChildNodes[1].Term.Name == "*", "Expected * operator."); //check that top operator is "+", not "*"
}
}//class
}//namespace

View File

@ -0,0 +1,29 @@
using Sanchime.Irony.Parsing.Parsers;
using Sanchime.Irony.Parsing.Scanners;
using Sanchime.Irony.Parsing.Terminals;
using System.Text.RegularExpressions;
using Xunit;
namespace Sanchime.Irony.Tests
{
public class RegexLiteralTests
{
//The following test method and a fix are contributed by ashmind codeplex user
[Fact]
public void TestRegExLiteral()
{
Parser parser; Token token;
var term = new RegexLiteral("RegEx");
parser = TestHelper.CreateParser(term);
token = parser.ParseInput(@"/abc\\\/de/gm ");
Assert.False(token == null, "Failed to produce a token on valid string.");
Assert.True(term == token.Terminal, "Failed to scan a string - invalid Terminal in the returned token.");
Assert.False(token.Value == null, "Token Value field is null - should be Regex object.");
var regex = token.Value as Regex;
Assert.False(regex == null, "Failed to create Regex object.");
var match = regex.Match(@"00abc\/de00");
Assert.True(match.Index == 2, "Failed to match a regular expression");
}
}//class
}//namespace

View File

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
<PackageReference Include="xunit" Version="2.4.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Irony.Interpreter\Sanchime.Irony.Interpreter.csproj" />
<ProjectReference Include="..\Irony\Sanchime.Irony.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,142 @@
using Sanchime.Irony.Parsing.Parsers;
using Sanchime.Irony.Parsing.Scanners;
using Sanchime.Irony.Parsing.Terminals;
using Xunit;
namespace Sanchime.Irony.Tests
{
public class StringLiteralTests
{
//handy option for stringLiteral tests: we use single quotes in test strings, and they are replaced by double quotes here
private static string ReplaceQuotes(string input)
{
return input.Replace("'", "\"");
}
//The following test method and a fix are contributed by ashmind codeplex user
[Fact]
public void TestString_QuoteJustBeforeEof()
{
Parser parser; Token token;
parser = TestHelper.CreateParser(new StringLiteral("String", "'"));
token = parser.ParseInput(@"'");
Assert.True(TokenCategory.Error == token.Terminal.Category, "Incorrect string was not parsed as syntax error.");
}
[Fact]
public void TestString_Python()
{
Parser parser; Token token;
parser = TestHelper.CreateParser(TerminalFactory.CreatePythonString("String"));
//1. Single quotes
token = parser.ParseInput(@"'00\a\b\t\n\v\f\r\'\\00' ");
Assert.True((string)token.Value == "00\a\b\t\n\v\f\r\'\\00", "Failed to process escaped characters.");
token = parser.ParseInput("'abcd\nefg' ");
Assert.True(token.IsError(), "Failed to detect erroneous multi-line string.");
token = parser.ParseInput("'''abcd\nefg''' ");
Assert.True((string)token.Value == "abcd\nefg", "Failed to process line break in triple-quote string.");
token = parser.ParseInput(@"'''abcd\" + "\n" + "efg''' ");
Assert.True((string)token.Value == "abcd\nefg", "Failed to process escaped line-break char.");
token = parser.ParseInput(@"r'00\a\b\t\n\v\f\r00' ");
Assert.True((string)token.Value == @"00\a\b\t\n\v\f\r00", "Failed to process string with disabled escapes.");
//2. Double quotes - we use TryMatchDoubles which replaces single quotes with doubles and then calls TryMatch
token = parser.ParseInput(ReplaceQuotes(@"'00\a\b\t\n\v\f\r\'\\00' "));
Assert.True((string)token.Value == "00\a\b\t\n\v\f\r\"\\00", "Failed to process escaped characters.");
token = parser.ParseInput(ReplaceQuotes("'abcd\nefg' "));
Assert.True(token.IsError(), "Failed to detect erroneous multi-line string. (Double quotes)");
token = parser.ParseInput(ReplaceQuotes("'''abcd\nefg''' "));
Assert.True((string)token.Value == "abcd\nefg", "Failed to process line break in triple-quote string. (Double quotes)");
token = parser.ParseInput(ReplaceQuotes(@"'''abcd\" + "\n" + "efg''' "));
Assert.True((string)token.Value == "abcd\nefg", "Failed to process escaped line-break char. (Double quotes)");
token = parser.ParseInput(ReplaceQuotes(@"r'00\a\b\t\n\v\f\r00' "));
Assert.True((string)token.Value == @"00\a\b\t\n\v\f\r00", "Failed to process string with disabled escapes. (Double quotes)");
}//method
[Fact]
public void TestString_CSharp()
{
Parser parser; Token token;
parser = TestHelper.CreateParser(TerminalFactory.CreateCSharpString("String"));
token = parser.ParseInput('"' + @"abcd\\" + '"' + " ");
Assert.True((string)token.Value == @"abcd\", "Failed to process double escape char at the end of the string.");
token = parser.ParseInput('"' + @"abcd\\\" + '"' + "efg" + '"' + " ");
Assert.True((string)token.Value == @"abcd\" + '"' + "efg", @"Failed to process '\\\ + double-quote' inside the string.");
//with Escapes
token = parser.ParseInput(ReplaceQuotes(@"'00\a\b\t\n\v\f\r\'\\00' "));
Assert.True((string)token.Value == "00\a\b\t\n\v\f\r\"\\00", "Failed to process escaped characters.");
token = parser.ParseInput(ReplaceQuotes("'abcd\nefg' "));
Assert.True(token.IsError(), "Failed to detect erroneous multi-line string.");
//with disabled escapes
token = parser.ParseInput(ReplaceQuotes(@"@'00\a\b\t\n\v\f\r00' "));
Assert.True((string)token.Value == @"00\a\b\t\n\v\f\r00", "Failed to process @-string with disabled escapes.");
token = parser.ParseInput(ReplaceQuotes("@'abc\ndef' "));
Assert.True((string)token.Value == "abc\ndef", "Failed to process @-string with linebreak.");
//Unicode and hex
token = parser.ParseInput(ReplaceQuotes(@"'abc\u0040def' "));
Assert.True((string)token.Value == "abc@def", "Failed to process unicode escape \\u.");
token = parser.ParseInput(ReplaceQuotes(@"'abc\U00000040def' "));
Assert.True((string)token.Value == "abc@def", "Failed to process unicode escape \\u.");
token = parser.ParseInput(ReplaceQuotes(@"'abc\x0040xyz' "));
Assert.True((string)token.Value == "abc@xyz", "Failed to process hex escape (4 digits).");
token = parser.ParseInput(ReplaceQuotes(@"'abc\x040xyz' "));
Assert.True((string)token.Value == "abc@xyz", "Failed to process hex escape (3 digits).");
token = parser.ParseInput(ReplaceQuotes(@"'abc\x40xyz' "));
Assert.True((string)token.Value == "abc@xyz", "Failed to process hex escape (2 digits).");
//octals
token = parser.ParseInput(ReplaceQuotes(@"'abc\0601xyz' ")); //the last digit "1" should not be included in octal number
Assert.True((string)token.Value == "abc01xyz", "Failed to process octal escape (3 + 1 digits).");
token = parser.ParseInput(ReplaceQuotes(@"'abc\060xyz' "));
Assert.True((string)token.Value == "abc0xyz", "Failed to process octal escape (3 digits).");
token = parser.ParseInput(ReplaceQuotes(@"'abc\60xyz' "));
Assert.True((string)token.Value == "abc0xyz", "Failed to process octal escape (2 digits).");
token = parser.ParseInput(ReplaceQuotes(@"'abc\0xyz' "));
Assert.True((string)token.Value == "abc\0xyz", "Failed to process octal escape (1 digit).");
}
[Fact]
public void TestString_CSharpChar()
{
Parser parser; Token token;
parser = TestHelper.CreateParser(TerminalFactory.CreateCSharpChar("Char"));
token = parser.ParseInput("'a' ");
Assert.True((char)token.Value == 'a', "Failed to process char.");
token = parser.ParseInput(@"'\n' ");
Assert.True((char)token.Value == '\n', "Failed to process new-line char.");
token = parser.ParseInput(@"'' ");
Assert.True(token.IsError(), "Failed to recognize empty quotes as invalid char literal.");
token = parser.ParseInput(@"'abc' ");
Assert.True(token.IsError(), "Failed to recognize multi-char sequence as invalid char literal.");
//Note: unlike strings, c# char literals don't allow the "@" prefix
}
[Fact]
public void TestString_VB()
{
Parser parser; Token token;
parser = TestHelper.CreateParser(TerminalFactory.CreateVbString("String"));
//VB has no escapes - so make sure term doesn't catch any escapes
token = parser.ParseInput(ReplaceQuotes(@"'00\a\b\t\n\v\f\r\\00' "));
Assert.True((string)token.Value == @"00\a\b\t\n\v\f\r\\00", "Failed to process string with \\ characters.");
token = parser.ParseInput(ReplaceQuotes("'abcd\nefg' "));
Assert.True(token.IsError(), "Failed to detect erroneous multi-line string.");
token = parser.ParseInput(ReplaceQuotes("'abcd''efg' "));
Assert.True((string)token.Value == "abcd\"efg", "Failed to process a string with doubled double-quote char.");
//Test char suffix "c"
token = parser.ParseInput(ReplaceQuotes("'A'c "));
Assert.True((char)token.Value == 'A', "Failed to process a character");
token = parser.ParseInput(ReplaceQuotes("''c "));
Assert.True(token.IsError(), "Failed to detect an error for an empty char.");
token = parser.ParseInput(ReplaceQuotes("'ab'C "));
Assert.True(token.IsError(), "Failed to detect error in multi-char sequence.");
}
}//class
}//namespace

View File

@ -0,0 +1,74 @@
using Sanchime.Irony.Parsing.Grammars;
using Sanchime.Irony.Parsing.Parsers;
using Sanchime.Irony.Parsing.Scanners;
using Sanchime.Irony.Parsing.Terminals;
using System;
using Xunit;
namespace Sanchime.Irony.Tests
{
public static class TestHelper
{
//A skeleton for a grammar with a single terminal, followed by optional terminator
private class TerminalTestGrammar : Grammar
{
public string Terminator;
public TerminalTestGrammar(Terminal terminal, string terminator = null) : base(caseSensitive: true)
{
Terminator = terminator;
var rule = new BnfExpression(terminal);
if (Terminator != null)
{
MarkReservedWords(Terminator);
rule += Terminator;
}
Root = new NonTerminal("Root");
Root.Rule = rule;
}
}//class
public static Parser CreateParser(Terminal terminal, string terminator = "end")
{
var grammar = new TerminalTestGrammar(terminal, terminator);
var parser = new Parser(grammar);
CheckGrammarErrors(parser);
return parser;
}
public static void CheckGrammarErrors(Parser parser)
{
var errors = parser.Language.Errors;
if (errors.Count > 0)
throw new Exception("Unexpected grammar contains error(s): " + string.Join("\n", errors));
}
public static void CheckParseErrors(ParseTree parseTree)
{
if (parseTree.HasErrors())
throw new Exception("Unexpected parse error(s): " + string.Join("\n", parseTree.ParserMessages));
}
public static Token ParseInput(this Parser parser, string input, bool useTerminator = true)
{
var g = (TerminalTestGrammar)parser.Language.Grammar;
useTerminator &= g.Terminator != null;
if (useTerminator)
input += " " + g.Terminator;
var tree = parser.Parse(input);
//If error, then return this error token, this is probably what is expected.
var first = tree.Tokens[0];
if (first.IsError())
return first;
//Verify that last or before-last token is a terminator
if (useTerminator)
{
Assert.True(tree.Tokens.Count >= 2, "Wrong # of tokens - expected at least 2. Input: " + input);
var count = tree.Tokens.Count;
//The last is EOF, the one before last should be a terminator
Assert.True(g.Terminator == tree.Tokens[count - 2].Text, "Input terminator not found in the second token. Input: " + input);
}
return tree.Tokens[0];
}
}//class
}

View File

@ -0,0 +1,119 @@
using Sanchime.Irony.Parsing.Grammars;
using Sanchime.Irony.Parsing.Parsers.SpecialActionsHints;
using Sanchime.Irony.Parsing.Terminals;
namespace Sanchime.Irony.Tests.TokenPreviewResolution
{
[Language("Grammar with conflicts, no hints", "1.1", "Grammar with conflicts, no hints.")]
public class ConflictGrammarNoHints : Grammar
{
public ConflictGrammarNoHints()
: base(true)
{
var name = new IdentifierTerminal("id");
var stmt = new NonTerminal("Statement");
var stmtList = new NonTerminal("StatementList");
var fieldModifier = new NonTerminal("fieldModifier");
var propModifier = new NonTerminal("propModifier");
var methodModifier = new NonTerminal("methodModifier");
var fieldModifierList = new NonTerminal("fieldModifierList");
var propModifierList = new NonTerminal("propModifierList");
var methodModifierList = new NonTerminal("methodModifierList");
var fieldDef = new NonTerminal("fieldDef");
var propDef = new NonTerminal("propDef");
var methodDef = new NonTerminal("methodDef");
//Rules
Root = stmtList;
stmtList.Rule = MakePlusRule(stmtList, stmt);
stmt.Rule = fieldDef | propDef | methodDef;
fieldDef.Rule = fieldModifierList + name + name + ";";
propDef.Rule = propModifierList + name + name + "{" + "}";
methodDef.Rule = methodModifierList + name + name + "(" + ")" + "{" + "}";
fieldModifierList.Rule = MakeStarRule(fieldModifierList, fieldModifier);
propModifierList.Rule = MakeStarRule(propModifierList, propModifier);
methodModifierList.Rule = MakeStarRule(methodModifierList, methodModifier);
// That's the key of the problem: 3 modifiers have common members
// so parser automaton has hard time deciding which modifiers list to produce -
// is it a field, prop or method we are beginning to parse?
fieldModifier.Rule = ToTerm("public") | "private" | "readonly" | "volatile";
propModifier.Rule = ToTerm("public") | "private" | "readonly" | "override";
methodModifier.Rule = ToTerm("public") | "private" | "override";
MarkReservedWords("public", "private", "readonly", "volatile", "override");
}
}
[Language("Grammar with conflicts #2", "1.1", "Conflict grammar with hints added to productions.")]
public class ConflictGrammarWithHintsInRules : Grammar
{
public ConflictGrammarWithHintsInRules() : base(true)
{
var name = new IdentifierTerminal("id");
var definition = new NonTerminal("definition");
var fieldDef = new NonTerminal("fieldDef");
var propDef = new NonTerminal("propDef");
var fieldModifier = new NonTerminal("fieldModifier");
var propModifier = new NonTerminal("propModifier");
definition.Rule = fieldDef | propDef;
fieldDef.Rule = fieldModifier + name + name + ";";
propDef.Rule = propModifier + name + name + "{" + "}";
var fieldHint = ReduceIf(";", comesBefore: "{");
fieldModifier.Rule = "public" + fieldHint | "private" + fieldHint | "readonly";
propModifier.Rule = ToTerm("public") | "private" | "override";
Root = definition;
}
}//class
[Language("Grammar with conflicts #4", "1.1", "Test conflict grammar with conflicts and hints: hints are added to non-terminals.")]
public class ConflictGrammarWithHintsOnTerms : Grammar
{
public ConflictGrammarWithHintsOnTerms()
: base(true)
{
var name = new IdentifierTerminal("id");
var stmt = new NonTerminal("Statement");
var stmtList = new NonTerminal("StatementList");
var fieldModifier = new NonTerminal("fieldModifier");
var propModifier = new NonTerminal("propModifier");
var methodModifier = new NonTerminal("methodModifier");
var fieldModifierList = new NonTerminal("fieldModifierList");
var propModifierList = new NonTerminal("propModifierList");
var methodModifierList = new NonTerminal("methodModifierList");
var fieldDef = new NonTerminal("fieldDef");
var propDef = new NonTerminal("propDef");
var methodDef = new NonTerminal("methodDef");
//Rules
Root = stmtList;
stmtList.Rule = MakePlusRule(stmtList, stmt);
stmt.Rule = fieldDef | propDef | methodDef;
fieldDef.Rule = fieldModifierList + name + name + ";";
propDef.Rule = propModifierList + name + name + "{" + "}";
methodDef.Rule = methodModifierList + name + name + "(" + ")" + "{" + "}";
fieldModifierList.Rule = MakeStarRule(fieldModifierList, fieldModifier);
propModifierList.Rule = MakeStarRule(propModifierList, propModifier);
methodModifierList.Rule = MakeStarRule(methodModifierList, methodModifier);
fieldModifier.Rule = ToTerm("public") | "private" | "readonly" | "volatile";
propModifier.Rule = ToTerm("public") | "private" | "readonly" | "override";
methodModifier.Rule = ToTerm("public") | "private" | "override";
// conflict resolution
var fieldHint = new TokenPreviewHint(PreferredActionType.Reduce, thisSymbol: ";", comesBefore: new string[] { "(", "{" });
fieldModifier.AddHintToAll(fieldHint);
fieldModifierList.AddHintToAll(fieldHint);
var propHint = new TokenPreviewHint(PreferredActionType.Reduce, thisSymbol: "{", comesBefore: new string[] { ";", "(" });
propModifier.AddHintToAll(propHint);
propModifierList.AddHintToAll(propHint);
MarkReservedWords("public", "private", "readonly", "volatile", "override");
}
}
}

View File

@ -0,0 +1,117 @@
using Sanchime.Irony.Parsing.Grammars;
using Sanchime.Irony.Parsing.Parsers;
using System.Linq;
using Xunit;
namespace Sanchime.Irony.Tests.TokenPreviewResolution
{
public class ConflictResolutionTests
{
// samples to be parsed
private const string FieldSample = "private int SomeField;";
private const string PropertySample = "public string Name {}";
private const string FieldListSample = "private int Field1; public string Field2;";
private const string MixedListSample = @"
public int Size {}
private string TableName;
override void Run()
{
}";
// Full grammar, no hints - expect errors ---------------------------------------------------------------------
[Fact]
public void TestConflictGrammarNoHints_HasErrors()
{
var grammar = new ConflictGrammarNoHints();
var parser = new Parser(grammar);
Assert.True(parser.Language.Errors.Count > 0);
//Cannot parse mixed list
var sample = MixedListSample;
var tree = parser.Parse(sample);
Assert.NotNull(tree);
Assert.True(tree.HasErrors());
}
// Hints in Rules --------------------------------------------------------------------------
[Fact]
public void TestConflictGrammarWithHintsOnRules()
{
var grammar = new ConflictGrammarWithHintsInRules();
var parser = new Parser(grammar);
Assert.True(parser.Language.Errors.Count == 0);
// Field sample
var sample = FieldSample;
var tree = parser.Parse(sample);
Assert.NotNull(tree);
Assert.False(tree.HasErrors());
Assert.NotNull(tree.Root);
var term = tree.Root.Term as NonTerminal;
Assert.NotNull(term);
Assert.Equal("definition", term.Name);
Assert.Single(tree.Root.ChildNodes);
var modNode = tree.Root.ChildNodes[0].ChildNodes[0];
Assert.Equal("fieldModifier", modNode.Term.Name);
//Property
sample = PropertySample;
tree = parser.Parse(sample);
Assert.NotNull(tree);
Assert.False(tree.HasErrors());
Assert.NotNull(tree.Root);
term = tree.Root.Term as NonTerminal;
Assert.NotNull(term);
Assert.Equal("definition", term.Name);
Assert.Single(tree.Root.ChildNodes);
modNode = tree.Root.ChildNodes[0].ChildNodes[0];
Assert.Equal("propModifier", modNode.Term.Name);
}
//Hints on terms ---------------------------------------------------------------------
[Fact]
public void TestConflictGrammar_HintsOnTerms()
{
var grammar = new ConflictGrammarWithHintsOnTerms();
var parser = new Parser(grammar);
Assert.True(parser.Language.Errors.Count == 0);
//Field list sample
var sample = FieldListSample;
var tree = parser.Parse(sample);
Assert.NotNull(tree);
Assert.False(tree.HasErrors());
Assert.NotNull(tree.Root);
var term = tree.Root.Term as NonTerminal;
Assert.NotNull(term);
Assert.Equal("StatementList", term.Name);
Assert.Equal(2, tree.Root.ChildNodes.Count);
var nodes = tree.Root.ChildNodes.Select(t => t.ChildNodes[0]).ToArray();
Assert.Equal("fieldDef", nodes[0].Term.Name);
Assert.Equal("fieldDef", nodes[1].Term.Name);
//Mixed sample
sample = MixedListSample;
tree = parser.Parse(sample);
Assert.NotNull(tree);
Assert.False(tree.HasErrors());
Assert.NotNull(tree.Root);
term = tree.Root.Term as NonTerminal;
Assert.NotNull(term);
Assert.Equal("StatementList", term.Name);
Assert.Equal(3, tree.Root.ChildNodes.Count);
nodes = tree.Root.ChildNodes.Select(t => t.ChildNodes[0]).ToArray();
Assert.Equal("propDef", nodes[0].Term.Name);
Assert.Equal("fieldDef", nodes[1].Term.Name);
Assert.Equal("methodDef", nodes[2].Term.Name);
}
}
}

133
src/Irony/Ast/AstBuilder.cs Normal file
View File

@ -0,0 +1,133 @@
#region License
/* **********************************************************************************
* Copyright (c) Roman Ivantsov
* This source code is subject to terms and conditions of the MIT License
* for Irony. A copy of the license can be found in the License.txt file
* at the root of this distribution.
* By using this source code in any fashion, you are agreeing to be bound by the terms of the
* MIT License.
* You must not remove this notice from this software.
* **********************************************************************************/
#endregion
using System.Reflection.Emit;
namespace Sanchime.Irony.Ast
{
public class AstBuilder
{
public AstContext Context;
public AstBuilder(AstContext context)
{
Context = context;
}
public virtual void BuildAst(ParseTree parseTree)
{
if (parseTree.Root == null)
return;
Context.Messages = parseTree.ParserMessages;
if (!Context.Language.AstDataVerified)
VerifyLanguageData();
if (Context.Language.ErrorLevel == GrammarErrorLevel.Error)
return;
BuildAst(parseTree.Root);
}
public virtual void VerifyLanguageData()
{
var gd = Context.Language.GrammarData;
//Collect all terminals and non-terminals
var terms = new BnfTermSet();
//SL does not understand co/contravariance, so doing merge one-by-one
foreach (var t in gd.Terminals) terms.Add(t);
foreach (var t in gd.NonTerminals) terms.Add(t);
var missingList = new BnfTermList();
foreach (var term in terms)
{
if (term is Terminal terminal && terminal.Category != TokenCategory.Content) continue; //only content terminals
if (term.Flags.IsSet(TermFlags.NoAstNode)) continue;
var config = term.AstConfig;
if (config.NodeCreator != null || config.DefaultNodeCreator != null) continue;
//We must check NodeType
if (config.NodeType == null)
config.NodeType = GetDefaultNodeType(term);
if (config.NodeType == null)
missingList.Add(term);
else
config.DefaultNodeCreator = CompileDefaultNodeCreator(config.NodeType);
}
if (missingList.Count > 0)
// AST node type is not specified for term {0}. Either assign Term.AstConfig.NodeType, or specify default type(s) in AstBuilder.
Context.AddMessage(ErrorLevel.Error, SourceLocation.Empty, Resources.ErrNodeTypeNotSetOn, string.Join(", ", missingList));
Context.Language.AstDataVerified = true;
}
protected virtual Type GetDefaultNodeType(BnfTerm term) => term switch
{
NumberLiteral or StringLiteral => Context.DefaultLiteralNodeType,
IdentifierTerminal => Context.DefaultIdentifierNodeType,
_ => Context.DefaultNodeType
};
public virtual void BuildAst(ParseTreeNode parseNode)
{
var term = parseNode.Term;
if (term.Flags.IsSet(TermFlags.NoAstNode) || parseNode.AstNode != null) return;
//children first
var processChildren = !parseNode.Term.Flags.IsSet(TermFlags.AstDelayChildren) && parseNode.ChildNodes.Count > 0;
if (processChildren)
{
var mappedChildNodes = parseNode.GetMappedChildNodes();
for (int i = 0; i < mappedChildNodes.Count; i++)
BuildAst(mappedChildNodes[i]);
}
// 创建节点
//We know that either NodeCreator or DefaultNodeCreator is set; VerifyAstData create the DefaultNodeCreator
var config = term.AstConfig;
if (config.NodeCreator != null)
{
config.NodeCreator(Context, parseNode);
// We assume that Node creator method creates node and initializes it, so parser does not need to call
// IAstNodeInit.Init() method on node object. But we do call AstNodeCreated custom event on term.
}
else
{
//Invoke the default creator compiled when we verified the data
parseNode.AstNode = config.DefaultNodeCreator();
//Initialize node
if (parseNode.AstNode is IAstNodeInit iInit)
iInit.Init(Context, parseNode);
}
//Invoke the event on term
term.OnAstNodeCreated(parseNode);
}//method
//Contributed by William Horner (wmh)
private DefaultAstNodeCreator CompileDefaultNodeCreator(Type nodeType)
{
var constr = nodeType.GetConstructor(Type.EmptyTypes);
var method = new DynamicMethod("CreateAstNode", nodeType, Type.EmptyTypes);
var il = method.GetILGenerator();
il.Emit(OpCodes.Newobj, constr);
il.Emit(OpCodes.Ret);
var result = (DefaultAstNodeCreator)method.CreateDelegate(typeof(DefaultAstNodeCreator));
return result;
}
/*
//A list of of child nodes based on AstPartsMap. By default, the same as ChildNodes
private ParseTreeNodeList _mappedChildNodes;
public ParseTreeNodeList MappedChildNodes {
get {
if (_mappedChildNodes == null)
_mappedChildNodes = GetMappedChildNodes();
return _mappedChildNodes;
}
}
*/
}//class
}

View File

@ -0,0 +1,39 @@
#region License
/* **********************************************************************************
* Copyright (c) Roman Ivantsov
* This source code is subject to terms and conditions of the MIT License
* for Irony. A copy of the license can be found in the License.txt file
* at the root of this distribution.
* By using this source code in any fashion, you are agreeing to be bound by the terms of the
* MIT License.
* You must not remove this notice from this software.
* **********************************************************************************/
#endregion
namespace Sanchime.Irony.Ast
{
public class AstContext
{
public readonly LanguageData Language;
public Type DefaultNodeType;
public Type DefaultLiteralNodeType; //default node type for literals
public Type DefaultIdentifierNodeType; //default node type for identifiers
public Dictionary<object, object> Values = new Dictionary<object, object>();
public LogMessageList Messages;
public AstContext(LanguageData language)
{
Language = language;
}
public void AddMessage(ErrorLevel level, SourceLocation location, string message, params object[] args)
{
if (args != null && args.Length > 0)
message = string.Format(message, args);
Messages.Add(new LogMessage(level, location, message, null));
}
}//class
}//ns

View File

@ -0,0 +1,19 @@
namespace Sanchime.Irony.Ast
{
public static class AstExtensions
{
public static ParseTreeNodeList GetMappedChildNodes(this ParseTreeNode node)
{
var term = node.Term;
if (!term.HasAstConfig())
return node.ChildNodes;
var map = term.AstConfig.PartsMap;
//If no map then mapped list is the same as original
if (map == null) return node.ChildNodes;
//Create mapped list
var result = new ParseTreeNodeList();
result.AddRange(from key in map select node.ChildNodes[key]);
return result;
}
}
}

View File

@ -0,0 +1,43 @@
#region License
/* **********************************************************************************
* Copyright (c) Roman Ivantsov
* This source code is subject to terms and conditions of the MIT License
* for Irony. A copy of the license can be found in the License.txt file
* at the root of this distribution.
* By using this source code in any fashion, you are agreeing to be bound by the terms of the
* MIT License.
* You must not remove this notice from this software.
* **********************************************************************************/
#endregion
using System.Collections;
namespace Sanchime.Irony.Ast
{
// Grammar Explorer uses this interface to discover and display the AST tree after parsing the input
// (Grammar Explorer additionally uses ToString method of the node to get the text representation of the node)
public interface IBrowsableAstNode
{
int Position { get; }
IEnumerable GetChildNodes();
}
// Note that we expect more than one interpreter/AST implementation.
// Irony.Interpreter namespace provides just one of them. That's why the following AST interfaces
// are here, in top Irony namespace and not in Irony.Interpreter.Ast.
// In the future, I plan to introduce advanced interpreter, with its own set of AST classes - it will live
// in a separate assembly Irony.Interpreter2.dll.
// Basic interface for AST nodes; Init method is the chance for AST node to get references to its child nodes, and all
// related information gathered during parsing
// Implementing this interface is a minimum required from custom AST node class to enable its creation by Irony AST builder
// Alternatively, if your custom AST node class does not implement this interface then you can create
// and initialize node instances using AstNodeCreator delegate attached to corresponding non-terminal in your grammar.
public interface IAstNodeInit
{
void Init(AstContext context, ParseTreeNode parseNode);
}
}

View File

@ -0,0 +1,59 @@
#region License
/* **********************************************************************************
* Copyright (c) Roman Ivantsov
* This source code is subject to terms and conditions of the MIT License
* for Irony. A copy of the license can be found in the License.txt file
* at the root of this distribution.
* By using this source code in any fashion, you are agreeing to be bound by the terms of the
* MIT License.
* You must not remove this notice from this software.
* **********************************************************************************/
#endregion
namespace Sanchime.Irony.Ast
{
public class AstNodeEventArgs : EventArgs
{
public AstNodeEventArgs(ParseTreeNode parseTreeNode)
{
ParseTreeNode = parseTreeNode;
}
public readonly ParseTreeNode ParseTreeNode;
public object AstNode
{
get { return ParseTreeNode.AstNode; }
}
}
public delegate void AstNodeCreator(AstContext context, ParseTreeNode parseNode);
public delegate object DefaultAstNodeCreator();
public class AstNodeConfig
{
public Type NodeType;
public object Data; //config data passed to AstNode
public AstNodeCreator NodeCreator; // a custom method for creating AST nodes
public DefaultAstNodeCreator DefaultNodeCreator; //default method for creating AST nodes; compiled dynamic method, wrapper around "new nodeType();"
// An optional map (selector, filter) of child AST nodes. This facility provides a way to adjust the "map" of child nodes in various languages to
// the structure of a standard AST nodes (that can be shared betweeen languages).
// ParseTreeNode object has two properties containing list nodes: ChildNodes and MappedChildNodes.
// If term.AstPartsMap is null, these two child node lists are identical and contain all child nodes.
// If AstParts is not null, then MappedChildNodes will contain child nodes identified by indexes in the map.
// For example, if we set
// term.AstPartsMap = new int[] {1, 4, 2};
// then MappedChildNodes will contain 3 child nodes, which are under indexes 1, 4, 2 in ChildNodes list.
// The mapping is performed in CoreParser.cs, method CheckCreateMappedChildNodeList.
public int[] PartsMap;
public bool CanCreateNode()
{
return NodeCreator != null || NodeType != null;
}
}//AstNodeConfig class
}

View File

@ -0,0 +1,8 @@
global using Sanchime.Irony.Parsing.Data;
global using Sanchime.Irony.Parsing.Grammars;
global using Sanchime.Irony.Parsing.Parsers;
global using Sanchime.Irony.Parsing.Scanners;
global using Sanchime.Irony.Parsing.Terminals;
global using Sanchime.Irony.Utilities;
global using System.Text;
global using System.Text.RegularExpressions;

Some files were not shown because too many files have changed in this diff Show More