├── module ├── PSLambda.types.ps1xml ├── PSLambda.psm1 ├── PSLambda.format.ps1xml └── PSLambda.psd1 ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── debugHarness.ps1 ├── src └── PSLambda │ ├── .vscode │ └── launch.json │ ├── PSLambda.csproj │ ├── LoopScope.cs │ ├── Commands │ ├── ICommandHandler.cs │ ├── WithCommand.cs │ ├── LockCommand.cs │ ├── CommandService.cs │ ├── DefaultCommand.cs │ ├── ObjectAndBodyCommandHandler.cs │ └── GenericCommand.cs │ ├── ScopeHandle.cs │ ├── stylecop.json │ ├── IParseErrorWriter.cs │ ├── LoopScopeStack.cs │ ├── PSVariableWrapper.cs │ ├── VariableUtils.cs │ ├── Empty.cs │ ├── LabelScope.cs │ ├── PSLambdaAssemblyInitializer.cs │ ├── PSLambda.ruleset │ ├── ExpressionExtensions.cs │ ├── NewPSDelegateCommand.cs │ ├── Strings.cs │ ├── LabelScopeStack.cs │ ├── DelegateTypeConverter.cs │ ├── SpecialVariables.cs │ ├── PSDelegate.cs │ ├── ParseErrorWriter.cs │ ├── ParseWriterExtensions.cs │ ├── resources │ ├── CompilerStrings.resx │ └── ErrorStrings.resx │ ├── VariableScopeStack.cs │ ├── VariableScope.cs │ ├── ReflectionCache.cs │ └── DelegateSyntaxVisitor.cs ├── .circleci └── config.yml ├── test ├── PSLambda.Tests.ps1 ├── ParseError.Tests.ps1 ├── Scoping.Tests.ps1 ├── PSDelegate.Tests.ps1 ├── Commands.Tests.ps1 ├── IfStatements.Tests.ps1 ├── MethodResolution.Tests.ps1 ├── TryCatchFinally.Tests.ps1 ├── Loops.Tests.ps1 └── MiscLanguageFeatures.Tests.ps1 ├── tools ├── InvokeCircleCI.ps1 ├── AssertRequiredModule.ps1 ├── GetOpenCover.ps1 └── GetDotNet.ps1 ├── LICENSE ├── appveyor.yml ├── ScriptAnalyzerSettings.psd1 ├── PSLambda.sln ├── .gitattributes ├── docs ├── CODE_OF_CONDUCT.md └── CONTRIBUTING.md ├── .gitignore └── PSLambda.build.ps1 /module/PSLambda.types.ps1xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | PSLambda.PSDelegate 5 | 6 | PSLambda.DelegateTypeConverter 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "ms-vscode.csharp", 6 | "ms-vscode.powershell", 7 | "DavidAnson.vscode-markdownlint", 8 | ], 9 | } 10 | -------------------------------------------------------------------------------- /module/PSLambda.psm1: -------------------------------------------------------------------------------- 1 | Import-Module $PSScriptRoot\PSLambda.dll 2 | 3 | # Can't be in psd1 as TypesToProcess loads before the assembly and it tries to resolve the 4 | # TypeConverter that isn't in the AppDomain yet. 5 | Update-TypeData -AppendPath $PSScriptRoot\PSLambda.types.ps1xml 6 | 7 | Export-ModuleMember -Cmdlet New-PSDelegate 8 | -------------------------------------------------------------------------------- /debugHarness.ps1: -------------------------------------------------------------------------------- 1 | # Use this file to debug the module. 2 | Import-Module -Name $PSScriptRoot\Release\PSLambda\*\PSLambda.psd1 3 | 4 | $delegate = New-PSDelegate { 5 | $didFail = $false 6 | try { 7 | throw 8 | } catch [InvalidOperationException] { 9 | $didFail = $true 10 | } 11 | 12 | return $didFail 13 | } 14 | -------------------------------------------------------------------------------- /src/PSLambda/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Attach", 9 | "type": "clr", 10 | "request": "attach", 11 | "processId": "${command:pickProcess}" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.0 2 | jobs: 3 | build: 4 | working_directory: ~/pslambda 5 | docker: 6 | - image: microsoft/powershell@sha256:c0bd0f7ad40bcc76130cb5e772515546e3d200f8a416a273b2605a942d9775f5 7 | steps: 8 | - checkout 9 | - run: 10 | name: Run build script 11 | command: 'pwsh -File ./tools/InvokeCircleCI.ps1' 12 | - store_artifacts: 13 | path: ./testresults/pester.xml 14 | prefix: tests 15 | -------------------------------------------------------------------------------- /test/PSLambda.Tests.ps1: -------------------------------------------------------------------------------- 1 | $moduleName = 'PSLambda' 2 | $manifestPath = "$PSScriptRoot\..\Release\$moduleName\*\$moduleName.psd1" 3 | 4 | Describe 'module manifest values' { 5 | It 'can retrieve manfiest data' { 6 | $script:manifest = Test-ModuleManifest $manifestPath 7 | } 8 | It 'has the correct name' { 9 | $script:manifest.Name | Should Be $moduleName 10 | } 11 | It 'has the correct guid' { 12 | $script:manifest.Guid | Should Be '242ef850-1f6d-4647-acbe-26d010c4a3f5' 13 | } 14 | } 15 | 16 | -------------------------------------------------------------------------------- /tools/InvokeCircleCI.ps1: -------------------------------------------------------------------------------- 1 | $env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = 'true' 2 | $env:DOTNET_CLI_TELEMETRY_OPTOUT = 'true' 3 | 4 | & "$PSScriptRoot/../build.ps1" -Force 5 | Invoke-Build -File $PSScriptRoot/../PSLambda.build.ps1 -Configuration Release -Task Prerelease 6 | 7 | $resultsFile = "$PSScriptRoot/../testresults/pester.xml" 8 | 9 | $passed = (Test-Path $resultsFile) -and 0 -eq ([int]([xml](Get-Content $resultsFile -Raw)).'test-results'.failures) 10 | 11 | if (-not $passed) { 12 | $Error | Format-List * -Force 13 | exit 1 14 | } 15 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": ".NET FullCLR Attach", 6 | "type": "clr", 7 | "request": "attach", 8 | "processId": "${command:pickProcess}", 9 | "justMyCode": true, 10 | }, 11 | { 12 | "name": ".NET CoreCLR Attach", 13 | "type": "coreclr", 14 | "request": "attach", 15 | "processId": "${command:pickProcess}", 16 | "justMyCode": true, 17 | }, 18 | ], 19 | } 20 | -------------------------------------------------------------------------------- /module/PSLambda.format.ps1xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PSLambda.PSDelegate 6 | 7 | PSLambda.PSDelegate 8 | 9 | 10 | 11 | 12 | 13 | ScriptBlock 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/PSLambda/PSLambda.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netstandard2.0 4 | true 5 | PSLambda.ruleset 6 | true 7 | preview 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/PSLambda/LoopScope.cs: -------------------------------------------------------------------------------- 1 | using System.Linq.Expressions; 2 | 3 | namespace PSLambda 4 | { 5 | /// 6 | /// Represents a scope in which the break or continue keywords 7 | /// may be used. 8 | /// 9 | internal class LoopScope 10 | { 11 | /// 12 | /// Gets or sets the parent scope. 13 | /// 14 | public LoopScope Parent { get; set; } 15 | 16 | /// 17 | /// Gets or sets the label for the break keyword. 18 | /// 19 | public LabelTarget Break { get; set; } 20 | 21 | /// 22 | /// Gets or sets the label for the continue keyword. 23 | /// 24 | public LabelTarget Continue { get; set; } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | //-------- Files configuration -------- 3 | 4 | // When enabled, will trim trailing whitespace when you save a file. 5 | "files.trimTrailingWhitespace": true, 6 | 7 | // When enabled, insert a final new line at the end of the file when saving it. 8 | "files.insertFinalNewline": true, 9 | 10 | // Configure glob patterns for excluding files and folders in searches. Inherits all glob patterns from the files.exclude setting. 11 | "search.exclude": { 12 | "Release": true, 13 | "tools/dotnet": true, 14 | "tools/opencover": true, 15 | }, 16 | 17 | //-------- PowerShell Configuration -------- 18 | 19 | // Use a custom PowerShell Script Analyzer settings file for this workspace. 20 | // Relative paths for this setting are always relative to the workspace root dir. 21 | "powershell.scriptAnalysis.settingsPath": "ScriptAnalyzerSettings.psd1", 22 | } 23 | -------------------------------------------------------------------------------- /src/PSLambda/Commands/ICommandHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Linq.Expressions; 2 | using System.Management.Automation.Language; 3 | 4 | namespace PSLambda.Commands 5 | { 6 | /// 7 | /// Provides handling for a custom command. 8 | /// 9 | internal interface ICommandHandler 10 | { 11 | /// 12 | /// Gets the name of the command. 13 | /// 14 | string CommandName { get; } 15 | 16 | /// 17 | /// Creates a Linq expression for a representing 18 | /// the custom command. 19 | /// 20 | /// The AST to convert. 21 | /// The requesting the expression. 22 | /// An expression representing the command. 23 | Expression ProcessAst(CommandAst commandAst, CompileVisitor visitor); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tools/AssertRequiredModule.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param( 3 | [Parameter(Mandatory)] 4 | [ValidateNotNullOrEmpty()] 5 | [string] $Name, 6 | 7 | [Parameter(Mandatory)] 8 | [version] $RequiredVersion, 9 | 10 | [Parameter()] 11 | [ValidateSet('CurrentUser', 'AllUsers')] 12 | [string] $Scope = 'CurrentUser', 13 | 14 | [Parameter()] 15 | [switch] $Force 16 | ) 17 | end { 18 | if (Get-Module $Name -ErrorAction Ignore) { 19 | Remove-Module $Name -Force 20 | } 21 | 22 | $importModuleSplat = @{ 23 | RequiredVersion = $RequiredVersion 24 | Name = $Name 25 | ErrorAction = 'Stop' 26 | } 27 | 28 | # TODO: Install required versions into the tools folder 29 | try { 30 | Import-Module @importModuleSplat -Force 31 | } catch [System.IO.FileNotFoundException] { 32 | Install-Module @importModuleSplat -Force:$Force.IsPresent -Scope $Scope 33 | Import-Module @importModuleSplat -Force 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/PSLambda/ScopeHandle.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PSLambda 4 | { 5 | /// 6 | /// Represents a handle for the current scope. 7 | /// 8 | internal class ScopeHandle : IDisposable 9 | { 10 | private readonly Action _disposer; 11 | 12 | private bool _isDisposed; 13 | 14 | /// 15 | /// Initializes a new instance of the class. 16 | /// 17 | /// The action that disposes of the scope. 18 | public ScopeHandle(Action disposer) 19 | { 20 | _disposer = disposer; 21 | } 22 | 23 | /// 24 | /// Disposes of the current scope. 25 | /// 26 | public void Dispose() 27 | { 28 | if (_isDisposed) 29 | { 30 | return; 31 | } 32 | 33 | _disposer?.Invoke(); 34 | _isDisposed = true; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/PSLambda/stylecop.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", 3 | "settings": { 4 | "indentation": { 5 | "indentationSize": 4, 6 | "tabSize": 4, 7 | "useTabs": false 8 | }, 9 | "orderingRules": { 10 | "elementOrder": [ 11 | "kind", 12 | "accessibility", 13 | "constant", 14 | "static", 15 | "readonly" 16 | ], 17 | "systemUsingDirectivesFirst": true, 18 | "usingDirectivesPlacement": "outsideNamespace", 19 | "blankLinesBetweenUsingGroups": "allow" 20 | }, 21 | "layoutRules": { 22 | "newlineAtEndOfFile": "require" 23 | }, 24 | "documentationRules": { 25 | "xmlHeader": false, 26 | "documentationCulture": "en-US", 27 | "documentExposedElements": true, 28 | "documentInternalElements": true 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Patrick M. Meinecke 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /test/ParseError.Tests.ps1: -------------------------------------------------------------------------------- 1 | $moduleName = 'PSLambda' 2 | $manifestPath = "$PSScriptRoot\..\Release\$moduleName\*\$moduleName.psd1" 3 | 4 | Import-Module $manifestPath -Force 5 | 6 | Describe 'parse error tests' { 7 | It 'unsupported AST type throws a parse exception' -TestCases ( 8 | @{ Ast = 'FunctionDefinitionAst'; Test = { function Test {} }}, 9 | @{ Ast = 'TypeDefinitionAst'; Test = { class Test {} }}, 10 | @{ Ast = 'CommandAst'; Test = { Get-ChildItem }}, 11 | @{ Ast = 'DataStatementAst'; Test = { data {} }}, 12 | @{ Ast = 'FileRedirectionAst'; Test = { $ExecutionContext > 'test' }}, 13 | @{ Ast = 'MergingRedirectionAst'; Test = { $ExecutionContext 2>&1 }}, 14 | @{ Ast = 'TrapStatementAst'; Test = { trap {} end{ $ExecutionContext }}}, 15 | @{ Ast = 'AttributeAst'; Test = { [Parameter()]$thing = $null }}, 16 | @{ Ast = 'UsingExpressionAst'; Test = { $using:ExecutionContext }} 17 | ) -Test { 18 | param($Test) 19 | end { 20 | { New-PSDelegate $Test } | Should -Throw 'Unable to compile ScriptBlock due to an unsupported element' 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/Scoping.Tests.ps1: -------------------------------------------------------------------------------- 1 | $moduleName = 'PSLambda' 2 | $manifestPath = "$PSScriptRoot\..\Release\$moduleName\*\$moduleName.psd1" 3 | 4 | Import-Module $manifestPath -Force 5 | 6 | Describe 'variable scoping tests' { 7 | It 'can access current scope variables' { 8 | & { 9 | $v = 'test' 10 | $delegate = New-PSDelegate { $v } 11 | $delegate.Invoke() | Should -Be test 12 | } 13 | } 14 | 15 | It 'can change current scope variables' { 16 | & { 17 | $v = 'test' 18 | $delegate = New-PSDelegate { $v = 'newvalue' } 19 | $delegate.Invoke() 20 | $v | Should -Be newvalue 21 | } 22 | } 23 | 24 | It 'can not access variables outside of the current scope' { 25 | $v = 'test' 26 | & { 27 | { (New-PSDelegate { $v }).Invoke()} | Should -Throw 'The variable "v" was referenced before' 28 | } 29 | } 30 | 31 | It 'can access all scope variables' { 32 | $delegate = New-PSDelegate { $ExecutionContext } 33 | $delegate.Invoke() | Should -BeOfType System.Management.Automation.EngineIntrinsics 34 | } 35 | 36 | It 'property types "strongly typed" PowerShell variables' { 37 | [int] $definitelyNewVar = 0 38 | $delegate = New-PSDelegate { $definitelyNewVar + 1 } 39 | $delegate.Invoke() | Should -Be 1 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/PSLambda/IParseErrorWriter.cs: -------------------------------------------------------------------------------- 1 | using System.Management.Automation.Language; 2 | 3 | namespace PSLambda 4 | { 5 | /// 6 | /// Provides uniform writing and management of objects. 7 | /// 8 | internal interface IParseErrorWriter 9 | { 10 | /// 11 | /// Reports a . Handling of the 12 | /// may defer between implementations. 13 | /// 14 | /// 15 | /// The of the error being reported. 16 | /// 17 | /// The id of the error. 18 | /// 19 | /// The message to display in the 20 | /// when parsing is completed. 21 | /// 22 | void ReportParseError( 23 | IScriptExtent extent, 24 | string id, 25 | string message); 26 | 27 | /// 28 | /// Throws if the error limit has been hit. Error limit may vary 29 | /// between implementations. 30 | /// 31 | void ThrowIfErrorLimitHit(); 32 | 33 | /// 34 | /// Throws if any error has been reported. 35 | /// 36 | void ThrowIfAnyErrors(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: 1.0.0.{build} 2 | image: Visual Studio 2017 3 | environment: 4 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true 5 | DOTNET_CLI_TELEMETRY_OPTOUT: true 6 | install: 7 | - ps: >- 8 | Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force 9 | 10 | Install-Module Pester -RequiredVersion 4.3.1 -Scope CurrentUser -Force -SkipPublisherCheck 11 | 12 | Install-Module InvokeBuild -RequiredVersion 5.4.1 -Scope CurrentUser -Force 13 | 14 | Install-Module platyPS -RequiredVersion 0.9.0 -Scope CurrentUser -Force 15 | 16 | choco install codecov --no-progress 17 | build_script: 18 | - ps: >- 19 | . "$PWD\build.ps1" -Force 20 | 21 | $resultsFile = "$PWD\testresults\pester.xml" 22 | 23 | $passed = (Test-Path $resultsFile) -and 0 -eq ([int]([xml](Get-Content $resultsFile -Raw)).'test-results'.failures) 24 | 25 | 26 | if ($passed) { 27 | [System.Net.WebClient]::new().UploadFile("https://ci.appveyor.com/api/testresults/nunit/${env:APPVEYOR_JOB_ID}", $resultsFile) 28 | codecov -f "$PWD\testresults\opencover.xml" 29 | } else { 30 | $Error | Format-List * -Force 31 | exit 1; 32 | } 33 | on_finish: 34 | - ps: >- 35 | Add-Type -AssemblyName System.IO.Compression.FileSystem 36 | 37 | $zipPath = "$pwd\PSLambda.zip" 38 | 39 | [System.IO.Compression.ZipFile]::CreateFromDirectory("$pwd\Release", $zipPath) 40 | 41 | Push-AppveyorArtifact $zipPath 42 | -------------------------------------------------------------------------------- /ScriptAnalyzerSettings.psd1: -------------------------------------------------------------------------------- 1 | # The PowerShell Script Analyzer will generate a warning 2 | # diagnostic record for this file due to a bug - 3 | # https://github.com/PowerShell/PSScriptAnalyzer/issues/472 4 | @{ 5 | # Only diagnostic records of the specified severity will be generated. 6 | # Uncomment the following line if you only want Errors and Warnings but 7 | # not Information diagnostic records. 8 | 9 | # Severity = @('Error','Warning') 10 | 11 | # Analyze **only** the following rules. Use IncludeRules when you want 12 | # to invoke only a small subset of the defualt rules. 13 | 14 | # IncludeRules = @('PSAvoidDefaultValueSwitchParameter', 15 | # 'PSMissingModuleManifestField', 16 | # 'PSReservedCmdletChar', 17 | # 'PSReservedParams', 18 | # 'PSShouldProcess', 19 | # 'PSUseApprovedVerbs', 20 | # 'PSUseDeclaredVarsMoreThanAssigments') 21 | 22 | # Do not analyze the following rules. Use ExcludeRules when you have 23 | # commented out the IncludeRules settings above and want to include all 24 | # the default rules except for those you exclude below. 25 | # Note: if a rule is in both IncludeRules and ExcludeRules, the rule 26 | # will be excluded. 27 | 28 | # ExcludeRules = @('PSAvoidUsingWriteHost') 29 | ExcludeRules = 'PSUseShouldProcessForStateChangingFunctions' 30 | } 31 | -------------------------------------------------------------------------------- /src/PSLambda/LoopScopeStack.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | 4 | namespace PSLambda 5 | { 6 | /// 7 | /// Represents a set of scopes in which the break or continue keywords 8 | /// may be used. 9 | /// 10 | internal class LoopScopeStack 11 | { 12 | private LoopScope _current; 13 | 14 | /// 15 | /// Gets the current label for the break keyword. 16 | /// 17 | internal LabelTarget Break => _current?.Break; 18 | 19 | /// 20 | /// Gets the current label for the continue keyword. 21 | /// 22 | internal LabelTarget Continue => _current?.Continue; 23 | 24 | /// 25 | /// Creates a new scope in which the break or continue keywords 26 | /// may be used. 27 | /// 28 | /// 29 | /// A handle that will return to the previous scope when disposed. 30 | /// 31 | internal IDisposable NewScope() 32 | { 33 | _current = new LoopScope() 34 | { 35 | Parent = _current, 36 | Break = Expression.Label(), 37 | Continue = Expression.Label(), 38 | }; 39 | 40 | return new ScopeHandle(() => _current = _current?.Parent); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/PSLambda/PSVariableWrapper.cs: -------------------------------------------------------------------------------- 1 | using System.Management.Automation; 2 | 3 | namespace PSLambda 4 | { 5 | /// 6 | /// Provides a strongly typed wrapper for objects. 7 | /// 8 | /// The type of object contained by the . 9 | internal class PSVariableWrapper 10 | { 11 | private readonly object _syncObject = new object(); 12 | 13 | private readonly PSVariable _wrappedVariable; 14 | 15 | /// 16 | /// Initializes a new instance of the class. 17 | /// 18 | /// The to wrap. 19 | public PSVariableWrapper(PSVariable variable) 20 | { 21 | _wrappedVariable = variable; 22 | } 23 | 24 | /// 25 | /// Gets or sets the value of the object. 26 | /// 27 | public TValue Value 28 | { 29 | get 30 | { 31 | lock (_syncObject) 32 | { 33 | return LanguagePrimitives.ConvertTo(_wrappedVariable.Value); 34 | } 35 | } 36 | 37 | set 38 | { 39 | lock (_syncObject) 40 | { 41 | _wrappedVariable.Value = value; 42 | } 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/PSLambda/VariableUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Management.Automation.Language; 3 | 4 | namespace PSLambda 5 | { 6 | /// 7 | /// Provides utility methods for variable expression operations. 8 | /// 9 | internal static class VariableUtils 10 | { 11 | /// 12 | /// Determines if the specified variable is referencing the automatic 13 | /// variable $_ or $PSItem. 14 | /// 15 | /// The variable expression to test. 16 | /// 17 | /// if the specified variable references 18 | /// dollar under, otherwise . 19 | /// 20 | public static bool IsDollarUnder(VariableExpressionAst variableExpressionAst) 21 | => IsDollarUnder(variableExpressionAst.VariablePath.UserPath); 22 | 23 | /// 24 | /// Determines if the specified variable is referencing the automatic 25 | /// variable $_ or $PSItem. 26 | /// 27 | /// The variable name to test. 28 | /// 29 | /// if the specified variable references 30 | /// dollar under, otherwise . 31 | /// 32 | public static bool IsDollarUnder(string variableName) 33 | { 34 | return variableName.Equals("_", StringComparison.Ordinal) 35 | || variableName.Equals("PSItem", StringComparison.Ordinal); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/PSLambda/Empty.cs: -------------------------------------------------------------------------------- 1 | using System.Management.Automation.Language; 2 | 3 | namespace PSLambda 4 | { 5 | /// 6 | /// Provides utility methods for empty elements. 7 | /// 8 | internal static class Empty 9 | { 10 | /// 11 | /// Gets an empty . 12 | /// 13 | internal static IScriptExtent Extent => new EmptyScriptExtent(); 14 | 15 | /// 16 | /// Gets an empty . 17 | /// 18 | internal static IScriptPosition Position => new EmptyScriptPosition(); 19 | 20 | private class EmptyScriptExtent : IScriptExtent 21 | { 22 | public int EndColumnNumber => 0; 23 | 24 | public int EndLineNumber => 0; 25 | 26 | public int EndOffset => 0; 27 | 28 | public IScriptPosition EndScriptPosition => Position; 29 | 30 | public string File => string.Empty; 31 | 32 | public int StartColumnNumber => 0; 33 | 34 | public int StartLineNumber => 0; 35 | 36 | public int StartOffset => 0; 37 | 38 | public IScriptPosition StartScriptPosition => Position; 39 | 40 | public string Text => string.Empty; 41 | } 42 | 43 | private class EmptyScriptPosition : IScriptPosition 44 | { 45 | public int ColumnNumber => 0; 46 | 47 | public string File => string.Empty; 48 | 49 | public string Line => string.Empty; 50 | 51 | public int LineNumber => 0; 52 | 53 | public int Offset => 0; 54 | 55 | public string GetFullScript() => string.Empty; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /PSLambda.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | # Visual Studio 15 3 | VisualStudioVersion = 15.0.26124.0 4 | MinimumVisualStudioVersion = 15.0.26124.0 5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PSLambda", "src\PSLambda\PSLambda.csproj", "{6b92400e-adfe-4fa8-8c10-7c0d47d6a5f4}" 6 | EndProject 7 | Global 8 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 9 | Debug|Any CPU = Debug|Any CPU 10 | Debug|x64 = Debug|x64 11 | Debug|x86 = Debug|x86 12 | Release|Any CPU = Release|Any CPU 13 | Release|x64 = Release|x64 14 | Release|x86 = Release|x86 15 | EndGlobalSection 16 | GlobalSection(SolutionProperties) = preSolution 17 | HideSolutionNode = FALSE 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {6b92400e-adfe-4fa8-8c10-7c0d47d6a5f4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {6b92400e-adfe-4fa8-8c10-7c0d47d6a5f4}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {6b92400e-adfe-4fa8-8c10-7c0d47d6a5f4}.Debug|x64.ActiveCfg = Debug|x64 23 | {6b92400e-adfe-4fa8-8c10-7c0d47d6a5f4}.Debug|x64.Build.0 = Debug|x64 24 | {6b92400e-adfe-4fa8-8c10-7c0d47d6a5f4}.Debug|x86.ActiveCfg = Debug|x86 25 | {6b92400e-adfe-4fa8-8c10-7c0d47d6a5f4}.Debug|x86.Build.0 = Debug|x86 26 | {6b92400e-adfe-4fa8-8c10-7c0d47d6a5f4}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {6b92400e-adfe-4fa8-8c10-7c0d47d6a5f4}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {6b92400e-adfe-4fa8-8c10-7c0d47d6a5f4}.Release|x64.ActiveCfg = Release|x64 29 | {6b92400e-adfe-4fa8-8c10-7c0d47d6a5f4}.Release|x64.Build.0 = Release|x64 30 | {6b92400e-adfe-4fa8-8c10-7c0d47d6a5f4}.Release|x86.ActiveCfg = Release|x86 31 | {6b92400e-adfe-4fa8-8c10-7c0d47d6a5f4}.Release|x86.Build.0 = Release|x86 32 | EndGlobalSection 33 | EndGlobal 34 | 35 | -------------------------------------------------------------------------------- /src/PSLambda/LabelScope.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | 4 | namespace PSLambda 5 | { 6 | /// 7 | /// Represents the a scope in which the return keyword will work. 8 | /// 9 | internal class LabelScope 10 | { 11 | /// 12 | /// The parent scope. 13 | /// 14 | internal readonly LabelScope _parent; 15 | 16 | private LabelTarget _label; 17 | 18 | /// 19 | /// Initializes a new instance of the class. 20 | /// 21 | internal LabelScope() 22 | { 23 | } 24 | 25 | /// 26 | /// Initializes a new instance of the class. 27 | /// 28 | /// The parent . 29 | internal LabelScope(LabelScope parent) 30 | { 31 | _parent = parent; 32 | } 33 | 34 | /// 35 | /// Gets or sets a value indicating whether an explicit return statement has been used. 36 | /// 37 | public bool IsReturnRequested { get; set; } 38 | 39 | /// 40 | /// Gets or sets the implied or explicit return type. 41 | /// 42 | public Type ReturnType { get; set; } 43 | 44 | /// 45 | /// Gets the current return label. 46 | /// 47 | public LabelTarget Label 48 | { 49 | get 50 | { 51 | if (_label != null) 52 | { 53 | return _label; 54 | } 55 | 56 | _label = Expression.Label(ReturnType); 57 | return _label; 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/PSLambda/PSLambdaAssemblyInitializer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Management.Automation; 4 | 5 | namespace PSLambda 6 | { 7 | /// 8 | /// Provides initialization services when the module is imported. 9 | /// 10 | public class PSLambdaAssemblyInitializer : IModuleAssemblyInitializer 11 | { 12 | private const string TypeAcceleratorTypeName = "System.Management.Automation.TypeAccelerators"; 13 | 14 | private const string GetPropertyName = "Get"; 15 | 16 | /// 17 | /// Attempts to create the type accelerator for . 18 | /// 19 | public void OnImport() 20 | { 21 | var accelType = typeof(PSObject).Assembly.GetType(TypeAcceleratorTypeName); 22 | if (accelType == null) 23 | { 24 | return; 25 | } 26 | 27 | var getProperty = accelType.GetProperty(GetPropertyName); 28 | if (getProperty == null) 29 | { 30 | return; 31 | } 32 | 33 | if (!(getProperty.GetValue(null) is Dictionary existing)) 34 | { 35 | return; 36 | } 37 | 38 | if (existing.ContainsKey(Strings.PSDelegateTypeAcceleratorName)) 39 | { 40 | return; 41 | } 42 | 43 | var addMethod = accelType.GetMethod( 44 | Strings.AddMethodName, 45 | new[] { typeof(string), typeof(Type) }); 46 | if (addMethod == null) 47 | { 48 | return; 49 | } 50 | 51 | try 52 | { 53 | addMethod.Invoke( 54 | null, 55 | new object[] { Strings.PSDelegateTypeAcceleratorName, typeof(PSDelegate) }); 56 | } 57 | catch (Exception) 58 | { 59 | return; 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/PSLambda/Commands/WithCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | using System.Management.Automation.Language; 4 | 5 | namespace PSLambda.Commands 6 | { 7 | /// 8 | /// Provides handling for the "with" custom command. 9 | /// 10 | internal class WithCommand : ObjectAndBodyCommandHandler 11 | { 12 | /// 13 | /// Gets the name of the command. 14 | /// 15 | public override string CommandName { get; } = "with"; 16 | 17 | /// 18 | /// Creates a Linq expression for a representing 19 | /// the "with" command. 20 | /// 21 | /// The AST to convert. 22 | /// The AST containing the target of the keyword. 23 | /// The AST containing the body of the keyword. 24 | /// The requesting the expression. 25 | /// An expression representing the command. 26 | protected override Expression ProcessObjectAndBody( 27 | CommandAst commandAst, 28 | CommandElementAst targetAst, 29 | ScriptBlockExpressionAst bodyAst, 30 | CompileVisitor visitor) 31 | { 32 | var disposeVar = Expression.Variable(typeof(IDisposable)); 33 | return visitor.NewBlock(() => 34 | Expression.Block( 35 | typeof(void), 36 | new[] { disposeVar }, 37 | Expression.Assign( 38 | disposeVar, 39 | Expression.Convert( 40 | targetAst.Compile(visitor), 41 | typeof(IDisposable))), 42 | Expression.TryFinally( 43 | bodyAst.ScriptBlock.EndBlock.Compile(visitor), 44 | Expression.Call(disposeVar, ReflectionCache.IDisposable_Dispose)))); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /test/PSDelegate.Tests.ps1: -------------------------------------------------------------------------------- 1 | using namespace System.Collections.Generic 2 | using namespace System.Threading.Tasks 3 | 4 | $moduleName = 'PSLambda' 5 | $manifestPath = "$PSScriptRoot\..\Release\$moduleName\*\$moduleName.psd1" 6 | 7 | Import-Module $manifestPath -Force 8 | 9 | Describe 'Delegate conversion' { 10 | It 'can handle Task.Run' { 11 | & { 12 | $myVar = 'test' 13 | [Task]::Run([action][psdelegate]{ $myVar = 'newValue' }).GetAwaiter().GetResult() 14 | $myVar | Should -Be 'newValue' 15 | } 16 | } 17 | 18 | It 'can infer parameters from conversion' { 19 | $originalObject = [object]::new() 20 | $task = [Task[psobject]]::Factory.StartNew( 21 | [psdelegate]{ ($state) => { [PSObject]::AsPSObject($state) }}, 22 | $originalObject) 23 | 24 | $newObject = $task.GetAwaiter().GetResult() 25 | $originalObject.GetHashCode() | Should -Be $newObject.psobject.BaseObject.GetHashCode() 26 | } 27 | 28 | It 'retains locals from origin scope' { 29 | $delegate = & { 30 | $myVar = 'goodValue' 31 | [psdelegate]{ $myVar } 32 | } 33 | 34 | $myVar = 'badValue' 35 | $delegate.Invoke() | Should -Be 'goodValue' 36 | } 37 | 38 | It 'changes locals from origin scope' { 39 | & { 40 | $myVar = 'oldValue' 41 | $delegate = [psdelegate]{ $myVar = 'newValue' } 42 | & { 43 | $myVar = 'newScopeValue' 44 | $delegate.Invoke() 45 | $myVar | Should -Be 'newScopeValue' 46 | } 47 | 48 | $myVar | Should -Be 'newValue' 49 | } 50 | } 51 | 52 | It 'can be converted inside existing delegate with convert expression' { 53 | $task = [Task[int]]::Factory.StartNew([psdelegate]{ 54 | $total = 0 55 | $list = [List[int]]::new(0..5) 56 | $list.ForEach([action[int]]{ ($i) => { $total = $total + $i }}) 57 | return $total 58 | }) 59 | 60 | $task.GetAwaiter().GetResult() | Should -Be 15 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tools/GetOpenCover.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding(SupportsShouldProcess, ConfirmImpact='High')] 2 | param( 3 | [string] $Version = '4.6.519', 4 | [switch] $Force 5 | ) 6 | end { 7 | function GetVersionNumber { 8 | param([System.Management.Automation.CommandInfo] $Command) 9 | end { 10 | return (& $Command -version) ` 11 | -replace 'OpenCover version ' ` 12 | -replace '\.0$' 13 | } 14 | } 15 | 16 | $TARGET_FOLDER = "$PSScriptRoot\opencover" 17 | $TARGET_ARCHIVE = "$PSScriptRoot\opencover.zip" 18 | $TARGET_NAME = 'OpenCover.Console.exe' 19 | 20 | $ErrorActionPreference = 'Stop' 21 | 22 | if ($openCover = Get-Command $TARGET_FOLDER\$TARGET_NAME -ea 0) { 23 | if (($found = GetVersionNumber $openCover) -eq $Version) { 24 | return $openCover 25 | } 26 | 27 | Write-Host -ForegroundColor Yellow Found OpenCover $found but require $Version, replacing... 28 | 29 | if ($Force.IsPresent -or $PSCmdlet.ShouldProcess($TARGET_FOLDER, 'Remove-Item')) { 30 | Remove-Item $TARGET_FOLDER -Recurse 31 | } else { 32 | throw 'Existing version of OpenCover must be removed before it can be redownloaded.' 33 | } 34 | } 35 | Write-Host -ForegroundColor Green Downloading OpenCover version $Version 36 | 37 | $url = "https://github.com/OpenCover/opencover/releases/download/$Version/opencover.$Version.zip" 38 | if ($Force.IsPresent -or $PSCmdlet.ShouldProcess($url, 'Invoke-WebRequest')) { 39 | $oldSecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol 40 | try { 41 | [System.Net.ServicePointManager]::SecurityProtocol = 'Tls, Tls11, Tls12' 42 | Invoke-WebRequest $url -OutFile $TARGET_ARCHIVE 43 | } finally { 44 | [System.Net.ServicePointManager]::SecurityProtocol = $oldSecurityProtocol 45 | } 46 | 47 | Expand-Archive $TARGET_ARCHIVE -DestinationPath $TARGET_FOLDER -Force 48 | Remove-Item $TARGET_ARCHIVE 49 | } else { 50 | throw 'OpenCover is required to generate code coverage.' 51 | } 52 | 53 | 54 | return Get-Command $TARGET_FOLDER\$TARGET_NAME 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/PSLambda/PSLambda.ruleset: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /test/Commands.Tests.ps1: -------------------------------------------------------------------------------- 1 | using namespace System.Collections.ObjectModel 2 | using namespace System.Diagnostics 3 | using namespace System.Management.Automation 4 | using namespace System.Threading.Tasks 5 | 6 | $moduleName = 'PSLambda' 7 | $manifestPath = "$PSScriptRoot\..\Release\$moduleName\*\$moduleName.psd1" 8 | 9 | Import-Module $manifestPath -Force 10 | 11 | Describe 'Custom command handlers' { 12 | It 'with' { 13 | $delegate = [psdelegate]{ 14 | $ps = [powershell]::Create([RunspaceMode]::NewRunspace) 15 | with ($ps) { 16 | $ps.AddScript('$ExecutionContext').Invoke() 17 | } 18 | 19 | if ($ps.Runspace.RunspaceStateInfo.State -ne [Runspaces.RunspaceState]::Closed) { 20 | throw [RuntimeException]::new('Runspace was not closed after the with statement ended.') 21 | } 22 | } 23 | 24 | $delegate.Invoke() 25 | } 26 | 27 | It 'generic' { 28 | ([psdelegate]{ generic([array]::Empty(), [int]) }).Invoke().GetType() | Should -Be ([int[]]) 29 | } 30 | 31 | It 'default' { 32 | ([psdelegate]{ default([ConsoleColor]) }).Invoke() | Should -Be ([System.ConsoleColor]::Black) 33 | } 34 | 35 | It 'lock' { 36 | # Need a way better way to test this 37 | $delegate = New-PSDelegate { 38 | $syncObject = [object]::new() 39 | $collection = [Collection[TimeSpan]]::new() 40 | $stopWatch = [Stopwatch]::StartNew() 41 | 42 | $tasks = [Collection[Task]]::new() 43 | for ($i = 0; $i -lt 3; $i++) { 44 | $tasks.Add( 45 | [Task]::Run({ 46 | lock ($syncObject) { 47 | [System.Threading.Thread]::Sleep(100) 48 | $collection.Add($stopWatch.Elapsed) 49 | $stopWatch.Restart() 50 | } 51 | })) 52 | } 53 | 54 | [Task]::WaitAll($tasks.ToArray()) 55 | return $collection 56 | } 57 | 58 | $times = $delegate.Invoke() 59 | foreach ($time in $times) { 60 | $time.TotalMilliseconds | Should -BeGreaterThan 100 61 | $time.TotalMilliseconds | Should -BeLessThan 150 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/PSLambda/Commands/LockCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Linq.Expressions; 2 | using System.Management.Automation.Language; 3 | 4 | namespace PSLambda.Commands 5 | { 6 | /// 7 | /// Provides handling for the "lock" custom command. 8 | /// 9 | internal class LockCommand : ObjectAndBodyCommandHandler 10 | { 11 | /// 12 | /// Gets the name of the command. 13 | /// 14 | public override string CommandName { get; } = "lock"; 15 | 16 | /// 17 | /// Creates a Linq expression for a representing 18 | /// the "lock" command. 19 | /// 20 | /// The AST to convert. 21 | /// The AST containing the target of the keyword. 22 | /// The AST containing the body of the keyword. 23 | /// The requesting the expression. 24 | /// An expression representing the command. 25 | protected override Expression ProcessObjectAndBody( 26 | CommandAst commandAst, 27 | CommandElementAst targetAst, 28 | ScriptBlockExpressionAst bodyAst, 29 | CompileVisitor visitor) 30 | { 31 | var lockVar = Expression.Variable(typeof(object)); 32 | var lockTakenVar = Expression.Variable(typeof(bool)); 33 | return visitor.NewBlock(() => 34 | Expression.Block( 35 | typeof(void), 36 | new[] { lockVar, lockTakenVar }, 37 | Expression.Call( 38 | ReflectionCache.Monitor_Enter, 39 | Expression.Assign( 40 | lockVar, 41 | Expression.Convert( 42 | targetAst.Compile(visitor), 43 | typeof(object))), 44 | lockTakenVar), 45 | Expression.TryFinally( 46 | bodyAst.ScriptBlock.EndBlock.Compile(visitor), 47 | Expression.IfThen( 48 | lockTakenVar, 49 | Expression.Call( 50 | ReflectionCache.Monitor_Exit, 51 | lockVar))))); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/PSLambda/Commands/CommandService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq.Expressions; 4 | using System.Management.Automation.Language; 5 | 6 | namespace PSLambda.Commands 7 | { 8 | /// 9 | /// Provides management of custom command handlers. 10 | /// 11 | internal class CommandService 12 | { 13 | private readonly Dictionary _commandRegistry = new Dictionary(StringComparer.OrdinalIgnoreCase); 14 | 15 | /// 16 | /// Initializes a new instance of the class. 17 | /// 18 | public CommandService() 19 | { 20 | RegisterCommand(new DefaultCommand()); 21 | RegisterCommand(new WithCommand()); 22 | RegisterCommand(new GenericCommand()); 23 | RegisterCommand(new LockCommand()); 24 | } 25 | 26 | /// 27 | /// Registers a command handler. 28 | /// 29 | /// The handler to register. 30 | public void RegisterCommand(ICommandHandler handler) 31 | { 32 | if (_commandRegistry.ContainsKey(handler.CommandName)) 33 | { 34 | return; 35 | } 36 | 37 | _commandRegistry.Add(handler.CommandName, handler); 38 | } 39 | 40 | /// 41 | /// Attempt to process a as a custom command. 42 | /// 43 | /// The to process. 44 | /// 45 | /// The requesting the expression. 46 | /// 47 | /// 48 | /// The result if a command handler was found. 49 | /// 50 | /// true if a commmand handler was matched, otherwise false. 51 | public bool TryProcessAst(CommandAst commandAst, CompileVisitor visitor, out Expression expression) 52 | { 53 | if (_commandRegistry.TryGetValue(commandAst.GetCommandName(), out ICommandHandler handler)) 54 | { 55 | expression = handler.ProcessAst(commandAst, visitor); 56 | return true; 57 | } 58 | 59 | expression = null; 60 | return false; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // Available variables which can be used inside of strings. 2 | // ${workspaceRoot}: the root folder of the team 3 | // ${file}: the current opened file 4 | // ${relativeFile}: the current opened file relative to workspaceRoot 5 | // ${fileBasename}: the current opened file's basename 6 | // ${fileDirname}: the current opened file's dirname 7 | // ${fileExtname}: the current opened file's extension 8 | // ${cwd}: the current working directory of the spawned process 9 | { 10 | // See https://go.microsoft.com/fwlink/?LinkId=733558 11 | // for the documentation about the tasks.json format 12 | "version": "2.0.0", 13 | "windows": { 14 | "options": { 15 | "shell": { 16 | "executable": "powershell.exe", 17 | "args": [ "-ExecutionPolicy Bypass", "-NoProfile", "-Command" ], 18 | }, 19 | }, 20 | }, 21 | "linux": { 22 | "options": { 23 | "shell": { 24 | "executable": "/usr/bin/pwsh", 25 | "args": [ "-NoProfile", "-Command" ], 26 | }, 27 | }, 28 | }, 29 | "osx": { 30 | "options": { 31 | "shell": { 32 | "executable": "/usr/local/bin/pwsh", 33 | "args": [ "-NoProfile", "-Command" ], 34 | }, 35 | }, 36 | }, 37 | "type": "shell", 38 | "tasks": [ 39 | { 40 | "label": "Clean", 41 | "command": "Invoke-Build", 42 | "args": [ "-Task", "Clean" ], 43 | }, 44 | { 45 | "label": "Quick Build", 46 | "command": "dotnet", 47 | "args": [ "build", "/property:GenerateFullPaths=true" ], 48 | "group": { "kind": "build", "isDefault": true, }, 49 | "problemMatcher": "$msCompile", 50 | }, 51 | { 52 | "label": "Build", 53 | "command": "& ./build.ps1 -Configuration Release -Build", 54 | "group": "build", 55 | }, 56 | { 57 | "label": "Test", 58 | "command": "& ./build.ps1 -Configuration Release -Test", 59 | "group": { "kind": "test", "isDefault": true, }, 60 | }, 61 | { 62 | "label": "Test With Coverage", 63 | "command": "& ./build.ps1 -Configuration Debug -Test -GenerateCodeCoverage", 64 | "group": "test", 65 | }, 66 | { 67 | "label": "Install", 68 | "command": "& ./build.ps1 -Configuration Release -Task Install", 69 | } 70 | ] 71 | } 72 | -------------------------------------------------------------------------------- /src/PSLambda/ExpressionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq.Expressions; 4 | using System.Management.Automation.Language; 5 | 6 | namespace PSLambda 7 | { 8 | /// 9 | /// Provides utility methods for creating objects. 10 | /// 11 | internal static class ExpressionExtensions 12 | { 13 | private static readonly Expression[] s_emptyExpressions = new Expression[0]; 14 | 15 | /// 16 | /// Compile all asts in a given . 17 | /// 18 | /// The objects to compile. 19 | /// The requesting the compile. 20 | /// The type of to expect in the list. 21 | /// The compiled objects. 22 | public static Expression[] CompileAll(this IList asts, CompileVisitor visitor) 23 | where TAst : Ast 24 | { 25 | if (asts == null || asts.Count == 0) 26 | { 27 | return s_emptyExpressions; 28 | } 29 | 30 | var expressions = new Expression[asts.Count]; 31 | for (var i = 0; i < asts.Count; i++) 32 | { 33 | expressions[i] = (Expression)asts[i].Visit(visitor); 34 | } 35 | 36 | return expressions; 37 | } 38 | 39 | /// 40 | /// Compile a into a . 41 | /// 42 | /// The to compile. 43 | /// The requesting the compile. 44 | /// The compiled object. 45 | public static Expression Compile(this Ast ast, CompileVisitor visitor) 46 | { 47 | try 48 | { 49 | return (Expression)ast.Visit(visitor); 50 | } 51 | catch (ArgumentException e) 52 | { 53 | visitor.Errors.ReportParseError(ast.Extent, e); 54 | return Expression.Empty(); 55 | } 56 | catch (InvalidOperationException e) 57 | { 58 | visitor.Errors.ReportParseError(ast.Extent, e); 59 | return Expression.Empty(); 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/PSLambda/Commands/DefaultCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | using System.Management.Automation; 4 | using System.Management.Automation.Language; 5 | 6 | namespace PSLambda.Commands 7 | { 8 | /// 9 | /// Provides handling for the "default" custom command. 10 | /// 11 | internal class DefaultCommand : ICommandHandler 12 | { 13 | /// 14 | /// Gets the name of the command. 15 | /// 16 | public string CommandName { get; } = "default"; 17 | 18 | /// 19 | /// Creates a Linq expression for a representing 20 | /// the "default" command. 21 | /// 22 | /// The AST to convert. 23 | /// The requesting the expression. 24 | /// An expression representing the command. 25 | public Expression ProcessAst(CommandAst commandAst, CompileVisitor visitor) 26 | { 27 | if (commandAst.CommandElements == null || commandAst.CommandElements.Count != 2) 28 | { 29 | visitor.TryResolveType(null, out _); 30 | return Expression.Empty(); 31 | } 32 | 33 | // Is using syntax "default([TypeName])" 34 | if (commandAst.CommandElements[1] is ParenExpressionAst paren && 35 | paren.Pipeline is PipelineAst pipeline && 36 | pipeline.PipelineElements.Count == 1 && 37 | pipeline.PipelineElements[0] is CommandExpressionAst commandExpression && 38 | commandExpression.Expression is TypeExpressionAst && 39 | visitor.TryResolveType(commandExpression.Expression, out Type resolvedType)) 40 | { 41 | return Expression.Default(resolvedType); 42 | } 43 | 44 | // Is using syntax "default TypeName" 45 | if (commandAst.CommandElements[1] is StringConstantExpressionAst stringConstant && 46 | LanguagePrimitives.TryConvertTo(stringConstant.Value, out resolvedType)) 47 | { 48 | return Expression.Default(resolvedType); 49 | } 50 | 51 | // Unknown syntax, but this method will produce parse errors for us. 52 | if (visitor.TryResolveType(commandAst.CommandElements[1], out resolvedType)) 53 | { 54 | return Expression.Default(resolvedType); 55 | } 56 | 57 | return Expression.Empty(); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/PSLambda/NewPSDelegateCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Management.Automation; 4 | using System.Management.Automation.Language; 5 | using System.Management.Automation.Runspaces; 6 | 7 | namespace PSLambda 8 | { 9 | /// 10 | /// Represents the New-PSDelegate command. 11 | /// 12 | [Cmdlet(VerbsCommon.New, "PSDelegate")] 13 | [OutputType(typeof(Delegate))] 14 | public class NewPSDelegateCommand : PSCmdlet 15 | { 16 | /// 17 | /// Gets or sets the value for the parameter "DelegateType". 18 | /// 19 | [Parameter] 20 | [ValidateNotNull] 21 | public Type DelegateType { get; set; } 22 | 23 | /// 24 | /// Gets or sets the value for the parameter "Expression". 25 | /// 26 | [Parameter(Position = 0, Mandatory = true)] 27 | [ValidateNotNull] 28 | public ScriptBlock Expression { get; set; } 29 | 30 | /// 31 | /// The EndProcessing method. 32 | /// 33 | protected override void EndProcessing() 34 | { 35 | var variables = SessionState.InvokeCommand.InvokeScript( 36 | "Get-Variable -Scope 0", 37 | false, 38 | PipelineResultTypes.Output, 39 | null, 40 | null) 41 | .Select(pso => pso.BaseObject) 42 | .Cast() 43 | .Where(v => !SpecialVariables.IgnoreLocal.Contains(v.Name)); 44 | 45 | try 46 | { 47 | if (DelegateType == null) 48 | { 49 | WriteObject( 50 | CompileVisitor.CompileAst( 51 | (EngineIntrinsics)SessionState.PSVariable.GetValue(Strings.ExecutionContextVariableName), 52 | (ScriptBlockAst)Expression.Ast, 53 | variables.ToArray()), 54 | enumerateCollection: false); 55 | return; 56 | } 57 | 58 | WriteObject( 59 | CompileVisitor.CompileAst( 60 | (EngineIntrinsics)SessionState.PSVariable.GetValue(Strings.ExecutionContextVariableName), 61 | (ScriptBlockAst)Expression.Ast, 62 | variables.ToArray(), 63 | DelegateType), 64 | enumerateCollection: false); 65 | } 66 | catch (ParseException e) 67 | { 68 | ThrowTerminatingError(new ErrorRecord(e.ErrorRecord, e)); 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /test/IfStatements.Tests.ps1: -------------------------------------------------------------------------------- 1 | $moduleName = 'PSLambda' 2 | $manifestPath = "$PSScriptRoot\..\Release\$moduleName\*\$moduleName.psd1" 3 | 4 | Import-Module $manifestPath -Force 5 | 6 | Describe 'If statements' { 7 | It 'if statement only' { 8 | $delegate = New-PSDelegate { 9 | ([int] $a) => { 10 | if ($a -eq 1) { 11 | return $true 12 | } 13 | 14 | return $false 15 | } 16 | } 17 | 18 | $delegate.Invoke(1) | Should -Be $true 19 | $delegate.Invoke(0) | Should -Be $false 20 | } 21 | 22 | It 'if else' { 23 | $delegate = New-PSDelegate { 24 | ([int] $a) => { 25 | if ($a -eq 1) { 26 | return 'if' 27 | } else { 28 | return 'else' 29 | } 30 | } 31 | } 32 | 33 | $delegate.Invoke(1) | Should -Be 'if' 34 | $delegate.Invoke(0) | Should -Be 'else' 35 | } 36 | 37 | It 'if else if' { 38 | $delegate = New-PSDelegate { 39 | ([int] $a) => { 40 | if ($a -eq 1) { 41 | return 'if' 42 | } elseif ($a -eq 2) { 43 | return 'elseif' 44 | } 45 | 46 | return 'none' 47 | } 48 | } 49 | 50 | $delegate.Invoke(2) | Should -Be 'elseif' 51 | $delegate.Invoke(1) | Should -Be 'if' 52 | $delegate.Invoke(0) | Should -Be 'none' 53 | } 54 | 55 | It 'if else-if else' { 56 | $delegate = New-PSDelegate { 57 | ([int] $a) => { 58 | if ($a -eq 1) { 59 | return 'if' 60 | } elseif ($a -eq 2) { 61 | return 'elseif' 62 | } else { 63 | return 'else' 64 | } 65 | } 66 | } 67 | 68 | $delegate.Invoke(2) | Should -Be 'elseif' 69 | $delegate.Invoke(1) | Should -Be 'if' 70 | $delegate.Invoke(0) | Should -Be 'else' 71 | } 72 | 73 | It 'if with multiple else-ifs' { 74 | $delegate = New-PSDelegate { 75 | ([int] $a) => { 76 | if ($a -eq 1) { 77 | return 'if' 78 | } elseif ($a -eq 2) { 79 | return 'elseif' 80 | } elseif ($a -eq 3) { 81 | return 'elseif1' 82 | } elseif ($a -eq 4) { 83 | return 'elseif2' 84 | } else { 85 | return 'else' 86 | } 87 | } 88 | } 89 | 90 | $delegate.Invoke(4) | Should -Be 'elseif2' 91 | $delegate.Invoke(3) | Should -Be 'elseif1' 92 | $delegate.Invoke(2) | Should -Be 'elseif' 93 | $delegate.Invoke(1) | Should -Be 'if' 94 | $delegate.Invoke(0) | Should -Be 'else' 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/PSLambda/Commands/ObjectAndBodyCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.Linq.Expressions; 3 | using System.Management.Automation.Language; 4 | 5 | namespace PSLambda.Commands 6 | { 7 | /// 8 | /// Provides a base for commands that follow the format of 9 | /// `commandName ($objectTarget) { body }`. 10 | /// 11 | internal abstract class ObjectAndBodyCommandHandler : ICommandHandler 12 | { 13 | /// 14 | /// Gets the name of the command. 15 | /// 16 | public abstract string CommandName { get; } 17 | 18 | /// 19 | /// Creates a Linq expression for a representing 20 | /// a custom command. 21 | /// 22 | /// The AST to convert. 23 | /// The requesting the expression. 24 | /// An expression representing the command. 25 | public Expression ProcessAst(CommandAst commandAst, CompileVisitor visitor) 26 | { 27 | if (commandAst.CommandElements.Count != 3) 28 | { 29 | visitor.Errors.ReportParseError( 30 | commandAst.Extent, 31 | nameof(ErrorStrings.MissingKeywordElements), 32 | string.Format( 33 | CultureInfo.CurrentCulture, 34 | ErrorStrings.MissingKeywordElements, 35 | CommandName)); 36 | return Expression.Empty(); 37 | } 38 | 39 | if (!(commandAst.CommandElements[2] is ScriptBlockExpressionAst bodyAst)) 40 | { 41 | visitor.Errors.ReportParseError( 42 | commandAst.Extent, 43 | nameof(ErrorStrings.MissingKeywordBody), 44 | string.Format( 45 | CultureInfo.CurrentCulture, 46 | ErrorStrings.MissingKeywordBody, 47 | CommandName)); 48 | return Expression.Empty(); 49 | } 50 | 51 | return ProcessObjectAndBody( 52 | commandAst, 53 | commandAst.CommandElements[1], 54 | bodyAst, 55 | visitor); 56 | } 57 | 58 | /// 59 | /// Creates a Linq expression for a representing 60 | /// a custom command. 61 | /// 62 | /// The AST to convert. 63 | /// The AST containing the target of the keyword. 64 | /// The AST containing the body of the keyword. 65 | /// The requesting the expression. 66 | /// An expression representing the command. 67 | protected abstract Expression ProcessObjectAndBody( 68 | CommandAst commandAst, 69 | CommandElementAst targetAst, 70 | ScriptBlockExpressionAst bodyAst, 71 | CompileVisitor visitor); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/PSLambda/Commands/GenericCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | using System.Management.Automation.Language; 4 | 5 | namespace PSLambda.Commands 6 | { 7 | /// 8 | /// Provides handling for the "generic" custom command. 9 | /// 10 | internal class GenericCommand : ICommandHandler 11 | { 12 | /// 13 | /// Gets the name of the command. 14 | /// 15 | public string CommandName { get; } = "generic"; 16 | 17 | /// 18 | /// Creates a Linq expression for a representing 19 | /// the "generic" command. 20 | /// 21 | /// The AST to convert. 22 | /// The requesting the expression. 23 | /// An expression representing the command. 24 | public Expression ProcessAst(CommandAst commandAst, CompileVisitor visitor) 25 | { 26 | if (commandAst.CommandElements.Count != 2) 27 | { 28 | return ReportInvalidSyntax(commandAst.Extent, visitor); 29 | } 30 | 31 | if (!(commandAst.CommandElements[1] is ParenExpressionAst paren)) 32 | { 33 | return ReportInvalidSyntax(commandAst.Extent, visitor); 34 | } 35 | 36 | if (!(paren.Pipeline is PipelineAst pipeline) || pipeline.PipelineElements.Count != 1) 37 | { 38 | return ReportInvalidSyntax(commandAst.Extent, visitor); 39 | } 40 | 41 | if (!(pipeline.PipelineElements[0] is CommandExpressionAst commandExpression)) 42 | { 43 | return ReportInvalidSyntax(commandAst.Extent, visitor); 44 | } 45 | 46 | var arrayLiteral = commandExpression.Expression as ArrayLiteralAst; 47 | if (arrayLiteral.Elements.Count < 2) 48 | { 49 | return ReportInvalidSyntax(commandAst.Extent, visitor); 50 | } 51 | 52 | if (!(arrayLiteral.Elements[0] is InvokeMemberExpressionAst memberExpression)) 53 | { 54 | return ReportInvalidSyntax(commandAst.Extent, visitor); 55 | } 56 | 57 | var genericArguments = new Type[arrayLiteral.Elements.Count - 1]; 58 | for (var i = 1; i < arrayLiteral.Elements.Count; i++) 59 | { 60 | if (visitor.TryResolveType(arrayLiteral.Elements[i], out Type resolvedType)) 61 | { 62 | genericArguments[i - 1] = resolvedType; 63 | continue; 64 | } 65 | 66 | // If a type didn't resolve then a parse error was generated, so exit. 67 | return Expression.Empty(); 68 | } 69 | 70 | return visitor.CompileInvokeMemberExpression(memberExpression, genericArguments); 71 | } 72 | 73 | private Expression ReportInvalidSyntax(IScriptExtent extent, CompileVisitor visitor) 74 | { 75 | visitor.Errors.ReportParseError( 76 | extent, 77 | nameof(ErrorStrings.InvalidGenericSyntax), 78 | ErrorStrings.InvalidGenericSyntax); 79 | return Expression.Empty(); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /docs/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at seeminglyscience@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /src/PSLambda/Strings.cs: -------------------------------------------------------------------------------- 1 | namespace PSLambda 2 | { 3 | /// 4 | /// Provides string constant values for various strings used throughout the project. 5 | /// 6 | internal class Strings 7 | { 8 | /// 9 | /// Constant containing a string similar to "GetEnumerator". 10 | /// 11 | public const string GetEnumeratorMethodName = "GetEnumerator"; 12 | 13 | /// 14 | /// Constant containing a string similar to "get_Current". 15 | /// 16 | public const string EnumeratorGetCurrentMethodName = "get_Current"; 17 | 18 | /// 19 | /// Constant containing a string similar to "psdelegate". 20 | /// 21 | public const string PSDelegateTypeAcceleratorName = "psdelegate"; 22 | 23 | /// 24 | /// Constant containing a string similar to "ExecutionContext". 25 | /// 26 | public const string ExecutionContextVariableName = "ExecutionContext"; 27 | 28 | /// 29 | /// Constant containing a string similar to "foreach". 30 | /// 31 | public const string ForEachVariableName = "foreach"; 32 | 33 | /// 34 | /// Constant containing a string similar to "null". 35 | /// 36 | public const string NullVariableName = "null"; 37 | 38 | /// 39 | /// Constant containing a string similar to "true". 40 | /// 41 | public const string TrueVariableName = "true"; 42 | 43 | /// 44 | /// Constant containing a string similar to "false". 45 | /// 46 | public const string FalseVariableName = "false"; 47 | 48 | /// 49 | /// Constant containing a string similar to "new". 50 | /// 51 | public const string ConstructorMemberName = "new"; 52 | 53 | /// 54 | /// Constant containing a string similar to "OperatorNotSupported". 55 | /// 56 | public const string OperatorNotSupportedId = "OperatorNotSupported"; 57 | 58 | /// 59 | /// Constant containing a string similar to ">". 60 | /// 61 | public const string DelegateSyntaxCommandName = ">"; 62 | 63 | /// 64 | /// Constant containing a string similar to "Invoke". 65 | /// 66 | public const string DelegateInvokeMethodName = "Invoke"; 67 | 68 | /// 69 | /// Constant containing a string similar to "Value". 70 | /// 71 | public const string PSVariableWrapperValuePropertyName = "Value"; 72 | 73 | /// 74 | /// Constant containing a string similar to "Item". 75 | /// 76 | public const string DefaultIndexerPropertyName = "Item"; 77 | 78 | /// 79 | /// Constant containing a string similar to "ElementAtOrDefault". 80 | /// 81 | public const string ElementAtOrDefaultMethodName = "ElementAtOrDefault"; 82 | 83 | /// 84 | /// Constant containing a string similar to "Add". 85 | /// 86 | public const string AddMethodName = "Add"; 87 | 88 | /// 89 | /// Constant containing a string similar to "ToArray". 90 | /// 91 | public const string ToArrayMethodName = "ToArray"; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /module/PSLambda.psd1: -------------------------------------------------------------------------------- 1 | # 2 | # Module manifest for module 'PSLambda' 3 | # 4 | # Generated by: Patrick M. Meinecke 5 | # 6 | # Generated on: 4/20/2018 7 | # 8 | 9 | @{ 10 | 11 | # Script module or binary module file associated with this manifest. 12 | RootModule = 'PSLambda.psm1' 13 | 14 | # Version number of this module. 15 | ModuleVersion = '0.2.0' 16 | 17 | # ID used to uniquely identify this module 18 | GUID = '242ef850-1f6d-4647-acbe-26d010c4a3f5' 19 | 20 | # Author of this module 21 | Author = 'Patrick M. Meinecke' 22 | 23 | # Company or vendor of this module 24 | CompanyName = 'Community' 25 | 26 | # Copyright statement for this module 27 | Copyright = '(c) 2018 Patrick M. Meinecke. All rights reserved.' 28 | 29 | # Description of the functionality provided by this module 30 | Description = 'A runtime delegate compiler for PowerShell ScriptBlock objects.' 31 | 32 | # Minimum version of the Windows PowerShell engine required by this module 33 | PowerShellVersion = '5.1' 34 | 35 | # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 36 | DotNetFrameworkVersion = '4.7.1' 37 | 38 | # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 39 | CLRVersion = '4.0' 40 | 41 | # Processor architecture (None, X86, Amd64) required by this module 42 | ProcessorArchitecture = 'None' 43 | 44 | # Format files (.ps1xml) to be loaded when importing this module 45 | FormatsToProcess = 'PSLambda.format.ps1xml' 46 | 47 | # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. 48 | FunctionsToExport = @() 49 | 50 | # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. 51 | CmdletsToExport = 'New-PSDelegate' 52 | 53 | # Variables to export from this module 54 | VariablesToExport = @() 55 | 56 | # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. 57 | AliasesToExport = @() 58 | 59 | # List of all files packaged with this module 60 | FileList = 'PSLambda.psd1', 61 | 'PSLambda.psm1', 62 | 'PSLambda.types.ps1xml', 63 | 'PSLambda.format.ps1xml', 64 | 'PSLambda.dll', 65 | 'PSLambda.pdb', 66 | 'PSLambda.deps.json', 67 | 'PSLambda.xml' 68 | 69 | # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. 70 | PrivateData = @{ 71 | 72 | PSData = @{ 73 | 74 | # Tags applied to this module. These help with module discovery in online galleries. 75 | Tags = @() 76 | 77 | # A URL to the license for this module. 78 | LicenseUri = 'https://github.com/SeeminglyScience/PSLambda/blob/master/LICENSE' 79 | 80 | # A URL to the main website for this project. 81 | ProjectUri = 'https://github.com/SeeminglyScience/PSLambda' 82 | 83 | # A URL to an icon representing this module. 84 | # IconUri = '' 85 | 86 | # ReleaseNotes of this module 87 | ReleaseNotes = @' 88 | - Add extension method resolution 89 | - Add generic parameter inference 90 | - Fix scriptblocks in method argument binding 91 | - Fix ForEach enumerator typing 92 | - Fix index expressions for non IList types 93 | '@ 94 | 95 | } # End of PSData hashtable 96 | 97 | } # End of PrivateData hashtable 98 | 99 | } 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /src/PSLambda/LabelScopeStack.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | 6 | namespace PSLambda 7 | { 8 | /// 9 | /// Represents the current set of scopes in which the return keyword will work. 10 | /// 11 | internal class LabelScopeStack 12 | { 13 | private LabelScope _current; 14 | 15 | /// 16 | /// Gets the implied or explicit return type. 17 | /// 18 | public Type ReturnType => _current?.ReturnType; 19 | 20 | /// 21 | /// Creates a new scope in which the return keyword will work. 22 | /// 23 | /// 24 | /// A handle that will return to the previous scope when disposed. 25 | /// 26 | public IDisposable NewScope() 27 | { 28 | _current = _current == null ? new LabelScope() : new LabelScope(_current); 29 | return new ScopeHandle(() => _current = _current?._parent); 30 | } 31 | 32 | /// 33 | /// Creates a new scope in which the return keyword will work while specifying an 34 | /// expected return type. 35 | /// 36 | /// The expected return type. 37 | /// 38 | /// A handle that will return to the previous scope when disposed. 39 | /// 40 | public IDisposable NewScope(Type returnType) 41 | { 42 | _current = _current == null ? new LabelScope() : new LabelScope(_current); 43 | _current.ReturnType = returnType; 44 | return new ScopeHandle(() => _current = _current?._parent); 45 | } 46 | 47 | /// 48 | /// Gets the specified statement adding the return label if applicable. 49 | /// 50 | /// The expressions to precent the label. 51 | /// 52 | /// A value indicating whether an explicit return statement should be required. 53 | /// 54 | /// 55 | /// If a return label is required the supplied expressions will be returned followed 56 | /// by the required label, otherwise they will be returned unchanged. 57 | /// 58 | public Expression[] WithReturn(IEnumerable expressions, bool requireExplicitReturn = true) 59 | { 60 | if (_current.IsReturnRequested) 61 | { 62 | return expressions.Concat( 63 | new[] 64 | { 65 | Expression.Label( 66 | _current.Label, 67 | Expression.Default(_current.ReturnType)), 68 | }).ToArray(); 69 | } 70 | 71 | if (!requireExplicitReturn) 72 | { 73 | return expressions.ToArray(); 74 | } 75 | 76 | _current.IsReturnRequested = true; 77 | _current.ReturnType = typeof(void); 78 | return expressions.Concat(new[] { Expression.Label(_current.Label) }).ToArray(); 79 | } 80 | 81 | /// 82 | /// Gets an existing return label if one has already been defined, otherwise 83 | /// one is created. 84 | /// 85 | /// The expected return type. 86 | /// The requested. 87 | public LabelTarget GetOrCreateReturnLabel(Type type) 88 | { 89 | if (_current != null && _current.IsReturnRequested) 90 | { 91 | return _current.Label; 92 | } 93 | 94 | _current.IsReturnRequested = true; 95 | _current.ReturnType = type; 96 | return _current.Label; 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /test/MethodResolution.Tests.ps1: -------------------------------------------------------------------------------- 1 | using namespace System 2 | using namespace System.Management.Automation 3 | using namespace System.Management.Automation.Language 4 | 5 | $moduleName = 'PSLambda' 6 | $manifestPath = "$PSScriptRoot\..\Release\$moduleName\*\$moduleName.psd1" 7 | 8 | Import-Module $manifestPath -Force 9 | 10 | Describe 'Method resolution tests' { 11 | It 'can resolve generic parameters from usage' { 12 | $delegate = New-PSDelegate { 13 | $ExecutionContext.InvokeProvider.ChildItem.Get('function:', $true). 14 | Select{ $pso => $pso.BaseObject }. 15 | OfType([g[System.Management.Automation.FunctionInfo]]). 16 | FirstOrDefault{ $f => $f.Name.Equals('TabExpansion2') } 17 | } 18 | 19 | $delegate.Invoke().Name | Should -Be TabExpansion2 20 | } 21 | 22 | It 'can resolve extension methods other than Linq' { 23 | $sb = [scriptblock]::Create(' 24 | using namespace System.Threading.Tasks 25 | 26 | New-PSDelegate { 27 | $task = [Task]::FromResult([Task]::Run{ return "testing" }) 28 | return $task.Unwrap() 29 | }') 30 | 31 | $delegate = $sb.Invoke() 32 | $delegate.Invoke().GetAwaiter().GetResult() | Should -Be testing 33 | } 34 | 35 | It 'can pass variables by ref' { 36 | $delegate = New-PSDelegate { 37 | [Token[]] $tokens = $null 38 | [ParseError[]] $errors = $null 39 | $ast = [Parser]::ParseInput('{', $tokens, $errors) 40 | return [Tuple]::Create($ast, $tokens, $errors) 41 | } 42 | 43 | $result = $delegate.Invoke() 44 | $result.Item1 | Should -BeOfType System.Management.Automation.Language.Ast 45 | $result.Item2.GetType() | Should -Be ([Token[]]) 46 | $result.Item2.Length | Should -Be 2 47 | $result.Item3.GetType() | Should -Be ([ParseError[]]) 48 | $result.Item3.Length | Should -Be 1 49 | } 50 | 51 | It 'can convert delegates to other than Func/Action ' { 52 | $delegate = New-PSDelegate { 53 | $list = [System.Collections.Generic.List[int]]::new(0..10) 54 | return $list.ConvertAll{ $i => $i.ToString() } 55 | } 56 | 57 | $delegate.Invoke().GetType() | Should -Be ([System.Collections.Generic.List[string]]) 58 | } 59 | 60 | It 'can resolve many generic parameters' { 61 | $delegate = New-PSDelegate { 62 | [Tuple]::Create( 63 | 10, 64 | [Exception]::new(), 65 | 'string', 66 | $Host, 67 | [runspace]::DefaultRunspace, 68 | [ConsoleColor]::Black, 69 | @{}). 70 | ToValueTuple() 71 | } 72 | 73 | $delegate.Invoke().GetType() | 74 | Should -Be ( 75 | [ValueTuple[int, exception, string, Host.PSHost, runspace, ConsoleColor, hashtable]]) 76 | } 77 | 78 | It 'throws the correct message when a member does not exist' { 79 | $expectedMsg = 80 | "'System.String' does not contain a definition for 'RandomName' " + 81 | "and no extension method 'RandomName' accepting a first argument " + 82 | "of type 'System.String' could be found." 83 | 84 | { New-PSDelegate { ''.RandomName() }} | Should -Throw $expectedMsg 85 | } 86 | 87 | It 'throws the correct message when method arguments do not match' { 88 | $expectedMsg = 89 | "'System.String' does not contain a definition for a method " + 90 | "named 'Compare' that takes the specified arguments." 91 | 92 | { New-PSDelegate { [string]::Compare() }} | Should -Throw $expectedMsg 93 | 94 | $expectedMsg = 95 | "'System.Management.Automation.Host.PSHostUserInterface' does not " + 96 | "contain a definition for a method named 'WriteLine' that takes the " + 97 | "specified arguments." 98 | 99 | { New-PSDelegate { $Host.UI.WriteLine(10) }} | Should -Throw $expectedMsg 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /test/TryCatchFinally.Tests.ps1: -------------------------------------------------------------------------------- 1 | $moduleName = 'PSLambda' 2 | $manifestPath = "$PSScriptRoot\..\Release\$moduleName\*\$moduleName.psd1" 3 | 4 | Import-Module $manifestPath -Force 5 | 6 | Describe 'try/catch/finally tests' { 7 | It 'catch all' { 8 | $delegate = New-PSDelegate { 9 | $didFail = $false 10 | try { 11 | throw 12 | } catch { 13 | $didFail = $true 14 | } 15 | 16 | return $didFail 17 | } 18 | 19 | $delegate.Invoke() | Should -Be $true 20 | } 21 | 22 | It 'catch without matching type throws' { 23 | $delegate = New-PSDelegate { 24 | $didFail = $false 25 | try { 26 | throw 27 | } catch [InvalidOperationException] { 28 | $didFail = $true 29 | } 30 | 31 | return $didFail 32 | } 33 | 34 | { $delegate.Invoke() } | Should -Throw 'ScriptHalted' 35 | } 36 | 37 | It 'catch with filter can does not throw' { 38 | $delegate = New-PSDelegate { 39 | $didFail = $false 40 | try { 41 | throw [InvalidOperationException]::new() 42 | } catch [InvalidOperationException] { 43 | $didFail = $true 44 | } 45 | 46 | return $didFail 47 | } 48 | 49 | $delegate.Invoke() | Should -Be $true 50 | } 51 | 52 | It 'catch with multiple filters' { 53 | $delegate = New-PSDelegate { 54 | $catch = 'none' 55 | try { 56 | throw [InvalidOperationException]::new() 57 | } catch [ArgumentException] { 58 | $catch = 'argument' 59 | } catch [InvalidOperationException] { 60 | $catch = 'invalid operation' 61 | } 62 | catch { 63 | $catch = 'catch all' 64 | } 65 | 66 | return $catch 67 | } 68 | 69 | $delegate.Invoke() | Should -Be 'invalid operation' 70 | } 71 | 72 | It 'finally fires when thrown with no catch' { 73 | $didFail = $false 74 | $delegate = New-PSDelegate { 75 | try { 76 | throw 77 | } finally { 78 | $didFail = $true 79 | } 80 | } 81 | 82 | { $delegate.Invoke() } | Should -Throw ScriptHalted 83 | $didFail | Should -Be $true 84 | } 85 | 86 | It 'finally fires when caught' { 87 | $didFire = $false 88 | $delegate = New-PSDelegate { 89 | try { 90 | throw 91 | } catch { 92 | return 93 | } finally { 94 | $didFire = $true 95 | } 96 | } 97 | 98 | $delegate.Invoke() 99 | $didFire | Should -Be $true 100 | } 101 | 102 | It 'finally fires with no catch block' { 103 | $didFire = $false 104 | $delegate = New-PSDelegate { 105 | try { 106 | return 107 | } finally { 108 | $didFire = $true 109 | } 110 | } 111 | 112 | $delegate.Invoke() 113 | $didFire | Should -Be $true 114 | } 115 | 116 | It 'assigns $_ in catch all' { 117 | $delegate = New-PSDelegate { 118 | try { 119 | throw [InvalidOperationException]::new() 120 | } catch { 121 | return 'caught "{0}"' -f $PSItem.GetType().FullName 122 | } 123 | 124 | return 'failed' 125 | } 126 | 127 | $delegate.Invoke() | Should -Be 'caught "System.InvalidOperationException"' 128 | } 129 | 130 | It 'strongly types $_ in explicitly typed catch' { 131 | $delegate = New-PSDelegate { 132 | try { 133 | throw [System.Management.Automation.RuntimeException]::new() 134 | } catch [System.Management.Automation.RuntimeException] { 135 | return $PSItem.ErrorRecord 136 | } 137 | 138 | return default([System.Management.Automation.ErrorRecord]) 139 | } 140 | 141 | $record = $delegate.Invoke() 142 | $record | Should -BeOfType System.Management.Automation.ErrorRecord 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/PSLambda/DelegateTypeConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Management.Automation; 5 | using System.Management.Automation.Language; 6 | 7 | namespace PSLambda 8 | { 9 | /// 10 | /// Provides conversion from to any type. 11 | /// 12 | public class DelegateTypeConverter : PSTypeConverter 13 | { 14 | private static readonly Dictionary> s_delegateCache = new Dictionary>(); 15 | 16 | private static readonly object s_syncObject = new object(); 17 | 18 | /// 19 | /// Determines if a object can be converted to a specific type. 20 | /// 21 | /// The value to convert. 22 | /// The type to conver to. 23 | /// A value indicating whether the object can be converted. 24 | public override bool CanConvertFrom(object sourceValue, Type destinationType) 25 | { 26 | return sourceValue is PSDelegate && typeof(Delegate).IsAssignableFrom(destinationType); 27 | } 28 | 29 | /// 30 | /// Determines if a object can be converted to a specific type. 31 | /// 32 | /// The value to convert. 33 | /// The type to conver to. 34 | /// A value indicating whether the object can be converted. 35 | public override bool CanConvertTo(object sourceValue, Type destinationType) 36 | { 37 | return sourceValue is PSDelegate && typeof(Delegate).IsAssignableFrom(destinationType); 38 | } 39 | 40 | /// 41 | /// Converts a object to a type. 42 | /// 43 | /// The to convert. 44 | /// 45 | /// The type inheriting from to convert to. 46 | /// 47 | /// The parameter is not used. 48 | /// The parameter is not used. 49 | /// The converted object. 50 | public override object ConvertFrom(object sourceValue, Type destinationType, IFormatProvider formatProvider, bool ignoreCase) 51 | { 52 | return ConvertToDelegate((PSDelegate)sourceValue, destinationType); 53 | } 54 | 55 | /// 56 | /// Converts a object to a type. 57 | /// 58 | /// The to convert. 59 | /// 60 | /// The type inheriting from to convert to. 61 | /// 62 | /// The parameter is not used. 63 | /// The parameter is not used. 64 | /// The converted object. 65 | public override object ConvertTo(object sourceValue, Type destinationType, IFormatProvider formatProvider, bool ignoreCase) 66 | { 67 | return ConvertToDelegate((PSDelegate)sourceValue, destinationType); 68 | } 69 | 70 | private Delegate ConvertToDelegate(PSDelegate psDelegate, Type destinationType) 71 | { 72 | lock (s_syncObject) 73 | { 74 | if (!s_delegateCache.TryGetValue(psDelegate, out Dictionary cacheEntry)) 75 | { 76 | cacheEntry = new Dictionary(); 77 | s_delegateCache.Add(psDelegate, cacheEntry); 78 | } 79 | 80 | if (!cacheEntry.TryGetValue(destinationType, out Delegate compiledDelegate)) 81 | { 82 | compiledDelegate = CompileVisitor.CompileAst( 83 | psDelegate.EngineIntrinsics, 84 | (ScriptBlockAst)psDelegate.ScriptBlock.Ast, 85 | psDelegate.Locals.Values.ToArray(), 86 | destinationType); 87 | 88 | cacheEntry.Add(destinationType, compiledDelegate); 89 | } 90 | 91 | return compiledDelegate; 92 | } 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/PSLambda/SpecialVariables.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq.Expressions; 4 | using System.Management.Automation; 5 | using System.Management.Automation.Host; 6 | 7 | namespace PSLambda 8 | { 9 | /// 10 | /// Provides information about special variables created by the PowerShell engine. 11 | /// 12 | internal static class SpecialVariables 13 | { 14 | /// 15 | /// Contains the names and types of variables that should be available from any scope. 16 | /// 17 | internal static readonly Dictionary AllScope = new Dictionary(StringComparer.OrdinalIgnoreCase) 18 | { 19 | { "?", typeof(bool) }, 20 | { "ExecutionContext", typeof(EngineIntrinsics) }, 21 | { "Home", typeof(string) }, 22 | { "Host", typeof(PSHost) }, 23 | { "PID", typeof(int) }, 24 | { "PSCulture", typeof(string) }, 25 | { "PSHome", typeof(string) }, 26 | { "PSUICulture", typeof(string) }, 27 | { "PSVersionTable", typeof(System.Collections.Hashtable) }, 28 | { "PSEdition", typeof(string) }, 29 | { "ShellId", typeof(string) }, 30 | { "MaximumHistoryCount", typeof(int) }, 31 | }; 32 | 33 | /// 34 | /// Provides the names and constant objects for variables that 35 | /// should are language features like true. 36 | /// 37 | internal static readonly Dictionary Constants = new Dictionary(StringComparer.OrdinalIgnoreCase) 38 | { 39 | { Strings.TrueVariableName, Expression.Constant(true, typeof(bool)) }, 40 | { Strings.FalseVariableName, Expression.Constant(false, typeof(bool)) }, 41 | { Strings.NullVariableName, Expression.Constant(null) }, 42 | }; 43 | 44 | /// 45 | /// Provides the names of variables that should be ignored when aquiring local 46 | /// scope variables. 47 | /// 48 | internal static readonly HashSet IgnoreLocal = new HashSet(StringComparer.OrdinalIgnoreCase) 49 | { 50 | "?", 51 | "ExecutionContext", 52 | "Home", 53 | "Host", 54 | "PID", 55 | "PSCulture", 56 | "PSHome", 57 | "PSUICulture", 58 | "PSVersionTable", 59 | "PSEdition", 60 | "ShellId", 61 | "MaximumHistoryCount", 62 | "MyInvocation", 63 | "OFS", 64 | "OutputEncoding", 65 | "VerboseHelpErrors", 66 | "LogEngineHealthEvent", 67 | "LogEngineLifecycleEvent", 68 | "LogCommandHealthEvent", 69 | "LogCommandLifecycleEvent", 70 | "LogProviderHealthEvent", 71 | "LogProviderLifecycleEvent", 72 | "LogSettingsEvent", 73 | "PSLogUserData", 74 | "NestedPromptLevel", 75 | "CurrentlyExecutingCommand", 76 | "PSBoundParameters", 77 | "Matches", 78 | "LASTEXITCODE", 79 | "PSDebugContext", 80 | "StackTrace", 81 | "^", 82 | "$", 83 | "?", 84 | "args", 85 | "input", 86 | "error", 87 | "PSEmailServer", 88 | "PSDefaultParameterValues", 89 | "PSScriptRoot", 90 | "PSCommandPath", 91 | "PSSenderInfo", 92 | "foreach", 93 | "switch", 94 | "PWD", 95 | "null", 96 | "true", 97 | "false", 98 | "PSModuleAutoLoadingPreference", 99 | "IsLinux", 100 | "IsMacOS", 101 | "IsWindows", 102 | "IsCoreCLR", 103 | "DebugPreference", 104 | "WarningPreference", 105 | "ErrorActionPreference", 106 | "InformationPreference", 107 | "ProgressPreference", 108 | "VerbosePreference", 109 | "WhatIfPreference", 110 | "ConfirmPreference", 111 | "ErrorView", 112 | "PSSessionConfigurationName", 113 | "PSSessionApplicationName", 114 | "ExecutionContext", 115 | "Host", 116 | "PID", 117 | "PSCulture", 118 | "PSHOME", 119 | "PSUICulture", 120 | "PSVersionTable", 121 | "PSEdition", 122 | "ShellId", 123 | }; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /tools/GetDotNet.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param( 3 | [Parameter()] 4 | [ValidateNotNullOrEmpty()] 5 | [string] $Version = '3.0.100', 6 | 7 | [Parameter()] 8 | [string] $PreviewVersion = 'preview8-013656', 9 | 10 | [Parameter()] 11 | [switch] $Unix 12 | ) 13 | begin { 14 | function TestDotNetVersion([System.Management.Automation.CommandInfo] $command) { 15 | $existingVersion, $preview = (& $command --version) -split '-',2 16 | if (-not $existingVersion) { 17 | return $false 18 | } 19 | 20 | $existingVersion = $existingVersion -as [version] 21 | $targetVersion = $Version -as [version] 22 | if ($existingVersion -gt $targetVersion) { 23 | return $true 24 | } 25 | 26 | if ($existingVersion -lt $targetVersion) { 27 | return $false 28 | } 29 | 30 | if ($PreviewVersion) { 31 | if (-not $preview) { 32 | return $true 33 | } 34 | 35 | return $preview -ge $PreviewVersion 36 | } elseif ($preview) { 37 | return $false 38 | } 39 | 40 | return $true 41 | } 42 | 43 | $targetFolder = "$PSScriptRoot/dotnet" 44 | $executableName = 'dotnet.exe' 45 | if ($Unix.IsPresent) { 46 | $executableName = 'dotnet' 47 | } 48 | } 49 | end { 50 | $versionText = $Version 51 | if ($PreviewVersion) { 52 | $versionText = $Version, $PreviewVersion -join '-' 53 | } 54 | 55 | if (($dotnet = Get-Command $executableName -ea 0) -and (TestDotNetVersion $dotnet)) { 56 | return $dotnet 57 | } 58 | 59 | $localAppData = [Environment]::GetFolderPath( 60 | [Environment+SpecialFolder]::LocalApplicationData, 61 | [Environment+SpecialFolderOption]::Create) 62 | 63 | $localAppData = Join-Path $localAppData -ChildPath 'Microsoft/dotnet' 64 | if ($dotnet = Get-Command $localAppData/$executableName -ea 0) { 65 | if (TestDotNetVersion $dotnet) { 66 | return $dotnet 67 | } 68 | 69 | # If dotnet is already installed to local AppData but is not the version we are expecting, 70 | # don't remove it. Instead try to install to the project directory (and check for an 71 | # existing one). 72 | if ($dotnet = Get-Command $targetFolder/$executableName -ea 0) { 73 | if (TestDotNetVersion $dotnet) { 74 | return $dotnet 75 | } 76 | 77 | Write-Host -ForegroundColor Yellow Found dotnet $found but require $Version, replacing... 78 | Remove-Item $targetFolder -Recurse 79 | $dotnet = $null 80 | } 81 | } else { 82 | # The Core SDK isn't already installed to local AppData, so install there. 83 | $targetFolder = $PSCmdlet.GetUnresolvedProviderPathFromPSPath($localAppData) 84 | if (-not (Test-Path $targetFolder)) { 85 | $null = New-Item $targetFolder -ItemType Directory -Force -ErrorAction Stop 86 | } 87 | } 88 | 89 | Write-Host -ForegroundColor Green Downloading dotnet version $Version 90 | try { 91 | $installerPath = $null 92 | if ($Unix.IsPresent) { 93 | $uri = "https://dot.net/v1/dotnet-install.sh" 94 | $installerPath = [System.IO.Path]::GetTempPath() + 'dotnet-install.sh' 95 | $scriptText = [System.Net.WebClient]::new().DownloadString($uri) 96 | Set-Content $installerPath -Value $scriptText -Encoding UTF8 97 | $installer = { 98 | param($Version, $InstallDir) 99 | end { 100 | & (Get-Command bash) $installerPath -Version $Version -InstallDir $InstallDir 101 | } 102 | } 103 | } else { 104 | $uri = "https://dot.net/v1/dotnet-install.ps1" 105 | $scriptText = [System.Net.WebClient]::new().DownloadString($uri) 106 | 107 | # Stop the official script from hard exiting at times... 108 | $safeScriptText = $scriptText -replace 'exit 0', 'return' 109 | $installer = [scriptblock]::Create($safeScriptText) 110 | } 111 | 112 | $null = & $installer -Version $versionText -InstallDir $targetFolder 113 | } finally { 114 | if (-not [string]::IsNullOrEmpty($installerPath) -and (Test-Path $installerPath)) { 115 | Remove-Item $installerPath -ErrorAction Ignore 116 | } 117 | } 118 | 119 | $found = Get-Command $targetFolder/$executableName 120 | if (-not (TestDotNetVersion $found)) { 121 | throw 'The dotnet CLI was downloaded without errors but appears to be the incorrect version.' 122 | } 123 | 124 | return $found 125 | } 126 | -------------------------------------------------------------------------------- /src/PSLambda/PSDelegate.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Management.Automation; 5 | using System.Management.Automation.Language; 6 | 7 | namespace PSLambda 8 | { 9 | /// 10 | /// Represents an that is not yet compiled but can be 11 | /// converted implicitly by the PowerShell engine. 12 | /// 13 | public sealed class PSDelegate 14 | { 15 | private Delegate _defaultDelegate; 16 | 17 | /// 18 | /// Initializes a new instance of the class. 19 | /// 20 | /// The to compile. 21 | public PSDelegate(ScriptBlock scriptBlock) 22 | { 23 | using (var pwsh = PowerShell.Create(RunspaceMode.CurrentRunspace)) 24 | { 25 | Locals = 26 | pwsh.AddCommand("Microsoft.PowerShell.Utility\\Get-Variable") 27 | .AddParameter("Scope", 0) 28 | .Invoke() 29 | .Where(v => !SpecialVariables.IgnoreLocal.Contains(v.Name)) 30 | .ToDictionary(v => v.Name); 31 | 32 | pwsh.Commands.Clear(); 33 | 34 | EngineIntrinsics = 35 | pwsh.AddCommand("Microsoft.PowerShell.Utility\\Get-Variable") 36 | .AddParameter("Name", "ExecutionContext") 37 | .AddParameter("ValueOnly", true) 38 | .Invoke() 39 | .First(); 40 | } 41 | 42 | ScriptBlock = scriptBlock; 43 | } 44 | 45 | /// 46 | /// Initializes a new instance of the class. 47 | /// 48 | /// The to compile. 49 | /// 50 | /// The instance for the current runspace. 51 | /// 52 | /// 53 | /// Any objects that are local to the 54 | /// current scope with the exception of AllScope variables. 55 | /// 56 | internal PSDelegate(ScriptBlock scriptBlock, EngineIntrinsics engine, Dictionary locals) 57 | { 58 | Locals = locals; 59 | EngineIntrinsics = engine; 60 | ScriptBlock = scriptBlock; 61 | } 62 | 63 | /// 64 | /// Gets the ScriptBlock represented by the delegate. 65 | /// 66 | public ScriptBlock ScriptBlock { get; } 67 | 68 | /// 69 | /// Gets the from the origin 70 | /// . 71 | /// 72 | internal EngineIntrinsics EngineIntrinsics { get; } 73 | 74 | /// 75 | /// Gets the objects from origin SessionStateScope. 76 | /// 77 | internal Dictionary Locals { get; } 78 | 79 | /// 80 | /// Gets the default that is used for the 81 | /// method. 82 | /// 83 | internal Delegate DefaultDelegate 84 | { 85 | get 86 | { 87 | if (_defaultDelegate != null) 88 | { 89 | return _defaultDelegate; 90 | } 91 | 92 | return _defaultDelegate = CreateDefaultDelegate(); 93 | } 94 | } 95 | 96 | /// 97 | /// Invokes the compiled represented by this object. If the 98 | /// has not yet been compiled, it will be compiled prior to 99 | /// invocation. 100 | /// 101 | /// Arguments to pass to the . 102 | /// The result returned by the . 103 | public object Invoke(params object[] arguments) 104 | { 105 | return DefaultDelegate.DynamicInvoke(arguments); 106 | } 107 | 108 | private Delegate CreateDefaultDelegate() 109 | { 110 | return CompileVisitor.CompileAst( 111 | EngineIntrinsics, 112 | (ScriptBlockAst)ScriptBlock.Ast, 113 | Locals.Values.ToArray()); 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/PSLambda/ParseErrorWriter.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Management.Automation; 3 | using System.Management.Automation.Language; 4 | 5 | namespace PSLambda 6 | { 7 | /// 8 | /// Provides uniform writing and management of objects. 9 | /// 10 | internal abstract class ParseErrorWriter : IParseErrorWriter 11 | { 12 | /// 13 | /// Initializes a new instance of the class. 14 | /// 15 | protected ParseErrorWriter() 16 | { 17 | } 18 | 19 | /// 20 | /// Creates an instance of the default . This 21 | /// error writer will report full error messages and will throw after a maximum 22 | /// of three errors. 23 | /// 24 | /// The defualt . 25 | public static ParseErrorWriter CreateDefault() => new DefaultParseErrorWriter(); 26 | 27 | /// 28 | /// Creates an instance of the null . This 29 | /// error writer will not report any error messages and will throw immediately 30 | /// after the first error has been reported. 31 | /// 32 | /// The null . 33 | public static ParseErrorWriter CreateNull() => new NullParseErrorWriter(); 34 | 35 | /// 36 | /// Reports a . Handling of the 37 | /// may defer between implementations. 38 | /// 39 | /// 40 | /// The of the error being reported. 41 | /// 42 | /// The id of the error. 43 | /// 44 | /// The message to display in the 45 | /// when parsing is completed. 46 | /// 47 | public virtual void ReportParseError(IScriptExtent extent, string id, string message) 48 | { 49 | } 50 | 51 | /// 52 | /// Throws if the error limit has been hit. Error limit may vary 53 | /// between implementations. 54 | /// 55 | public virtual void ThrowIfErrorLimitHit() 56 | { 57 | } 58 | 59 | /// 60 | /// Throws if any error has been reported. 61 | /// 62 | public virtual void ThrowIfAnyErrors() 63 | { 64 | } 65 | 66 | private class NullParseErrorWriter : ParseErrorWriter 67 | { 68 | private int _errorCount; 69 | 70 | public override void ReportParseError(IScriptExtent extent, string id, string message) 71 | { 72 | _errorCount++; 73 | throw new ParseException(); 74 | } 75 | 76 | public override void ThrowIfAnyErrors() 77 | { 78 | if (_errorCount > 0) 79 | { 80 | throw new ParseException(); 81 | } 82 | } 83 | 84 | public override void ThrowIfErrorLimitHit() 85 | { 86 | if (_errorCount > 0) 87 | { 88 | throw new ParseException(); 89 | } 90 | } 91 | } 92 | 93 | private class DefaultParseErrorWriter : ParseErrorWriter 94 | { 95 | private const int ErrorLimit = 3; 96 | 97 | private readonly List _errors = new List(); 98 | 99 | public override void ReportParseError(IScriptExtent extent, string id, string message) 100 | { 101 | _errors.Add( 102 | new ParseError( 103 | extent, 104 | id, 105 | message)); 106 | 107 | ThrowIfErrorLimitHit(); 108 | } 109 | 110 | public override void ThrowIfErrorLimitHit() 111 | { 112 | if (_errors.Count < ErrorLimit) 113 | { 114 | return; 115 | } 116 | 117 | throw new ParseException(_errors.ToArray()); 118 | } 119 | 120 | public override void ThrowIfAnyErrors() 121 | { 122 | if (_errors.Count < 1) 123 | { 124 | return; 125 | } 126 | 127 | throw new ParseException(_errors.ToArray()); 128 | } 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | tools/opencover 5 | tools/dotnet 6 | tools/ResGen 7 | src/PSLambda/gen 8 | 9 | # User-specific files 10 | *.suo 11 | *.user 12 | *.userosscache 13 | *.sln.docstates 14 | 15 | # User-specific files (MonoDevelop/Xamarin Studio) 16 | *.userprefs 17 | 18 | # Build results 19 | [Dd]ebug/ 20 | [Dd]ebugPublic/ 21 | [Rr]elease/ 22 | [Rr]eleases/ 23 | x64/ 24 | x86/ 25 | bld/ 26 | [Bb]in/ 27 | [Oo]bj/ 28 | [Ll]og/ 29 | 30 | # Visual Studio 2015 cache/options directory 31 | .vs/ 32 | # Uncomment if you have tasks that create the project's static files in wwwroot 33 | #wwwroot/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # DNX 49 | project.lock.json 50 | project.fragment.lock.json 51 | artifacts/ 52 | 53 | *_i.c 54 | *_p.c 55 | *_i.h 56 | *.ilk 57 | *.meta 58 | *.obj 59 | *.pch 60 | *.pdb 61 | *.pgc 62 | *.pgd 63 | *.rsp 64 | *.sbr 65 | *.tlb 66 | *.tli 67 | *.tlh 68 | *.tmp 69 | *.tmp_proj 70 | *.log 71 | *.vspscc 72 | *.vssscc 73 | .builds 74 | *.pidb 75 | *.svclog 76 | *.scc 77 | 78 | # Chutzpah Test files 79 | _Chutzpah* 80 | 81 | # Visual C++ cache files 82 | ipch/ 83 | *.aps 84 | *.ncb 85 | *.opendb 86 | *.opensdf 87 | *.sdf 88 | *.cachefile 89 | *.VC.db 90 | *.VC.VC.opendb 91 | 92 | # Visual Studio profiler 93 | *.psess 94 | *.vsp 95 | *.vspx 96 | *.sap 97 | 98 | # TFS 2012 Local Workspace 99 | $tf/ 100 | 101 | # Guidance Automation Toolkit 102 | *.gpState 103 | 104 | # ReSharper is a .NET coding add-in 105 | _ReSharper*/ 106 | *.[Rr]e[Ss]harper 107 | *.DotSettings.user 108 | 109 | # JustCode is a .NET coding add-in 110 | .JustCode 111 | 112 | # TeamCity is a build add-in 113 | _TeamCity* 114 | 115 | # DotCover is a Code Coverage Tool 116 | *.dotCover 117 | 118 | # NCrunch 119 | _NCrunch_* 120 | .*crunch*.local.xml 121 | nCrunchTemp_* 122 | 123 | # MightyMoose 124 | *.mm.* 125 | AutoTest.Net/ 126 | 127 | # Web workbench (sass) 128 | .sass-cache/ 129 | 130 | # Installshield output folder 131 | [Ee]xpress/ 132 | 133 | # DocProject is a documentation generator add-in 134 | DocProject/buildhelp/ 135 | DocProject/Help/*.HxT 136 | DocProject/Help/*.HxC 137 | DocProject/Help/*.hhc 138 | DocProject/Help/*.hhk 139 | DocProject/Help/*.hhp 140 | DocProject/Help/Html2 141 | DocProject/Help/html 142 | 143 | # Click-Once directory 144 | publish/ 145 | 146 | # Publish Web Output 147 | *.[Pp]ublish.xml 148 | *.azurePubxml 149 | # TODO: Comment the next line if you want to checkin your web deploy settings 150 | # but database connection strings (with potential passwords) will be unencrypted 151 | #*.pubxml 152 | *.publishproj 153 | 154 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 155 | # checkin your Azure Web App publish settings, but sensitive information contained 156 | # in these scripts will be unencrypted 157 | PublishScripts/ 158 | 159 | # NuGet Packages 160 | *.nupkg 161 | # The packages folder can be ignored because of Package Restore 162 | **/packages/* 163 | # except build/, which is used as an MSBuild target. 164 | !**/packages/build/ 165 | # Uncomment if necessary however generally it will be regenerated when needed 166 | #!**/packages/repositories.config 167 | # NuGet v3's project.json files produces more ignoreable files 168 | *.nuget.props 169 | *.nuget.targets 170 | 171 | # Microsoft Azure Build Output 172 | csx/ 173 | *.build.csdef 174 | 175 | # Microsoft Azure Emulator 176 | ecf/ 177 | rcf/ 178 | 179 | # Windows Store app package directories and files 180 | AppPackages/ 181 | BundleArtifacts/ 182 | Package.StoreAssociation.xml 183 | _pkginfo.txt 184 | 185 | # Visual Studio cache files 186 | # files ending in .cache can be ignored 187 | *.[Cc]ache 188 | # but keep track of directories ending in .cache 189 | !*.[Cc]ache/ 190 | 191 | # Others 192 | ClientBin/ 193 | ~$* 194 | *~ 195 | *.dbmdl 196 | *.dbproj.schemaview 197 | *.jfm 198 | *.pfx 199 | *.publishsettings 200 | node_modules/ 201 | orleans.codegen.cs 202 | 203 | # Since there are multiple workflows, uncomment next line to ignore bower_components 204 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 205 | #bower_components/ 206 | 207 | # RIA/Silverlight projects 208 | Generated_Code/ 209 | 210 | # Backup & report files from converting an old project file 211 | # to a newer Visual Studio version. Backup files are not needed, 212 | # because we have git ;-) 213 | _UpgradeReport_Files/ 214 | Backup*/ 215 | UpgradeLog*.XML 216 | UpgradeLog*.htm 217 | 218 | # SQL Server files 219 | *.mdf 220 | *.ldf 221 | 222 | # Business Intelligence projects 223 | *.rdl.data 224 | *.bim.layout 225 | *.bim_*.settings 226 | 227 | # Microsoft Fakes 228 | FakesAssemblies/ 229 | 230 | # GhostDoc plugin setting file 231 | *.GhostDoc.xml 232 | 233 | # Node.js Tools for Visual Studio 234 | .ntvs_analysis.dat 235 | 236 | # Visual Studio 6 build log 237 | *.plg 238 | 239 | # Visual Studio 6 workspace options file 240 | *.opt 241 | 242 | # Visual Studio LightSwitch build output 243 | **/*.HTMLClient/GeneratedArtifacts 244 | **/*.DesktopClient/GeneratedArtifacts 245 | **/*.DesktopClient/ModelManifest.xml 246 | **/*.Server/GeneratedArtifacts 247 | **/*.Server/ModelManifest.xml 248 | _Pvt_Extensions 249 | 250 | # Paket dependency manager 251 | .paket/paket.exe 252 | paket-files/ 253 | 254 | # FAKE - F# Make 255 | .fake/ 256 | 257 | # JetBrains Rider 258 | .idea/ 259 | *.sln.iml 260 | 261 | # CodeRush 262 | .cr/ 263 | 264 | # Python Tools for Visual Studio (PTVS) 265 | __pycache__/ 266 | *.pyc 267 | -------------------------------------------------------------------------------- /src/PSLambda/ParseWriterExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Management.Automation.Language; 4 | 5 | namespace PSLambda 6 | { 7 | /// 8 | /// Provides utility methods for reporting specific types of 9 | /// objects. 10 | /// 11 | internal static class ParseWriterExtensions 12 | { 13 | /// 14 | /// Generate a citing an unsupported type. 15 | /// 16 | /// The that should issue the report. 17 | /// The that is not supported. 18 | internal static void ReportNotSupportedAstType( 19 | this IParseErrorWriter writer, 20 | Ast ast) 21 | { 22 | writer.ReportNotSupported( 23 | ast.Extent, 24 | ast.GetType().Name, 25 | string.Format( 26 | CultureInfo.CurrentCulture, 27 | ErrorStrings.AstNotSupported, 28 | ast.GetType().Name)); 29 | } 30 | 31 | /// 32 | /// Generate a citing an unsupported element. 33 | /// 34 | /// The that should issue the report. 35 | /// The of the unsupported element. 36 | /// The ID to be shown in the . 37 | /// The message to be shown in the . 38 | internal static void ReportNotSupported( 39 | this IParseErrorWriter writer, 40 | IScriptExtent extent, 41 | string id, 42 | string message) 43 | { 44 | writer.ReportParseError( 45 | extent, 46 | string.Format( 47 | CultureInfo.CurrentCulture, 48 | "{0}.{1}", 49 | nameof(ErrorStrings.ElementNotSupported), 50 | id), 51 | string.Format( 52 | CultureInfo.CurrentCulture, 53 | ErrorStrings.ElementNotSupported, 54 | message)); 55 | } 56 | 57 | /// 58 | /// Generate a citing a missing element. 59 | /// 60 | /// The that should issue the report. 61 | /// The of the missing element. 62 | /// The ID to be shown in the . 63 | /// The message to be shown in the . 64 | internal static void ReportMissing( 65 | this IParseErrorWriter writer, 66 | IScriptExtent extent, 67 | string id, 68 | string message) 69 | { 70 | writer.ReportParseError( 71 | extent, 72 | string.Format( 73 | CultureInfo.CurrentCulture, 74 | "{0}.{1}", 75 | nameof(ErrorStrings.ElementMissing), 76 | id), 77 | string.Format( 78 | CultureInfo.CurrentCulture, 79 | ErrorStrings.ElementMissing, 80 | message)); 81 | } 82 | 83 | /// 84 | /// Generate a . 85 | /// 86 | /// The that should issue the report. 87 | /// The of element in error. 88 | /// The ID to be shown in the . 89 | /// The message to be shown in the . 90 | internal static void ReportParseError( 91 | this IParseErrorWriter writer, 92 | IScriptExtent extent, 93 | string id = nameof(ErrorStrings.CompileTimeParseError), 94 | string message = "") 95 | { 96 | if (string.IsNullOrWhiteSpace(message)) 97 | { 98 | message = ErrorStrings.CompileTimeParseError; 99 | } 100 | 101 | writer.ReportParseError(extent, id, message); 102 | } 103 | 104 | /// 105 | /// Generate a . 106 | /// 107 | /// The that should issue the report. 108 | /// The of element in error. 109 | /// 110 | /// The exception housing the message to be shown in the . 111 | /// 112 | /// The ID to be shown in the . 113 | internal static void ReportParseError( 114 | this IParseErrorWriter writer, 115 | IScriptExtent extent, 116 | Exception exception, 117 | string id = "") 118 | { 119 | if (string.IsNullOrEmpty(id)) 120 | { 121 | id = exception.GetType().Name; 122 | } 123 | 124 | writer.ReportParseError( 125 | extent, 126 | id, 127 | exception.Message); 128 | } 129 | 130 | /// 131 | /// Generate a . 132 | /// 133 | /// The that should issue the report. 134 | /// The of element in error. 135 | internal static void ReportNonConstantTypeAs( 136 | this IParseErrorWriter writer, 137 | IScriptExtent extent) 138 | { 139 | writer.ReportNotSupported( 140 | extent, 141 | nameof(ErrorStrings.NonConstantTypeAs), 142 | ErrorStrings.NonConstantTypeAs); 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/PSLambda/resources/CompilerStrings.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | ScriptHalted 122 | 123 | 124 | Begin Block 125 | 126 | 127 | Process Block 128 | 129 | 130 | End Block 131 | 132 | 133 | -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guidelines 2 | 3 | We welcome many kinds of community contributions to this project! Whether it's a feature implementation, 4 | bug fix, or a good idea, please create an issue so that we can discuss it. It is not necessary to create an 5 | issue before sending a pull request but it may speed up the process if we can discuss your idea before 6 | you start implementing it. 7 | 8 | Because this project exposes a couple different public APIs, we must be very mindful of any potential breaking 9 | changes. Some contributions may not be accepted if they risk introducing breaking changes or if they 10 | don't match the goals of the project. The core maintainer team has the right of final approval over 11 | any contribution to this project. However, we are very happy to hear community feedback on any decision 12 | so that we can ensure we are solving the right problems in the right way. 13 | 14 | ## Ways to Contribute 15 | 16 | - File a bug or feature request as an [issue](https://github.com/SeeminglyScience/PSLambda/issues) 17 | - Comment on existing issues to give your feedback on how they should be fixed/implemented 18 | - Contribute a bug fix or feature implementation by submitting a pull request 19 | - Contribute more unit tests for feature areas that lack good coverage 20 | - Review the pull requests that others submit to ensure they follow [established guidelines](#pull-request-guidelines) 21 | 22 | ## Code Contribution Guidelines 23 | 24 | Here's a high level list of guidelines to follow to ensure your code contribution is accepted: 25 | 26 | - Follow established guidelines for coding style and design 27 | - Follow established guidelines for commit hygiene 28 | - Write unit tests to validate new features and bug fixes 29 | - Ensure that the 'Release' build and unit tests pass locally 30 | - Respond to all review feedback and final commit cleanup 31 | 32 | ### Practice Good Commit Hygiene 33 | 34 | First of all, make sure you are practicing [good commit hygiene](http://blog.ericbmerritt.com/2011/09/21/commit-hygiene-and-git.html) 35 | so that your commits provide a good history of the changes you are making. To be more specific: 36 | 37 | - **Write good commit messages** 38 | 39 | Commit messages should be clearly written so that a person can look at the commit log and understand 40 | how and why a given change was made. Here is a great model commit message taken from a [blog post 41 | by Tim Pope](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html): 42 | 43 | Capitalized, short (50 chars or less) summary 44 | 45 | More detailed explanatory text, if necessary. Wrap it to about 72 46 | characters or so. In some contexts, the first line is treated as the 47 | subject of an email and the rest of the text as the body. The blank 48 | line separating the summary from the body is critical (unless you omit 49 | the body entirely); tools like rebase can get confused if you run the 50 | two together. 51 | 52 | Write your commit message in the imperative: "Fix bug" and not "Fixed bug" 53 | or "Fixes bug." This convention matches up with commit messages generated 54 | by commands like git merge and git revert. 55 | 56 | Further paragraphs come after blank lines. 57 | 58 | - Bullet points are okay, too 59 | 60 | - Typically a hyphen or asterisk is used for the bullet, followed by a 61 | single space, with blank lines in between, but conventions vary here 62 | 63 | - Use a hanging indent 64 | 65 | A change that fixes a known bug with an issue filed should use the proper syntax so that the [issue 66 | is automatically closed](https://help.github.com/articles/closing-issues-via-commit-messages/) once 67 | your change is merged. Here's an example of what such a commit message should look like: 68 | 69 | Fix #3: Catch NullReferenceException from DoThing 70 | 71 | This change adds a try/catch block to catch a NullReferenceException that 72 | gets thrown by DoThing [...] 73 | 74 | - **Squash your commits** 75 | 76 | If you are introducing a new feature but have implemented it over multiple commits, 77 | please [squash those commits](http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html) 78 | into a single commit that contains all the changes in one place. This especially applies to any "oops" 79 | commits where a file is forgotten or a typo is being fixed. Following this approach makes it a lot easier 80 | to pull those changes to other branches or roll back the change if necessary. 81 | 82 | - **Keep individual commits for larger changes** 83 | 84 | You can certainly maintain individual commits for different phases of a big change. For example, if 85 | you want to reorganize some files before adding new functionality, have your first commit contain all 86 | of the file move changes and then the following commit can have all of the feature additions. We 87 | highly recommend this approach so that larger commits don't turn into a jumbled mess. 88 | 89 | ### Build 'Release' Before Submitting 90 | 91 | Before you send out your pull request, make sure that you have run a Release configuration build of the 92 | project and that all new and existing tests are passing. The Release configuration build ensures that 93 | all public API interfaces have been documented correctly otherwise it throws an error. We have turned 94 | on this check so that our project will always have good generated documentation. 95 | 96 | ### Follow the Pull Request Process 97 | 98 | - **Create your pull request** 99 | 100 | Use the [typical process](https://help.github.com/articles/using-pull-requests/) to send a pull request 101 | from your fork of the project. In your pull request message, please give a high-level summary of the 102 | changes that you have made so that reviewers understand the intent of the changes. You should receive 103 | initial comments within a day or two, but please feel free to ping if things are taking longer than 104 | expected. 105 | 106 | - **Respond to code review feedback** 107 | 108 | If the reviewers ask you to make changes, make them as a new commit to your branch and push them so 109 | that they are made available for a final review pass. Do not rebase the fixes just yet so that the 110 | commit hash changes don't upset GitHub's pull request UI. 111 | 112 | - **If necessary, do a final rebase** 113 | 114 | Once your final changes have been accepted, we may ask you to do a final rebase to have your commits 115 | so that they follow our commit guidelines. If specific guidance is given, please follow it when 116 | rebasing your commits. Once you do your final push, we will merge your changes! 117 | -------------------------------------------------------------------------------- /test/Loops.Tests.ps1: -------------------------------------------------------------------------------- 1 | $moduleName = 'PSLambda' 2 | $manifestPath = "$PSScriptRoot\..\Release\$moduleName\*\$moduleName.psd1" 3 | 4 | Import-Module $manifestPath -Force 5 | 6 | Describe 'basic loop functionality' { 7 | Context 'foreach statement tests' { 8 | It 'can enumerate IEnumerable<>' { 9 | $delegate = New-PSDelegate { 10 | $total = 0 11 | foreach($item in 0..10) { 12 | $total = $item + $total 13 | } 14 | 15 | return $total 16 | } 17 | 18 | $delegate.Invoke() | Should -Be 55 19 | } 20 | 21 | It 'can enumerate IDictionary' { 22 | $delegate = New-PSDelegate { 23 | $hashtable = @{ 24 | one = 'two' 25 | three = 'four' 26 | } 27 | 28 | $results = [System.Collections.Generic.List[string]]::new() 29 | foreach($item in $hashtable) { 30 | $results.Add($item.Value.ToString()) 31 | } 32 | 33 | return $results 34 | } 35 | 36 | $results = $delegate.Invoke() 37 | $results | Should -Contain two 38 | $results | Should -Contain four 39 | } 40 | 41 | It 'prioritizes IEnumerable<> over IDictionary' { 42 | $delegate = New-PSDelegate { 43 | $map = [System.Collections.Generic.Dictionary[string, int]]::new() 44 | $map.Add('test', 10) 45 | $map.Add('test2', 30) 46 | 47 | $results = [System.Collections.Generic.List[int]]::new() 48 | foreach ($item in $map) { 49 | $results.Add($item.Value) 50 | } 51 | 52 | return $results 53 | } 54 | 55 | $delegate.Invoke() | Should -Be 10, 30 56 | } 57 | 58 | It 'can enumerable IEnumerable' { 59 | $delegate = New-PSDelegate { 60 | $list = [System.Collections.ArrayList]::new() 61 | $list.Add([object]10) 62 | $list.Add('test2') 63 | 64 | $results = [System.Collections.Generic.List[string]]::new() 65 | foreach ($item in $list) { 66 | $results.Add($item.ToString()) 67 | } 68 | 69 | return $results 70 | } 71 | 72 | $delegate.Invoke() | Should -Be 10, test2 73 | } 74 | 75 | It 'throws the correct message when target is not IEnumerable' { 76 | $expectedMsg = 77 | "The foreach statement cannot operate on variables of type " + 78 | "'System.Int32' because 'System.Int32' does not contain a " + 79 | "public definition for 'GetEnumerator'" 80 | 81 | { New-PSDelegate { foreach ($a in 10) {}}} | Should -Throw $expectedMsg 82 | } 83 | } 84 | 85 | It 'for statement' { 86 | $delegate = New-PSDelegate { 87 | [int] $total = 0 88 | for ([int] $i = 0; $i -lt 10; $i++) { 89 | $total = $i + $total 90 | } 91 | 92 | return $total 93 | } 94 | 95 | $delegate.Invoke() | Should -Be 45 96 | } 97 | 98 | It 'while statement' { 99 | $delegate = New-PSDelegate { 100 | [int] $i = 0 101 | while ($i -lt 10) { 102 | $i++ 103 | } 104 | 105 | return $i 106 | } 107 | 108 | $delegate.Invoke() | Should -Be 10 109 | } 110 | 111 | It 'do while statement' { 112 | $delegate = New-PSDelegate { 113 | [int] $i = 0 114 | do { 115 | $i++ 116 | } while ($i -lt 10) 117 | 118 | return $i 119 | } 120 | 121 | $delegate.Invoke() | Should -Be 10 122 | } 123 | 124 | It 'do until statement' { 125 | $delegate = New-PSDelegate { 126 | [int] $i = 0 127 | do { 128 | $i++ 129 | } until ($i -gt 10) 130 | 131 | return $i 132 | } 133 | 134 | $delegate.Invoke() | Should -Be 11 135 | } 136 | 137 | It 'break continues after loop' { 138 | $delegate = New-PSDelegate { 139 | $i = 0 140 | $continuedAfterBreak = $false 141 | while ($i -lt 10) { 142 | $i++ 143 | if ($i -eq 5) { 144 | break 145 | $continuedAfterBreak = $true 146 | } 147 | } 148 | 149 | if ($continuedAfterBreak) { 150 | throw 'code after "break" was executed' 151 | } 152 | 153 | return $i 154 | } 155 | 156 | $delegate.Invoke() | Should -Be 5 157 | } 158 | 159 | It 'continue steps to next interation' { 160 | $delegate = New-PSDelegate { 161 | $i = 0 162 | $continuedAfterContinue = $false 163 | while ($i -lt 10) { 164 | $i++ 165 | continue 166 | $continuedAfterContinue = $true 167 | } 168 | 169 | if ($continuedAfterContinue) { 170 | throw 'code after "continue" was executed' 171 | } 172 | 173 | return $i 174 | } 175 | 176 | $delegate.Invoke() | Should -Be 10 177 | } 178 | 179 | Context 'switch statement' { 180 | It 'chooses correct value' { 181 | $hitValues = [System.Collections.Generic.List[string]]::new() 182 | $delegate = New-PSDelegate { 183 | foreach ($value in 'value1', 'value2', 'value3', 'invalid') { 184 | switch ($value) { 185 | value1 { $hitValues.Add('option1') } 186 | value2 { $hitValues.Add('option2') } 187 | value3 { $hitValues.Add('option3') } 188 | default { throw } 189 | } 190 | } 191 | } 192 | 193 | { $delegate.Invoke() } | Should -Throw 194 | $hitValues | Should -Be 'option1', 'option2', 'option3' 195 | } 196 | 197 | It 'can have a single value' { 198 | $delegate = New-PSDelegate { 199 | switch ('value') { 200 | value { return 'value' } 201 | } 202 | } 203 | 204 | $delegate.Invoke() | Should -Be 'value' 205 | } 206 | 207 | It 'can have a default without cases' { 208 | $delegate = New-PSDelegate { 209 | switch ('value') { 210 | default { return 'value' } 211 | } 212 | } 213 | 214 | $delegate.Invoke() | Should -Be 'value' 215 | } 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /src/PSLambda/VariableScopeStack.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Linq.Expressions; 4 | using System.Management.Automation.Language; 5 | 6 | namespace PSLambda 7 | { 8 | /// 9 | /// Represents the current set of variable scopes for a block objects. 10 | /// 11 | internal class VariableScopeStack 12 | { 13 | private static readonly ParameterExpression[] s_emptyParameters = new ParameterExpression[0]; 14 | 15 | private VariableScope _current; 16 | 17 | /// 18 | /// Initializes a new instance of the class. 19 | /// 20 | internal VariableScopeStack() 21 | { 22 | _current = new VariableScope(s_emptyParameters); 23 | } 24 | 25 | /// 26 | /// Creates a new variable scope. 27 | /// 28 | /// 29 | /// A handle that will return to the previous scope when disposed. 30 | /// 31 | internal IDisposable NewScope() 32 | { 33 | _current = new VariableScope(_current); 34 | return new ScopeHandle(() => _current = _current?.Parent); 35 | } 36 | 37 | /// 38 | /// Creates a new variable scope. 39 | /// 40 | /// Parameters that should be available from the new scope. 41 | /// 42 | /// A handle that will return to the previous scope when disposed. 43 | /// 44 | internal IDisposable NewScope(ParameterExpression[] parameters) 45 | { 46 | var handle = NewScope(); 47 | var currentScope = _current; 48 | foreach (var parameter in parameters) 49 | { 50 | currentScope.Parameters.Add(parameter.Name, parameter); 51 | } 52 | 53 | return handle; 54 | } 55 | 56 | /// 57 | /// Creates a new variable expression without looking at previous scopes. 58 | /// 59 | /// The name of the variable. 60 | /// The type of the variable. 61 | /// The variable . 62 | internal ParameterExpression NewVariable(string name, Type type) 63 | { 64 | return _current.NewVariable(name, type); 65 | } 66 | 67 | /// 68 | /// Gets a variable from the current or a parent scope. 69 | /// 70 | /// 71 | /// The to obtain a variable for. 72 | /// 73 | /// 74 | /// A value indicating whether the variable has already been defined. 75 | /// 76 | /// The resolved variable . 77 | internal ParameterExpression GetVariable( 78 | VariableExpressionAst variableExpressionAst, 79 | out bool alreadyDefined) 80 | { 81 | return _current.GetOrCreateVariable(variableExpressionAst.VariablePath.UserPath, variableExpressionAst.StaticType, out alreadyDefined); 82 | } 83 | 84 | /// 85 | /// Gets a variable from the current or a parent scope. 86 | /// 87 | /// The name of the variable. 88 | /// The type of the variable. 89 | /// The resolved variable . 90 | internal ParameterExpression GetVariable(string name, Type type) 91 | { 92 | return _current.GetOrCreateVariable(name, type); 93 | } 94 | 95 | /// 96 | /// Gets all variables declared in the current scope. 97 | /// 98 | /// 99 | /// The variable objects declared in the current scope. 100 | /// 101 | internal ParameterExpression[] GetVariables() 102 | { 103 | return _current.Variables.Values.ToArray(); 104 | } 105 | 106 | /// 107 | /// Gets all parameters declared in the current scope. 108 | /// 109 | /// 110 | /// The parameter objects declared in the current scope. 111 | /// 112 | internal ParameterExpression[] GetParameters() 113 | { 114 | return _current.Parameters.Values.ToArray(); 115 | } 116 | 117 | /// 118 | /// Gets the automatic variable $_ or $PSItem from the 119 | /// current scope, or closest parent scope where it is defined. 120 | /// 121 | /// 122 | /// The parameter expression referencing dollar under if defined; 123 | /// otherwise . 124 | /// 125 | /// 126 | /// if a defined dollar under variable was 127 | /// found; otherwise . 128 | /// 129 | internal bool TryGetDollarUnder(out ParameterExpression dollarUnder) 130 | { 131 | dollarUnder = GetDollarUnder(); 132 | return dollarUnder != null; 133 | } 134 | 135 | /// 136 | /// Gets the automatic variable $_ or $PSItem from the 137 | /// current scope, or closest parent scope where it is defined. 138 | /// 139 | /// 140 | /// The parameter expression referencing dollar under if defined; 141 | /// otherwise . 142 | /// 143 | internal ParameterExpression GetDollarUnder() => _current.GetDollarUnder(); 144 | 145 | /// 146 | /// Sets the automatic variable $_ or $PSItem in the current 147 | /// scope. If the variable exists in a parent scope and the type is the same, 148 | /// then the parent variable will be used. If the variable exists in a parent 149 | /// scope but the type is not the same, the variable will be renamed behind 150 | /// the scenes. 151 | /// 152 | /// 153 | /// The static type that dollar under should contain. 154 | /// 155 | /// 156 | /// The parameter expression referencing dollar under. 157 | /// 158 | internal ParameterExpression SetDollarUnder(Type type) => _current.SetDollarUnder(type); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /test/MiscLanguageFeatures.Tests.ps1: -------------------------------------------------------------------------------- 1 | $moduleName = 'PSLambda' 2 | $manifestPath = "$PSScriptRoot\..\Release\$moduleName\*\$moduleName.psd1" 3 | 4 | Import-Module $manifestPath -Force 5 | 6 | Describe 'Misc Language Features' { 7 | It 'expandable string expression' { 8 | $delegate = New-PSDelegate { 9 | $one = "1" 10 | $two = 2 11 | $three = 'something' 12 | return "This is a string with $one random $two numbers and $three" 13 | } 14 | 15 | $delegate.Invoke() | Should -Be 'This is a string with 1 random 2 numbers and something' 16 | } 17 | 18 | Context 'hashtable tests' { 19 | It 'handles varied types of values' { 20 | $delegate = New-PSDelegate { 21 | return @{ 22 | 'string' = 'value' 23 | string2 = 10 24 | Object = [object]::new() 25 | } 26 | } 27 | 28 | $hashtable = $delegate.Invoke() 29 | $hashtable['string'] | Should -Be 'value' 30 | $hashtable['string2'] | Should -Be 10 31 | $hashtable['Object'] | Should -BeOfType object 32 | $hashtable['object'] | Should -BeOfType object 33 | } 34 | 35 | It 'can initialize an empty hashtable' { 36 | (New-PSDelegate { @{} }).Invoke().GetType() | Should -Be ([hashtable]) 37 | } 38 | } 39 | 40 | Context 'array literal' { 41 | It 'int' { 42 | $result = (New-PSDelegate { 1, 2, 3 }).Invoke() 43 | $result.GetType() | Should -Be ([int[]]) 44 | $result | Should -Be 1, 2, 3 45 | } 46 | 47 | It 'string' { 48 | $result = (New-PSDelegate { 'one', 'two', 'three' }).Invoke() 49 | $result.GetType() | Should -Be ([string[]]) 50 | $result | Should -Be one, two, three 51 | } 52 | 53 | It 'bool' { 54 | $result = (New-PSDelegate { $true, $false, $true }).Invoke() 55 | $result.GetType() | Should -Be ([bool[]]) 56 | $result | Should -Be $true, $false, $true 57 | } 58 | 59 | It 'type' { 60 | $result = (New-PSDelegate { [type], [string], [int] }).Invoke() 61 | $result.GetType() | Should -Be ([type[]]) 62 | $result | Should -Be ([type], [string], [int]) 63 | } 64 | } 65 | 66 | Context 'array initializer' { 67 | It 'does not double arrays' { 68 | $result = (New-PSDelegate { @(0, 1) }).Invoke() 69 | $result.GetType() | Should -Be ([int[]]) 70 | $result[0].GetType() | Should -Be ([int]) 71 | $result | Should -Be 0, 1 72 | } 73 | 74 | It 'does create an array for single items' { 75 | $result = (New-PSDelegate { @(1) }).Invoke() 76 | $result.GetType() | Should -Be ([int[]]) 77 | $result[0].GetType() | Should -Be ([int]) 78 | $result | Should -Be 1 79 | } 80 | 81 | It 'creates an empty array' { 82 | $result = (New-PSDelegate { @() }).Invoke() 83 | $result.GetType() | Should -Be ([object[]]) 84 | $result.Length | Should -Be 0 85 | } 86 | 87 | It 'can take multiple statements in an array' { 88 | $delegate = New-PSDelegate { 89 | return @( 90 | 0..10 91 | 10..20) 92 | } 93 | 94 | $result = $delegate.Invoke() 95 | $result.GetType() | Should -Be ([int[][]]) 96 | $result.Count | Should -Be 2 97 | $result[0] | Should -Be (0..10) 98 | $result[1] | Should -Be (10..20) 99 | } 100 | } 101 | 102 | Context 'assignments' { 103 | It 'can infer type of rhs' { 104 | $result = (New-PSDelegate { $a = 10; return $a++ }).Invoke() 105 | $result | Should -BeOfType int 106 | $result | Should -Be 11 107 | } 108 | 109 | It 'can access variables assigned from parent scopes' { 110 | $result = (New-PSDelegate { 111 | $a = 10 112 | if ($true) { 113 | $a += 1 114 | } 115 | 116 | return $a 117 | }).Invoke() 118 | 119 | $result | Should -Be 11 120 | } 121 | 122 | It 'can not access a variable declared in a child scope' { 123 | { (New-PSDelegate { if ($true) { $a = 10 }; return $a }).Invoke() } | 124 | Should -Throw 'The variable "a" was referenced before it was defined or was defined in a sibling scope' 125 | } 126 | 127 | It 'can assign to an index operation' { 128 | $delegate = New-PSDelegate { 129 | $hash = @{ Key = 'Value' } 130 | $hash['Key'] = 'NewValue' 131 | return $hash 132 | } 133 | 134 | $delegate.Invoke().Key | Should -Be 'NewValue' 135 | } 136 | 137 | It 'can assign to a property' { 138 | $delegate = New-PSDelegate { 139 | $verboseRecord = [System.Management.Automation.VerboseRecord]::new('original message') 140 | $verboseRecord.Message = 'new message' 141 | return $verboseRecord 142 | } 143 | 144 | $delegate.Invoke().Message | Should -Be 'new message' 145 | } 146 | 147 | It 'minus equals' { 148 | $delegate = New-PSDelegate { 149 | $a = 10 150 | $a -= 5 151 | return $a 152 | } 153 | 154 | $delegate.Invoke() | Should -Be 5 155 | } 156 | 157 | It 'multiply equals' { 158 | $delegate = New-PSDelegate { 159 | $a = 10 160 | $a *= 5 161 | return $a 162 | } 163 | 164 | $delegate.Invoke() | Should -Be 50 165 | } 166 | 167 | It 'divide equals' { 168 | $delegate = New-PSDelegate { 169 | $a = 10 170 | $a /= 5 171 | return $a 172 | } 173 | 174 | $delegate.Invoke() | Should -Be 2 175 | } 176 | 177 | It 'remainder equals' { 178 | $delegate = New-PSDelegate { 179 | $a = 10 180 | $a %= 6 181 | return $a 182 | } 183 | 184 | $delegate.Invoke() | Should -Be 4 185 | } 186 | } 187 | 188 | Context 'indexer inference' { 189 | It 'indexed IList<> are typed property' { 190 | $delegate = New-PSDelegate { 191 | $list = [System.Collections.Generic.List[string]]::new() 192 | $list.Add('test') 193 | return $list[0].EndsWith('t') 194 | } 195 | 196 | $delegate.Invoke() | Should -Be $true 197 | } 198 | 199 | It 'indexed IDictionary<,> are typed property' { 200 | $delegate = New-PSDelegate { 201 | $list = [System.Collections.Generic.Dictionary[string, type]]::new() 202 | $list.Add('test', [type]) 203 | return $list['test'].Namespace 204 | } 205 | 206 | $delegate.Invoke() | Should -Be 'System' 207 | } 208 | 209 | It 'can index IList' { 210 | $delegate = New-PSDelegate { 211 | $list = [System.Collections.ArrayList]::new() 212 | $list.AddRange([object[]]('one', 'two', 'three')) 213 | return [string]$list[1] -eq 'two' 214 | } 215 | 216 | $delegate.Invoke() | Should -Be $true 217 | } 218 | 219 | It 'can index custom indexers' { 220 | $delegate = New-PSDelegate { 221 | $pso = [psobject]::AsPSObject([System.Text.StringBuilder]::new()) 222 | $pso.Methods['Append'].Invoke(@('testing')) 223 | return $pso 224 | } 225 | 226 | $result = $delegate.Invoke() 227 | $result | Should -BeOfType System.Text.StringBuilder 228 | $result.ToString() | Should -Be testing 229 | } 230 | 231 | It 'throws the correct message when indexer cannot be found' { 232 | $expectedMsg = 'The indexer could not be determined for the type "System.Int32".' 233 | { New-PSDelegate { (10)[0] }} | Should -Throw $expectedMsg 234 | } 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/PSLambda/resources/ErrorStrings.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Unable to compile ScriptBlock due to unexpected expression. The specified element must be a constant. 122 | 123 | 124 | Unable to resolve the type "{0}". 125 | 126 | 127 | Expected element to be a type literal expression. 128 | 129 | 130 | -as or -is expression with non-literal type. 131 | 132 | 133 | Unable to compile ScriptBlock. 134 | 135 | 136 | Unable to compile ScriptBlock due to a missing element: "{0}". 137 | 138 | 139 | Unable to compile ScriptBlock due to an unsupported element: "{0}". 140 | 141 | 142 | AST of type "{0}". 143 | 144 | 145 | The variable "{0}" was referenced before it was defined or was defined in a sibling scope. 146 | 147 | 148 | '{0}' is not a member of type '{1}'. 149 | 150 | 151 | The indexer could not be determined for the type "{0}". 152 | 153 | 154 | Multiple pipeline elements 155 | 156 | 157 | Invalid syntax for generic method invocation. The syntax is "generic($Target.Method(arguments), [type], [type])". 158 | 159 | 160 | The {0} keyword requires the third argument to be a ScriptBlock expression. 161 | 162 | 163 | The {0} keyword requires both a target expression and a ScriptBlock expression as arguments. 164 | 165 | 166 | '{0}' does not contain a definition for '{1}' and no extension method '{1}' accepting a first argument of type '{0}' could be found. 167 | 168 | 169 | '{0}' does not contain a definition for a method named '{1}' that takes the specified arguments. 170 | 171 | 172 | The foreach statement cannot operate on variables of type '{0}' because '{0}' does not contain a public definition for 'GetEnumerator' 173 | 174 | 175 | -------------------------------------------------------------------------------- /PSLambda.build.ps1: -------------------------------------------------------------------------------- 1 | #requires -Module InvokeBuild -Version 5.1 2 | 3 | [CmdletBinding()] 4 | param( 5 | [Parameter()] 6 | [ValidateSet('Debug', 'Release')] 7 | [string] $Configuration = 'Debug', 8 | 9 | [Parameter()] 10 | [string] $Framework = 'netstandard2.0', 11 | 12 | [Parameter()] 13 | [switch] $GenerateCodeCoverage, 14 | 15 | [Parameter()] 16 | [switch] $Force 17 | ) 18 | 19 | $ModuleName = 'PSLambda' 20 | $Culture = 'en-US' 21 | $ShouldAnalyze = $false 22 | $ShouldTest = $true 23 | 24 | $FailOnError = @{ 25 | ErrorAction = [System.Management.Automation.ActionPreference]::Stop 26 | } 27 | 28 | $Silent = @{ 29 | ErrorAction = [System.Management.Automation.ActionPreference]::Ignore 30 | WarningAction = [System.Management.Automation.ActionPreference]::Ignore 31 | } 32 | 33 | $Manifest = Test-ModuleManifest -Path "$PSScriptRoot/module/$ModuleName.psd1" @Silent 34 | $Version = $Manifest.Version 35 | $PowerShellPath = "$PSScriptRoot/module" 36 | $CSharpPath = "$PSScriptRoot/src" 37 | $ReleasePath = "$PSScriptRoot/Release/$ModuleName/$Version" 38 | $DocsPath = "$PSScriptRoot/docs" 39 | $TestPath = "$PSScriptRoot/test/$ModuleName.Tests" 40 | $ToolsPath = "$PSScriptRoot/tools" 41 | $HasDocs = Test-Path $DocsPath/$Culture/*.md 42 | $HasTests = Test-Path $TestPath/* 43 | $IsUnix = $PSEdition -eq 'Core' -and -not $IsWindows 44 | 45 | function InvokeWithPSModulePath([scriptblock] $Action) { 46 | $oldPSModulePath = $env:PSModulePath 47 | try { 48 | $env:PSModulePath = Join-Path (Split-Path $pwsh.Path) -ChildPath 'Modules' 49 | . $Action 50 | } finally { 51 | $env:PSModulePath = $oldPSModulePath 52 | } 53 | } 54 | 55 | task Clean { 56 | if (Test-Path $ReleasePath) { 57 | Remove-Item $ReleasePath -Recurse 58 | } 59 | 60 | New-Item -ItemType Directory $ReleasePath | Out-Null 61 | } 62 | 63 | task BuildDocs -If { $HasDocs } { 64 | New-ExternalHelp -Path $DocsPath/$Culture -OutputPath $ReleasePath/$Culture | Out-Null 65 | } 66 | 67 | task AssertDependencies AssertPowerShellCore, AssertRequiredModules, AssertDotNet, AssertOpenCover 68 | 69 | task AssertOpenCover -If { $GenerateCodeCoverage.IsPresent } { 70 | if ($Discovery.IsUnix) { 71 | Write-Warning 'Generating code coverage from .NET core is currently unsupported, disabling code coverage generation.' 72 | $script:GenerateCodeCoverage = $false 73 | return 74 | } 75 | 76 | $script:openCover = & "$ToolsPath\GetOpenCover.ps1" -Force:$Force.IsPresent 77 | } 78 | 79 | task AssertPowerShellCore { 80 | $script:pwsh = $pwsh = Get-Command pwsh @Silent 81 | if ($pwsh) { 82 | return 83 | } 84 | 85 | if ($Force.IsPresent) { 86 | choco install powershell-core --version 6.2.2 -y 87 | } else { 88 | choco install powershell-core --verison 6.2.2 89 | } 90 | 91 | $script:pwsh = Get-Command $env:ProgramFiles/PowerShell/6/pwsh.exe @FailOnError 92 | } 93 | 94 | task AssertRequiredModules { 95 | $assertRequiredModule = Get-Command $ToolsPath/AssertRequiredModule.ps1 @FailOnError 96 | & $assertRequiredModule platyPS -RequiredVersion 0.14.0 -Force:$Force.IsPresent 97 | & $assertRequiredModule Pester -RequiredVersion 4.8.1 -Force:$Force.IsPresent 98 | } 99 | 100 | task AssertDotNet { 101 | $script:dotnet = & $ToolsPath/GetDotNet.ps1 -Unix:$IsUnix 102 | } 103 | 104 | task AssertPSResGen { 105 | # Download the ResGen tool used by PowerShell core internally. This will need to be replaced 106 | # when the dotnet cli gains support for it. 107 | if (-not (Test-Path $ToolsPath/ResGen)) { 108 | New-Item -ItemType Directory $ToolsPath/ResGen | Out-Null 109 | } 110 | 111 | if (-not (Test-Path $ToolsPath/ResGen/Program.cs)) { 112 | $programUri = 'https://raw.githubusercontent.com/PowerShell/PowerShell/v6.2.2/src/ResGen/Program.cs' 113 | Invoke-WebRequest $programUri -OutFile $ToolsPath/ResGen/Program.cs @FailOnError 114 | } 115 | 116 | if (-not (Test-Path $ToolsPath/ResGen/ResGen.csproj)) { 117 | $projUri = 'https://raw.githubusercontent.com/PowerShell/PowerShell/v6.2.2/src/ResGen/ResGen.csproj' 118 | Invoke-WebRequest $projUri -OutFile $ToolsPath/ResGen/ResGen.csproj @FailOnError 119 | } 120 | } 121 | 122 | task ResGenImpl { 123 | Push-Location $CSharpPath/$ModuleName 124 | try { 125 | & $dotnet run --project $ToolsPath/ResGen/ResGen.csproj 126 | } finally { 127 | Pop-Location 128 | } 129 | } 130 | 131 | task BuildManaged { 132 | & $dotnet publish --framework $Framework --configuration $Configuration --verbosity q -nologo 133 | } 134 | 135 | task CopyToRelease { 136 | $splat = @{ 137 | Destination = $ReleasePath 138 | Force = $true 139 | ErrorAction = [System.Management.Automation.ActionPreference]::Stop 140 | } 141 | 142 | $itemsToCopy = ( 143 | "$ModuleName.psm1", 144 | "$ModuleName.psd1", 145 | "$ModuleName.format.ps1xml", 146 | "$ModuleName.types.ps1xml") 147 | 148 | foreach ($itemName in $itemsToCopy) { 149 | $splat['LiteralPath'] = Join-Path $PowerShellPath -ChildPath $itemName 150 | Copy-Item @splat 151 | } 152 | 153 | $itemsToCopy = ( 154 | "$ModuleName.deps.json", 155 | "$ModuleName.dll", 156 | "$ModuleName.pdb", 157 | "$ModuleName.xml") 158 | 159 | $publishPath = Join-Path $CSharpPath -ChildPath "$ModuleName/bin/$Configuration/$Framework/publish" 160 | foreach ($itemName in $itemsToCopy) { 161 | $splat['LiteralPath'] = Join-Path $publishPath -ChildPath $itemName 162 | Copy-Item @splat 163 | } 164 | } 165 | 166 | task Analyze -If { $ShouldAnalyze } { 167 | Invoke-ScriptAnalyzer -Path $ReleasePath -Settings $PSScriptRoot/ScriptAnalyzerSettings.psd1 -Recurse 168 | } 169 | 170 | task DoTest { 171 | if ($Discovery.IsUnix) { 172 | $scriptString = ' 173 | $projectPath = "{0}" 174 | Invoke-Pester "$projectPath" -OutputFormat NUnitXml -OutputFile "$projectPath\testresults\pester.xml" 175 | ' -f $PSScriptRoot 176 | } else { 177 | $scriptString = ' 178 | Set-ExecutionPolicy Bypass -Force -Scope Process 179 | $projectPath = "{0}" 180 | Invoke-Pester "$projectPath" -OutputFormat NUnitXml -OutputFile "$projectPath\testresults\pester.xml" 181 | ' -f $PSScriptRoot 182 | } 183 | 184 | $encodedCommand = 185 | [convert]::ToBase64String( 186 | [System.Text.Encoding]::Unicode.GetBytes( 187 | $scriptString)) 188 | 189 | $powershellCommand = 'powershell' 190 | if ($Discovery.IsUnix) { 191 | $powershellCommand = 'pwsh' 192 | } 193 | 194 | $powershell = (Get-Command $powershellCommand).Source 195 | 196 | if (-not $GenerateCodeCoverage.IsPresent) { 197 | & $powershell -NoProfile -EncodedCommand $encodedCommand 198 | return 199 | } 200 | 201 | # OpenCover needs full pdb's. I'm very open to suggestions for streamlining this... 202 | & $dotnet clean --verbosity q -nologo 203 | & $dotnet build --configuration $Configuration --framework netstandard2.0 --verbosity q -nologo /p:DebugType=Full 204 | 205 | $release = Join-Path $ReleasePath -ChildPath $ModuleName 206 | $coverage = Join-Path $CSharpPath -ChildPath "$ModuleName/bin/$Configuration/$Framework/$ModuleName" 207 | 208 | Rename-Item "$release.pdb" -NewName "$ModuleName.pdb.tmp" 209 | Rename-Item "$release.dll" -NewName "$ModuleName.dll.tmp" 210 | Copy-Item "$coverage.pdb" "$release.pdb" 211 | Copy-Item "$coverage.dll" "$release.dll" 212 | 213 | & $openCover ` 214 | -target:$powershell ` 215 | -register:user ` 216 | -output:$PSScriptRoot\testresults\opencover.xml ` 217 | -hideskipped:all ` 218 | -filter:+[PSLambda*]* ` 219 | -targetargs:"-NoProfile -EncodedCommand $encodedCommand" 220 | 221 | Remove-Item "$release.pdb" 222 | Remove-Item "$release.dll" 223 | Rename-Item "$release.pdb.tmp" -NewName "$ModuleName.pdb" 224 | Rename-Item "$release.dll.tmp" -NewName "$ModuleName.dll" 225 | } 226 | 227 | task DoInstall { 228 | $installBase = $Home 229 | if ($profile) { 230 | $installBase = $profile | Split-Path 231 | } 232 | 233 | $installPath = "$installBase/Modules/$ModuleName/$Version" 234 | if (-not (Test-Path $installPath)) { 235 | $null = New-Item $installPath -ItemType Directory 236 | } 237 | 238 | Copy-Item -Path $ReleasePath/* -Destination $installPath -Force -Recurse 239 | } 240 | 241 | task DoPublish { 242 | if (-not (Test-Path $env:USERPROFILE/.PSGallery/apikey.xml)) { 243 | throw 'Could not find PSGallery API key!' 244 | } 245 | 246 | $apiKey = (Import-Clixml $env:USERPROFILE/.PSGallery/apikey.xml).GetNetworkCredential().Password 247 | Publish-Module -Name $ReleasePath -NuGetApiKey $apiKey -Confirm 248 | } 249 | 250 | task ResGen -Jobs AssertPSResGen, ResGenImpl 251 | 252 | task Build -Jobs Clean, AssertDependencies, ResGen, BuildManaged, CopyToRelease, BuildDocs 253 | 254 | task Test -Jobs Build, DoTest 255 | 256 | task PrePublish -Jobs Test 257 | 258 | task Install -Jobs Test, DoInstall 259 | 260 | task Publish -Jobs Test, DoPublish 261 | 262 | task . Build 263 | -------------------------------------------------------------------------------- /src/PSLambda/VariableScope.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | 6 | namespace PSLambda 7 | { 8 | /// 9 | /// Represents the variable scope for a single block . 10 | /// 11 | internal class VariableScope 12 | { 13 | private static readonly string[] s_dollarUnderNameCache = 14 | { 15 | "PSItem", "PSItem2", "PSItem3", "PSItem4", "PSItem5", 16 | "PSItem6", "PSItem7", "PSItem8", "PSItem9", "PSItem10", 17 | }; 18 | 19 | private (ParameterExpression Parameter, int Version) _dollarUnder; 20 | 21 | /// 22 | /// Initializes a new instance of the class. 23 | /// 24 | /// Parameters to include in the scope. 25 | public VariableScope(ParameterExpression[] parameters) 26 | { 27 | Parameters = parameters.ToDictionary(p => p.Name); 28 | } 29 | 30 | /// 31 | /// Initializes a new instance of the class. 32 | /// 33 | /// The parent object. 34 | public VariableScope(VariableScope parent) 35 | { 36 | Parent = parent; 37 | } 38 | 39 | /// 40 | /// Gets the parent . 41 | /// 42 | internal VariableScope Parent { get; } 43 | 44 | /// 45 | /// Gets the parameter objects for the current scope. 46 | /// 47 | internal Dictionary Parameters { get; } = new Dictionary(); 48 | 49 | /// 50 | /// Gets the variable objects for the current scope. 51 | /// 52 | internal Dictionary Variables { get; } = new Dictionary(); 53 | 54 | /// 55 | /// Creates a new variable expression without looking at previous scopes. 56 | /// 57 | /// The name of the variable. 58 | /// The type of the variable. 59 | /// The variable . 60 | internal ParameterExpression NewVariable(string name, Type type) 61 | { 62 | ParameterExpression variable = Expression.Variable(type, name); 63 | Variables.Add(name, variable); 64 | return variable; 65 | } 66 | 67 | /// 68 | /// Gets an already defined variable if it already exists, 69 | /// otherwise one is created. 70 | /// 71 | /// The name of the variable. 72 | /// The type of the variable. 73 | /// The variable . 74 | internal ParameterExpression GetOrCreateVariable(string name, Type type) 75 | { 76 | return GetOrCreateVariable(name, type, out _); 77 | } 78 | 79 | /// 80 | /// Gets an already defined variable if it already exists, 81 | /// otherwise one is created. 82 | /// 83 | /// The name of the variable. 84 | /// The type of the variable. 85 | /// 86 | /// A value indicating whether the variable has already been defined. 87 | /// 88 | /// The variable . 89 | internal ParameterExpression GetOrCreateVariable(string name, Type type, out bool alreadyDefined) 90 | { 91 | if (TryGetVariable(name, out ParameterExpression existingVar)) 92 | { 93 | alreadyDefined = true; 94 | return existingVar; 95 | } 96 | 97 | alreadyDefined = false; 98 | var newVariable = Expression.Parameter(type ?? typeof(object), name); 99 | Variables.Add(name, newVariable); 100 | return newVariable; 101 | } 102 | 103 | /// 104 | /// Gets an already defined variable if it already exists, 105 | /// otherwise one is created. 106 | /// 107 | /// The name of the variable. 108 | /// 109 | /// A function that retrieves the variable's type if it has not already been defined. 110 | /// 111 | /// The variable . 112 | internal ParameterExpression GetOrCreateVariable(string name, Func typeGetter) 113 | { 114 | if (TryGetVariable(name, out ParameterExpression existingVar)) 115 | { 116 | return existingVar; 117 | } 118 | 119 | var newVariable = Expression.Parameter(typeGetter() ?? typeof(object), name); 120 | Variables.Add(name, newVariable); 121 | return newVariable; 122 | } 123 | 124 | /// 125 | /// Attempts to get a variable that has already been defined. 126 | /// 127 | /// The name of the variable. 128 | /// The already defined variable . 129 | /// 130 | /// A value indicating whether an existing variable was found. 131 | /// 132 | internal bool TryGetVariable(string name, out ParameterExpression existingVariable) 133 | { 134 | if (Variables.TryGetValue(name, out existingVariable)) 135 | { 136 | return true; 137 | } 138 | 139 | if (Parameters.TryGetValue(name, out existingVariable)) 140 | { 141 | return true; 142 | } 143 | 144 | if (Parent != null && Parent.TryGetVariable(name, out existingVariable)) 145 | { 146 | return true; 147 | } 148 | 149 | return false; 150 | } 151 | 152 | /// 153 | /// Gets the automatic variable $_ or $PSItem from the 154 | /// current scope, or closest parent scope where it is defined. 155 | /// 156 | /// 157 | /// The parameter expression referencing dollar under if defined; 158 | /// otherwise . 159 | /// 160 | internal ParameterExpression GetDollarUnder() 161 | { 162 | for (VariableScope current = this; current != null; current = current.Parent) 163 | { 164 | if (current._dollarUnder.Parameter != null) 165 | { 166 | return current._dollarUnder.Parameter; 167 | } 168 | } 169 | 170 | return null; 171 | } 172 | 173 | /// 174 | /// Sets the automatic variable $_ or $PSItem in the current 175 | /// scope. If the variable exists in a parent scope and the type is the same, 176 | /// then the parent variable will be used. If the variable exists in a parent 177 | /// scope but the type is not the same, the variable will be renamed behind 178 | /// the scenes. 179 | /// 180 | /// 181 | /// The static type that dollar under should contain. 182 | /// 183 | /// 184 | /// The parameter expression referencing dollar under. 185 | /// 186 | internal ParameterExpression SetDollarUnder(Type type) 187 | { 188 | static string GetDollarUnderName(int version) 189 | { 190 | if (version < s_dollarUnderNameCache.Length) 191 | { 192 | return s_dollarUnderNameCache[version]; 193 | } 194 | 195 | return string.Concat("PSItem", version); 196 | } 197 | 198 | for (VariableScope scope = Parent; scope != null; scope = scope.Parent) 199 | { 200 | var dollarUnder = scope._dollarUnder; 201 | if (dollarUnder.Parameter == null) 202 | { 203 | continue; 204 | } 205 | 206 | if (_dollarUnder.Parameter.Type == type) 207 | { 208 | _dollarUnder = dollarUnder; 209 | return _dollarUnder.Parameter; 210 | } 211 | 212 | var version = dollarUnder.Version + 1; 213 | _dollarUnder = (Expression.Parameter(type, GetDollarUnderName(version)), version); 214 | return _dollarUnder.Parameter; 215 | } 216 | 217 | _dollarUnder = (Expression.Parameter(type, "PSItem"), 0); 218 | return _dollarUnder.Parameter; 219 | } 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /src/PSLambda/ReflectionCache.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Management.Automation; 6 | using System.Reflection; 7 | using System.Text.RegularExpressions; 8 | 9 | namespace PSLambda 10 | { 11 | /// 12 | /// Provides cached objects for generating method and property invocation 13 | /// objects. 14 | /// 15 | internal static class ReflectionCache 16 | { 17 | /// 18 | /// Resolves to . 19 | /// 20 | public static readonly MethodInfo LanguagePrimitives_GetEnumerator = 21 | typeof(LanguagePrimitives).GetMethod("GetEnumerator", new[] { typeof(object) }); 22 | 23 | /// 24 | /// Resolves to . 25 | /// 26 | public static readonly MethodInfo LanguagePrimitives_IsTrue = 27 | typeof(LanguagePrimitives).GetMethod("IsTrue", new[] { typeof(object) }); 28 | 29 | /// 30 | /// Resolves to . 31 | /// 32 | public static readonly MethodInfo LanguagePrimitives_Compare = 33 | typeof(LanguagePrimitives).GetMethod("Compare", new[] { typeof(object), typeof(object), typeof(bool) }); 34 | 35 | /// 36 | /// Resolves to . 37 | /// 38 | public static readonly MethodInfo LanguagePrimitives_ConvertToGeneric = 39 | typeof(LanguagePrimitives).GetMethod("ConvertTo", new[] { typeof(object) }); 40 | 41 | /// 42 | /// Resolves to . 43 | /// 44 | public static readonly MethodInfo LanguagePrimitives_ConvertTo = 45 | typeof(LanguagePrimitives).GetMethod("ConvertTo", new[] { typeof(object), typeof(Type) }); 46 | 47 | /// 48 | /// Resolves to . 49 | /// 50 | public static readonly MethodInfo LangaugePrimitives_TryConvertToGeneric = 51 | (MethodInfo)typeof(LanguagePrimitives).FindMembers( 52 | MemberTypes.Method, 53 | BindingFlags.Static | BindingFlags.Public, 54 | (m, filterCriteria) => 55 | { 56 | var method = m as MethodInfo; 57 | if (!method.IsGenericMethod) 58 | { 59 | return false; 60 | } 61 | 62 | var parameters = method.GetParameters(); 63 | return parameters.Length == 2 64 | && parameters[0].ParameterType == typeof(object) 65 | && parameters[1].ParameterType.IsByRef; 66 | }, 67 | null) 68 | .FirstOrDefault(); 69 | 70 | /// 71 | /// Resolves to . 72 | /// 73 | public static readonly ConstructorInfo WildcardPattern_Ctor = 74 | typeof(WildcardPattern).GetConstructor(new[] { typeof(string), typeof(WildcardOptions) }); 75 | 76 | /// 77 | /// Resolves to . 78 | /// 79 | public static readonly MethodInfo Regex_IsMatch = 80 | typeof(Regex).GetMethod("IsMatch", new[] { typeof(string), typeof(string), typeof(RegexOptions) }); 81 | 82 | /// 83 | /// Resolves to . 84 | /// 85 | public static readonly MethodInfo Regex_Replace = 86 | typeof(Regex).GetMethod("Replace", new[] { typeof(string), typeof(string), typeof(string), typeof(RegexOptions) }); 87 | 88 | /// 89 | /// Resolves to . 90 | /// 91 | public static readonly MethodInfo Regex_Split = 92 | typeof(Regex).GetMethod("Split", new[] { typeof(string), typeof(string), typeof(RegexOptions) }); 93 | 94 | /// 95 | /// Resolves to . 96 | /// 97 | public static readonly MethodInfo WildcardPattern_IsMatch = 98 | typeof(WildcardPattern).GetMethod("IsMatch", new[] { typeof(string) }); 99 | 100 | /// 101 | /// Resolves to . 102 | /// 103 | public static readonly MethodInfo IEnumerator_MoveNext = 104 | typeof(IEnumerator).GetMethod("MoveNext"); 105 | 106 | /// 107 | /// Resolves to . 108 | /// 109 | public static readonly MethodInfo String_Join = 110 | typeof(string).GetMethod("Join", new[] { typeof(string), typeof(string[]) }); 111 | 112 | /// 113 | /// Resolves to . 114 | /// 115 | public static readonly MethodInfo String_Format = 116 | typeof(string).GetMethod("Format", new[] { typeof(IFormatProvider), typeof(string), typeof(object[]) }); 117 | 118 | /// 119 | /// Resolves to . 120 | /// 121 | public static readonly PropertyInfo CultureInfo_CurrentCulture = 122 | typeof(System.Globalization.CultureInfo).GetProperty("CurrentCulture"); 123 | 124 | /// 125 | /// Resolves to the non-public type System.Management.Automation.ArgumentTypeConverterAttribute. 126 | /// 127 | public static readonly Type ArgumentTypeConverterAttribute = 128 | typeof(PSObject).Assembly.GetType("System.Management.Automation.ArgumentTypeConverterAttribute"); 129 | 130 | /// 131 | /// Resolves to the non-public property System.Management.Automation.ArgumentTypeConverterAttribute.TargetType. 132 | /// 133 | public static readonly PropertyInfo ArgumentTypeConverterAttribute_TargetType = 134 | ArgumentTypeConverterAttribute?.GetProperty("TargetType", BindingFlags.Instance | BindingFlags.NonPublic); 135 | 136 | /// 137 | /// Resolves to . 138 | /// 139 | public static readonly ConstructorInfo RuntimeException_Ctor = 140 | typeof(RuntimeException).GetConstructor(new[] { typeof(string) }); 141 | 142 | /// 143 | /// Resolves to . 144 | /// 145 | public static readonly MethodInfo IDisposable_Dispose = 146 | typeof(IDisposable).GetMethod("Dispose"); 147 | 148 | /// 149 | /// Resolves to the indexer for . 150 | /// 151 | public static readonly PropertyInfo IList_Item = 152 | typeof(IList).GetProperty("Item"); 153 | 154 | /// 155 | /// Resolves to the indexer for . 156 | /// 157 | public static readonly PropertyInfo IDictionary_Item = 158 | typeof(IDictionary).GetProperty("Item"); 159 | 160 | /// 161 | /// Resolves to the non-public parameterless constructor for System.Management.Automation.ExitException. 162 | /// 163 | public static readonly ConstructorInfo ExitException_Ctor = 164 | typeof(ExitException).GetConstructor( 165 | BindingFlags.Instance | BindingFlags.NonPublic, 166 | null, 167 | new Type[0], 168 | new ParameterModifier[0]); 169 | 170 | /// 171 | /// Resolves to the non-public constructor System.Management.Automation.ExitException(Object). 172 | /// 173 | public static readonly ConstructorInfo ExitException_Ctor_Object = 174 | typeof(ExitException).GetConstructor( 175 | BindingFlags.Instance | BindingFlags.NonPublic, 176 | null, 177 | new[] { typeof(object) }, 178 | new[] { new ParameterModifier(1) }); 179 | 180 | /// 181 | /// Resolves to . 182 | /// 183 | public static readonly MethodInfo Hashtable_Add = 184 | typeof(Hashtable).GetMethod("Add", new[] { typeof(object), typeof(object) }); 185 | 186 | /// 187 | /// Resolves to . 188 | /// 189 | public static readonly ConstructorInfo Hashtable_Ctor = 190 | typeof(Hashtable).GetConstructor(new[] { typeof(int), typeof(IEqualityComparer) }); 191 | 192 | /// 193 | /// Resolves to . 194 | /// 195 | public static readonly MethodInfo IEnumerator_get_Current = 196 | typeof(IEnumerator).GetMethod(Strings.EnumeratorGetCurrentMethodName, Type.EmptyTypes); 197 | 198 | /// 199 | /// Resolves to . 200 | /// 201 | public static readonly MethodInfo IEnumerable_GetEnumerator = 202 | typeof(IEnumerable).GetMethod(Strings.GetEnumeratorMethodName, Type.EmptyTypes); 203 | 204 | /// 205 | /// Resolves to . 206 | /// 207 | public static readonly PropertyInfo StringComparer_CurrentCultureIgnoreCase = 208 | typeof(StringComparer).GetProperty("CurrentCultureIgnoreCase"); 209 | 210 | /// 211 | /// Resolves to . 212 | /// 213 | public static readonly MethodInfo ExpressionUtils_PSEqualsIgnoreCase = 214 | typeof(ExpressionUtils).GetMethod("PSEqualsIgnoreCase", BindingFlags.Static | BindingFlags.NonPublic); 215 | 216 | /// 217 | /// Resolves to . 218 | /// 219 | public static readonly MethodInfo ExpressionUtils_GetRange = 220 | typeof(ExpressionUtils).GetMethod("GetRange", BindingFlags.Static | BindingFlags.NonPublic); 221 | 222 | /// 223 | /// Resolves to . 224 | /// 225 | public static readonly MethodInfo Monitor_Enter = 226 | typeof(System.Threading.Monitor).GetMethod( 227 | "Enter", 228 | new[] { typeof(object), typeof(bool).MakeByRefType() }); 229 | 230 | /// 231 | /// Resolves to . 232 | /// 233 | public static readonly MethodInfo Monitor_Exit = 234 | typeof(System.Threading.Monitor).GetMethod("Exit", new[] { typeof(object) }); 235 | 236 | /// 237 | /// Resolves to . 238 | /// 239 | public static readonly MethodInfo IEnumerable_T_GetEnumerator = 240 | typeof(IEnumerable<>).GetMethod(Strings.GetEnumeratorMethodName, Type.EmptyTypes); 241 | 242 | /// 243 | /// Resolves to . 244 | /// 245 | public static readonly MethodInfo IEnumerator_T_get_Current = 246 | typeof(IEnumerator<>).GetMethod(Strings.EnumeratorGetCurrentMethodName, Type.EmptyTypes); 247 | 248 | /// 249 | /// Resolves to . 250 | /// 251 | public static readonly MethodInfo IDictionary_GetEnumerator = 252 | typeof(IDictionary).GetMethod(Strings.GetEnumeratorMethodName, Type.EmptyTypes); 253 | 254 | /// 255 | /// Resolves to . 256 | /// 257 | public static readonly MethodInfo IDictionaryEnumerator_get_Entry = 258 | typeof(IDictionaryEnumerator).GetMethod("get_Entry", Type.EmptyTypes); 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /src/PSLambda/DelegateSyntaxVisitor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Management.Automation.Language; 4 | 5 | namespace PSLambda 6 | { 7 | /// 8 | /// Provides the ability to reshape a when it fits 9 | /// the custom syntax for anonymous method expressions. 10 | /// 11 | internal class DelegateSyntaxVisitor : ICustomAstVisitor 12 | { 13 | private readonly IParseErrorWriter _errorWriter; 14 | 15 | private readonly List> _variables = new List>(); 16 | 17 | private IScriptExtent _paramBlockExtent; 18 | 19 | private bool _failed; 20 | 21 | /// 22 | /// Initializes a new instance of the class. 23 | /// 24 | /// The to report errors to. 25 | internal DelegateSyntaxVisitor(IParseErrorWriter errorWriter) 26 | { 27 | _errorWriter = errorWriter; 28 | } 29 | 30 | #pragma warning disable SA1600 31 | public object VisitScriptBlockExpression(ScriptBlockExpressionAst scriptBlockExpressionAst) 32 | { 33 | return scriptBlockExpressionAst.ScriptBlock.Visit(this); 34 | } 35 | 36 | public object VisitScriptBlock(ScriptBlockAst scriptBlockAst) 37 | { 38 | if (scriptBlockAst.ParamBlock != null) 39 | { 40 | return scriptBlockAst; 41 | } 42 | 43 | if (scriptBlockAst.BeginBlock != null) 44 | { 45 | _errorWriter.ReportNotSupported( 46 | scriptBlockAst.BeginBlock.Extent, 47 | nameof(CompilerStrings.BeginBlock), 48 | CompilerStrings.BeginBlock); 49 | return null; 50 | } 51 | 52 | if (scriptBlockAst.ProcessBlock != null) 53 | { 54 | _errorWriter.ReportNotSupported( 55 | scriptBlockAst.ProcessBlock.Extent, 56 | nameof(CompilerStrings.ProcessBlock), 57 | CompilerStrings.ProcessBlock); 58 | return null; 59 | } 60 | 61 | if (scriptBlockAst.EndBlock == null) 62 | { 63 | _errorWriter.ReportMissing( 64 | scriptBlockAst.Extent, 65 | nameof(CompilerStrings.EndBlock), 66 | CompilerStrings.EndBlock); 67 | return null; 68 | } 69 | 70 | var body = scriptBlockAst.EndBlock.Visit(this); 71 | if (_failed) 72 | { 73 | return scriptBlockAst; 74 | } 75 | 76 | _errorWriter.ThrowIfAnyErrors(); 77 | var parameters = new ParameterAst[_variables.Count]; 78 | for (var i = 0; i < parameters.Length; i++) 79 | { 80 | parameters[i] = new ParameterAst( 81 | _variables[i].Item2.Extent, 82 | (VariableExpressionAst)_variables[i].Item2.Copy(), 83 | new[] { new TypeConstraintAst(_variables[i].Item1.Extent, _variables[i].Item1) }, 84 | null); 85 | } 86 | 87 | var paramBlock = 88 | new ParamBlockAst( 89 | _paramBlockExtent, 90 | Array.Empty(), 91 | parameters); 92 | 93 | return new ScriptBlockAst( 94 | scriptBlockAst.Extent, 95 | paramBlock, 96 | null, 97 | null, 98 | (NamedBlockAst)((Ast)body).Copy(), 99 | null); 100 | } 101 | 102 | public object VisitNamedBlock(NamedBlockAst namedBlockAst) 103 | { 104 | if (namedBlockAst.Statements == null || 105 | namedBlockAst.Statements.Count == 0) 106 | { 107 | _errorWriter.ReportMissing( 108 | namedBlockAst.Extent, 109 | nameof(CompilerStrings.EndBlock), 110 | CompilerStrings.EndBlock); 111 | return null; 112 | } 113 | 114 | if (namedBlockAst.Statements.Count > 1) 115 | { 116 | _failed = true; 117 | return null; 118 | } 119 | 120 | var body = namedBlockAst.Statements[0].Visit(this); 121 | if (body == null) 122 | { 123 | _failed = true; 124 | return null; 125 | } 126 | 127 | return body; 128 | } 129 | 130 | public object VisitPipeline(PipelineAst pipelineAst) 131 | { 132 | return pipelineAst.PipelineElements[0].Visit(this); 133 | } 134 | 135 | public object VisitAssignmentStatement(AssignmentStatementAst assignmentStatementAst) 136 | { 137 | _paramBlockExtent = assignmentStatementAst.Left.Extent; 138 | DelegateParameterVisitor.AddVariables( 139 | assignmentStatementAst.Left, 140 | _variables); 141 | 142 | if (!(assignmentStatementAst.Right is PipelineAst pipeline)) 143 | { 144 | return null; 145 | } 146 | 147 | if (!(pipeline.PipelineElements[0] is CommandAst commandAst) || 148 | commandAst.GetCommandName() != Strings.DelegateSyntaxCommandName || 149 | commandAst.CommandElements.Count != 2) 150 | { 151 | return null; 152 | } 153 | 154 | if (commandAst.CommandElements[1] is ScriptBlockExpressionAst sbAst) 155 | { 156 | return sbAst.ScriptBlock.EndBlock; 157 | } 158 | 159 | var expression = commandAst.CommandElements[1] as ExpressionAst; 160 | 161 | var statements = 162 | new StatementAst[] 163 | { 164 | new CommandExpressionAst( 165 | expression.Extent, 166 | (ExpressionAst)expression.Copy(), 167 | Array.Empty()), 168 | }; 169 | 170 | var statementBlockAst = new StatementBlockAst( 171 | commandAst.CommandElements[1].Extent, 172 | statements, 173 | Array.Empty()); 174 | 175 | return new NamedBlockAst( 176 | commandAst.CommandElements[1].Extent, 177 | TokenKind.End, 178 | statementBlockAst, 179 | unnamed: true); 180 | } 181 | #pragma warning restore SA1600 182 | 183 | private class DelegateParameterVisitor : AstVisitor 184 | { 185 | private static readonly ITypeName s_objectTypeName = new TypeName( 186 | Empty.Extent, 187 | typeof(object).FullName); 188 | 189 | private List> _variables = new List>(); 190 | 191 | private DelegateParameterVisitor() 192 | { 193 | } 194 | 195 | public static void AddVariables( 196 | ExpressionAst expression, 197 | List> variables) 198 | { 199 | var visitor = new DelegateParameterVisitor 200 | { 201 | _variables = variables, 202 | }; 203 | 204 | expression.Visit(visitor); 205 | } 206 | 207 | public override AstVisitAction VisitConvertExpression(ConvertExpressionAst convertExpressionAst) 208 | { 209 | if (convertExpressionAst.Child is VariableExpressionAst variable) 210 | { 211 | _variables.Add( 212 | Tuple.Create( 213 | convertExpressionAst.Attribute.TypeName, 214 | variable)); 215 | 216 | return AstVisitAction.SkipChildren; 217 | } 218 | 219 | return AstVisitAction.StopVisit; 220 | } 221 | 222 | public override AstVisitAction VisitVariableExpression(VariableExpressionAst variableExpressionAst) 223 | { 224 | _variables.Add( 225 | Tuple.Create( 226 | s_objectTypeName, 227 | variableExpressionAst)); 228 | 229 | return AstVisitAction.SkipChildren; 230 | } 231 | } 232 | 233 | #pragma warning disable SA1600, SA1201, SA1516 234 | public object VisitArrayExpression(ArrayExpressionAst arrayExpressionAst) => null; 235 | public object VisitArrayLiteral(ArrayLiteralAst arrayLiteralAst) => null; 236 | public object VisitAttribute(AttributeAst attributeAst) => null; 237 | public object VisitAttributedExpression(AttributedExpressionAst attributedExpressionAst) => null; 238 | public object VisitBinaryExpression(BinaryExpressionAst binaryExpressionAst) => null; 239 | public object VisitBlockStatement(BlockStatementAst blockStatementAst) => null; 240 | public object VisitBreakStatement(BreakStatementAst breakStatementAst) => null; 241 | public object VisitCatchClause(CatchClauseAst catchClauseAst) => null; 242 | public object VisitCommand(CommandAst commandAst) => null; 243 | public object VisitCommandExpression(CommandExpressionAst commandExpressionAst) => null; 244 | public object VisitCommandParameter(CommandParameterAst commandParameterAst) => null; 245 | public object VisitConstantExpression(ConstantExpressionAst constantExpressionAst) => null; 246 | public object VisitContinueStatement(ContinueStatementAst continueStatementAst) => null; 247 | public object VisitConvertExpression(ConvertExpressionAst convertExpressionAst) => null; 248 | public object VisitDataStatement(DataStatementAst dataStatementAst) => null; 249 | public object VisitDoUntilStatement(DoUntilStatementAst doUntilStatementAst) => null; 250 | public object VisitDoWhileStatement(DoWhileStatementAst doWhileStatementAst) => null; 251 | public object VisitErrorExpression(ErrorExpressionAst errorExpressionAst) => null; 252 | public object VisitErrorStatement(ErrorStatementAst errorStatementAst) => null; 253 | public object VisitExitStatement(ExitStatementAst exitStatementAst) => null; 254 | public object VisitExpandableStringExpression(ExpandableStringExpressionAst expandableStringExpressionAst) => null; 255 | public object VisitFileRedirection(FileRedirectionAst fileRedirectionAst) => null; 256 | public object VisitForEachStatement(ForEachStatementAst forEachStatementAst) => null; 257 | public object VisitForStatement(ForStatementAst forStatementAst) => null; 258 | public object VisitFunctionDefinition(FunctionDefinitionAst functionDefinitionAst) => null; 259 | public object VisitHashtable(HashtableAst hashtableAst) => null; 260 | public object VisitIfStatement(IfStatementAst ifStmtAst) => null; 261 | public object VisitIndexExpression(IndexExpressionAst indexExpressionAst) => null; 262 | public object VisitInvokeMemberExpression(InvokeMemberExpressionAst invokeMemberExpressionAst) => null; 263 | public object VisitMemberExpression(MemberExpressionAst memberExpressionAst) => null; 264 | public object VisitMergingRedirection(MergingRedirectionAst mergingRedirectionAst) => null; 265 | public object VisitNamedAttributeArgument(NamedAttributeArgumentAst namedAttributeArgumentAst) => null; 266 | public object VisitParamBlock(ParamBlockAst paramBlockAst) => null; 267 | public object VisitParameter(ParameterAst parameterAst) => null; 268 | public object VisitParenExpression(ParenExpressionAst parenExpressionAst) => null; 269 | public object VisitReturnStatement(ReturnStatementAst returnStatementAst) => null; 270 | public object VisitStatementBlock(StatementBlockAst statementBlockAst) => null; 271 | public object VisitStringConstantExpression(StringConstantExpressionAst stringConstantExpressionAst) => null; 272 | public object VisitSubExpression(SubExpressionAst subExpressionAst) => null; 273 | public object VisitSwitchStatement(SwitchStatementAst switchStatementAst) => null; 274 | public object VisitThrowStatement(ThrowStatementAst throwStatementAst) => null; 275 | public object VisitTrap(TrapStatementAst trapStatementAst) => null; 276 | public object VisitTryStatement(TryStatementAst tryStatementAst) => null; 277 | public object VisitTypeConstraint(TypeConstraintAst typeConstraintAst) => null; 278 | public object VisitTypeExpression(TypeExpressionAst typeExpressionAst) => null; 279 | public object VisitUnaryExpression(UnaryExpressionAst unaryExpressionAst) => null; 280 | public object VisitUsingExpression(UsingExpressionAst usingExpressionAst) => null; 281 | public object VisitVariableExpression(VariableExpressionAst variableExpressionAst) => null; 282 | public object VisitWhileStatement(WhileStatementAst whileStatementAst) => null; 283 | #pragma warning restore SA1600, SA1201, SA1516 284 | } 285 | } 286 | --------------------------------------------------------------------------------