From 43bae3590e07efe88a3bb29c9862c955f20b53f1 Mon Sep 17 00:00:00 2001 From: Chrys Lovelace Date: Tue, 21 Feb 2017 21:44:20 -0500 Subject: [PATCH] initial commit of v 0.1 --- .gitattributes | 63 + .gitignore | 245 +++ Lilac.sln | 22 + Lilac/AST/Association.cs | 8 + Lilac/AST/Context.cs | 156 ++ Lilac/AST/Definitions/Definition.cs | 12 + Lilac/AST/Definitions/OperatorDefinition.cs | 14 + Lilac/AST/Expressions/AssignmentExpression.cs | 18 + Lilac/AST/Expressions/BindingExpression.cs | 18 + .../AST/Expressions/ConditionalExpression.cs | 28 + Lilac/AST/Expressions/EmptyExpression.cs | 17 + Lilac/AST/Expressions/Expression.cs | 9 + .../AST/Expressions/FunctionCallExpression.cs | 51 + .../FunctionDefinitionExpression.cs | 23 + Lilac/AST/Expressions/GroupExpression.cs | 22 + Lilac/AST/Expressions/IdentifierExpression.cs | 17 + Lilac/AST/Expressions/LambdaExpression.cs | 22 + Lilac/AST/Expressions/LinkedListExpression.cs | 20 + Lilac/AST/Expressions/ListExpression.cs | 20 + .../AST/Expressions/MemberAccessExpression.cs | 48 + .../Expressions/MemberAssignmentExpression.cs | 19 + .../Expressions/MutableBindingExpression.cs | 18 + Lilac/AST/Expressions/NamespaceExpression.cs | 22 + .../NamespacedIdentifierExpression.cs | 20 + .../Expressions/NumberLiteralExpression.cs | 20 + .../AST/Expressions/OperatorCallExpression.cs | 40 + .../OperatorDefinitionExpression.cs | 31 + Lilac/AST/Expressions/OperatorExpression.cs | 13 + .../Expressions/StringLiteralExpression.cs | 17 + Lilac/AST/Expressions/TopLevelExpression.cs | 24 + Lilac/AST/Expressions/UsingExpression.cs | 19 + Lilac/AST/GroupType.cs | 9 + Lilac/AST/IExpressionVisitor.cs | 30 + Lilac/App.config | 6 + Lilac/Attributes/BuiltInFunctionAttribute.cs | 20 + Lilac/Attributes/BuiltInMemberAttribute.cs | 16 + Lilac/Attributes/BuiltInMethodAttribute.cs | 17 + Lilac/Attributes/BuiltInValueAttribute.cs | 16 + Lilac/Exceptions/NumericTypeException.cs | 11 + Lilac/Exceptions/ParseException.cs | 18 + Lilac/Exceptions/SyntaxException.cs | 11 + Lilac/Interpreter/Binding.cs | 12 + Lilac/Interpreter/Evaluator.cs | 293 ++++ Lilac/Interpreter/Interpreter.cs | 179 +++ Lilac/Interpreter/Scope.cs | 154 ++ Lilac/Lilac.csproj | 149 ++ Lilac/Parser/Lexer.cs | 147 ++ Lilac/Parser/Parser.cs | 619 ++++++++ Lilac/Parser/ParserState.cs | 204 +++ Lilac/Parser/Token.cs | 15 + Lilac/Parser/TokenDefinition.cs | 50 + Lilac/Parser/TokenType.cs | 26 + Lilac/Program.cs | 10 + Lilac/Properties/AssemblyInfo.cs | 36 + Lilac/Utilities/BidirectionalIterator.cs | 63 + Lilac/Utilities/Extensions.cs | 59 + Lilac/Utilities/IBidirectionalIterator.cs | 10 + Lilac/Utilities/Maybe.cs | 52 + Lilac/Utilities/MemberContainer.cs | 83 + Lilac/Utilities/Result.cs | 57 + Lilac/Values/Boolean.cs | 91 ++ Lilac/Values/BuiltInFunction.cs | 27 + Lilac/Values/Char.cs | 123 ++ Lilac/Values/CurriedFunction.cs | 34 + Lilac/Values/Function.cs | 48 + Lilac/Values/List.cs | 114 ++ Lilac/Values/Number.cs | 1372 +++++++++++++++++ Lilac/Values/Pair.cs | 72 + Lilac/Values/String.cs | 293 ++++ Lilac/Values/Unit.cs | 24 + Lilac/Values/Value.cs | 178 +++ Lilac/list.li | 13 + Lilac/packages.config | 7 + Lilac/test.li | 32 + 74 files changed, 5876 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 Lilac.sln create mode 100644 Lilac/AST/Association.cs create mode 100644 Lilac/AST/Context.cs create mode 100644 Lilac/AST/Definitions/Definition.cs create mode 100644 Lilac/AST/Definitions/OperatorDefinition.cs create mode 100644 Lilac/AST/Expressions/AssignmentExpression.cs create mode 100644 Lilac/AST/Expressions/BindingExpression.cs create mode 100644 Lilac/AST/Expressions/ConditionalExpression.cs create mode 100644 Lilac/AST/Expressions/EmptyExpression.cs create mode 100644 Lilac/AST/Expressions/Expression.cs create mode 100644 Lilac/AST/Expressions/FunctionCallExpression.cs create mode 100644 Lilac/AST/Expressions/FunctionDefinitionExpression.cs create mode 100644 Lilac/AST/Expressions/GroupExpression.cs create mode 100644 Lilac/AST/Expressions/IdentifierExpression.cs create mode 100644 Lilac/AST/Expressions/LambdaExpression.cs create mode 100644 Lilac/AST/Expressions/LinkedListExpression.cs create mode 100644 Lilac/AST/Expressions/ListExpression.cs create mode 100644 Lilac/AST/Expressions/MemberAccessExpression.cs create mode 100644 Lilac/AST/Expressions/MemberAssignmentExpression.cs create mode 100644 Lilac/AST/Expressions/MutableBindingExpression.cs create mode 100644 Lilac/AST/Expressions/NamespaceExpression.cs create mode 100644 Lilac/AST/Expressions/NamespacedIdentifierExpression.cs create mode 100644 Lilac/AST/Expressions/NumberLiteralExpression.cs create mode 100644 Lilac/AST/Expressions/OperatorCallExpression.cs create mode 100644 Lilac/AST/Expressions/OperatorDefinitionExpression.cs create mode 100644 Lilac/AST/Expressions/OperatorExpression.cs create mode 100644 Lilac/AST/Expressions/StringLiteralExpression.cs create mode 100644 Lilac/AST/Expressions/TopLevelExpression.cs create mode 100644 Lilac/AST/Expressions/UsingExpression.cs create mode 100644 Lilac/AST/GroupType.cs create mode 100644 Lilac/AST/IExpressionVisitor.cs create mode 100644 Lilac/App.config create mode 100644 Lilac/Attributes/BuiltInFunctionAttribute.cs create mode 100644 Lilac/Attributes/BuiltInMemberAttribute.cs create mode 100644 Lilac/Attributes/BuiltInMethodAttribute.cs create mode 100644 Lilac/Attributes/BuiltInValueAttribute.cs create mode 100644 Lilac/Exceptions/NumericTypeException.cs create mode 100644 Lilac/Exceptions/ParseException.cs create mode 100644 Lilac/Exceptions/SyntaxException.cs create mode 100644 Lilac/Interpreter/Binding.cs create mode 100644 Lilac/Interpreter/Evaluator.cs create mode 100644 Lilac/Interpreter/Interpreter.cs create mode 100644 Lilac/Interpreter/Scope.cs create mode 100644 Lilac/Lilac.csproj create mode 100644 Lilac/Parser/Lexer.cs create mode 100644 Lilac/Parser/Parser.cs create mode 100644 Lilac/Parser/ParserState.cs create mode 100644 Lilac/Parser/Token.cs create mode 100644 Lilac/Parser/TokenDefinition.cs create mode 100644 Lilac/Parser/TokenType.cs create mode 100644 Lilac/Program.cs create mode 100644 Lilac/Properties/AssemblyInfo.cs create mode 100644 Lilac/Utilities/BidirectionalIterator.cs create mode 100644 Lilac/Utilities/Extensions.cs create mode 100644 Lilac/Utilities/IBidirectionalIterator.cs create mode 100644 Lilac/Utilities/Maybe.cs create mode 100644 Lilac/Utilities/MemberContainer.cs create mode 100644 Lilac/Utilities/Result.cs create mode 100644 Lilac/Values/Boolean.cs create mode 100644 Lilac/Values/BuiltInFunction.cs create mode 100644 Lilac/Values/Char.cs create mode 100644 Lilac/Values/CurriedFunction.cs create mode 100644 Lilac/Values/Function.cs create mode 100644 Lilac/Values/List.cs create mode 100644 Lilac/Values/Number.cs create mode 100644 Lilac/Values/Pair.cs create mode 100644 Lilac/Values/String.cs create mode 100644 Lilac/Values/Unit.cs create mode 100644 Lilac/Values/Value.cs create mode 100644 Lilac/list.li create mode 100644 Lilac/packages.config create mode 100644 Lilac/test.li diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3a2238d --- /dev/null +++ b/.gitignore @@ -0,0 +1,245 @@ +## 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/ +[Xx]64/ +[Xx]86/ +[Bb]uild/ +bld/ +[Bb]in/ +[Oo]bj/ + +# 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 + +# 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: Un-comment the next line if you do not want to checkin +# your web deploy settings because they may include unencrypted +# passwords +#*.pubxml +*.publishproj + +# 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/ + +# Microsoft Azure ApplicationInsights config file +ApplicationInsights.config + +# Windows Store app package directory +AppPackages/ +BundleArtifacts/ + +# 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/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# 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 + +# LightSwitch generated files +GeneratedArtifacts/ +ModelManifest.xml + +# Paket dependency manager +.paket/paket.exe + +# FAKE - F# Make +.fake/ \ No newline at end of file diff --git a/Lilac.sln b/Lilac.sln new file mode 100644 index 0000000..a18abba --- /dev/null +++ b/Lilac.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 14 +VisualStudioVersion = 14.0.25420.1 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lilac", "Lilac\Lilac.csproj", "{940AF5DF-EBDE-4EFC-8B03-8CC3B1829DD0}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {940AF5DF-EBDE-4EFC-8B03-8CC3B1829DD0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {940AF5DF-EBDE-4EFC-8B03-8CC3B1829DD0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {940AF5DF-EBDE-4EFC-8B03-8CC3B1829DD0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {940AF5DF-EBDE-4EFC-8B03-8CC3B1829DD0}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/Lilac/AST/Association.cs b/Lilac/AST/Association.cs new file mode 100644 index 0000000..903c2e0 --- /dev/null +++ b/Lilac/AST/Association.cs @@ -0,0 +1,8 @@ +namespace Lilac.AST +{ + public enum Association + { + L, + R + } +} \ No newline at end of file diff --git a/Lilac/AST/Context.cs b/Lilac/AST/Context.cs new file mode 100644 index 0000000..b67a54a --- /dev/null +++ b/Lilac/AST/Context.cs @@ -0,0 +1,156 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Lilac.AST.Definitions; +using Lilac.Exceptions; + +namespace Lilac.AST +{ + public class Context + { + public ImmutableDictionary Definitions { get; private set; } + public ImmutableDictionary Namespaces { get; private set; } + public ImmutableHashSet UsedNamespaces { get; private set; } + public Context Parent { get; private set; } + + private Context() + { + Definitions = ImmutableDictionary.Empty; + Namespaces = ImmutableDictionary.Empty; + UsedNamespaces = ImmutableHashSet.Empty; + } + + public Context(IEnumerable definitions) + { + Definitions = definitions.ToImmutableDictionary(d => d.Name); + Namespaces = ImmutableDictionary.Empty; + UsedNamespaces = ImmutableHashSet.Empty; + } + + public Context NewChild() + { + return new Context + { + Definitions = ImmutableDictionary.Empty, + Namespaces = Namespaces, + Parent = this, + UsedNamespaces = ImmutableHashSet.Empty + }; + } + + public Context AddDefinition(Definition definition) + { + return new Context + { + Definitions = Definitions.Add(definition.Name, definition), + Namespaces = Namespaces, + Parent = Parent, + UsedNamespaces = UsedNamespaces + }; + } + + public Context AddNamespace(IList namespaces, Context context) + { + var ns = namespaces[0]; + if (namespaces.Count == 1) + { + return new Context + { + Definitions = Definitions, + Namespaces = Namespaces.Add(ns, context), + UsedNamespaces = UsedNamespaces, + Parent = Parent + }; + } + Context nextContext; + if (Namespaces.TryGetValue(ns, out nextContext)) + return new Context + { + Definitions = Definitions, + Namespaces = Namespaces.SetItem(ns, nextContext.AddNamespace(namespaces.Skip(1).ToList(), context)), + UsedNamespaces = UsedNamespaces, + Parent = Parent + }; + else + return new Context + { + Definitions = Definitions, + Namespaces = Namespaces.Add(ns, new Context().AddNamespace(namespaces.Skip(1).ToList(), context)), + UsedNamespaces = UsedNamespaces, + Parent = Parent + }; + } + + public Context AddNamespacedDefinition(IList namespaces, Definition definition) + { + if (namespaces.Count == 0) return AddDefinition(definition); + Context context; + var ns = namespaces[0]; + if (Namespaces.TryGetValue(ns, out context)) + return new Context + { + Definitions = Definitions, + Namespaces = Namespaces.SetItem(ns, context.AddNamespacedDefinition(namespaces.Skip(1).ToList(), definition)), + UsedNamespaces = UsedNamespaces, + Parent = Parent + }; + else + return new Context + { + Definitions = Definitions, + Namespaces = Namespaces.Add(ns, new Context().AddNamespacedDefinition(namespaces.Skip(1).ToList(), definition)), + UsedNamespaces = UsedNamespaces, + Parent = Parent + }; + } + + public Definition GetDefinition(string name) + { + Definition definition; + return Definitions.TryGetValue(name, out definition) + ? definition + : (UsedNamespaces.Any(ns => ns.Definitions.TryGetValue(name, out definition)) + ? definition + : Parent?.GetDefinition(name)); + } + + + public Definition GetNamespacedDefinition(IList namespaces, string name) + { + if (namespaces.Count == 0) return GetDefinition(name); + Context context; + var ns = namespaces[0]; + return Namespaces.TryGetValue(ns, out context) + ? context.GetNamespacedDefinition(namespaces.Skip(1).ToList(), name) + : Parent?.GetNamespacedDefinition(namespaces, name); + } + + + public Context GetNamespace(IList namespaces) + { + if (namespaces.Count == 0) return this; + Context context; + var ns = namespaces[0]; + if (Namespaces.TryGetValue(ns, out context)) return context.GetNamespace(namespaces.Skip(1).ToList()); + foreach (var used in UsedNamespaces) + { + context = used.GetNamespace(namespaces); + if (context != null) return context; + } + return Parent?.GetNamespace(namespaces); + } + + public Context UseNamespace(IList namespaces) + { + var ns = GetNamespace(namespaces); + if (ns == null) throw new ParseException($"Could not find namespace '{string.Join(".", namespaces)}'!"); + return new Context + { + Definitions = Definitions, + Namespaces = Namespaces, + Parent = Parent, + UsedNamespaces = UsedNamespaces.Add(ns) + }; + } + } +} \ No newline at end of file diff --git a/Lilac/AST/Definitions/Definition.cs b/Lilac/AST/Definitions/Definition.cs new file mode 100644 index 0000000..c3354a8 --- /dev/null +++ b/Lilac/AST/Definitions/Definition.cs @@ -0,0 +1,12 @@ +namespace Lilac.AST.Definitions +{ + public class Definition + { + public string Name { get; set; } + + public Definition(string name) + { + Name = name; + } + } +} \ No newline at end of file diff --git a/Lilac/AST/Definitions/OperatorDefinition.cs b/Lilac/AST/Definitions/OperatorDefinition.cs new file mode 100644 index 0000000..e254d12 --- /dev/null +++ b/Lilac/AST/Definitions/OperatorDefinition.cs @@ -0,0 +1,14 @@ +namespace Lilac.AST.Definitions +{ + public class OperatorDefinition : Definition + { + public decimal Precedence { get; set; } + public Association Association { get; set; } + + public OperatorDefinition(string name, decimal precedence, Association association) : base(name) + { + Precedence = precedence; + Association = association; + } + } +} \ No newline at end of file diff --git a/Lilac/AST/Expressions/AssignmentExpression.cs b/Lilac/AST/Expressions/AssignmentExpression.cs new file mode 100644 index 0000000..355220b --- /dev/null +++ b/Lilac/AST/Expressions/AssignmentExpression.cs @@ -0,0 +1,18 @@ +namespace Lilac.AST.Expressions +{ + public class AssignmentExpression : Expression + { + public string Name { get; set; } + public Expression ValueExpression { get; set; } + + public override string ToString() + { + return $"set! {Name} = {ValueExpression.ToString()}"; + } + + public override T Accept(IExpressionVisitor visitor) + { + return visitor.VisitAssignment(this); + } + } +} \ No newline at end of file diff --git a/Lilac/AST/Expressions/BindingExpression.cs b/Lilac/AST/Expressions/BindingExpression.cs new file mode 100644 index 0000000..052f148 --- /dev/null +++ b/Lilac/AST/Expressions/BindingExpression.cs @@ -0,0 +1,18 @@ +namespace Lilac.AST.Expressions +{ + public class BindingExpression : Expression + { + public string Name { get; set; } + public Expression ValueExpression { get; set; } + + public override string ToString() + { + return $"let {Name} = {ValueExpression.ToString()}"; + } + + public override T Accept(IExpressionVisitor visitor) + { + return visitor.VisitBinding(this); + } + } +} \ No newline at end of file diff --git a/Lilac/AST/Expressions/ConditionalExpression.cs b/Lilac/AST/Expressions/ConditionalExpression.cs new file mode 100644 index 0000000..bbc5aa6 --- /dev/null +++ b/Lilac/AST/Expressions/ConditionalExpression.cs @@ -0,0 +1,28 @@ +using System.Text; + +namespace Lilac.AST.Expressions +{ + public class ConditionalExpression : Expression + { + public Expression Condition { get; set; } + public Expression ThenExpression { get; set; } + public Expression ElseExpression { get; set; } + + public override string ToString() + { + var sb = new StringBuilder() + .Append("if ").Append(Condition.ToString()) + .Append(" then ").Append(ThenExpression.ToString()); + + if (ElseExpression != null) + sb.Append(" else ").Append(ElseExpression.ToString()); + + return sb.ToString(); + } + + public override T Accept(IExpressionVisitor visitor) + { + return visitor.VisitConditional(this); + } + } +} \ No newline at end of file diff --git a/Lilac/AST/Expressions/EmptyExpression.cs b/Lilac/AST/Expressions/EmptyExpression.cs new file mode 100644 index 0000000..feb6a06 --- /dev/null +++ b/Lilac/AST/Expressions/EmptyExpression.cs @@ -0,0 +1,17 @@ +using System; + +namespace Lilac.AST.Expressions +{ + public class EmptyExpression : Expression + { + public override string ToString() + { + return string.Empty; + } + + public override T Accept(IExpressionVisitor visitor) + { + throw new NotSupportedException(); + } + } +} \ No newline at end of file diff --git a/Lilac/AST/Expressions/Expression.cs b/Lilac/AST/Expressions/Expression.cs new file mode 100644 index 0000000..c77de9f --- /dev/null +++ b/Lilac/AST/Expressions/Expression.cs @@ -0,0 +1,9 @@ +namespace Lilac.AST.Expressions +{ + public abstract class Expression + { + public abstract T Accept(IExpressionVisitor visitor); + + public virtual Expression ResolvePrecedence() => this; + } +} \ No newline at end of file diff --git a/Lilac/AST/Expressions/FunctionCallExpression.cs b/Lilac/AST/Expressions/FunctionCallExpression.cs new file mode 100644 index 0000000..fd0020d --- /dev/null +++ b/Lilac/AST/Expressions/FunctionCallExpression.cs @@ -0,0 +1,51 @@ +namespace Lilac.AST.Expressions +{ + public class FunctionCallExpression : Expression + { + public Expression Function { get; set; } + public Expression Argument { get; set; } + + public override string ToString() + { + return $"({Function.ToString()} {Argument.ToString()})"; + } + + public override T Accept(IExpressionVisitor visitor) + { + return visitor.VisitFunctionCall(this); + } + + public override Expression ResolvePrecedence() + { + var rhsFunc = Argument as FunctionCallExpression; + if (rhsFunc != null) + { + return new FunctionCallExpression + { + Function = new FunctionCallExpression + { + Function = Function, + Argument = rhsFunc.Function + }.ResolvePrecedence(), + Argument = rhsFunc.Argument + }; + } + var rhsOp = Argument as OperatorCallExpression; + if (rhsOp != null) + { + return new OperatorCallExpression + { + Lhs = new FunctionCallExpression + { + Function = Function, + Argument = rhsOp.Lhs + }.ResolvePrecedence(), + Name = rhsOp.Name, + Rhs = rhsOp.Rhs + }; + } + + return this; + } + } +} \ No newline at end of file diff --git a/Lilac/AST/Expressions/FunctionDefinitionExpression.cs b/Lilac/AST/Expressions/FunctionDefinitionExpression.cs new file mode 100644 index 0000000..23df3b6 --- /dev/null +++ b/Lilac/AST/Expressions/FunctionDefinitionExpression.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Linq; +using Lilac.Utilities; + +namespace Lilac.AST.Expressions +{ + public class FunctionDefinitionExpression : Expression + { + public string Name { get; set; } + public List Parameters { get; set; } + public Expression Body { get; set; } + + public override string ToString() + { + return $"let {Name} {Parameters.PrettyPrintParameters()} = {Body.ToString()}"; + } + + public override T Accept(IExpressionVisitor visitor) + { + return visitor.VisitFunctionDefinition(this); + } + } +} \ No newline at end of file diff --git a/Lilac/AST/Expressions/GroupExpression.cs b/Lilac/AST/Expressions/GroupExpression.cs new file mode 100644 index 0000000..45bf8d6 --- /dev/null +++ b/Lilac/AST/Expressions/GroupExpression.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Lilac.AST.Expressions +{ + public class GroupExpression : Expression + { + public GroupType GroupType { get; set; } + public List Expressions { get; set; } = new List(); + + public override string ToString() + { + return $"({string.Join("; ", Expressions)})"; + } + + public override T Accept(IExpressionVisitor visitor) + { + return visitor.VisitGroup(this); + } + } +} \ No newline at end of file diff --git a/Lilac/AST/Expressions/IdentifierExpression.cs b/Lilac/AST/Expressions/IdentifierExpression.cs new file mode 100644 index 0000000..19613c5 --- /dev/null +++ b/Lilac/AST/Expressions/IdentifierExpression.cs @@ -0,0 +1,17 @@ +namespace Lilac.AST.Expressions +{ + public class IdentifierExpression : Expression + { + public string Name { get; set; } + + public override string ToString() + { + return Name; + } + + public override T Accept(IExpressionVisitor visitor) + { + return visitor.VisitIdentifier(this); + } + } +} \ No newline at end of file diff --git a/Lilac/AST/Expressions/LambdaExpression.cs b/Lilac/AST/Expressions/LambdaExpression.cs new file mode 100644 index 0000000..936d042 --- /dev/null +++ b/Lilac/AST/Expressions/LambdaExpression.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using Lilac.Utilities; + +namespace Lilac.AST.Expressions +{ + public class LambdaExpression : Expression + { + public Expression Body { get; set; } + public List Parameters { get; set; } + + public override string ToString() + { + return $"lambda {Parameters.PrettyPrintParameters()} = {Body.ToString()}"; + } + + public override T Accept(IExpressionVisitor visitor) + { + return visitor.VisitLambda(this); + } + + } +} \ No newline at end of file diff --git a/Lilac/AST/Expressions/LinkedListExpression.cs b/Lilac/AST/Expressions/LinkedListExpression.cs new file mode 100644 index 0000000..e6af64d --- /dev/null +++ b/Lilac/AST/Expressions/LinkedListExpression.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Lilac.AST.Expressions +{ + public class LinkedListExpression : Expression + { + public List Expressions { get; set; } + + public override string ToString() + { + return $"({string.Join("; ", Expressions)})"; + } + + public override T Accept(IExpressionVisitor visitor) + { + return visitor.VisitLinkedList(this); + } + } +} \ No newline at end of file diff --git a/Lilac/AST/Expressions/ListExpression.cs b/Lilac/AST/Expressions/ListExpression.cs new file mode 100644 index 0000000..ec43201 --- /dev/null +++ b/Lilac/AST/Expressions/ListExpression.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Lilac.AST.Expressions +{ + public class ListExpression : Expression + { + public List Expressions { get; set; } + + public override string ToString() + { + return $"[{string.Join("; ", Expressions)}]"; + } + + public override T Accept(IExpressionVisitor visitor) + { + return visitor.VisitList(this); + } + } +} \ No newline at end of file diff --git a/Lilac/AST/Expressions/MemberAccessExpression.cs b/Lilac/AST/Expressions/MemberAccessExpression.cs new file mode 100644 index 0000000..e5f731a --- /dev/null +++ b/Lilac/AST/Expressions/MemberAccessExpression.cs @@ -0,0 +1,48 @@ +namespace Lilac.AST.Expressions +{ + public class MemberAccessExpression : Expression + { + public Expression Target { get; set; } + public string Member { get; set; } + + public override string ToString() => $"{Target.ToString()}.{Member}"; + + public override T Accept(IExpressionVisitor visitor) + { + return visitor.VisitMemberAccess(this); + } + + public override Expression ResolvePrecedence() + { + var lhsFunc = Target as FunctionCallExpression; + if (lhsFunc != null) + { + return new FunctionCallExpression + { + Function = lhsFunc.Function, + Argument = new MemberAccessExpression + { + Target = lhsFunc.Argument, + Member = Member + }.ResolvePrecedence() + }; + } + var lhsOp = Target as OperatorCallExpression; + if (lhsOp != null) + { + return new OperatorCallExpression + { + Lhs = lhsOp.Lhs, + Name = lhsOp.Name, + Rhs = new MemberAccessExpression + { + Target = lhsOp.Rhs, + Member = Member + }.ResolvePrecedence() + }; + } + + return this; + } + } +} \ No newline at end of file diff --git a/Lilac/AST/Expressions/MemberAssignmentExpression.cs b/Lilac/AST/Expressions/MemberAssignmentExpression.cs new file mode 100644 index 0000000..a952c84 --- /dev/null +++ b/Lilac/AST/Expressions/MemberAssignmentExpression.cs @@ -0,0 +1,19 @@ +namespace Lilac.AST.Expressions +{ + public class MemberAssignmentExpression : Expression + { + public Expression Target { get; set; } + public string Member { get; set; } + public Expression ValueExpression { get; set; } + + public override string ToString() + { + return $"set! {Target.ToString()}.{Member} = {ValueExpression.ToString()}"; + } + + public override T Accept(IExpressionVisitor visitor) + { + return visitor.VisitMemberAssignment(this); + } + } +} \ No newline at end of file diff --git a/Lilac/AST/Expressions/MutableBindingExpression.cs b/Lilac/AST/Expressions/MutableBindingExpression.cs new file mode 100644 index 0000000..2c3c1b1 --- /dev/null +++ b/Lilac/AST/Expressions/MutableBindingExpression.cs @@ -0,0 +1,18 @@ +namespace Lilac.AST.Expressions +{ + public class MutableBindingExpression : Expression + { + public string Name { get; set; } + public Expression ValueExpression { get; set; } + + public override string ToString() + { + return $"let ref {Name} = {ValueExpression.ToString()}"; + } + + public override T Accept(IExpressionVisitor visitor) + { + return visitor.VisitMutableBinding(this); + } + } +} \ No newline at end of file diff --git a/Lilac/AST/Expressions/NamespaceExpression.cs b/Lilac/AST/Expressions/NamespaceExpression.cs new file mode 100644 index 0000000..2ae8ed9 --- /dev/null +++ b/Lilac/AST/Expressions/NamespaceExpression.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Lilac.AST.Expressions +{ + public class NamespaceExpression : Expression + { + public List Namespaces { get; set; } + + public List Expressions { get; set; } + public GroupType GroupType { get; set; } + + public override string ToString() => + $"namespace {string.Join(".", Namespaces)} = {Environment.NewLine} {string.Join(Environment.NewLine, Expressions.Select(e => e.ToString()))}"; + + public override T Accept(IExpressionVisitor visitor) + { + return visitor.VisitNamespace(this); + } + } +} \ No newline at end of file diff --git a/Lilac/AST/Expressions/NamespacedIdentifierExpression.cs b/Lilac/AST/Expressions/NamespacedIdentifierExpression.cs new file mode 100644 index 0000000..9e18120 --- /dev/null +++ b/Lilac/AST/Expressions/NamespacedIdentifierExpression.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; + +namespace Lilac.AST.Expressions +{ + public class NamespacedIdentifierExpression : Expression + { + public override string ToString() + { + return $"{string.Join(".", Namespaces)}.{Name}"; + } + + public override T Accept(IExpressionVisitor visitor) + { + return visitor.VisitNamespacedIdentifier(this); + } + + public List Namespaces { get; set; } + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/Lilac/AST/Expressions/NumberLiteralExpression.cs b/Lilac/AST/Expressions/NumberLiteralExpression.cs new file mode 100644 index 0000000..38a3540 --- /dev/null +++ b/Lilac/AST/Expressions/NumberLiteralExpression.cs @@ -0,0 +1,20 @@ +using Lilac.Parser; + +namespace Lilac.AST.Expressions +{ + public class NumberLiteralExpression : Expression + { + public string Value { get; set; } + public TokenType LiteralType { get; set; } + + public override string ToString() + { + return Value; + } + + public override T Accept(IExpressionVisitor visitor) + { + return visitor.VisitNumberLiteral(this); + } + } +} \ No newline at end of file diff --git a/Lilac/AST/Expressions/OperatorCallExpression.cs b/Lilac/AST/Expressions/OperatorCallExpression.cs new file mode 100644 index 0000000..fd0f31b --- /dev/null +++ b/Lilac/AST/Expressions/OperatorCallExpression.cs @@ -0,0 +1,40 @@ +namespace Lilac.AST.Expressions +{ + public class OperatorCallExpression : Expression + { + public string Name { get; set; } + public Expression Lhs { get; set; } + public Expression Rhs { get; set; } + + public override string ToString() + { + return $"({Lhs.ToString()} {Name} {Rhs.ToString()})"; + } + + public override T Accept(IExpressionVisitor visitor) + { + return visitor.VisitOperatorCall(this); + } + + public override Expression ResolvePrecedence() + { + var rhsOp = Rhs as OperatorCallExpression; + if (rhsOp != null) + { + return new OperatorCallExpression + { + Lhs = new OperatorCallExpression + { + Lhs = Lhs, + Name = Name, + Rhs = rhsOp.Lhs + }.ResolvePrecedence(), + Name = rhsOp.Name, + Rhs = rhsOp.Rhs + }; + } + + return this; + } + } +} \ No newline at end of file diff --git a/Lilac/AST/Expressions/OperatorDefinitionExpression.cs b/Lilac/AST/Expressions/OperatorDefinitionExpression.cs new file mode 100644 index 0000000..495a1c1 --- /dev/null +++ b/Lilac/AST/Expressions/OperatorDefinitionExpression.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Text; +using Lilac.Utilities; + +namespace Lilac.AST.Expressions +{ + public class OperatorDefinitionExpression : Expression + { + public string Name { get; set; } + public List Parameters { get; set; } + public Expression Body { get; set; } + public decimal Precedence { get; set; } + public Association Association { get; set; } + + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append("let operator ").Append(Name); + sb.Append(" precedence ").Append(Precedence); + sb.Append(" associates ").Append(Association); + sb.Append(Parameters.PrettyPrintParameters()); + sb.Append(" = ").Append(Body.ToString()); + return sb.ToString(); + } + + public override T Accept(IExpressionVisitor visitor) + { + return visitor.VisitOperatorDefinition(this); + } + } +} \ No newline at end of file diff --git a/Lilac/AST/Expressions/OperatorExpression.cs b/Lilac/AST/Expressions/OperatorExpression.cs new file mode 100644 index 0000000..8b8c165 --- /dev/null +++ b/Lilac/AST/Expressions/OperatorExpression.cs @@ -0,0 +1,13 @@ +namespace Lilac.AST.Expressions +{ + public class OperatorExpression : Expression + { + public string Name { get; set; } + public override string ToString() => Name; + + public override T Accept(IExpressionVisitor visitor) + { + return visitor.VisitOperator(this); + } + } +} \ No newline at end of file diff --git a/Lilac/AST/Expressions/StringLiteralExpression.cs b/Lilac/AST/Expressions/StringLiteralExpression.cs new file mode 100644 index 0000000..f5c1244 --- /dev/null +++ b/Lilac/AST/Expressions/StringLiteralExpression.cs @@ -0,0 +1,17 @@ +namespace Lilac.AST.Expressions +{ + public class StringLiteralExpression : Expression + { + public string Value { get; set; } + + public override string ToString() + { + return Value; + } + + public override T Accept(IExpressionVisitor visitor) + { + return visitor.VisitStringLiteral(this); + } + } +} \ No newline at end of file diff --git a/Lilac/AST/Expressions/TopLevelExpression.cs b/Lilac/AST/Expressions/TopLevelExpression.cs new file mode 100644 index 0000000..4f3a1ae --- /dev/null +++ b/Lilac/AST/Expressions/TopLevelExpression.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Lilac.AST.Expressions +{ + public class TopLevelExpression : Expression + { + public GroupType GroupType { get; set; } + public List Expressions { get; set; } = new List(); + + public override string ToString() + { + return Expressions.Any() + ? string.Join(Environment.NewLine, Expressions.Select(e => e.ToString())) + : "()"; + } + + public override T Accept(IExpressionVisitor visitor) + { + return visitor.VisitTopLevelExpression(this); + } + } +} \ No newline at end of file diff --git a/Lilac/AST/Expressions/UsingExpression.cs b/Lilac/AST/Expressions/UsingExpression.cs new file mode 100644 index 0000000..a1f4c2d --- /dev/null +++ b/Lilac/AST/Expressions/UsingExpression.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; + +namespace Lilac.AST.Expressions +{ + public class UsingExpression : Expression + { + public override string ToString() + { + return $"using {string.Join(".", Namespaces)}"; + } + + public override T Accept(IExpressionVisitor visitor) + { + return visitor.VisitUsing(this); + } + + public List Namespaces { get; set; } + } +} \ No newline at end of file diff --git a/Lilac/AST/GroupType.cs b/Lilac/AST/GroupType.cs new file mode 100644 index 0000000..87b8b19 --- /dev/null +++ b/Lilac/AST/GroupType.cs @@ -0,0 +1,9 @@ +namespace Lilac.AST +{ + public enum GroupType + { + Indented, + Parenthesized, + TopLevel + } +} \ No newline at end of file diff --git a/Lilac/AST/IExpressionVisitor.cs b/Lilac/AST/IExpressionVisitor.cs new file mode 100644 index 0000000..74b499a --- /dev/null +++ b/Lilac/AST/IExpressionVisitor.cs @@ -0,0 +1,30 @@ +using Lilac.AST.Expressions; + +namespace Lilac.AST +{ + public interface IExpressionVisitor + { + T VisitAssignment(AssignmentExpression assignment); + T VisitBinding(BindingExpression binding); + T VisitConditional(ConditionalExpression conditional); + T VisitFunctionCall(FunctionCallExpression functionCall); + T VisitFunctionDefinition(FunctionDefinitionExpression functionDefinition); + T VisitGroup(GroupExpression group); + T VisitIdentifier(IdentifierExpression identifier); + T VisitMutableBinding(MutableBindingExpression mutableBinding); + T VisitNumberLiteral(NumberLiteralExpression numberLiteral); + T VisitOperatorCall(OperatorCallExpression operatorCall); + T VisitOperatorDefinition(OperatorDefinitionExpression operatorDefinition); + T VisitStringLiteral(StringLiteralExpression stringLiteral); + T VisitTopLevelExpression(TopLevelExpression topLevelExpression); + T VisitNamespacedIdentifier(NamespacedIdentifierExpression namespacedIdentifier); + T VisitUsing(UsingExpression usingExpression); + T VisitOperator(OperatorExpression operatorExpression); + T VisitMemberAccess(MemberAccessExpression memberAccess); + T VisitNamespace(NamespaceExpression namespaceExpression); + T VisitMemberAssignment(MemberAssignmentExpression memberAssignment); + T VisitList(ListExpression list); + T VisitLambda(LambdaExpression lambdaExpression); + T VisitLinkedList(LinkedListExpression linkedListExpression); + } +} \ No newline at end of file diff --git a/Lilac/App.config b/Lilac/App.config new file mode 100644 index 0000000..88fa402 --- /dev/null +++ b/Lilac/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Lilac/Attributes/BuiltInFunctionAttribute.cs b/Lilac/Attributes/BuiltInFunctionAttribute.cs new file mode 100644 index 0000000..c2be9cd --- /dev/null +++ b/Lilac/Attributes/BuiltInFunctionAttribute.cs @@ -0,0 +1,20 @@ +using System; + +namespace Lilac.Attributes +{ + + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + public class BuiltInFunctionAttribute : Attribute + { + public string Name { get; } + public Type DelegateType { get; } + public bool IsOperator { get; set; } + public string Namespace { get; set; } + + public BuiltInFunctionAttribute(string name, Type type) + { + Name = name; + DelegateType = type; + } + } +} \ No newline at end of file diff --git a/Lilac/Attributes/BuiltInMemberAttribute.cs b/Lilac/Attributes/BuiltInMemberAttribute.cs new file mode 100644 index 0000000..0f1f4a7 --- /dev/null +++ b/Lilac/Attributes/BuiltInMemberAttribute.cs @@ -0,0 +1,16 @@ +using System; + +namespace Lilac.Attributes +{ + [AttributeUsage(AttributeTargets.Property, AllowMultiple = true)] + public class BuiltInMemberAttribute : Attribute + { + public string Name { get; } + public bool GetOnly { get; set; } + + public BuiltInMemberAttribute(string name) + { + Name = name; + } + } +} \ No newline at end of file diff --git a/Lilac/Attributes/BuiltInMethodAttribute.cs b/Lilac/Attributes/BuiltInMethodAttribute.cs new file mode 100644 index 0000000..b495f14 --- /dev/null +++ b/Lilac/Attributes/BuiltInMethodAttribute.cs @@ -0,0 +1,17 @@ +using System; + +namespace Lilac.Attributes +{ + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + public class BuiltInMethodAttribute : Attribute + { + public string Name { get; } + public Type DelegateType { get; } + + public BuiltInMethodAttribute(string name, Type delegateType) + { + Name = name; + DelegateType = delegateType; + } + } +} \ No newline at end of file diff --git a/Lilac/Attributes/BuiltInValueAttribute.cs b/Lilac/Attributes/BuiltInValueAttribute.cs new file mode 100644 index 0000000..0b369aa --- /dev/null +++ b/Lilac/Attributes/BuiltInValueAttribute.cs @@ -0,0 +1,16 @@ +using System; + +namespace Lilac.Attributes +{ + [AttributeUsage(AttributeTargets.Property, AllowMultiple = true)] + public class BuiltInValueAttribute : Attribute + { + public string Name { get; } + public string Namespace { get; set; } + + public BuiltInValueAttribute(string name) + { + Name = name; + } + } +} \ No newline at end of file diff --git a/Lilac/Exceptions/NumericTypeException.cs b/Lilac/Exceptions/NumericTypeException.cs new file mode 100644 index 0000000..16e5a71 --- /dev/null +++ b/Lilac/Exceptions/NumericTypeException.cs @@ -0,0 +1,11 @@ +using System; + +namespace Lilac.Exceptions +{ + public class NumericTypeException : Exception + { + public NumericTypeException() { } + public NumericTypeException(string message) : base(message) { } + public NumericTypeException(string message, Exception innerException) : base(message, innerException) { } + } +} \ No newline at end of file diff --git a/Lilac/Exceptions/ParseException.cs b/Lilac/Exceptions/ParseException.cs new file mode 100644 index 0000000..f82080d --- /dev/null +++ b/Lilac/Exceptions/ParseException.cs @@ -0,0 +1,18 @@ +using System; +using Lilac.Parser; + +namespace Lilac.Exceptions +{ + public class ParseException : Exception + { + public ParserState State { get; set; } + + public ParseException() { } + public ParseException(string message) : base(message) { } + public ParseException(string message, ParserState state) : base(message) + { + State = state; + } + public ParseException(string message, Exception innerException) : base(message, innerException) { } + } +} \ No newline at end of file diff --git a/Lilac/Exceptions/SyntaxException.cs b/Lilac/Exceptions/SyntaxException.cs new file mode 100644 index 0000000..29ef578 --- /dev/null +++ b/Lilac/Exceptions/SyntaxException.cs @@ -0,0 +1,11 @@ +using System; + +namespace Lilac.Exceptions +{ + public class SyntaxException : Exception + { + public SyntaxException() { } + public SyntaxException(string message) : base(message) { } + public SyntaxException(string message, Exception innerException) : base(message, innerException) { } + } +} \ No newline at end of file diff --git a/Lilac/Interpreter/Binding.cs b/Lilac/Interpreter/Binding.cs new file mode 100644 index 0000000..32c64cb --- /dev/null +++ b/Lilac/Interpreter/Binding.cs @@ -0,0 +1,12 @@ +using Lilac.Values; + +namespace Lilac.Interpreter +{ + public class Binding + { + public string Name { get; set; } + public bool IsMutable { get; set; } + public Value Value { get; set; } + + } +} \ No newline at end of file diff --git a/Lilac/Interpreter/Evaluator.cs b/Lilac/Interpreter/Evaluator.cs new file mode 100644 index 0000000..c26d008 --- /dev/null +++ b/Lilac/Interpreter/Evaluator.cs @@ -0,0 +1,293 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Lilac.AST; +using Lilac.AST.Expressions; +using Lilac.Parser; +using Lilac.Values; +using String = Lilac.Values.String; + +namespace Lilac.Interpreter +{ + public class Evaluator : IExpressionVisitor + { + private Stack Scopes { get; set; } + public Scope CurrentScope => Scopes.Peek(); + + public Evaluator(Scope topLevelScope) + { + ResetScope(topLevelScope); + } + + public void ResetScope(Scope scope) + { + Scopes = new Stack(); + Scopes.Push(new Scope(scope)); + } + + public Value Evaluate(Expression expression) + { + return expression.Accept(this); + } + + private void PushScope() + { + Scopes.Push(new Scope(CurrentScope)); + } + + public void PushScope(Scope scope) + { + Scopes.Push(scope); + } + + private void PopScope() + { + Scopes.Pop(); + } + + public Value VisitAssignment(AssignmentExpression assignment) + { + var value = assignment.ValueExpression.Accept(this); + CurrentScope.SetValue(assignment.Name, value); + return Unit.Value; + } + + public Value VisitBinding(BindingExpression binding) + { + var value = binding.ValueExpression.Accept(this); + CurrentScope.BindValue(binding.Name, value); + return Unit.Value; + } + + public Value VisitConditional(ConditionalExpression conditional) + { + var condition = conditional.Condition.Accept(this); + return condition.AsBool() + ? conditional.ThenExpression.Accept(this) + : conditional.ElseExpression?.Accept(this) ?? Unit.Value; + } + + public Value VisitFunctionCall(FunctionCallExpression functionCall) + { + var callable = functionCall.Function.Accept(this); + var argument = functionCall.Argument.Accept(this); + return Call(callable, argument); + } + + public Value VisitFunctionDefinition(FunctionDefinitionExpression functionDefinition) + { + var value = new Function(functionDefinition, CurrentScope); + CurrentScope.BindValue(functionDefinition.Name, value); + return Unit.Value; + } + + public Value VisitGroup(GroupExpression group) + { + PushScope(); + var value = group.Expressions.Select(expr => expr.Accept(this)).LastOrDefault(); + PopScope(); + return value ?? Unit.Value; + } + + public Value VisitIdentifier(IdentifierExpression identifier) + { + return CurrentScope.GetValue(identifier.Name); + } + + public Value VisitMutableBinding(MutableBindingExpression mutableBinding) + { + var value = mutableBinding.ValueExpression.Accept(this); + CurrentScope.BindValue(mutableBinding.Name, value, true); + return Unit.Value; + } + + public Value VisitNumberLiteral(NumberLiteralExpression numberLiteral) + { + switch (numberLiteral.LiteralType) + { + case TokenType.DecimalNumber: + return Number.ParseDecimalLiteral(numberLiteral.Value); + case TokenType.BinaryNumber: + return Number.ParseBinaryLiteral(numberLiteral.Value); + case TokenType.HexNumber: + return Number.ParseHexLiteral(numberLiteral.Value); + case TokenType.RationalNumber: + return Number.ParseRationalLiteral(numberLiteral.Value); + case TokenType.ComplexNumber: + return Number.ParseComplexLiteral(numberLiteral.Value); + default: + return Number.Parse(numberLiteral.Value); + } + } + + public Value VisitOperatorCall(OperatorCallExpression operatorCall) + { + var op = CurrentScope.GetValue(operatorCall.Name); + var lhs = operatorCall.Lhs.Accept(this); + var rhs = operatorCall.Rhs.Accept(this); + return Call(Call(op, lhs), rhs); + } + + public Value VisitOperatorDefinition(OperatorDefinitionExpression operatorDefinition) + { + var value = new Function(operatorDefinition, CurrentScope); + CurrentScope.BindValue(operatorDefinition.Name, value); + return Unit.Value; + } + + public Value VisitStringLiteral(StringLiteralExpression stringLiteral) + { + return String.Parse(stringLiteral.Value); + } + + public Value VisitTopLevelExpression(TopLevelExpression topLevelExpression) + { + var value = topLevelExpression.Expressions.Select(expr => expr.Accept(this)).LastOrDefault(); + return value ?? Unit.Value; + } + + public Value VisitNamespacedIdentifier(NamespacedIdentifierExpression namespacedIdentifier) + { + return CurrentScope.GetNamespacedBinding(namespacedIdentifier.Name, namespacedIdentifier.Namespaces).Value; + } + + public Value VisitUsing(UsingExpression usingExpression) + { + CurrentScope.UseNamespace(usingExpression.Namespaces); + return Unit.Value; + } + + public Value VisitOperator(OperatorExpression operatorExpression) + { + return CurrentScope.GetValue(operatorExpression.Name); + } + + public Value VisitMemberAccess(MemberAccessExpression memberAccess) + { + var target = memberAccess.Target.Accept(this); + var value = target.GetMember(memberAccess.Member); + if (value == null) throw new Exception("Member not found!"); + return value; + } + + public Value VisitNamespace(NamespaceExpression namespaceExpression) + { + PushScope(); + var value = namespaceExpression.Expressions.Select(expr => expr.Accept(this)).LastOrDefault(); + var ns = CurrentScope; + PopScope(); + CurrentScope.AddNamespace(namespaceExpression.Namespaces, ns); + return value ?? Unit.Value; + } + + public Value VisitMemberAssignment(MemberAssignmentExpression memberAssignment) + { + var target = memberAssignment.Target.Accept(this); + var value = memberAssignment.ValueExpression.Accept(this); + if (!target.SetMember(memberAssignment.Member, value)) + throw new Exception("Member not found!"); + return Unit.Value; + } + + public Value VisitList(ListExpression list) + { + return new List(list.Expressions.Select(e => e.Accept(this))); + } + + public Value VisitLambda(LambdaExpression lambdaExpression) + { + return new Function(lambdaExpression, CurrentScope); + } + + public Value VisitLinkedList(LinkedListExpression linkedList) + { + return Pair.LinkedList(linkedList.Expressions.Select(e => + { + var group = e as GroupExpression; + return group != null + ? VisitLinkedList(new LinkedListExpression {Expressions = group.Expressions}) + : e.Accept(this); + }).ToList()); + } + + private Value Call(Value callable, Value argument) + { + if (!callable.IsCallable()) + throw new Exception($"{callable} is not callable!"); + + var function = callable as Function; + if (function != null) + { + return CallFunction(function, argument); + } + var curried = callable as CurriedFunction; + if (curried != null) + { + return CallCurried(curried, argument); + } + var builtIn = callable as BuiltInFunction; + if (builtIn != null) + { + return CallBuiltIn(builtIn, argument); + } + + throw new Exception($"{callable} is not callable!"); + } + + private Value CallBuiltIn(BuiltInFunction builtIn, Value argument) + { + return builtIn.ParameterCount > 1 ? new CurriedFunction(builtIn).Apply(argument) : ExecuteBuiltIn(builtIn, new[] { argument }); + } + + private Value CallCurried(CurriedFunction curried, Value argument) + { + curried = curried.Apply(argument); + + var function = curried.Callable as Function; + if (function != null) + { + return curried.AppliedArguments.Count < function.Parameters.Count ? curried : ExecuteFunction(function, curried.AppliedArguments); + } + + var builtIn = curried.Callable as BuiltInFunction; + if (builtIn != null) + { + return curried.AppliedArguments.Count < builtIn.ParameterCount ? curried : ExecuteBuiltIn(builtIn, curried.AppliedArguments); + } + + throw new Exception($"{curried} is not callable!"); + } + + private Value ExecuteFunction(Function function, IReadOnlyList arguments) + { + PushScope(function.DeclaringScope); + PushScope(); + for (var i = 0; i < function.Parameters.Count; i++) + { + CurrentScope.BindValue(function.Parameters[i], arguments[i]); + } + var value = function.Body.Accept(this); + PopScope(); + PopScope(); + return value; + } + + private Value ExecuteBuiltIn(BuiltInFunction function, IEnumerable arguments) + { + try + { + return (Value)function.Method.DynamicInvoke(arguments.Take(function.ParameterCount).ToArray()) ?? Unit.Value; + } + catch (TargetInvocationException e) + { + throw e.InnerException ?? e; + } + } + + private Value CallFunction(Function function, Value argument) + { + return function.Parameters.Count > 1 ? new CurriedFunction(function).Apply(argument) : ExecuteFunction(function, new[] { argument }); + } + } +} \ No newline at end of file diff --git a/Lilac/Interpreter/Interpreter.cs b/Lilac/Interpreter/Interpreter.cs new file mode 100644 index 0000000..3d79e08 --- /dev/null +++ b/Lilac/Interpreter/Interpreter.cs @@ -0,0 +1,179 @@ +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using Lilac.AST; +using Lilac.AST.Definitions; +using Lilac.AST.Expressions; +using Lilac.Attributes; +using Lilac.Parser; +using Lilac.Values; +using String = Lilac.Values.String; + +namespace Lilac.Interpreter +{ + public static class Interpreter + { + #region Private Properties + + private static Scope BuiltInsScope { get; } + private static Context BuiltInsContext { get; set; } + private static Lexer Lexer { get; } = new Lexer(TokenDefinition.TokenDefinitions); + private static ParserState State { get; set; } + private static Parser Parser { get; } = Lilac.Parser.Parser.TopLevel(); + private static Evaluator Evaluator { get; } + + #endregion + + #region Type Constructor + + static Interpreter() + { + BuiltInsContext = new Context(Enumerable.Empty()); + BuiltInsScope = new Scope(); + foreach (var type in Assembly.GetExecutingAssembly().GetTypes()) + { + foreach ( + var method in type.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)) + { + var attributes = method.GetCustomAttributes(); + foreach (var attribute in attributes) + { + AddBuiltInFunction(attribute, method); + } + } + foreach ( + var property in + type.GetProperties(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)) + { + var attributes = property.GetCustomAttributes(); + foreach (var attribute in attributes) + { + AddBuiltInValue(attribute, property); + } + } + } + Evaluator = new Evaluator(BuiltInsScope); + } + + #endregion + + #region Private Methods + + private static void AddBuiltInValue(BuiltInValueAttribute attribute, PropertyInfo property) + { + if (string.IsNullOrWhiteSpace(attribute.Namespace)) + { + BuiltInsScope.BindValue(attribute.Name, (Value) property.GetValue(null)); + BuiltInsContext = BuiltInsContext.AddDefinition(new Definition(attribute.Name)); + } + else + { + var namespaces = attribute.Namespace.Split('.'); + BuiltInsScope.BindNamespacedValue(attribute.Name, (Value) property.GetValue(null), namespaces); + BuiltInsContext = BuiltInsContext.AddNamespacedDefinition(namespaces, new Definition(attribute.Name)); + } + } + + private static void AddBuiltInFunction(BuiltInFunctionAttribute attribute, MethodInfo method) + { + var definition = attribute.IsOperator + ? new OperatorDefinition(attribute.Name, 0, Association.L) + : new Definition(attribute.Name); + if (string.IsNullOrWhiteSpace(attribute.Namespace)) + { + BuiltInsScope.BindValue(attribute.Name, new BuiltInFunction(method, attribute.DelegateType)); + BuiltInsContext = BuiltInsContext.AddDefinition(definition); + } + else + { + var namespaces = attribute.Namespace.Split('.'); + BuiltInsScope.BindNamespacedValue(attribute.Name, new BuiltInFunction(method, attribute.DelegateType), + namespaces); + BuiltInsContext = BuiltInsContext.AddNamespacedDefinition(namespaces, definition); + } + } + + private static Value EvaluateProgram(TextReader text) + { + var scope = Evaluator.CurrentScope; + try + { + var tokens = Lexer.Tokenize(text).ToList(); + var state = State?.New(tokens) ?? new ParserState(tokens, BuiltInsContext); + var expr = Parser.Parse(ref state); + State = state; + var value = Evaluator.Evaluate(expr); + return value; + } + catch (Exception) + { + Evaluator.ResetScope(scope); + throw; + } + } + + #endregion + + #region Public Methods + + public static void RunRepl() + { + while (true) + { + Console.Write(">>> "); + var line = Console.ReadLine(); + if (line == null) break; + try + { + var value = EvaluateProgram(new StringReader(line)); + if (!(value is Unit)) Console.WriteLine(value); + } + catch (Exception e) + { + Console.WriteLine("Runtime Error: " + e.Message); + } + } + } + + public static Value Open(string file) + { + var text = File.OpenText(file); + var value = EvaluateProgram(text); + return value; + } + + #endregion + + #region Built In Functions + + [BuiltInFunction("open", typeof(Func))] + private static Value Open(String s) + { + return Open(s.ToString()); + } + + [BuiltInFunction("reset", typeof(Func))] + public static Unit Reset() + { + State = null; + Evaluator.ResetScope(BuiltInsScope); + Console.Clear(); + return Unit.Value; + } + + [BuiltInFunction("quit", typeof(Action))] + private static void Quit() + { + Environment.Exit(0); + } + + [BuiltInFunction("list-built-ins", typeof(Func))] + public static String ListBuiltIns() + { + return String.Get(BuiltInsScope.ListBindings()); + } + + #endregion + } +} \ No newline at end of file diff --git a/Lilac/Interpreter/Scope.cs b/Lilac/Interpreter/Scope.cs new file mode 100644 index 0000000..e40233d --- /dev/null +++ b/Lilac/Interpreter/Scope.cs @@ -0,0 +1,154 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Lilac.Values; + +namespace Lilac.Interpreter +{ + public class Scope + { + public HashSet UsedNamespaces { get; } = new HashSet(); + public Dictionary Namespaces { get; set; } = new Dictionary(); + public Dictionary Bindings { get; set; } = new Dictionary(); + public Scope ParentScope { get; set; } + + public Scope(){} + + public Scope(Scope parent) + { + ParentScope = parent; + } + + public string ListBindings(string prefix = "") + { + var sb = new StringBuilder(); + + foreach (var binding in Bindings) + { + sb.Append(prefix).Append(binding.Key).AppendLine(); + } + + foreach (var ns in Namespaces) + { + sb.Append(ns.Value.ListBindings($"{prefix}{ns.Key}.")); + } + + return sb.ToString(); + } + + public bool BindingExists(string name) + => Bindings.ContainsKey(name) || ParentScope?.BindingExists(name) == true; + + public Value GetValue(string name) + { + return GetBinding(name).Value; + } + + private bool TryGetBinding(string name, out Binding binding) + { + if (TryGetLocalBinding(name, out binding)) return true; + return ParentScope?.TryGetBinding(name, out binding) == true; + } + + private bool TryGetLocalBinding(string name, out Binding binding) + { + if (Bindings.TryGetValue(name, out binding)) return true; + foreach (var ns in UsedNamespaces) + { + if (ns.TryGetLocalBinding(name, out binding)) return true; + } + return false; + } + + private bool TryGetNamespace(string name, out Scope ns) + { + if (Namespaces.TryGetValue(name, out ns)) return true; + foreach (var usedNs in UsedNamespaces) + { + if (usedNs.TryGetNamespace(name, out ns)) return true; + } + return ParentScope?.TryGetNamespace(name, out ns) == true; + } + + public Binding GetBinding(string name) + { + Binding binding; + if (!TryGetBinding(name, out binding)) + throw new Exception($"Binding {name} not found!"); + return binding; + } + + public Scope GetNamespace(string name) + { + Scope space; + if (!TryGetNamespace(name, out space)) + throw new Exception($"Namespace {name} not found!"); + return space; + } + + public Scope GetNamespace(IList namespaces) + { + if (namespaces.Count == 0) + return this; + var space = GetNamespace(namespaces[0]); + return space.GetNamespace(namespaces.Skip(1).ToList()); + } + + public void UseNamespace(IList namespaces) + { + UsedNamespaces.Add(GetNamespace(namespaces)); + } + + public Binding GetNamespacedBinding(string name, IList namespaces) + { + if (namespaces.Count == 0) + return GetBinding(name); + var space = GetNamespace(namespaces[0]); + return space.GetNamespacedBinding(name, namespaces.Skip(1).ToArray()); + } + + public void BindValue(string name, Value value, bool isMutable = false) + { + if (Bindings.ContainsKey(name)) throw new Exception($"Local binding {name} already exists!"); + Bindings[name] = new Binding {Name = name, Value = value, IsMutable = isMutable}; + } + + public void BindNamespacedValue(string name, Value value, IList namespaces, bool isMutable = false) + { + if (namespaces.Count == 0) + { + BindValue(name, value, isMutable); + return; + } + Scope space; + if (!Namespaces.TryGetValue(namespaces[0], out space)) + { + Namespaces[namespaces[0]] = space = new Scope(); + } + space.BindNamespacedValue(name, value, namespaces.Skip(1).ToArray(), isMutable); + } + + public void SetValue(string name, Value value) + { + var binding = GetBinding(name); + if (!binding.IsMutable) throw new Exception($"Binding {name} is not mutable!"); + binding.Value = value; + } + + public void AddNamespace(List namespaces, Scope ns) + { + if (namespaces.Count == 1) + { + Namespaces.Add(namespaces[0], ns); + return; + } + Scope space; + if (!Namespaces.TryGetValue(namespaces[0], out space)) + { + Namespaces[namespaces[0]] = space = new Scope(); + } + space.AddNamespace(namespaces.Skip(1).ToList(), ns); + } + } +} \ No newline at end of file diff --git a/Lilac/Lilac.csproj b/Lilac/Lilac.csproj new file mode 100644 index 0000000..954b3bd --- /dev/null +++ b/Lilac/Lilac.csproj @@ -0,0 +1,149 @@ + + + + + Debug + AnyCPU + {940AF5DF-EBDE-4EFC-8B03-8CC3B1829DD0} + Exe + Properties + Lilac + Lilac + v4.5.2 + 512 + true + + + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + Lilac.Program + + + + ..\packages\BigRationalLibrary.1.0.0.0\lib\NETFramework40\BigRationalLibrary.dll + True + + + ..\packages\JetBrains.Annotations.10.2.1\lib\net\JetBrains.Annotations.dll + True + + + + ..\packages\System.Collections.Immutable.1.3.0\lib\portable-net45+win8+wp8+wpa81\System.Collections.Immutable.dll + True + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Always + + + + Always + + + + + \ No newline at end of file diff --git a/Lilac/Parser/Lexer.cs b/Lilac/Parser/Lexer.cs new file mode 100644 index 0000000..2e966e8 --- /dev/null +++ b/Lilac/Parser/Lexer.cs @@ -0,0 +1,147 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using Lilac.Exceptions; + +namespace Lilac.Parser +{ + public class Lexer + { + public List TokenDefinitions { get; } + + public Lexer(IEnumerable definitions) + { + TokenDefinitions = definitions.OrderBy(def => def.Priority).ToList(); + TabWidth = 4; + } + + public int TabWidth { get; set; } + private Regex Regex { get; set; } + private int Line { get; set; } + private Stack Indentations { get; set; } + + private Token NewToken(TokenType type, string content) + { + return new Token {TokenType = type, Content = content, Column = 0, Line = Line}; + } + private Token NewToken(TokenType type, string content, int column) + { + return new Token {TokenType = type, Content = content, Column = column, Line = Line}; + } + + public IEnumerable Tokenize(TextReader reader) + { + Indentations = new Stack(new []{0}); + Line = 0; + Regex = new Regex(string.Join("|", + TokenDefinitions.Select(td => $"(?<{td.TokenType}>{td.Regex})")) + "|(?.)", + RegexOptions.ExplicitCapture); + + yield return NewToken(TokenType.OpenGroup, "bof"); + + string line; + while ((line = reader.ReadLine()) != null) + { + foreach (var token in TokenizeLine(line)) + yield return token; + } + + while (Indentations.Count > 1) + { + Indentations.Pop(); + yield return NewToken(TokenType.CloseGroup, "dedent"); + yield return NewToken(TokenType.Newline, string.Empty); + } + + yield return NewToken(TokenType.CloseGroup, "eof"); + } + + + private IEnumerable TokenizeLine(string line) + { + ++Line; + + var tokens = GetLineTokens(line); + + if (!tokens.Any()) yield break; + + foreach (var token in GetIndentationTokens(line)) yield return token; + + foreach (var token in tokens) yield return token; + + yield return NewToken(TokenType.Newline, string.Empty); + } + + private List GetLineTokens(string line) + { + return GetRegexMatches(line).Select(match => + new + { + match.Value, + Definition = GetTokenDefinition(match), + Column = Indentations.Peek() + match.Index + }) + .Where(match => !match.Definition.IsIgnored) + .Select(match => NewToken(match.Definition.TokenType, match.Value, match.Column)) + .ToList(); + } + + private TokenDefinition GetTokenDefinition(Match match) + { + if (match.Groups["Unrecognized"].Success) + { + throw new SyntaxException($"Unrecognized token '{match.Value}'."); + } + return TokenDefinitions.Find(definition => match.Groups[definition.TokenType.ToString()].Success); + } + + private IEnumerable GetRegexMatches(string line) + { + return Regex.Matches(line.TrimStart(GetLeadingWhitespace(line))) + .Cast(); + } + + private IEnumerable GetIndentationTokens(string line) + { + var whitespace = GetLeadingWhitespace(line); + var indent = GetIndentationLevel(whitespace); + var currentIndent = Indentations.Peek(); + + if (indent == currentIndent) yield break; + + if (indent > currentIndent) + { + Indentations.Push(indent); + yield return NewToken(TokenType.OpenGroup, "indent"); + } + else + { + while (indent != Indentations.Peek()) + { + Indentations.Pop(); + yield return NewToken(TokenType.CloseGroup, "dedent"); + yield return NewToken(TokenType.Newline, string.Empty); + } + } + } + + private static char[] GetLeadingWhitespace(string line) + { + return line.TakeWhile(char.IsWhiteSpace).ToArray(); + } + + private int GetIndentationLevel(IEnumerable whitespace) + { + return whitespace.Sum(c => + { + switch (c) + { + case '\t': return TabWidth; + case ' ': return 1; + default: return 0; + } + }); + } + } +} \ No newline at end of file diff --git a/Lilac/Parser/Parser.cs b/Lilac/Parser/Parser.cs new file mode 100644 index 0000000..0de6ca9 --- /dev/null +++ b/Lilac/Parser/Parser.cs @@ -0,0 +1,619 @@ +using System; +using System.Collections.Generic; +using Lilac.AST; +using Lilac.AST.Definitions; +using Lilac.AST.Expressions; +using Lilac.Exceptions; +using Lilac.Utilities; + +namespace Lilac.Parser +{ + public delegate Result, ParseException> Parser(ParserState state); + + public static class Parser + { + #region Monad + + public static Parser AsParser(this T value) + => state => new Ok, ParseException>(Tuple.Create(state, value)); + + public static Parser Bind(this Parser parser, Func> func) + => state => + from result1 in parser(state) + from result2 in result1.Map((newState, output) => func(output)(newState)) + select result2; + + public static Parser SelectMany(this Parser parser, Func> func, + Func select) + => parser.Bind(val1 => func(val1).Bind(val2 => select(val1, val2).AsParser())); + + public static Parser Select(this Parser parser, Func func) + => parser.Bind(val => func(val).AsParser()); + + public static Parser Where(this Parser parser, Func predicate) => state => + from result in parser(state) + where predicate(result.Item2) + select result; + + #endregion + + #region Utility + + public static Result, ParseException> Ok(Tuple tuple) + { + return new Ok, ParseException>(tuple); + } + public static Result, ParseException> Ok(ParserState state, T value) + { + return new Ok, ParseException>(Tuple.Create(state, value)); + } + + public static Result, ParseException> Error(string message, ParserState state) + { + return new Error, ParseException>(new ParseException(message, state)); + } + + public static Result, ParseException> OnFailure(this Maybe> maybe, string message, ParserState state) + => Result, ParseException>.FromMaybe(maybe, new ParseException(message, state)); + + public static Expression Parse(this Parser parser, ParserState state) where T : Expression + => parser(state).Match( + ok => ok.Item2, + error => { throw error; }); + + public static Expression Parse(this Parser parser, ref ParserState state) where T : Expression + { + var result = parser(state); + var ok = result as Ok, ParseException>; + if (ok == null) throw ((Error, ParseException>) result).Value; + state = ok.Value.Item1; + return ok.Value.Item2; + } + + public static Parser AsExpression(this Parser parser) where T : Expression + => state => + from result in parser(state) + select result.Map((parserState, expression) => Tuple.Create(parserState, (Expression)expression)); + + public static Parser Or(this Parser parser1, Parser parser2) + where T1 : Expression where T2 : Expression + => state => parser1.AsExpression()(state).Match( + Ok, + error => parser2.AsExpression()(state)); + + public static Parser Or(this Parser parser1, Parser parser2) + => state => parser1(state).Match( + Ok, + error => parser2(state)); + + public static Parser> Star(this Parser parser) + => state => + { + Ok, ParseException> result; + var list = new List(); + while ((result = parser(state) as Ok, ParseException>) != null) + { + result.Value.Map((newState, output) => + { + state = newState; + list.Add(output); + }); + } + return Ok(state, list); + }; + + public static Parser> StarSep(this Parser parser, Parser sepParser) + => state => + { + Ok, ParseException> result; + var list = new List(); + + var first = parser(state) as Ok, ParseException>; + if (first == null) + { + return Ok(state, list); + } + + first.Value.Map((newState, output) => + { + state = newState; + list.Add(output); + }); + + var combined = from sep in sepParser from elem in parser select elem; + + while ((result = combined(state) as Ok, ParseException>) != null) + { + result.Value.Map((newState, output) => + { + state = newState; + list.Add(output); + }); + } + return Ok(state, list); + }; + + public static Parser> Plus(this Parser parser, string errorMessage = null) + => state => + { + var star = (Ok>, ParseException>) parser.Star()(state); + return star.Value.Map((newstate, list) => list.Count == 0) + ? Error>(errorMessage ?? "Expected one or more of something, got none.", state) + : star; + }; + + public static Parser> PlusSep(this Parser parser, Parser sepParser, string errorMessage = null) + => state => + { + var star = (Ok>, ParseException>) parser.StarSep(sepParser)(state); + return star.Value.Map((newstate, list) => list.Count == 0) + ? Error>(errorMessage ?? "Expected separated list of one or more of something, got none.", state) + : star; + }; + + public static Parser> Opt(this Parser parser) + => state => parser(state).Match( + ok => ok.Map((newState, output) => Ok(newState, output.ToMaybe())), + error => Ok(state, Maybe.Nothing)); + + public static Parser> IfSoContinueWith(this Parser parser, Func> cont) + => state => parser(state).Match( + ok => ok.Map((newState, output) => cont(output).Opt()(newState)), + error => Ok(state, Maybe.Nothing)); + + public static Parser WithOptLeadingIf(this Parser parser, Parser leading, + Func predicate) => + from l in leading.Opt() + from t in parser + where predicate(t) || l is Nothing + select t; + + public static Parser WithOptLeading(this Parser parser, Parser leading) => + from l in leading.Opt() + from t in parser + select t; + + public static Parser AddDefinition(Definition definition) + => state => Ok(state.AddDefinition(definition), new EmptyExpression()); + + public static Parser PushContext() + => state => Ok(state.PushContext(), new EmptyExpression()); + + public static Parser PopContext() + => state => Ok(state.PopContext(), state.Context); + + public static Parser UseNamespace(IList namespaces) + => state => Ok(state.UseNamespace(namespaces), new EmptyExpression()); + + public static Parser IsDefinedNamespace(IList namespaces) + => state => Ok(state, state.IsDefinedNamespace(namespaces)); + + public static Parser AddNamespace(IList namespaces, Context context) + => state => Ok(state.AddNamespace(namespaces, context), new EmptyExpression()); + + public static Parser TempReserveWords(params string[] words) + => state => Ok(state.TempReserveWords(words), words); + + public static Parser UnReserveWords(params string[] words) + => state => Ok(state.TempReserveWords(words), words); + + #endregion + + #region Terminals + + public static Parser Id() + => state => ( + from token in state.GetToken() + where token.TokenType == TokenType.Identifier && !state.IsDefinedOperator(token.Content) && !state.IsTempReserved(token.Content) + select Tuple.Create(state.NextToken($"Parsed {token}"), token.Content)) + .OnFailure($"Expected identifier, got {state.GetToken()}.", state); + + public static Parser Id(string name) + => state => ( + from token in state.GetToken() + where token.TokenType == TokenType.Identifier && token.Content == name && !state.IsDefinedOperator(token.Content) + select Tuple.Create(state.NextToken($"Parsed {token}"), token.Content)) + .OnFailure($"Expected identifier, got {state.GetToken()}.", state); + + public static Parser ReservedWord(string name) + => state => ( + from token in state.GetToken() + where (token.TokenType == TokenType.ReservedWord || state.IsTempReserved(token.Content)) && token.Content == name + select Tuple.Create(state.NextToken($"Parsed {token}"), new EmptyExpression())) + .OnFailure($"Expected '{name}', got {state.GetToken()}.", state); + + public static Parser IdNotEquals() + => state => ( + from token in state.GetToken() + where + token.Content != "=" && token.TokenType == TokenType.Identifier && + !state.IsDefinedOperator(token.Content) + select Tuple.Create(state.NextToken($"Parsed {token}"), token.Content)) + .OnFailure($"Expected identifier, got {state.GetToken()}.", state); + + public static Parser Equals() => state => ( + from token in state.GetToken() + where token.TokenType == TokenType.Identifier && token.Content == "=" + select Tuple.Create(state.NextToken($"Parsed {token}"), new EmptyExpression())) + .OnFailure($"Expected identifier, got {state.GetToken()}.", state); + + public static Parser Operator() + => state => ( + from token in state.GetToken() + where token.TokenType == TokenType.Identifier && state.IsDefinedOperator(token.Content) + select Tuple.Create(state.NextToken($"Parsed {token}"), token.Content)) + .OnFailure($"Expected operator, got {state.GetToken()}.", state); + + public static Maybe OpenGroupType(string name) + { + switch (name) + { + case "bof": + return GroupType.TopLevel.ToMaybe(); + case "(": + return GroupType.Parenthesized.ToMaybe(); + case "indent": + return GroupType.Indented.ToMaybe(); + default: + return Maybe.Nothing; + } + } + + public static Maybe CloseGroupType(string name) + { + switch (name) + { + case "eof": + return GroupType.TopLevel.ToMaybe(); + case ")": + return GroupType.Parenthesized.ToMaybe(); + case "dedent": + return GroupType.Indented.ToMaybe(); + default: + return Maybe.Nothing; + } + } + + public static Parser GroupOpen() + => state => ( + from token in state.GetToken() + from type in OpenGroupType(token.Content) + select Tuple.Create(state.NextToken($"Parsed {token}"), type)) + .OnFailure($"Expected group opener, got {state.GetToken()}.", state); + + public static Parser GroupOpen(GroupType type) + => state => ( + from token in state.GetToken() + from openType in OpenGroupType(token.Content) + where type == openType + select Tuple.Create(state.NextToken($"Parsed {token}"), type)) + .OnFailure($"Expected {type} group opener, got {state.GetToken()}.", state); + + public static Parser GroupClose(GroupType type) + => state => ( + from token in state.GetToken() + from closeType in CloseGroupType(token.Content) + where type == closeType + select Tuple.Create(state.NextToken($"Parsed {token}"), type)) + .OnFailure($"Expected {type} group closer, got {state.GetToken()}.", state); + + public static Parser ListOpen() + => state => ( + from token in state.GetToken() + where token.TokenType == TokenType.OpenList + select Tuple.Create(state.NextToken($"Parsed {token}"), new EmptyExpression())) + .OnFailure($"Expected list opener, got {state.GetToken()}.", state); + + public static Parser ListClose() + => state => ( + from token in state.GetToken() + where token.TokenType == TokenType.CloseList + select Tuple.Create(state.NextToken($"Parsed {token}"), new EmptyExpression())) + .OnFailure($"Expected list closer, got {state.GetToken()}.", state); + + public static Parser Period() + => state => ( + from token in state.GetToken() + where token.TokenType == TokenType.Period + select Tuple.Create(state.NextToken($"Parsed {token}"), new EmptyExpression())) + .OnFailure($"Expected period, got {state.GetToken()}.", state); + + public static Parser Backquote() + => state => ( + from token in state.GetToken() + where token.TokenType == TokenType.Backquote + select Tuple.Create(state.NextToken($"Parsed {token}"), new EmptyExpression())) + .OnFailure($"Expected period, got {state.GetToken()}.", state); + + public static Parser Comma() + => state => ( + from token in state.GetToken() + where token.TokenType == TokenType.Comma + select Tuple.Create(state.NextToken($"Parsed {token}"), new EmptyExpression())) + .OnFailure($"Expected comma, got {state.GetToken()}.", state); + + public static Parser Newline() + => state => ( + from token in state.GetToken() + where token.TokenType == TokenType.Newline + select Tuple.Create(state.NextToken($"Parsed {token}"), new EmptyExpression())) + .OnFailure($"Expected newline, got {state.GetToken()}.", state); + + public static Parser EmptyGroup() + => state => ( + from open in state.GetToken() + where open.TokenType == TokenType.OpenGroup && open.Content == "(" + from close in state.NextToken().GetToken() + where close.TokenType == TokenType.CloseGroup && close.Content == ")" + select Tuple.Create(state.NextToken().NextToken($"Parsed {open} and {close}"), new EmptyExpression())) + .OnFailure($"Expected (), got {state.GetToken()}.", state); + + public static Parser Number() + => state => ( + from token in state.GetToken() + where token.TokenType < TokenType.Number + select + Tuple.Create(state.NextToken($"Parsed {token}"), new NumberLiteralExpression {Value = token.Content, LiteralType = token.TokenType})) + .OnFailure($"Expected number, got {state.GetToken()}.", state); + + public static Parser String() + => state => ( + from token in state.GetToken() + where token.TokenType == TokenType.String + select + Tuple.Create(state.NextToken($"Parsed {token}"), new StringLiteralExpression {Value = token.Content})) + .OnFailure($"Expected string, got {state.GetToken()}.", state); + + #endregion + + #region NonTerminals + + public static Parser Definition() => + Let() + .Or(LetRef()) + .Or(FunctionDef()) + .Or(OperatorDef()) + .Or(Using()) + .Or(Namespace()) + .Or(Assignment()) + .Or(MemberAssignment()); + + public static Parser Expression() => + Number() + .Or(NamespacedId()) + .Or(Identifier()) + .Or(Cond()) + .Or(Group()) + .Or(List()) + .Or(LinkedList()) + .Or(String()) + .Or(OperatorFunction()) + .Or(Lambda()) + .PostfixExpression(); + + public static Parser Identifier() => + from id in Id() + select new IdentifierExpression {Name = id}; + + public static Parser OperatorFunction() => + from open in GroupOpen(GroupType.Parenthesized) + from op in Operator() + from close in GroupClose(GroupType.Parenthesized) + select new OperatorExpression {Name = op}; + + public static Parser FunctionCall(this Expression function) => + from argument in Expression() + select new FunctionCallExpression {Function = function, Argument = argument}; + + public static Parser OperatorCall(this Expression lhs) => + from op in Operator() + from rhs in Expression() + select new OperatorCallExpression {Lhs = lhs, Name = op, Rhs = rhs}; + + public static Parser MemberAccess(this Expression target) => + from dot in Period() + from member in Id() + select new MemberAccessExpression {Target = target, Member = member}; + + public static Parser PostfixExpression(this Parser parser) where T : Expression => + from expr in parser + from maybe in expr.OperatorCall() + .Or(expr.FunctionCall()) + .Or(expr.MemberAccess()) + .IfSoContinueWith(just => just.AsParser().PostfixExpression()) + select maybe.Match(e => e.ResolvePrecedence(), () => expr); + + public static Parser Group() => + from groupType in GroupOpen() + //.Or( + // from nl in Newline() + // from groupType in GroupOpen(GroupType.Indented) + // select groupType) + from nls1 in Newline().Star() + from pushScope in PushContext() + from exprs in Expression().Or(Definition()).StarSep(Newline().Plus()) + from popScope in PopContext() + from nls2 in Newline().Star() + from close in GroupClose(groupType) + select new GroupExpression {Expressions = exprs, GroupType = groupType}; + + public static Parser TopLevel() => + from open in GroupOpen(GroupType.TopLevel) + from exprs in Expression().Or(Definition()).StarSep(Newline().Plus()) + from nls in Newline().Star() + from close in GroupClose(GroupType.TopLevel) + select new TopLevelExpression {Expressions = exprs, GroupType = GroupType.TopLevel}; + + public static Parser Let() => + from letId in ReservedWord("let") + from id in Id() + from eq in Equals() + from expr in Expression().WithOptLeadingIf(Newline(), exp => exp is GroupExpression || exp is ListExpression) + select new BindingExpression {Name = id, ValueExpression = expr}; + + public static Parser LetRef() => + from letId in ReservedWord("let") + from refId in ReservedWord("ref") + from id in Id() + from eq in Equals() + from expr in Expression().WithOptLeadingIf(Newline(), exp => exp is GroupExpression || exp is ListExpression) + select new MutableBindingExpression { Name = id, ValueExpression = expr }; + + public static Parser> ArgList() => + (from empty in EmptyGroup() + select new List()) + .Or>(IdNotEquals().Plus("Expected argument list.")); + + public static Parser FunctionDef() => + from letId in ReservedWord("let") + from id in Id() + from args in ArgList() + from eq in Equals() + from pushScope in PushContext() + from expr in Expression().WithOptLeadingIf(Newline(), exp => exp is GroupExpression) + from popScope in PopContext() + from addFunction in AddDefinition(new Definition(id)) + select new FunctionDefinitionExpression + { + Name = id, + Parameters = args, + Body = expr + }; + + public static Parser OperatorDef() => + from letId in ReservedWord("let") + from opId in ReservedWord("operator") + from p in ( + from _ in Id("precedence") + from p in Number() + select decimal.Parse(p.Value)).Opt() + let precedence = p.GetValueOrDefault() + from a in ( + from _ in Id("associates") + from a in Id("L").Or(Id("R")) + select (Association) Enum.Parse(typeof(Association), a)).Opt() + let association = a.GetValueOrDefault() + from id in Id() + from args in ArgList() + from eq in Equals() + from pushScope in PushContext() + from expr in Expression().WithOptLeadingIf(Newline(), exp => exp is GroupExpression) + from popScope in PopContext() + from addOperator in AddDefinition(new OperatorDefinition(id, precedence, association)) + select new OperatorDefinitionExpression + { + Name = id, + Parameters = args, + Body = expr, + Precedence = precedence, + Association = association + }; + + public static Parser CondBranch() => Expression().Or(Assignment()).Or(MemberAssignment()); + + public static Parser Cond() => + from ifId in ReservedWord("if") + from condExpr in Expression() + from nl0 in Newline().Opt() + from thenId in ReservedWord("then") + from nl1 in Newline().Opt() + from thenExpr in CondBranch() + from elseExpr in ( + from nl2 in Newline().Opt() + from elseId in ReservedWord("else") + from nl3 in Newline().Opt() + from elseExpr in CondBranch() + select elseExpr).Opt() + select new ConditionalExpression + { + Condition = condExpr, + ThenExpression = thenExpr, + ElseExpression = elseExpr.GetValueOrDefault() + }; + + public static Parser Assignment() => + from set in ReservedWord("set!") + from id in Id() + from eq in Equals() + from expr in Expression().WithOptLeadingIf(Newline(), exp => exp is GroupExpression || exp is ListExpression) + select new AssignmentExpression { Name = id, ValueExpression = expr }; + + public static Parser MemberAssignment() => + from set in ReservedWord("set!") + from target in Expression() + let eqExpr = target as OperatorCallExpression + where eqExpr?.Name == "=" + let lhs = eqExpr.Lhs as MemberAccessExpression + where lhs != null + select new MemberAssignmentExpression + { + Target = lhs.Target, + Member = lhs.Member, + ValueExpression = eqExpr.Rhs + }; + + public static Parser NamespacedId() => + from namespaces in (from id in Id() from dot in Period() select id).Plus() + from isDefined in IsDefinedNamespace(namespaces) + where isDefined + from id in Id() + select new NamespacedIdentifierExpression { Namespaces = namespaces, Name = id }; + + public static Parser Using() => + from usingId in ReservedWord("using") + from namespaces in Id().PlusSep(Period()) + from use in UseNamespace(namespaces) + select new UsingExpression {Namespaces = namespaces}; + + public static Parser Namespace() => + from nsId in ReservedWord("namespace") + from namespaces in Id().PlusSep(Period()) + from eq in Equals() + from nl in Newline().Opt() + from groupType in GroupOpen() + from nls1 in Newline().Star() + from pushScope in PushContext() + from exprs in Expression().Or(Definition()).StarSep(Newline().Plus()) + from popScope in PopContext() + from addNamespace in AddNamespace(namespaces, popScope) + from nls2 in Newline().Star() + from close in GroupClose(groupType) + select new NamespaceExpression { Namespaces = namespaces, Expressions = exprs, GroupType = groupType }; + + public static Parser List() => + from open in ListOpen() + from exprs in ( + from nl1 in Newline() + from grp in Group() + where grp.GroupType == GroupType.Indented + from nl2 in Newline() + select grp.Expressions + ).Or>(Expression().StarSep(Newline())) + from close in ListClose() + select new ListExpression {Expressions = exprs}; + + public static Parser LinkedList() => + from quote in Backquote() + from open in GroupOpen(GroupType.Parenthesized) + from exprs in ( + from nl1 in Newline() + from grp in Group() + where grp.GroupType == GroupType.Indented + from nl2 in Newline() + select grp.Expressions + ).Or>(Expression().StarSep(Newline())) + from close in GroupClose(GroupType.Parenthesized) + select new LinkedListExpression { Expressions = exprs}; + + public static Parser Lambda() => + from lambda in ReservedWord("lambda") + from args in ArgList() + from eq in Equals() + from expr in Expression().WithOptLeadingIf(Newline(), exp => exp is GroupExpression) + select new LambdaExpression {Parameters = args, Body = expr}; + + + + #endregion + } +} \ No newline at end of file diff --git a/Lilac/Parser/ParserState.cs b/Lilac/Parser/ParserState.cs new file mode 100644 index 0000000..6c9dafe --- /dev/null +++ b/Lilac/Parser/ParserState.cs @@ -0,0 +1,204 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using Lilac.AST; +using Lilac.AST.Definitions; +using Lilac.Utilities; + +namespace Lilac.Parser +{ + public class ParserState + { + private IBidirectionalIterator TokenStream { get; set; } + public Context Context { get; private set; } + private ImmutableList Messages { get; set; } + private ImmutableHashSet TempReservedWords { get; set; } + + private ParserState() { } + + public ParserState(IEnumerable tokens, Context context) + { + TokenStream = new BidirectionalIterator(tokens); + TokenStream.MoveNext(); + Messages = ImmutableList.Empty; + Context = context; + TempReservedWords = ImmutableHashSet.Empty; + } + + public ParserState New(IEnumerable tokens) + { + return new ParserState(tokens, Context); + } + + public bool IsDefinedOperator(string name) + { + return Context.GetDefinition(name) is OperatorDefinition; + } + + public ParserState Copy() + { + return new ParserState + { + TokenStream = TokenStream.Copy(), + Context = Context, + Messages = Messages, + TempReservedWords = TempReservedWords + }; + } + + public ParserState AddDefinition(Definition definition) + { + return new ParserState + { + TokenStream = TokenStream.Copy(), + Context = Context.AddDefinition(definition), + Messages = Messages, + TempReservedWords = TempReservedWords + }; + } + + public ParserState NextToken() + { + var state = new ParserState + { + TokenStream = TokenStream.Copy(), + Context = Context, + Messages = Messages, + TempReservedWords = TempReservedWords + }; + state.TokenStream.MoveNext(); + return state; + } + + public ParserState NextToken(string message) + { + var state = new ParserState + { + TokenStream = TokenStream.Copy(), + Context = Context, + Messages = Messages.Add(message), + TempReservedWords = TempReservedWords + }; + state.TokenStream.MoveNext(); + return state; + } + + public Maybe GetToken() + { + try + { + return TokenStream.Current.ToMaybe(); + } + catch (Exception) + { + return Maybe.Nothing; + } + } + + public ParserState PushContext() + { + return new ParserState + { + TokenStream = TokenStream.Copy(), + Context = Context.NewChild(), + Messages = Messages, + TempReservedWords = TempReservedWords + }; + } + + public ParserState PopContext() + { + return new ParserState + { + TokenStream = TokenStream.Copy(), + Context = Context.Parent, + Messages = Messages, + TempReservedWords = TempReservedWords + }; + } + + public ParserState AddNamespacedDefinition(IList namespaces, Definition definition) + { + return new ParserState + { + TokenStream = TokenStream.Copy(), + Context = Context.AddNamespacedDefinition(namespaces, definition), + Messages = Messages, + TempReservedWords = TempReservedWords + }; + } + + public ParserState UseNamespace(IList namespaces) + { + return new ParserState + { + TokenStream = TokenStream.Copy(), + Context = Context.UseNamespace(namespaces), + Messages = Messages, + TempReservedWords = TempReservedWords + }; + } + + public bool IsDefinedNamespace(IList namespaces) + { + return Context.GetNamespace(namespaces) != null; + } + + public ParserState AddNamespace(IList namespaces, Context context) + { + return new ParserState + { + TokenStream = TokenStream.Copy(), + Context = Context.AddNamespace(namespaces, context), + Messages = Messages, + TempReservedWords = TempReservedWords + }; + } + + public ParserState TempReserveWords(IEnumerable words) + { + return new ParserState + { + TokenStream = TokenStream.Copy(), + Context = Context, + Messages = Messages, + TempReservedWords = TempReservedWords.Union(words) + }; + } + + public ParserState TempReserveWords(params string[] words) + { + return new ParserState + { + TokenStream = TokenStream.Copy(), + Context = Context, + Messages = Messages, + TempReservedWords = TempReservedWords.Union(words) + }; + } + + public ParserState UnReserveWords(IEnumerable words) + { + return new ParserState + { + TokenStream = TokenStream.Copy(), + Context = Context, + Messages = Messages, + TempReservedWords = TempReservedWords.Except(words) + }; + } + + public ParserState UnReserveWords(params string[] words) + { + return new ParserState + { + TokenStream = TokenStream.Copy(), + Context = Context, + Messages = Messages, + TempReservedWords = TempReservedWords.Except(words) + }; + } + + public bool IsTempReserved(string word) => TempReservedWords.Contains(word); + } +} \ No newline at end of file diff --git a/Lilac/Parser/Token.cs b/Lilac/Parser/Token.cs new file mode 100644 index 0000000..52d5ef9 --- /dev/null +++ b/Lilac/Parser/Token.cs @@ -0,0 +1,15 @@ +namespace Lilac.Parser +{ + public class Token + { + public TokenType TokenType { get; set; } + public string Content { get; set; } + public int Line { get; set; } + public int Column { get; set; } + + public override string ToString() + { + return $"[{TokenType}:{Content} at {Line},{Column}]"; + } + } +} \ No newline at end of file diff --git a/Lilac/Parser/TokenDefinition.cs b/Lilac/Parser/TokenDefinition.cs new file mode 100644 index 0000000..58e883b --- /dev/null +++ b/Lilac/Parser/TokenDefinition.cs @@ -0,0 +1,50 @@ +using System; +using System.Linq; +using JetBrains.Annotations; + +namespace Lilac.Parser +{ + public class TokenDefinition + { + public static readonly TokenDefinition[] TokenDefinitions = + { + new TokenDefinition(TokenType.DecimalNumber, @"[+-]?[0-9]+(\.[0-9]+)?([eE]-?[0-9]+)?", priority: -1), + new TokenDefinition(TokenType.BinaryNumber, @"0[bB][01]+", priority: -2), + new TokenDefinition(TokenType.HexNumber, @"0[xX][0-9a-fA-F]+", priority: -2), + new TokenDefinition(TokenType.RationalNumber, @"[+-]?[0-9]+\s*\/\s*[0-9]+", priority: -2), + new TokenDefinition(TokenType.ComplexNumber, @"[+-]?[0-9]+(\.[0-9]+)?([eE]-?[0-9]+)?\s*[+-]\s*[0-9]+(\.[0-9]+)?([eE]-?[0-9]+)?i", priority: -2), + new TokenDefinition(TokenType.Identifier, @"[^\s[\](){},\.""';`]+"), + new TokenDefinition(TokenType.String, @"""(\\""|[^""])*"""), + new TokenDefinition(TokenType.OpenGroup, @"\("), + new TokenDefinition(TokenType.CloseGroup, @"\)"), + new TokenDefinition(TokenType.OpenList, @"\["), + new TokenDefinition(TokenType.CloseList, @"\]"), + new TokenDefinition(TokenType.Period, @"\."), + new TokenDefinition(TokenType.Comma, @","), + new TokenDefinition(TokenType.Backquote, @"`"), + new TokenDefinition(TokenType.Newline, @";"), + new TokenDefinition(TokenType.Whitespace, @"\s+", true), + new TokenDefinition(TokenType.Comment, @"'[^']*'?", true), + ReservedWords("let", "ref", "if", "then", "else", "operator", "set!", "using", "namespace", "lambda") + }; + + public string Regex { get; } + public TokenType TokenType { get; } + public int Priority { get; } + public bool IsIgnored { get; } + + public TokenDefinition(TokenType type, [RegexPattern] string regex, bool isIgnored = false, int priority = 0) + { + TokenType = type; + Regex = regex; + IsIgnored = isIgnored; + Priority = priority; + } + + public static TokenDefinition ReservedWords(params string[] reservedWords) + { + var regex = string.Join("|", reservedWords.Select(System.Text.RegularExpressions.Regex.Escape)); + return new TokenDefinition(TokenType.ReservedWord, regex, priority: -1); + } + } +} \ No newline at end of file diff --git a/Lilac/Parser/TokenType.cs b/Lilac/Parser/TokenType.cs new file mode 100644 index 0000000..449e645 --- /dev/null +++ b/Lilac/Parser/TokenType.cs @@ -0,0 +1,26 @@ +namespace Lilac.Parser +{ + public enum TokenType + { + DecimalNumber, + BinaryNumber, + HexNumber, + RationalNumber, + ComplexNumber, + Number, + Identifier, + String, + OpenGroup, + CloseGroup, + OpenList, + CloseList, + Period, + Backquote, + Comma, + Newline, + Whitespace, + Comment, + ReservedWord, + Unrecognized, + } +} \ No newline at end of file diff --git a/Lilac/Program.cs b/Lilac/Program.cs new file mode 100644 index 0000000..ded7f12 --- /dev/null +++ b/Lilac/Program.cs @@ -0,0 +1,10 @@ +namespace Lilac +{ + static class Program + { + static void Main(string[] args) + { + Interpreter.Interpreter.RunRepl(); + } + } +} diff --git a/Lilac/Properties/AssemblyInfo.cs b/Lilac/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..52c1ca1 --- /dev/null +++ b/Lilac/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Lilac")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Lilac")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("940af5df-ebde-4efc-8b03-8cc3b1829dd0")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("0.1.*")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Lilac/Utilities/BidirectionalIterator.cs b/Lilac/Utilities/BidirectionalIterator.cs new file mode 100644 index 0000000..1a12da0 --- /dev/null +++ b/Lilac/Utilities/BidirectionalIterator.cs @@ -0,0 +1,63 @@ +using System.Collections; +using System.Collections.Generic; + +namespace Lilac.Utilities +{ + public class BidirectionalIterator : IBidirectionalIterator + { + private IEnumerator Enumerator { get; set; } + private List List { get; set; } + private int Index { get; set; } + private object Lock { get; set; } + + public BidirectionalIterator(IEnumerable source) + { + Enumerator = source.GetEnumerator(); + List = new List(); + Index = -1; + Lock = new object(); + } + + public T Current + { + get { lock (Lock) { return List[Index]; } } + } + + object IEnumerator.Current + { + get { lock (Lock) { return Current; } } + } + + public bool MoveNext() + { + lock (Lock) + { + if (Index == List.Count) + return false; + ++Index; + if (Index != List.Count) + return true; + if (!Enumerator.MoveNext()) + return false; + List.Add(Enumerator.Current); + return true; + } + } + + public bool MovePrevious() + { + if (Index > -1) + --Index; + return Index != -1; + } + + public IBidirectionalIterator Copy() + { + return (IBidirectionalIterator) MemberwiseClone(); + } + + public void Dispose() => Enumerator.Dispose(); + + public void Reset() => Index = -1; + } +} \ No newline at end of file diff --git a/Lilac/Utilities/Extensions.cs b/Lilac/Utilities/Extensions.cs new file mode 100644 index 0000000..9579142 --- /dev/null +++ b/Lilac/Utilities/Extensions.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Numerics; +using System.Text; + +namespace Lilac.Utilities +{ + public static class Extensions + { + public static TValue GetValueOrDefault(this IReadOnlyDictionary dict, TKey key, TValue defaultValue = default(TValue)) + { + TValue value; + return dict.TryGetValue(key, out value) ? value : defaultValue; + } + + public static TResult Map(this Tuple tuple, Func func) => func(tuple.Item1, tuple.Item2); + public static void Map(this Tuple tuple, Action action) => action(tuple.Item1, tuple.Item2); + + public static BigInteger Sum(this IEnumerable source) + => source.Aggregate(BigInteger.Zero, (current, bigInteger) => current + bigInteger); + + public static string CamelCaseToKebabCase(this string str) + { + var sb = new StringBuilder(); + for (var i = 0; i < str.Length; i++) + { + var c = str[i]; + if (char.IsUpper(c)) + { + if (i > 0) sb.Append('-'); + sb.Append(char.ToLower(c)); + } + else + { + sb.Append(c); + } + } + return sb.ToString(); + } + + public static string PrettyPrintParameters(this IReadOnlyList parameters) + { + return parameters.Count == 0 ? "()" : string.Join(" ", parameters); + } + + public static int[] ToUtf32(this string str) + { + var bytes = Encoding.UTF32.GetBytes(str); + var ints = new int[bytes.Length/4]; + for (var i = 0; i < ints.Length; i++) + { + ints[i] = BitConverter.ToInt32(bytes, i*4); + } + return ints; + } + } +} \ No newline at end of file diff --git a/Lilac/Utilities/IBidirectionalIterator.cs b/Lilac/Utilities/IBidirectionalIterator.cs new file mode 100644 index 0000000..d956021 --- /dev/null +++ b/Lilac/Utilities/IBidirectionalIterator.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace Lilac.Utilities +{ + public interface IBidirectionalIterator : IEnumerator + { + bool MovePrevious(); + IBidirectionalIterator Copy(); + } +} \ No newline at end of file diff --git a/Lilac/Utilities/Maybe.cs b/Lilac/Utilities/Maybe.cs new file mode 100644 index 0000000..eae1b92 --- /dev/null +++ b/Lilac/Utilities/Maybe.cs @@ -0,0 +1,52 @@ +using System; + +namespace Lilac.Utilities +{ + public abstract class Maybe + { + public static Maybe Nothing { get; } = new Nothing(); + public abstract T GetValueOrDefault(); + + public abstract TResult Match(Func justFunc, Func nothingFunc); + public abstract void Match(Action justAction, Action nothingAction); + + public Maybe Bind(Func> func) => Match(func, () => Maybe.Nothing); + + public Maybe SelectMany(Func> func, Func select) + => Bind(val1 => func(val1).Bind(val2 => select(val1, val2).ToMaybe())); + + public Maybe Select(Func func) => Bind(result => func(result).ToMaybe()); + + public Maybe Where(Func func) + => Bind(val => func(val) ? val.ToMaybe() : Nothing); + } + + public class Nothing : Maybe + { + public override T GetValueOrDefault() => default(T); + public override TResult Match(Func justFunc, Func nothingFunc) => nothingFunc(); + + public override void Match(Action justAction, Action nothingAction) => nothingAction(); + + public override string ToString() => "Nothing"; + } + + public class Just : Maybe + { + public T Value { get; } + public Just(T value) + { + Value = value; + } + + public override T GetValueOrDefault() => Value; + public override TResult Match(Func justFunc, Func nothingFunc) => justFunc(Value); + public override void Match(Action justAction, Action nothingAction) => justAction(Value); + public override string ToString() => Value.ToString(); + } + + public static class Maybe + { + public static Maybe ToMaybe(this T value) => value != null ? new Just(value) : Maybe.Nothing; + } +} \ No newline at end of file diff --git a/Lilac/Utilities/MemberContainer.cs b/Lilac/Utilities/MemberContainer.cs new file mode 100644 index 0000000..9e5d287 --- /dev/null +++ b/Lilac/Utilities/MemberContainer.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Reflection; +using Lilac.Attributes; +using Lilac.Values; + +// ReSharper disable StaticMemberInGenericType + +namespace Lilac.Utilities +{ + public static class MemberContainer + { + private static readonly ConstructorInfo BuiltInFunctionConstructor = typeof(BuiltInFunction).GetConstructor(new[] {typeof(MethodInfo), typeof(Type), typeof(object)}); + private static Dictionary> Getters { get; } = new Dictionary>(); + private static Dictionary> Setters { get; } = new Dictionary>(); + + static MemberContainer() + { + foreach (var method in typeof(T).GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) + { + var attributes = method.GetCustomAttributes(); + foreach (var attribute in attributes) + { + var getter = MakeMethodGetter(method, attribute.DelegateType); + Getters.Add(attribute.Name, getter); + } + } + foreach (var property in typeof(T).GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) + { + var attributes = property.GetCustomAttributes(); + foreach (var attribute in attributes) + { + var getter = MakeGetter(property); + Getters.Add(attribute.Name, getter); + if (attribute.GetOnly) continue; + var setter = MakeSetter(property); + Setters.Add(attribute.Name, setter); + } + } + } + + private static Func MakeMethodGetter(MethodInfo method, Type delegateType) + { + var target = Expression.Parameter(typeof(T), "target"); + var get = Expression.New(BuiltInFunctionConstructor, Expression.Constant(method), + Expression.Constant(delegateType), target); + var lambda = Expression.Lambda>(get, $"Get{method.Name}", new[] { target }); + return lambda.Compile(); + } + + private static Func MakeGetter(PropertyInfo property) + { + var target = Expression.Parameter(typeof(T), "target"); + var get = Expression.Property(target, property); + var lambda = Expression.Lambda>(get, $"Get{property.Name}", new [] {target}); + return lambda.Compile(); + } + + private static Action MakeSetter(PropertyInfo property) + { + var target = Expression.Parameter(typeof(T), "target"); + var value = Expression.Parameter(typeof(Value), "value"); + var set = Expression.Assign(Expression.Property(target, property), Expression.Convert(value, property.PropertyType)); + var lambda = Expression.Lambda>(set, $"Set{property.Name}", new[] { target, value }); + return lambda.Compile(); + } + + public static Value GetMember(T target, string name) + { + var getter = Getters.GetValueOrDefault(name); + return getter?.Invoke(target); + } + + public static bool SetMember(T target, string name, Value value) + { + var setter = Setters.GetValueOrDefault(name); + if (setter == null) return false; + setter(target, value); + return true; + } + } +} \ No newline at end of file diff --git a/Lilac/Utilities/Result.cs b/Lilac/Utilities/Result.cs new file mode 100644 index 0000000..ba0980b --- /dev/null +++ b/Lilac/Utilities/Result.cs @@ -0,0 +1,57 @@ +using System; + +namespace Lilac.Utilities +{ + public abstract class Result + { + public abstract TOutput Match(Func okFunc, Func errFunc); + public abstract void Match(Action okAction, Action errAction); + + public Result Bind(Func> func) + => Match(func, err => new Error(err)); + + public Result SelectMany(Func> func, + Func select) + => Bind(val1 => func(val1).Bind(val2 => new Ok(select(val1, val2)))); + + public Result Select(Func func) + => Bind(val1 => new Ok(func(val1))); + + public Result Where(Func func) + => Bind(val => func(val) + ? (Result) new Ok(val) + : new Error(default(TError))); + + public static Result FromMaybe(Maybe maybe, TError error) + => maybe.Match>( + just => new Ok(just), + () => new Error(error)); + } + + public class Ok : Result + { + public TResult Value { get; } + + public Ok(TResult value) + { + Value = value; + } + + public override TOutput Match(Func okFunc, Func errFunc) => okFunc(Value); + + public override void Match(Action okAction, Action errAction) => okAction(Value); + } + + public class Error : Result + { + public TError Value { get; } + public Error(TError value) + { + Value = value; + } + + public override TOutput Match(Func okFunc, Func errFunc) => errFunc(Value); + + public override void Match(Action okAction, Action errAction) => errAction(Value); + } +} \ No newline at end of file diff --git a/Lilac/Values/Boolean.cs b/Lilac/Values/Boolean.cs new file mode 100644 index 0000000..f48ba06 --- /dev/null +++ b/Lilac/Values/Boolean.cs @@ -0,0 +1,91 @@ +using System; +using Lilac.Attributes; + +namespace Lilac.Values +{ + public class Boolean : Value, IEquatable + { + private bool Value { get; } + + private Boolean(bool value) + { + Value = value; + } + + [BuiltInValue("true")] + public static Boolean True { get; } = new Boolean(true); + + [BuiltInValue("false")] + public static Boolean False { get; } = new Boolean(false); + + public static Boolean Get(bool value) => value ? True : False; + + public override bool AsBool() => Value; + + public override string ToString() => Value.ToString(); + + public override bool Equals(object obj) + { + return ReferenceEquals(obj, this) || Equals(obj as Boolean); + } + + public bool Equals(Boolean other) + { + if (ReferenceEquals(other, null)) return false; + return Value == other.Value; + } + + public override int GetHashCode() + { + return Value.GetHashCode(); + } + + [BuiltInFunction("and", typeof(Func), IsOperator = true)] + public static Boolean And(Value lhs, Value rhs) + { + return Get(lhs.AsBool() && rhs.AsBool()); + } + + [BuiltInFunction("or", typeof(Func), IsOperator = true)] + public static Boolean Or(Value lhs, Value rhs) + { + return Get(lhs.AsBool() || rhs.AsBool()); + } + + [BuiltInFunction("not", typeof(Func))] + public static Boolean Not(Value val) + { + return Get(!val.AsBool()); + } + + #region Operators + + public static Boolean operator &(Boolean lhs, Boolean rhs) + { + return And(lhs, rhs); + } + + public static Boolean operator |(Boolean lhs, Boolean rhs) + { + return Or(lhs, rhs); + } + + public static bool operator false(Boolean val) + { + return !val.Value; + } + + public static bool operator true(Boolean val) + { + return val.Value; + } + + public static Boolean operator !(Boolean val) + { + return Not(val); + } + + #endregion + + } +} \ No newline at end of file diff --git a/Lilac/Values/BuiltInFunction.cs b/Lilac/Values/BuiltInFunction.cs new file mode 100644 index 0000000..35a9fb1 --- /dev/null +++ b/Lilac/Values/BuiltInFunction.cs @@ -0,0 +1,27 @@ +using System; +using System.Reflection; + +namespace Lilac.Values +{ + public class BuiltInFunction : Value + { + public Delegate Method { get; } + public int ParameterCount { get; } + + public BuiltInFunction(MethodInfo methodInfo, Type delegateType, object target = null) + { + Method = methodInfo.CreateDelegate(delegateType, target); + ParameterCount = methodInfo.GetParameters().Length; + } + + public override string ToString() + { + return $"<#builtin {Method.Method}>"; + } + + public override bool IsCallable() + { + return true; + } + } +} \ No newline at end of file diff --git a/Lilac/Values/Char.cs b/Lilac/Values/Char.cs new file mode 100644 index 0000000..4c8f395 --- /dev/null +++ b/Lilac/Values/Char.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Lilac.Attributes; +using Lilac.Utilities; + +namespace Lilac.Values +{ + public class Char : Value, IEquatable, IComparable, IComparable + { + public Char(byte[] bytes) + { + Bytes = bytes; + } + + public Char(int value) + { + if (value < 0 || value > 0x10ffff || (value >= 0xd800 && value <= 0xdfff)) + throw new ArgumentOutOfRangeException(nameof(value), value, + "A valid UTF32 value is between 0x000000 and 0x10ffff, inclusive, and should not include surrogate codepoint values (0x00d800 ~ 0x00dfff)."); + Bytes = Encoding.Convert(Encoding.UTF32, Encoding.UTF8, BitConverter.GetBytes(value)); + } + + public Char(string value) + { + Bytes = Encoding.UTF8.GetBytes(value); + } + + public byte[] Bytes { get; private set; } + + public bool Equals(Char other) + { + return CompareTo(other) == 0; + } + + public int CompareTo(Char other) + { + if (ReferenceEquals(null, other)) return 1; + if (ReferenceEquals(other, this)) return 0; + return string.CompareOrdinal(ToString(), other.ToString()); + } + + public override Value GetMember(string name) => MemberContainer.GetMember(this, name); + + public override string ToString() + { + return Encoding.UTF8.GetString(Bytes); + } + + public override bool Equals(object obj) + { + return Equals(obj as Char); + } + + public override int GetHashCode() + { + return ToString().GetHashCode(); + } + + public int CompareTo(object obj) + { + return CompareTo(obj as Char); + } + + public override int CompareTo(Value other) + { + if (!(other is Char)) + throw new Exception($"Cannot compare char to {other.GetType().Name.CamelCaseToKebabCase()}!"); + return CompareTo((Char)other); + } + + [BuiltInFunction("char", typeof(Func))] + public static Char ToChar(Value val) + { + var str = val as String; + if (str != null) + { + if (str.CharIndices.Length != 1) + throw new Exception("Cannot convert string with length > 1 to char!"); + return new Char(str.Bytes); + } + var num = val as Number; + if (num != null) + { + return new Char(num.AsInt32()); + } + throw new Exception("Value must be a string or a number!"); + } + + [BuiltInFunction("char->utf8", typeof(Func))] + public static List CharToUtf8(Char c) + { + return new List(c.Bytes.Select(Number.NativeInt)); + } + + [BuiltInFunction("char->utf16", typeof(Func))] + public static List CharToUtf16(Char c) + { + var utf16Bytes = Encoding.Convert(Encoding.UTF8, Encoding.Unicode, c.Bytes); + var shorts = new short[utf16Bytes.Length/2]; + for (var i = 0; i < shorts.Length; i++) + { + shorts[i] = BitConverter.ToInt16(utf16Bytes, 2*i); + } + + return new List(shorts.Select(Number.NativeInt)); + } + + [BuiltInFunction("char->utf32", typeof(Func))] + public static List CharToUtf32(Char c) + { + var utf32Bytes = Encoding.Convert(Encoding.UTF8, Encoding.UTF32, c.Bytes); + var ints = new int[utf32Bytes.Length / 4]; + for (var i = 0; i < ints.Length; i++) + { + ints[i] = BitConverter.ToInt32(utf32Bytes, 4 * i); + } + + return new List(ints.Select(Number.NativeInt)); + } + } +} \ No newline at end of file diff --git a/Lilac/Values/CurriedFunction.cs b/Lilac/Values/CurriedFunction.cs new file mode 100644 index 0000000..b5e0b55 --- /dev/null +++ b/Lilac/Values/CurriedFunction.cs @@ -0,0 +1,34 @@ +using System.Collections.Immutable; + +namespace Lilac.Values +{ + public class CurriedFunction : Value + { + public Value Callable { get; set; } + public ImmutableList AppliedArguments { get; set; } + + public CurriedFunction(Value callable) + { + Callable = callable; + AppliedArguments = ImmutableList.Empty; + } + + public CurriedFunction Apply(Value argument) + { + return new CurriedFunction(Callable) + { + AppliedArguments = AppliedArguments.Add(argument) + }; + } + + public override string ToString() + { + return $"<#curried {Callable}/{AppliedArguments.Count}>"; + } + + public override bool IsCallable() + { + return Callable.IsCallable(); + } + } +} \ No newline at end of file diff --git a/Lilac/Values/Function.cs b/Lilac/Values/Function.cs new file mode 100644 index 0000000..e2bcae8 --- /dev/null +++ b/Lilac/Values/Function.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using Lilac.AST.Expressions; +using Lilac.Interpreter; + +namespace Lilac.Values +{ + public class Function : Value + { + public string DeclaringName { get; } + public Scope DeclaringScope { get; } + public IReadOnlyList Parameters { get; } + public Expression Body { get; } + + public Function(FunctionDefinitionExpression functionDefinition, Scope declaringScope) + { + DeclaringName = functionDefinition.Name; + Parameters = functionDefinition.Parameters; + Body = functionDefinition.Body; + DeclaringScope = declaringScope; + } + + public Function(OperatorDefinitionExpression operatorDefinition, Scope declaringScope) + { + DeclaringName = operatorDefinition.Name; + Parameters = operatorDefinition.Parameters; + Body = operatorDefinition.Body; + DeclaringScope = declaringScope; + } + + public Function(LambdaExpression lambdaExpression, Scope declaringScope) + { + DeclaringName = "anonymous function"; + Parameters = lambdaExpression.Parameters; + Body = lambdaExpression.Body; + DeclaringScope = declaringScope; + } + + public override string ToString() + { + return $"<#function {DeclaringName}:{Parameters.Count}>"; + } + + public override bool IsCallable() + { + return true; + } + } +} \ No newline at end of file diff --git a/Lilac/Values/List.cs b/Lilac/Values/List.cs new file mode 100644 index 0000000..57b8821 --- /dev/null +++ b/Lilac/Values/List.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Lilac.Attributes; +using Lilac.Utilities; + +namespace Lilac.Values +{ + public class List : Value + { + private List() { } + + public List(IEnumerable values) + { + Values = values.ToImmutableList(); + } + + #region Public Instance Properties + + public ImmutableList Values { get; private set; } + + [BuiltInMember("length", GetOnly = true)] + public Number Length => Number.NativeInt(Values.Count); + + [BuiltInMember("empty?", GetOnly = true)] + public Boolean IsEmpty => Boolean.Get(Length == Number.Zero); + + [BuiltInMember("copy", GetOnly = true)] + public List Copy => new List { Values = Values }; + + #endregion + + #region Public Instance Methods + + public override string ToString() + { + return $"[{string.Join("; ", Values)}]"; + } + + public override Value GetMember(string name) => MemberContainer.GetMember(this, name); + + [BuiltInMethod("at", typeof(Func))] + public Value At(Number number) + { + if (!number.IsInteger) + throw new Exception("Cannot index with non integral value!"); + if (number < Number.Zero || number >= Length) + throw new Exception("Index out of range!"); + var index = number.AsInt32(); + return Values[index]; + } + + [BuiltInMethod("add!", typeof(Func))] + public List Add(Value value) + { + Values = Values.Add(value); + return this; + } + + [BuiltInMethod("append!", typeof(Func))] + public List Append(List value) + { + Values = Values.AddRange(value.Values); + return this; + } + + [BuiltInMethod("concat", typeof(Func))] + public List Concat(List value) + { + return new List(Values.Concat(value.Values)); + } + + [BuiltInMethod("skip", typeof(Func))] + public List Skip(Number num) + { + return new List(Values.Skip(num.AsInt32())); + } + + [BuiltInMethod("take", typeof(Func))] + public List Take(Number num) + { + return new List(Values.Take(num.AsInt32())); + } + + [BuiltInMethod("reverse", typeof(Func))] + public List Reverse() + { + return new List(Values.Reverse()); + } + + #endregion + + #region Public Static Methods + + [BuiltInFunction("linked-list->list", typeof(Func))] + public static List LinkedListToList(Value linkedList) + { + if (!IsLinkedList(linkedList)) + throw new ArgumentException("Value is not a valid linked list!", nameof(linkedList)); + var list = new List(); + var current = linkedList as Pair; + while (current != null) + { + list.Add(current.CarValue); + current = current.CdrValue as Pair; + } + return new List(list); + } + + #endregion + + } +} \ No newline at end of file diff --git a/Lilac/Values/Number.cs b/Lilac/Values/Number.cs new file mode 100644 index 0000000..5a28cdb --- /dev/null +++ b/Lilac/Values/Number.cs @@ -0,0 +1,1372 @@ +using System; +using System.Globalization; +using System.Linq; +using System.Numerics; +using System.Text.RegularExpressions; +using JetBrains.Annotations; +using Lilac.Attributes; +using Lilac.Exceptions; +using Lilac.Utilities; +using Numerics; + +// ReSharper disable CompareOfFloatsByEqualityOperator + +namespace Lilac.Values +{ + public enum NumberType + { + NativeInt, + Integer, + Rational, + Real, + Complex + } + + public class Number : Value, IComparable, IEquatable, IComparable + { + #region Constructor + + private Number(NumberType type, object value) + { + Type = type; + Value = value; + } + + #endregion + + #region Private Properties + + private object Value { get; } + + #endregion + + #region Private Instance Methods + + private Number Raise() + { + switch (Type) + { + case NumberType.NativeInt: + return Integer((long)Value); + case NumberType.Integer: + return Rational(new BigRational((BigInteger)Value)); + case NumberType.Rational: + return Real((double)(BigRational)Value); + case NumberType.Real: + return Complex((double)Value); + case NumberType.Complex: + throw new NumericTypeException("Cannot raise numeric value of type complex!"); + default: + throw new NumericTypeException($"Invalid numeric type {Type}!"); + } + } + + private Number Lower() + { + switch (Type) + { + case NumberType.NativeInt: + throw new NumericTypeException("Cannot lower numeric value of type native-int!"); + case NumberType.Integer: + return NativeInt((long)(BigInteger)Value); + case NumberType.Rational: + return Integer(((BigRational)Value).GetWholePart()); + case NumberType.Real: + return Rational(RealToRational((double)Value)); + case NumberType.Complex: + return Real(((Complex)Value).Real); + default: + throw new ArgumentOutOfRangeException(); + } + } + + private Number LowerIfExact() + { + switch (Type) + { + case NumberType.NativeInt: + throw new NumericTypeException("Cannot lower numeric value of type native-int!"); + case NumberType.Integer: + return LowerIfExactInteger(); + case NumberType.Rational: + return LowerIfExactRational(); + case NumberType.Real: + return LowerIfExactReal(); + case NumberType.Complex: + return LowerIfExactComplex(); + default: + throw new ArgumentOutOfRangeException(); + } + } + + private Number LowerIfExactComplex() + { + var value = (Complex)Value; + return value.Imaginary == 0 ? Real(value.Real) : this; + } + + private Number LowerIfExactReal() + { + var value = (double)Value; + return double.IsInfinity(value) || double.IsNaN(value) ? this : Rational(RealToRational(value)); + } + + private Number LowerIfExactRational() + { + var value = (BigRational)Value; + return IsIntegral(value) ? Integer(value.GetWholePart()) : this; + } + + private Number LowerIfExactInteger() + { + try + { + return NativeInt((long)(BigInteger)Value); + } + catch (OverflowException) + { + return this; + } + } + + private Number RaiseTo(NumberType type) + { + var num = this; + while (num.Type < type) + num = num.Raise(); + return num; + } + + private Number LowerTo(NumberType type) + { + var num = this; + while (num.Type > type) + num = num.Lower(); + return num; + } + + private Number CoerceTo(NumberType type) + { + return RaiseTo(type).LowerTo(type); + } + + #endregion + + #region Private Static Methods + + private static bool IsIntegral(BigRational val) + { + return val.GetFractionPart() == BigRational.Zero; + } + + private static BigRational RealToRational(double value) + { + if (double.IsInfinity(value) || double.IsNaN(value)) + throw new NotFiniteNumberException("Cannot convert non-finite number to exact representation!", value); + var str = value.ToString("R"); + var radixIndex = str.IndexOf(".", StringComparison.Ordinal); + var eIndex = str.IndexOf("e", StringComparison.OrdinalIgnoreCase); + if (eIndex == -1) + { + var numer = BigInteger.Parse(str.Replace(".", "")); + if (radixIndex == -1) + return new BigRational(numer); + var denom = BigInteger.Pow(10, str.Length - radixIndex - 1); + return new BigRational(numer, denom); + } + else + { + var exponent = int.Parse(str.Substring(eIndex + 1)); + var numer = BigInteger.Parse(str.Substring(0, eIndex).Replace(".", "")); + var denom = BigInteger.Pow(10, eIndex - radixIndex - 1); + var power = BigInteger.Pow(10, Math.Abs(exponent)); + if (exponent > 0) + numer *= power; + else + denom *= power; + return new BigRational(numer, denom); + } + } + + private static void RaiseToSame([NotNull] ref Number lhs, [NotNull] ref Number rhs) + { + if (lhs == null) + throw new ArgumentNullException(nameof(lhs)); + if (rhs == null) + throw new ArgumentNullException(nameof(rhs)); + if (lhs.Type > rhs.Type) + { + rhs = rhs.RaiseTo(lhs.Type); + } + else if (rhs.Type > lhs.Type) + { + lhs = lhs.RaiseTo(rhs.Type); + } + } + + private static Number AddNativeInts(Number lhs, Number rhs) + { + try + { + return NativeInt(checked((long)lhs.Value + (long)rhs.Value)); + } + catch (OverflowException) + { + return Integer(BigInteger.Add((long)lhs.Value, (long)rhs.Value)); + } + } + + private static Number SubtractNativeInts(Number lhs, Number rhs) + { + try + { + return NativeInt(checked((long)lhs.Value - (long)rhs.Value)); + } + catch (OverflowException) + { + return Integer(BigInteger.Subtract((long)lhs.Value, (long)rhs.Value)); + } + } + + private static Number MultiplyNativeInts(Number lhs, Number rhs) + { + try + { + return NativeInt(checked((long)lhs.Value * (long)rhs.Value)); + } + catch (OverflowException) + { + return Integer(BigInteger.Multiply((long)lhs.Value, (long)rhs.Value)); + } + } + + private static Number DivideNativeInts(Number lhs, Number rhs) + { + long remainder; + var numer = (long)lhs.Value; + var denom = (long)rhs.Value; + var val = Math.DivRem(numer, denom, out remainder); + return remainder == 0 ? NativeInt(val) : Rational(numer, denom); + } + + private static Number DivideIntegers(Number lhs, Number rhs) + { + var numer = (BigInteger)lhs.Value; + var denom = (BigInteger)rhs.Value; + var rational = new BigRational(numer, denom); + return IsIntegral(rational) ? Integer(rational.GetWholePart()) : Rational(rational); + } + + private static Number PowNativeInts(Number lhs, Number rhs) + { + var lval = (long)lhs.Value; + var rval = (long)rhs.Value; + try + { + return Integer(BigInteger.Pow(lval, (int)rval)).LowerIfExactInteger(); + } + catch (OverflowException) + { + return Real(Math.Pow(lval, rval)); + } + } + + private static Number PowIntegers(Number lhs, Number rhs) + { + var lval = (BigInteger)lhs.Value; + var rval = (BigInteger)rhs.Value; + try + { + return Integer(BigInteger.Pow(lval, (int)rval)); + } + catch (OverflowException) + { + return Real(Math.Pow((double)lval, (double)rval)); + } + } + + private static Number PowRationals(Number lhs, Number rhs) + { + var lval = (BigRational)lhs.Value; + var rval = (BigRational)rhs.Value; + return IsIntegral(rval) ? Rational(BigRational.Pow(lval, rval.GetWholePart())) : Real(Math.Pow((double)lval, (double)rval)); + } + + private static Number RoundRational(Number number) + { + var value = (BigRational)number.Value; + var integer = value.GetWholePart(); + var fraction = value.GetFractionPart(); + if (fraction == new BigRational(1, 2) && !integer.IsEven) + ++integer; + if (fraction == new BigRational(-1, 2) && !integer.IsEven) + --integer; + return Integer(integer); + } + + #endregion + + #region Public Factory Methods + + public static Number NativeInt(byte value) => new Number(NumberType.NativeInt, (long) value); + + public static Number NativeInt(short value) => new Number(NumberType.NativeInt, (long) value); + + public static Number NativeInt(int value) => new Number(NumberType.NativeInt, (long) value); + + public static Number NativeInt(long value) => new Number(NumberType.NativeInt, value); + + public static Number Integer(BigInteger value) => new Number(NumberType.Integer, value); + + public static Number Rational(BigInteger numer, BigInteger denom) => new Number(NumberType.Rational, new BigRational(numer, denom)); + + public static Number Rational(BigRational value) => new Number(NumberType.Rational, value); + + public static Number Real(double value) => new Number(NumberType.Real, value); + + public static Number Complex(double value) => new Number(NumberType.Complex, new Complex(value, 0)); + + public static Number Complex(Complex value) => new Number(NumberType.Complex, value); + + #endregion + + #region Public Properties + + public NumberType Type { get; } + + public bool IsComplex => Type <= NumberType.Complex; + + public bool IsReal => ImagPart(this) == Zero; + + public bool IsRational => IsReal && IsFinite(this).AsBool(); + + public bool IsInteger + { + get + { + switch (Type) + { + case NumberType.NativeInt: + case NumberType.Integer: + return true; + case NumberType.Rational: + return IsIntegral((BigRational)Value); + case NumberType.Real: + case NumberType.Complex: + return IsRational && RealPart(this) == Truncate(RealPart(this)); + default: + throw new ArgumentOutOfRangeException(); + } + + } + } + + public bool IsNativeInt => Type == NumberType.NativeInt; + + #endregion + + #region Public Static Properties + + [BuiltInValue("zero", Namespace = "math")] + public static Number Zero { get; } = NativeInt(0); + + [BuiltInValue("inf", Namespace = "math"), BuiltInValue("+inf", Namespace = "math")] + public static Number PositiveInfinity { get; } = Real(double.PositiveInfinity); + + [BuiltInValue("-inf", Namespace = "math")] + public static Number NegativeInfinity { get; } = Real(double.NegativeInfinity); + + [BuiltInValue("nan", Namespace = "math")] + public static Number NotANumber { get; } = Real(double.NaN); + + [BuiltInValue("epsilon", Namespace = "math")] + public static Number Epsilon { get; } = Real(double.Epsilon); + + [BuiltInValue("pi", Namespace = "math")] + public static Number Pi { get; } = Real(Math.PI); + + [BuiltInValue("e", Namespace = "math")] + public static Number E { get; } = Real(Math.E); + + [BuiltInValue("i", Namespace = "math")] + public static Number I { get; } = Complex(new Complex(0, 1)); + + [BuiltInValue("-i", Namespace = "math")] + public static Number MinusI { get; } = Complex(new Complex(0, -1)); + + #endregion + + #region Public Instance Methods + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + return false; + if (ReferenceEquals(this, obj)) + return true; + return obj.GetType() == GetType() && Equals((Number) obj); + } + + public override int GetHashCode() + { + return Value?.GetHashCode() ?? 0; + } + + public int CompareTo(object obj) + { + return CompareTo(obj as Number); + } + + public int CompareTo(Number other) + { + if (ReferenceEquals(other, null)) + return 1; + var lhs = this; + RaiseToSame(ref lhs, ref other); + switch (lhs.Type) + { + case NumberType.NativeInt: + return ((long) lhs.Value).CompareTo((long) other.Value); + case NumberType.Integer: + return ((BigInteger) lhs.Value).CompareTo((BigInteger) other.Value); + case NumberType.Rational: + return ((BigRational) lhs.Value).CompareTo((BigRational) other.Value); + case NumberType.Real: + return ((double) lhs.Value).CompareTo((double) other.Value); + case NumberType.Complex: + throw new NumericTypeException("Ordering is not defined for complex numbers!"); + default: + throw new NumericTypeException($"Invalid numeric type {lhs.Type}!"); + } + } + + public override int CompareTo(Value other) + { + if (!(other is Number)) + throw new Exception($"Cannot compare number to {other.GetType().Name.CamelCaseToKebabCase()}!"); + return CompareTo((Number) other); + } + + public bool Equals(Number other) + { + if (ReferenceEquals(other, null)) + return false; + if (ReferenceEquals(other, this)) + return true; + var lhs = this; + RaiseToSame(ref lhs, ref other); + switch (lhs.Type) + { + case NumberType.NativeInt: + return (long) lhs.Value == (long) other.Value; + case NumberType.Integer: + return (BigInteger) lhs.Value == (BigInteger) other.Value; + case NumberType.Rational: + return (BigRational) lhs.Value == (BigRational) other.Value; + case NumberType.Real: + return (double) lhs.Value == (double) other.Value; + case NumberType.Complex: + return (Complex) lhs.Value == (Complex) other.Value; + default: + throw new NumericTypeException($"Invalid numeric type {lhs.Type}!"); + } + } + + public override string ToString() + { + switch (Type) + { + case NumberType.NativeInt: + case NumberType.Integer: + case NumberType.Rational: + return Value.ToString(); + case NumberType.Real: + return ((double) Value).ToString("R"); + case NumberType.Complex: + var complex = (Complex) Value; + var plus = complex.Imaginary < 0 ? "" : "+"; + return $"{complex.Real:R}{plus}{complex.Imaginary:R}i"; + default: + throw new NumericTypeException($"Invalid numeric type {Type}!"); + } + } + + public int AsInt32() + { + if (!IsInteger) + throw new NumericTypeException($"Cannot convert {Type} to Int32!"); + try + { + var num = CoerceTo(NumberType.NativeInt); + return checked ((int) (long) num.Value); + } + catch (OverflowException e) + { + throw new Exception("Value is too large to fit in an Int32!", e); + } + } + + #endregion + + #region Operators + + public static Number operator +(Number lhs, Number rhs) => Add(lhs, rhs); + + public static Number operator -(Number lhs, Number rhs) => Subtract(lhs, rhs); + + public static Number operator -(Number val) => Negate(val); + + public static Number operator *(Number lhs, Number rhs) => Multiply(lhs, rhs); + + public static Number operator /(Number lhs, Number rhs) => Divide(lhs, rhs); + + public static Number operator %(Number lhs, Number rhs) => Modulo(lhs, rhs); + + public static bool operator ==(Number lhs, Number rhs) => ReferenceEquals(lhs, null) ? ReferenceEquals(rhs, null) : lhs.Equals(rhs); + + public static bool operator !=(Number lhs, Number rhs) => !(lhs == rhs); + + public static bool operator >=(Number lhs, Number rhs) => lhs?.CompareTo(rhs) >= 0; + + public static bool operator <=(Number lhs, Number rhs) => lhs?.CompareTo(rhs) <= 0; + + public static bool operator >(Number lhs, Number rhs) => lhs?.CompareTo(rhs) > 0; + public static bool operator <(Number lhs, Number rhs) => lhs?.CompareTo(rhs) < 0; + + #endregion + + #region Public Static Methods + + public static Number Parse(string value) + { + if (value.StartsWith("0x", StringComparison.InvariantCultureIgnoreCase)) + return ParseHexLiteral(value); + if (value.StartsWith("0b", StringComparison.InvariantCultureIgnoreCase)) + return ParseBinaryLiteral(value); + if (value.EndsWith("i")) + return ParseComplexLiteral(value); + if (value.Contains("/")) + return ParseRationalLiteral(value); + return ParseDecimalLiteral(value); + } + + [BuiltInFunction("string->number", typeof(Func))] + public static Number Parse(String str) + { + return Parse(str.ToString()); + } + + public static Number ParseRationalLiteral(string value) + { + var match = Regex.Match(value, @"^\s*(-?\d*)\s*/\s*(\d*)\s*$"); + if (!match.Success) + throw new ParseException($"Failed to parse rational number from '{value}'!"); + var numer = BigInteger.Parse(match.Groups[1].Value); + var denom = BigInteger.Parse(match.Groups[2].Value); + if (numer == 0 && denom == 0) + return NotANumber; + if (numer == 0) + return Zero; + if (denom == 0) + return Real(numer.Sign*double.PositiveInfinity); + var rational = new BigRational(numer, denom); + return IsIntegral(rational) ? Integer(rational.GetWholePart()) : Rational(rational); + } + + public static Number ParseComplexLiteral(string value) + { + var match = Regex.Match(value, @"^\s*(\S*)\s*([+-])\s*(\S*)i\s*$"); + if (!match.Success) + throw new ParseException($"Failed to parse complex number from '{value}'!"); + var realPart = double.Parse(match.Groups[1].Value); + var imagSign = match.Groups[2].Value == "+" ? 1 : -1; + var imagPart = double.Parse(match.Groups[3].Value); + return Complex(new Complex(realPart, imagSign*imagPart)); + } + + public static Number ParseDecimalLiteral(string value) + { + if (value.Contains(".") || value.Contains("e") || value.Contains("E")) + { + return Real(double.Parse(value)); + } + try + { + return NativeInt(long.Parse(value)); + } + catch (OverflowException) + { + return Integer(BigInteger.Parse(value)); + } + } + + public static Number ParseBinaryLiteral(string value) + { + var bits = value.Skip(2).Reverse().Select(c => c == '0' ? 0 : 1).ToList(); + try + { + return NativeInt(bits.Select((b, i) => (long) b << i).Sum()); + } + catch (OverflowException) + { + return Integer(bits.Select((b, i) => b*BigInteger.Pow(2, i)).Sum()); + } + } + + public static Number ParseHexLiteral(string value) + { + value = value.Substring(2); + try + { + return NativeInt(long.Parse(value, NumberStyles.HexNumber)); + } + catch (OverflowException) + { + return Integer(BigInteger.Parse(value, NumberStyles.HexNumber)); + } + } + + [BuiltInFunction("exact", typeof(Func), Namespace = "math")] + public static Number Exact(Number num) + { + num = num.LowerTo(NumberType.Rational); + if (num.Type == NumberType.Rational && IsIntegral((BigRational) num.Value)) + num = num.Lower(); + return num; + } + + [BuiltInFunction("inexact", typeof(Func), Namespace = "math")] + public static Number InExact(Number num) => num.RaiseTo(NumberType.Real); + + [BuiltInFunction("+", typeof(Func), IsOperator = true)] + public static Number Add(Number lhs, Number rhs) + { + RaiseToSame(ref lhs, ref rhs); + switch (lhs.Type) + { + case NumberType.NativeInt: + return AddNativeInts(lhs, rhs); + case NumberType.Integer: + return Integer(BigInteger.Add((BigInteger) lhs.Value, (BigInteger) rhs.Value)); + case NumberType.Rational: + return Rational((BigRational) lhs.Value + (BigRational) rhs.Value); + case NumberType.Real: + return Real((double) lhs.Value + (double) rhs.Value); + case NumberType.Complex: + return Complex((Complex) lhs.Value + (Complex) rhs.Value); + default: + throw new NumericTypeException($"Invalid numeric type {lhs.Type}!"); + } + } + + [BuiltInFunction("-", typeof(Func), IsOperator = true)] + public static Number Subtract(Number lhs, Number rhs) + { + RaiseToSame(ref lhs, ref rhs); + switch (lhs.Type) + { + case NumberType.NativeInt: + return SubtractNativeInts(lhs, rhs); + case NumberType.Integer: + return Integer(BigInteger.Subtract((BigInteger) lhs.Value, (BigInteger) rhs.Value)); + case NumberType.Rational: + return Rational((BigRational) lhs.Value - (BigRational) rhs.Value); + case NumberType.Real: + return Real((double) lhs.Value - (double) rhs.Value); + case NumberType.Complex: + return Complex((Complex) lhs.Value - (Complex) rhs.Value); + default: + throw new NumericTypeException($"Invalid numeric type {lhs.Type}!"); + } + } + + [BuiltInFunction("negate", typeof(Func), Namespace = "math")] + public static Number Negate(Number val) + { + switch (val.Type) + { + case NumberType.NativeInt: + return NativeInt(-(long) val.Value); + case NumberType.Integer: + return Integer(-(BigInteger) val.Value); + case NumberType.Rational: + return Rational(-(BigRational) val.Value); + case NumberType.Real: + return Real(-(double) val.Value); + case NumberType.Complex: + return Complex(-(Complex) val.Value); + default: + throw new NumericTypeException($"Invalid numeric type {val.Type}!"); + } + } + + [BuiltInFunction("*", typeof(Func), IsOperator = true)] + public static Number Multiply(Number lhs, Number rhs) + { + RaiseToSame(ref lhs, ref rhs); + switch (lhs.Type) + { + case NumberType.NativeInt: + return MultiplyNativeInts(lhs, rhs); + case NumberType.Integer: + return Integer(BigInteger.Multiply((BigInteger) lhs.Value, (BigInteger) rhs.Value)); + case NumberType.Rational: + return Rational((BigRational) lhs.Value*(BigRational) rhs.Value); + case NumberType.Real: + return Real((double) lhs.Value*(double) rhs.Value); + case NumberType.Complex: + return Complex((Complex) lhs.Value*(Complex) rhs.Value); + default: + throw new NumericTypeException($"Invalid numeric type {lhs.Type}!"); + } + } + + [BuiltInFunction("/", typeof(Func), IsOperator = true)] + public static Number Divide(Number lhs, Number rhs) + { + if (rhs == Zero) + return DivideByZero(lhs); + RaiseToSame(ref lhs, ref rhs); + switch (lhs.Type) + { + case NumberType.NativeInt: + return DivideNativeInts(lhs, rhs); + case NumberType.Integer: + return DivideIntegers(lhs, rhs); + case NumberType.Rational: + return Rational((BigRational) lhs.Value/(BigRational) rhs.Value); + case NumberType.Real: + return Real((double) lhs.Value/(double) rhs.Value); + case NumberType.Complex: + return Complex((Complex) lhs.Value/(Complex) rhs.Value); + default: + throw new NumericTypeException($"Invalid numeric type {lhs.Type}!"); + } + } + + private static Number DivideByZero(Number lhs) + { + switch (lhs.Type) + { + case NumberType.NativeInt: + case NumberType.Integer: + case NumberType.Rational: + case NumberType.Real: + if (lhs == Zero) + return NotANumber; + return lhs > Zero ? PositiveInfinity : NegativeInfinity; + case NumberType.Complex: + return Complex((Complex) lhs.Value/0); + default: + throw new ArgumentOutOfRangeException(); + } + } + + [BuiltInFunction("%", typeof(Func), IsOperator = true)] + public static Number Modulo(Number lhs, Number rhs) + { + RaiseToSame(ref lhs, ref rhs); + switch (lhs.Type) + { + case NumberType.NativeInt: + return NativeInt((long) lhs.Value%(long) rhs.Value); + case NumberType.Integer: + return Integer(BigInteger.Remainder((BigInteger) lhs.Value, (BigInteger) rhs.Value)); + case NumberType.Rational: + return Rational((BigRational) lhs.Value%(BigRational) rhs.Value); + case NumberType.Real: + return Real(Math.IEEERemainder((double) lhs.Value, (double) rhs.Value)); + case NumberType.Complex: + throw new NumericTypeException("Modulo operation is not defined for complex numbers!"); + default: + throw new NumericTypeException($"Invalid numeric type {lhs.Type}!"); + } + } + + [BuiltInFunction("^", typeof(Func), IsOperator = true)] + public static Number Pow(Number lhs, Number rhs) + { + RaiseToSame(ref lhs, ref rhs); + switch (lhs.Type) + { + case NumberType.NativeInt: + return PowNativeInts(lhs, rhs); + case NumberType.Integer: + return PowIntegers(lhs, rhs); + case NumberType.Rational: + return PowRationals(lhs, rhs); + case NumberType.Real: + return Real(Math.Pow((double) lhs.Value, (double) rhs.Value)); + case NumberType.Complex: + return Complex(System.Numerics.Complex.Pow((Complex) lhs.Value, (Complex) rhs.Value)); + default: + throw new NumericTypeException($"Invalid numeric type {lhs.Type}!"); + } + } + + [BuiltInFunction("exact?", typeof(Func), Namespace = "math")] + public static Boolean IsExact(Number number) + { + return Boolean.Get(number.Type < NumberType.Real); + } + + [BuiltInFunction("inexact?", typeof(Func), Namespace = "math")] + public static Boolean IsInexact(Number number) + { + return Boolean.Get(number.Type >= NumberType.Real); + } + + [BuiltInFunction("zero?", typeof(Func), Namespace = "math")] + public static Boolean IsZero(Number number) + { + return Boolean.Get(number == Zero); + } + + [BuiltInFunction("positive?", typeof(Func), Namespace = "math")] + public static Boolean IsPositive(Number number) + { + return Boolean.Get(number > Zero); + } + + [BuiltInFunction("negative?", typeof(Func), Namespace = "math")] + public static Boolean IsNegative(Number number) + { + return Boolean.Get(number < Zero); + } + + [BuiltInFunction("even?", typeof(Func), Namespace = "math")] + public static Boolean IsEven(Number number) + { + switch (number.Type) + { + case NumberType.NativeInt: + case NumberType.Integer: + case NumberType.Rational: + case NumberType.Real: + return Boolean.Get(number%NativeInt(2) == Zero); + case NumberType.Complex: + return Boolean.False; + default: + throw new NumericTypeException($"Invalid numeric type {number.Type}!"); + } + } + + [BuiltInFunction("odd?", typeof(Func), Namespace = "math")] + public static Boolean IsOdd(Number number) + { + switch (number.Type) + { + case NumberType.NativeInt: + case NumberType.Integer: + case NumberType.Rational: + case NumberType.Real: + return Boolean.Get(number%NativeInt(2) == NativeInt(1)); + case NumberType.Complex: + return Boolean.False; + default: + throw new NumericTypeException($"Invalid numeric type {number.Type}!"); + } + } + + [BuiltInFunction("infinite?", typeof(Func), Namespace = "math")] + public static Boolean IsInfinite(Number number) + { + switch (number.Type) + { + case NumberType.NativeInt: + case NumberType.Integer: + case NumberType.Rational: + return Boolean.False; + case NumberType.Real: + var val = (double) number.Value; + return Boolean.Get(double.IsInfinity(val)); + case NumberType.Complex: + var cmp = (Complex) number.Value; + return Boolean.Get(double.IsInfinity(cmp.Real) || double.IsInfinity(cmp.Imaginary)); + default: + throw new NumericTypeException($"Invalid numeric type {number.Type}!"); + } + } + + [BuiltInFunction("nan?", typeof(Func), Namespace = "math")] + public static Boolean IsNan(Number number) + { + switch (number.Type) + { + case NumberType.NativeInt: + case NumberType.Integer: + case NumberType.Rational: + return Boolean.False; + case NumberType.Real: + var val = (double) number.Value; + return Boolean.Get(double.IsNaN(val)); + case NumberType.Complex: + var cmp = (Complex) number.Value; + return Boolean.Get(double.IsNaN(cmp.Real) || double.IsNaN(cmp.Imaginary)); + default: + throw new NumericTypeException($"Invalid numeric type {number.Type}!"); + } + } + + [BuiltInFunction("finite?", typeof(Func), Namespace = "math")] + public static Boolean IsFinite(Number number) + { + return !(IsInfinite(number) || IsNan(number)); + } + + [BuiltInFunction("max", typeof(Func), Namespace = "math")] + public static Number Max(Number lhs, Number rhs) + { + return lhs >= rhs ? lhs : rhs; + } + + [BuiltInFunction("min", typeof(Func), Namespace = "math")] + public static Number Min(Number lhs, Number rhs) + { + return lhs <= rhs ? lhs : rhs; + } + + [BuiltInFunction("abs", typeof(Func), Namespace = "math")] + public static Number Abs(Number number) + { + switch (number.Type) + { + case NumberType.NativeInt: + return NativeInt(Math.Abs((long) number.Value)); + case NumberType.Integer: + return Integer(BigInteger.Abs((BigInteger) number.Value)); + case NumberType.Rational: + return Rational(BigRational.Abs((BigRational) number.Value)); + case NumberType.Real: + return Real(Math.Abs((double) number.Value)); + case NumberType.Complex: + return Real(((Complex) number.Value).Magnitude); + default: + throw new NumericTypeException($"Invalid numeric type {number.Type}!"); + } + } + + [BuiltInFunction("exp", typeof(Func), Namespace = "math")] + public static Number Exp(Number number) + { + number = number.RaiseTo(NumberType.Real); + switch (number.Type) + { + case NumberType.Real: + return Real(Math.Exp((double) number.Value)); + case NumberType.Complex: + return Complex(System.Numerics.Complex.Exp((Complex) number.Value)); + default: + throw new NumericTypeException($"Invalid numeric type {number.Type}!"); + } + } + + [BuiltInFunction("sin", typeof(Func), Namespace = "math")] + public static Number Sin(Number number) + { + number = number.RaiseTo(NumberType.Real); + switch (number.Type) + { + case NumberType.Real: + return Real(Math.Sin((double) number.Value)); + case NumberType.Complex: + return Complex(System.Numerics.Complex.Sin((Complex) number.Value)); + default: + throw new NumericTypeException($"Invalid numeric type {number.Type}!"); + } + } + + [BuiltInFunction("cos", typeof(Func), Namespace = "math")] + public static Number Cos(Number number) + { + number = number.RaiseTo(NumberType.Real); + switch (number.Type) + { + case NumberType.Real: + return Real(Math.Cos((double) number.Value)); + case NumberType.Complex: + return Complex(System.Numerics.Complex.Cos((Complex) number.Value)); + default: + throw new NumericTypeException($"Invalid numeric type {number.Type}!"); + } + } + + [BuiltInFunction("tan", typeof(Func), Namespace = "math")] + public static Number Tan(Number number) + { + number = number.RaiseTo(NumberType.Real); + switch (number.Type) + { + case NumberType.Real: + return Real(Math.Tan((double) number.Value)); + case NumberType.Complex: + return Complex(System.Numerics.Complex.Tan((Complex) number.Value)); + default: + throw new NumericTypeException($"Invalid numeric type {number.Type}!"); + } + } + + [BuiltInFunction("asin", typeof(Func), Namespace = "math")] + public static Number Asin(Number number) + { + number = number.RaiseTo(NumberType.Real); + switch (number.Type) + { + case NumberType.Real: + return Real(Math.Asin((double) number.Value)); + case NumberType.Complex: + return Complex(System.Numerics.Complex.Asin((Complex) number.Value)); + default: + throw new NumericTypeException($"Invalid numeric type {number.Type}!"); + } + } + + [BuiltInFunction("acos", typeof(Func), Namespace = "math")] + public static Number Acos(Number number) + { + number = number.RaiseTo(NumberType.Real); + switch (number.Type) + { + case NumberType.Real: + return Real(Math.Acos((double) number.Value)); + case NumberType.Complex: + return Complex(System.Numerics.Complex.Acos((Complex) number.Value)); + default: + throw new NumericTypeException($"Invalid numeric type {number.Type}!"); + } + } + + [BuiltInFunction("atan", typeof(Func), Namespace = "math")] + public static Number Atan(Number number) + { + number = number.RaiseTo(NumberType.Real); + switch (number.Type) + { + case NumberType.Real: + return Real(Math.Atan((double) number.Value)); + case NumberType.Complex: + return Complex(System.Numerics.Complex.Atan((Complex) number.Value)); + default: + throw new NumericTypeException($"Invalid numeric type {number.Type}!"); + } + } + + [BuiltInFunction("sinh", typeof(Func), Namespace = "math")] + public static Number Sinh(Number number) + { + number = number.RaiseTo(NumberType.Real); + switch (number.Type) + { + case NumberType.Real: + return Real(Math.Sinh((double) number.Value)); + case NumberType.Complex: + return Complex(System.Numerics.Complex.Sinh((Complex) number.Value)); + default: + throw new NumericTypeException($"Invalid numeric type {number.Type}!"); + } + } + + [BuiltInFunction("cosh", typeof(Func), Namespace = "math")] + public static Number Cosh(Number number) + { + number = number.RaiseTo(NumberType.Real); + switch (number.Type) + { + case NumberType.Real: + return Real(Math.Cosh((double) number.Value)); + case NumberType.Complex: + return Complex(System.Numerics.Complex.Cosh((Complex) number.Value)); + default: + throw new NumericTypeException($"Invalid numeric type {number.Type}!"); + } + } + + [BuiltInFunction("tanh", typeof(Func), Namespace = "math")] + public static Number Tanh(Number number) + { + number = number.RaiseTo(NumberType.Real); + switch (number.Type) + { + case NumberType.Real: + return Real(Math.Tanh((double) number.Value)); + case NumberType.Complex: + return Complex(System.Numerics.Complex.Tanh((Complex) number.Value)); + default: + throw new NumericTypeException($"Invalid numeric type {number.Type}!"); + } + } + + [BuiltInFunction("atan2", typeof(Func), Namespace = "math")] + public static Number Atan2(Number y, Number x) + { + if (!y.IsReal) + throw new ArgumentOutOfRangeException(nameof(y), "Argument must be real!"); + if (!x.IsReal) + throw new ArgumentOutOfRangeException(nameof(x), "Argument must be real!"); + y = y.CoerceTo(NumberType.Real); + x = x.CoerceTo(NumberType.Real); + return Real(Math.Atan2((double) y.Value, (double) x.Value)); + } + + [BuiltInFunction("log", typeof(Func), Namespace = "math")] + public static Number Log(Number number) + { + number = number.RaiseTo(NumberType.Real); + if (number.IsReal && number < Zero) + number = number.Raise(); + switch (number.Type) + { + case NumberType.NativeInt: + case NumberType.Integer: + case NumberType.Rational: + throw new NumericTypeException($"Failed to coerce {number}!"); + case NumberType.Real: + return Real(Math.Log((double) number.Value)); + case NumberType.Complex: + return Complex(System.Numerics.Complex.Log((Complex) number.Value)); + default: + throw new NumericTypeException($"Invalid numeric type {number.Type}!"); + } + } + + [BuiltInFunction("logb", typeof(Func), Namespace = "math")] + public static Number LogB(Number number, Number b) + { + number = number.RaiseTo(NumberType.Real); + if (number.IsReal && number < Zero) + number = number.Raise(); + b = b.CoerceTo(NumberType.Real); + switch (number.Type) + { + case NumberType.NativeInt: + case NumberType.Integer: + case NumberType.Rational: + throw new NumericTypeException($"Failed to coerce {number}!"); + case NumberType.Real: + return Real(Math.Log((double) number.Value, (double) b.Value)); + case NumberType.Complex: + return Complex(System.Numerics.Complex.Log((Complex) number.Value, (double) b.Value)); + default: + throw new NumericTypeException($"Invalid numeric type {number.Type}!"); + } + } + + [BuiltInFunction("sqrt", typeof(Func), Namespace = "math")] + public static Number Sqrt(Number number) + { + number = number.RaiseTo(NumberType.Real); + if (number.IsReal && number < Zero) + number = number.Raise(); + switch (number.Type) + { + case NumberType.NativeInt: + case NumberType.Integer: + case NumberType.Rational: + throw new NumericTypeException($"Failed to coerce {number}!"); + case NumberType.Real: + return Real(Math.Sqrt((double) number.Value)); + case NumberType.Complex: + return Complex(System.Numerics.Complex.Sqrt((Complex) number.Value)); + default: + throw new NumericTypeException($"Invalid numeric type {number.Type}!"); + } + } + + [BuiltInFunction("reciprocal", typeof(Func), Namespace = "math")] + public static Number Reciprocal(Number number) => NativeInt(1)/number; + + [BuiltInFunction("numerator", typeof(Func), Namespace = "math")] + public static Number Numerator(Number number) + { + if (!number.IsRational) + throw new ArgumentOutOfRangeException(nameof(number), "Argument must be rational!"); + number = number.LowerTo(NumberType.Rational); + switch (number.Type) + { + case NumberType.NativeInt: + case NumberType.Integer: + return number; + case NumberType.Rational: + return Integer(((BigRational) number.Value).Numerator); + case NumberType.Real: + case NumberType.Complex: + throw new NumericTypeException($"Failed to coerce {number}!"); + default: + throw new NumericTypeException($"Invalid numeric type {number.Type}!"); + } + } + + [BuiltInFunction("denominator", typeof(Func), Namespace = "math")] + public static Number Denominator(Number number) + { + if (!number.IsRational) + throw new ArgumentOutOfRangeException(nameof(number), "Argument must be rational!"); + number = number.LowerTo(NumberType.Rational); + switch (number.Type) + { + case NumberType.NativeInt: + case NumberType.Integer: + return NativeInt(1); + case NumberType.Rational: + return Integer(((BigRational) number.Value).Denominator); + case NumberType.Real: + case NumberType.Complex: + throw new NumericTypeException($"Failed to coerce {number}!"); + default: + throw new NumericTypeException($"Invalid numeric type {number.Type}!"); + } + } + + [BuiltInFunction("floor", typeof(Func), Namespace = "math")] + public static Number Floor(Number number) + { + if (!number.IsReal) + throw new ArgumentOutOfRangeException(nameof(number), "Argument must be real!"); + number = number.LowerTo(NumberType.Real); + switch (number.Type) + { + case NumberType.NativeInt: + case NumberType.Integer: + return number; + case NumberType.Rational: + return Integer(((BigRational) number.Value).GetWholePart() - (IsNegative(number) ? 1 : 0)); + case NumberType.Real: + return Integer((BigInteger) Math.Floor((double) number.Value)); + case NumberType.Complex: + throw new NumericTypeException($"Failed to coerce {number}!"); + default: + throw new NumericTypeException($"Invalid numeric type {number.Type}!"); + } + } + + [BuiltInFunction("ceiling", typeof(Func), Namespace = "math")] + public static Number Ceiling(Number number) + { + if (!number.IsReal) + throw new ArgumentOutOfRangeException(nameof(number), "Argument must be real!"); + number = number.LowerTo(NumberType.Real); + switch (number.Type) + { + case NumberType.NativeInt: + case NumberType.Integer: + return number; + case NumberType.Rational: + return Integer(((BigRational) number.Value).GetWholePart() + (IsNegative(number) ? 0 : 1)); + case NumberType.Real: + return Integer((BigInteger) Math.Ceiling((double) number.Value)); + case NumberType.Complex: + throw new NumericTypeException($"Failed to coerce {number}!"); + default: + throw new NumericTypeException($"Invalid numeric type {number.Type}!"); + } + } + + [BuiltInFunction("truncate", typeof(Func), Namespace = "math")] + public static Number Truncate(Number number) + { + if (!number.IsReal) + throw new ArgumentOutOfRangeException(nameof(number), "Argument must be real!"); + number = number.LowerTo(NumberType.Real); + switch (number.Type) + { + case NumberType.NativeInt: + case NumberType.Integer: + return number; + case NumberType.Rational: + return Integer(((BigRational) number.Value).GetWholePart()); + case NumberType.Real: + return Integer((BigInteger) Math.Truncate((double) number.Value)); + case NumberType.Complex: + throw new NumericTypeException($"Failed to coerce {number}!"); + default: + throw new NumericTypeException($"Invalid numeric type {number.Type}!"); + } + } + + [BuiltInFunction("round", typeof(Func), Namespace = "math")] + public static Number Round(Number number) + { + if (!number.IsReal) + throw new ArgumentOutOfRangeException(nameof(number), "Argument must be real!"); + number = number.LowerTo(NumberType.Real); + switch (number.Type) + { + case NumberType.NativeInt: + case NumberType.Integer: + return number; + case NumberType.Rational: + return RoundRational(number); + case NumberType.Real: + return Integer((BigInteger) Math.Round((double) number.Value, MidpointRounding.ToEven)); + case NumberType.Complex: + throw new NumericTypeException($"Failed to coerce {number}!"); + default: + throw new NumericTypeException($"Invalid numeric type {number.Type}!"); + } + } + + [BuiltInFunction("fractional", typeof(Func), Namespace = "math")] + public static Number Fractional(Number number) + { + if (!number.IsReal) + throw new ArgumentOutOfRangeException(nameof(number), "Argument must be real!"); + number = number.LowerTo(NumberType.Real); + switch (number.Type) + { + case NumberType.NativeInt: + case NumberType.Integer: + return Zero; + case NumberType.Rational: + return Rational(((BigRational) number.Value).GetFractionPart()); + case NumberType.Real: + var val = (double) number.Value; + return Real(val - Math.Truncate(val)); + case NumberType.Complex: + throw new NumericTypeException($"Failed to coerce {number}!"); + default: + throw new NumericTypeException($"Invalid numeric type {number.Type}!"); + } + } + + [BuiltInFunction("complex", typeof(Func), Namespace = "math")] + public static Number Complex(Number number) => number.CoerceTo(NumberType.Complex); + + [BuiltInFunction("make-rectangular", typeof(Func), Namespace = "math")] + public static Number MakeRectangular(Number real, Number imaginary) + { + if (!real.IsReal) + throw new ArgumentOutOfRangeException(nameof(real), "Argument must be real!"); + if (!imaginary.IsReal) + throw new ArgumentOutOfRangeException(nameof(imaginary), "Argument must be real!"); + real = real.CoerceTo(NumberType.Real); + imaginary = imaginary.CoerceTo(NumberType.Real); + return Complex(new Complex((double) real.Value, (double) imaginary.Value)); + } + + [BuiltInFunction("make-polar", typeof(Func), Namespace = "math")] + public static Number MakePolar(Number magnitude, Number angle) + { + if (!magnitude.IsReal) + throw new ArgumentOutOfRangeException(nameof(magnitude), "Argument must be real!"); + if (!angle.IsReal) + throw new ArgumentOutOfRangeException(nameof(angle), "Argument must be real!"); + magnitude = magnitude.CoerceTo(NumberType.Real); + angle = angle.CoerceTo(NumberType.Real); + return Complex(System.Numerics.Complex.FromPolarCoordinates((double) magnitude.Value, (double) angle.Value)); + } + + [BuiltInFunction("real-part", typeof(Func), Namespace = "math")] + public static Number RealPart(Number number) => Real(((Complex) number.CoerceTo(NumberType.Complex).Value).Real); + + [BuiltInFunction("imag-part", typeof(Func), Namespace = "math")] + public static Number ImagPart(Number number) => Real(((Complex) number.CoerceTo(NumberType.Complex).Value).Imaginary); + + [BuiltInFunction("magnitude", typeof(Func), Namespace = "math")] + public static Number Magnitude(Number number) => Real(((Complex) number.CoerceTo(NumberType.Complex).Value).Magnitude); + + [BuiltInFunction("angle", typeof(Func), Namespace = "math")] + public static Number Angle(Number number) => Real(((Complex) number.CoerceTo(NumberType.Complex).Value).Phase); + + #endregion + } +} \ No newline at end of file diff --git a/Lilac/Values/Pair.cs b/Lilac/Values/Pair.cs new file mode 100644 index 0000000..3861d13 --- /dev/null +++ b/Lilac/Values/Pair.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Lilac.Attributes; +using Lilac.Utilities; + +namespace Lilac.Values +{ + public class Pair : Value + { + public Pair(Value car, Value cdr) + { + CarValue = car; + CdrValue = cdr; + } + + + + [BuiltInMember("car")] + public Value CarValue { get; set; } + + [BuiltInMember("cdr")] + public Value CdrValue { get; set; } + + public override Value GetMember(string name) => MemberContainer.GetMember(this, name); + public override bool SetMember(string name, Value value) => MemberContainer.SetMember(this, name, value); + public override string ToString() + { + return IsLinkedList(this) ? $"({LinkedListString()})" : $"({CarValue} . {CdrValue})"; + } + + private string LinkedListString() + { + if (CdrValue is Unit) return CarValue.ToString(); + return $"{CarValue}; {((Pair) CdrValue).LinkedListString()}"; + } + + public static Value LinkedList(IReadOnlyList values) + { + Value current = Unit.Value; + for (var i = values.Count - 1; i >= 0; i--) + { + current = new Pair(values[i], current); + } + return current; + } + + [BuiltInFunction("list->linked-list", typeof(Func))] + public static Value ListToLinkedList(List list) + { + return LinkedList(list.Values); + } + + [BuiltInFunction("cons", typeof(Func))] + public static Pair Cons(Value car, Value cdr) + { + return new Pair(car, cdr); + } + + [BuiltInFunction("car", typeof(Func))] + public static Value Car(Pair pair) + { + return pair.CarValue; + } + + [BuiltInFunction("cdr", typeof(Func))] + public static Value Cdr(Pair pair) + { + return pair.CdrValue; + } + } +} \ No newline at end of file diff --git a/Lilac/Values/String.cs b/Lilac/Values/String.cs new file mode 100644 index 0000000..64ec457 --- /dev/null +++ b/Lilac/Values/String.cs @@ -0,0 +1,293 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using Lilac.Attributes; +using Lilac.Utilities; + +// ReSharper disable AssignNullToNotNullAttribute + +namespace Lilac.Values +{ + public class String : Value, IComparable, IEquatable, IComparable + { + #region Private Static Properties + + private static Dictionary InternedConstructedStrings { get; } = new Dictionary(); + private static Dictionary InternedParsedStrings { get; } = new Dictionary(); + + private static IReadOnlyDictionary EscapeSequences { get; } = new Dictionary + { + {'\'', '\''}, + {'\"', '\"'}, + {'\\', '\\'}, + {'0', '\0'}, + {'a', '\a'}, + {'b', '\b'}, + {'f', '\f'}, + {'n', '\n'}, + {'r', '\r'}, + {'t', '\t'}, + {'v', '\v'} + }; + + #endregion + + #region Constructor + + + #endregion + + #region Public Instance Properties + + public int[] CharIndices { get; private set; } + public byte[] Bytes { get; private set; } + + public byte[] AsUtf16 => Encoding.Convert(Encoding.UTF8, Encoding.Unicode, Bytes); + public byte[] AsUtf32 => Encoding.Convert(Encoding.UTF8, Encoding.UTF32, Bytes); + + [BuiltInMember("length", GetOnly = true)] + public Number Length => Number.NativeInt(CharIndices.Length); + + [BuiltInMember("chars", GetOnly = true)] + public List Chars + { + get + { + var chars = new Char[CharIndices.Length]; + for (var i = 0; i < CharIndices.Length; i++) + { + var charStart = CharIndices[i]; + var charEnd = i == CharIndices.Length - 1 ? Bytes.Length : CharIndices[i + 1]; + var bytes = new byte[charEnd - charStart]; + Array.Copy(Bytes, charStart, bytes, 0, charEnd - charStart); + chars[i] = new Char(bytes); + } + return new List(chars); + } + } + + #endregion + + #region Public Instance Methods + + public override Value GetMember(string name) => MemberContainer.GetMember(this, name); + + public int CompareTo(String other) + { + if (ReferenceEquals(null, other)) return 1; + if (ReferenceEquals(other, this)) return 0; + return string.CompareOrdinal(ToString(), other.ToString()); + } + + public override bool Equals(object obj) + { + return Equals(obj as String); + } + + public override int GetHashCode() + { + return ToString().GetHashCode(); + } + + public int CompareTo(object obj) + { + return CompareTo(obj as String); + } + + public override int CompareTo(Value other) + { + if (!(other is String)) + throw new Exception($"Cannot compare string to {other.GetType().Name.CamelCaseToKebabCase()}!"); + return CompareTo((String)other); + } + + public bool Equals(String other) + { + return CompareTo(other) == 0; + } + + public override string ToString() + { + return Encoding.UTF8.GetString(Bytes); + } + + [BuiltInMethod("at", typeof(Func))] + public Char At(Number number) + { + if (!number.IsInteger) + throw new Exception("Cannot index with non integral value!"); + if (number < Number.Zero || number >= Length) + throw new Exception("Index out of range!"); + var index = number.AsInt32(); + var byteStart = CharIndices[index]; + var byteEnd = index == CharIndices.Length - 1 ? Bytes.Length : CharIndices[index + 1]; + var bytes = new byte[byteEnd - byteStart]; + Array.Copy(Bytes, byteStart, bytes, 0, byteEnd - byteStart); + return new Char(bytes); + } + + [BuiltInMethod("concat", typeof(Func))] + public String Concat(String other) => Concat(this, other); + + + #endregion + + #region Public Static Methods + + public static String Get(string str) + { + String value; + if (InternedConstructedStrings.TryGetValue(str, out value)) + { + return value; + } + + var bytes = new List(); + var charIndices = new List(); + var tee = StringInfo.GetTextElementEnumerator(str); + while (tee.MoveNext()) + { + charIndices.Add(bytes.Count); + bytes.AddRange(Encoding.UTF8.GetBytes(tee.GetTextElement())); + } + + value = new String + { + Bytes = bytes.ToArray(), + CharIndices = charIndices.ToArray() + }; + InternedConstructedStrings.Add(str, value); + return value; + } + + public static String Parse(string str) + { + String value; + if (InternedParsedStrings.TryGetValue(str, out value)) + return value; + + var sb = new StringBuilder(); + var substring = str.Substring(1, str.Length - 2); + for (var i = 0; i < substring.Length; i++) + { + var c = substring[i]; + if (c != '\\') + { + sb.Append(c); + } + else + { + var e = substring[++i]; + char escape; + if (EscapeSequences.TryGetValue(e, out escape)) + { + sb.Append(escape); + } + else + { + int hex; + switch (e) + { + case 'x': + hex = int.Parse(substring.Substring(i + 1, 2), NumberStyles.HexNumber); + sb.Append((char) hex); + i += 2; + break; + case 'u': + hex = int.Parse(substring.Substring(i + 1, 4), NumberStyles.HexNumber); + sb.Append((char)hex); + i += 4; + break; + case 'U': + hex = int.Parse(substring.Substring(i + 1, 8), NumberStyles.HexNumber); + sb.Append(char.ConvertFromUtf32(hex)); + i += 8; + break; + default: + throw new Exception($"Failed to parse string '{str}'!"); + } + } + } + } + + var s = Get(sb.ToString()); + InternedParsedStrings[str] = s; + return s; + } + + [BuiltInFunction("++", typeof(Func), IsOperator = true)] + public static String Concat(String lhs, String rhs) + { + var bytes = lhs.Bytes.Concat(rhs.Bytes).ToArray(); + var charIndices = lhs.CharIndices.Concat(rhs.CharIndices.Select(ci => ci + lhs.Bytes.Length)).ToArray(); + + return new String + { + Bytes = bytes, + CharIndices = charIndices + }; + } + + [BuiltInFunction("substring", typeof(Func))] + public static String Substring(String str, Number start, Number length) + { + var startIndex = start.AsInt32(); + if (startIndex < 0 || startIndex >= str.CharIndices.Length) + throw new ArgumentOutOfRangeException(nameof(start)); + if (length < Number.Zero) + throw new ArgumentOutOfRangeException(nameof(length)); + if (!Number.IsFinite(length).AsBool() || start + length > str.Length) + throw new ArgumentOutOfRangeException(nameof(length)); + var len = length.AsInt32(); + var byteStart = str.CharIndices[startIndex]; + var byteLen = str.CharIndices[startIndex + len] - byteStart; + var bytes = new byte[byteLen]; + Array.Copy(str.Bytes, byteStart, bytes, 0, byteLen); + var charIndices = str.CharIndices.Skip(startIndex).Take(len).Select(i => i - byteStart).ToArray(); + + return new String + { + Bytes = bytes, + CharIndices = charIndices + }; + } + + [BuiltInFunction("string->utf8", typeof(Func))] + public static List StringToUtf8(String str) + { + return new List(str.Bytes.Select(Number.NativeInt)); + } + + [BuiltInFunction("string->utf16", typeof(Func))] + public static List StringToUtf16(String str) + { + var utf16Bytes = Encoding.Convert(Encoding.UTF8, Encoding.Unicode, str.Bytes); + var shorts = new short[utf16Bytes.Length / 2]; + for (var i = 0; i < shorts.Length; i++) + { + shorts[i] = BitConverter.ToInt16(utf16Bytes, 2 * i); + } + + return new List(shorts.Select(Number.NativeInt)); + } + + [BuiltInFunction("string->utf32", typeof(Func))] + public static List StringToUtf32(String str) + { + var utf32Bytes = Encoding.Convert(Encoding.UTF8, Encoding.UTF32, str.Bytes); + var ints = new int[utf32Bytes.Length / 4]; + for (var i = 0; i < ints.Length; i++) + { + ints[i] = BitConverter.ToInt32(utf32Bytes, 4 * i); + } + + return new List(ints.Select(Number.NativeInt)); + } + + + + #endregion + } +} \ No newline at end of file diff --git a/Lilac/Values/Unit.cs b/Lilac/Values/Unit.cs new file mode 100644 index 0000000..21bf3c0 --- /dev/null +++ b/Lilac/Values/Unit.cs @@ -0,0 +1,24 @@ +using System; +using Lilac.Attributes; + +namespace Lilac.Values +{ + public class Unit : Value, IEquatable + { + private Unit() { } + + [BuiltInValue("nil")] + public static Unit Value { get; } = new Unit(); + + public bool Equals(Unit other) => !ReferenceEquals(other, null); + + public override bool Equals(object obj) => obj is Unit; + + public override int GetHashCode() => 0; + + public override string ToString() + { + return "()"; + } + } +} \ No newline at end of file diff --git a/Lilac/Values/Value.cs b/Lilac/Values/Value.cs new file mode 100644 index 0000000..56f9900 --- /dev/null +++ b/Lilac/Values/Value.cs @@ -0,0 +1,178 @@ +using System; +using Lilac.Attributes; +using Lilac.Utilities; + +namespace Lilac.Values +{ + public abstract class Value : IComparable + { + #region Virtual Methods + + public virtual bool AsBool() => true; + + public virtual bool IsCallable() => false; + + public virtual Value GetMember(string name) => null; + + public virtual bool SetMember(string name, Value value) => false; + + public virtual int CompareTo(Value other) + { + throw new Exception($"Type {GetType().Name.CamelCaseToKebabCase()} is not comparable!"); + } + + #endregion + + #region Built In Functions + + [BuiltInFunction("=", typeof(Func), IsOperator = true)] + public static Boolean Equals(Value lhs, Value rhs) + { + return Boolean.Get(lhs.Equals(rhs)); + } + + [BuiltInFunction("is", typeof(Func), IsOperator = true)] + public static Boolean Is(Value lhs, Value rhs) + { + return Boolean.Get(ReferenceEquals(lhs, rhs)); + } + + [BuiltInFunction("!=", typeof(Func), IsOperator = true)] + public static Boolean NotEquals(Value lhs, Value rhs) + { + return Boolean.Get(!lhs.Equals(rhs)); + } + + [BuiltInFunction("<=", typeof(Func), IsOperator = true)] + public static Boolean LessThanOrEquals(Value lhs, Value rhs) + { + return Boolean.Get(lhs.CompareTo(rhs) <= 0); + } + + [BuiltInFunction("<", typeof(Func), IsOperator = true)] + public static Boolean LessThan(Value lhs, Value rhs) + { + return Boolean.Get(lhs.CompareTo(rhs) < 0); + } + + [BuiltInFunction(">", typeof(Func), IsOperator = true)] + public static Boolean GreaterThan(Value lhs, Value rhs) + { + return Boolean.Get(lhs.CompareTo(rhs) > 0); + } + + [BuiltInFunction(">=", typeof(Func), IsOperator = true)] + public static Boolean GreaterThanOrEquals(Value lhs, Value rhs) + { + return Boolean.Get(lhs.CompareTo(rhs) >= 0); + } + + [BuiltInFunction("number?", typeof(Func))] + public static Boolean IsNumber(Value value) + { + return Boolean.Get(value is Number); + } + + [BuiltInFunction("boolean?", typeof(Func))] + public static Boolean IsBoolean(Value value) + { + return Boolean.Get(value is Boolean); + } + + [BuiltInFunction("string?", typeof(Func))] + public static Boolean IsString(Value value) + { + return Boolean.Get(value is String); + } + + [BuiltInFunction("char?", typeof(Func))] + public static Boolean IsChar(Value value) + { + return Boolean.Get(value is Char); + } + + [BuiltInFunction("list?", typeof(Func))] + public static Boolean IsList(Value value) + { + return Boolean.Get(value is List); + } + + [BuiltInFunction("pair?", typeof(Func))] + public static Boolean IsPair(Value value) + { + return Boolean.Get(value is Pair); + } + + [BuiltInFunction("linked-list?", typeof(Func))] + public static Boolean IsLinkedList(Value value) + { + var pair = value as Pair; + return Boolean.Get(value is Unit || (pair != null && IsLinkedList(pair.CdrValue).AsBool())); + } + + [BuiltInFunction("unit?", typeof(Func))] + public static Boolean IsUnit(Value value) + { + return Boolean.Get(value is Unit); + } + + [BuiltInFunction("callable?", typeof(Func))] + public static Boolean IsCallable(Value value) + { + return Boolean.Get(value.IsCallable()); + } + + [BuiltInFunction("complex?", typeof(Func))] + public static Boolean IsComplex(Value value) + { + var number = value as Number; + return Boolean.Get(number?.IsComplex == true); + } + + [BuiltInFunction("real?", typeof(Func))] + public static Boolean IsReal(Value value) + { + var number = value as Number; + return Boolean.Get(number?.IsReal == true); + } + + [BuiltInFunction("rational?", typeof(Func))] + public static Boolean IsRational(Value value) + { + var number = value as Number; + return Boolean.Get(number?.IsRational == true); + } + + [BuiltInFunction("integer?", typeof(Func))] + public static Boolean IsInteger(Value value) + { + var number = value as Number; + return Boolean.Get(number?.IsInteger == true); + } + + [BuiltInFunction("native-int?", typeof(Func))] + public static Boolean IsNativeInt(Value value) + { + var number = value as Number; + return Boolean.Get(number?.IsNativeInt == true); + } + + [BuiltInFunction("println", typeof(Func))] + public static Unit PrintLn(Value value) + { + Console.WriteLine(value); + return Unit.Value; + } + + [BuiltInFunction("typeof", typeof(Func))] + public static String TypeOf(Value value) + { + var num = value as Number; + var typename = num?.Type.ToString() ?? value.GetType().Name; + + return String.Get(typename.CamelCaseToKebabCase()); + } + + #endregion + } +} \ No newline at end of file diff --git a/Lilac/list.li b/Lilac/list.li new file mode 100644 index 0000000..9697ea0 --- /dev/null +++ b/Lilac/list.li @@ -0,0 +1,13 @@ +let foreach proc list = + let iter i = + if i = list.length + then () + else + proc (list.at i) + iter (i + 1) + iter 0 + +let map proc list = + let out = [] + foreach (lambda i = out.add! (proc i)) list + out \ No newline at end of file diff --git a/Lilac/packages.config b/Lilac/packages.config new file mode 100644 index 0000000..2508314 --- /dev/null +++ b/Lilac/packages.config @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/Lilac/test.li b/Lilac/test.li new file mode 100644 index 0000000..a276a14 --- /dev/null +++ b/Lilac/test.li @@ -0,0 +1,32 @@ +let ref current-x = 0 +let ref current-y = 0 + +set! current-x = 3 +set! current-y = 4 + +let x = current-x +let y = current-y + +let equals-3? x = x = 3 +let equals-4? x = x = 4 + +if equals-3? x and equals-4? y then + println "we're here" +else + println "keep lookin'" + +let is-even? x = x % 2 = 0 + +let operator >> f g x = g (f x) + +(is-even? >> println) x + +let print-3() = println 3 + +print-3() + +namespace a = + namespace b = + let c = 123 + +println a.b.c \ No newline at end of file