{"id":19080418,"url":"https://github.com/seeminglyscience/pslambda","last_synced_at":"2025-04-30T06:10:21.700Z","repository":{"id":87373525,"uuid":"130616502","full_name":"SeeminglyScience/PSLambda","owner":"SeeminglyScience","description":"A runtime compiler for PowerShell ScriptBlock objects.","archived":false,"fork":false,"pushed_at":"2019-08-24T17:49:16.000Z","size":109,"stargazers_count":63,"open_issues_count":15,"forks_count":3,"subscribers_count":5,"default_branch":"master","last_synced_at":"2025-04-18T21:30:56.708Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/SeeminglyScience.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"docs/CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"docs/CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-04-22T23:31:57.000Z","updated_at":"2025-01-27T10:14:08.000Z","dependencies_parsed_at":"2023-03-30T06:22:26.861Z","dependency_job_id":null,"html_url":"https://github.com/SeeminglyScience/PSLambda","commit_stats":null,"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SeeminglyScience%2FPSLambda","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SeeminglyScience%2FPSLambda/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SeeminglyScience%2FPSLambda/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SeeminglyScience%2FPSLambda/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/SeeminglyScience","download_url":"https://codeload.github.com/SeeminglyScience/PSLambda/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":251651231,"owners_count":21621716,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-09T02:23:33.080Z","updated_at":"2025-04-30T06:10:21.687Z","avatar_url":"https://github.com/SeeminglyScience.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# PSLambda\n\nPSLambda is a runtime compiler for PowerShell ScriptBlock objects. This project is in a very early\nstate and is not currently recommended for use in production environments.\n\nThis project adheres to the Contributor Covenant [code of conduct](https://github.com/SeeminglyScience/PSLambda/tree/master/docs/CODE_OF_CONDUCT.md).\nBy participating, you are expected to uphold this code. Please report unacceptable behavior to seeminglyscience@gmail.com.\n\n## Build status\n\n|AppVeyor (Windows)|CircleCI (Linux)|CodeCov|\n|---|---|---|\n|[![Build status](https://ci.appveyor.com/api/projects/status/8n8alv7moy661rr6?svg=true)](https://ci.appveyor.com/project/SeeminglyScience/pslambda) |[![CircleCI](https://circleci.com/gh/SeeminglyScience/PSLambda.svg?style=svg)](https://circleci.com/gh/SeeminglyScience/PSLambda)|[![codecov](https://codecov.io/gh/SeeminglyScience/PSLambda/branch/master/graph/badge.svg)](https://codecov.io/gh/SeeminglyScience/PSLambda)|\n\n## Features\n\n- C# like syntax (due to being compiled from interpreted Linq expression trees) with some PowerShell\n  convenience features built in. (See the \"Differences from PowerShell\" section)\n\n- Run in threads without a `DefaultRunspace` as it does not actually execute any PowerShell code.\n\n- Access and change variables local to the scope the delegate was created in (similar to closures in C#)\n\n- Runs faster than PowerShell in most situations.\n\n- Parse errors similar to the PowerShell parser for compiler errors, including extent of the error.\n\n## Motivation\n\nPowerShell is an excellent engine for exploration. When I'm exploring a new API, even if I intend to write\nthe actual project in C# I will do the majority of my exploration from PowerShell. Most of the time there\nare no issues with doing that. Sometimes though, in projects that make heavy use of delegates you can run\ninto issues.\n\nYes the PowerShell engine can convert from `ScriptBlock` to any `Delegate` type, but it's just\na wrapper.  It still requires that `ScriptBlock` to be ran in a `Runspace` at some point. Sometimes\nthat isn't possible, or just isn't ideal. Mainly when it comes to API's that are mainly `async`/`await`\nbased.\n\nI also just really like the idea of a more strict syntax in PowerShell without losing **too** much flavor,\nso it was a fun project to get up and running.\n\n## What would I use this for\n\nFor most folks the answer is probably nothing. This is pretty niche, if you haven't specifically wished\nsomething like this existed in the past, it probably isn't applicable to you.\n\nThe reason cited in \"Motivation\" (interactive API exploration and prototyping) is the only real application\nI am specifically planning for. If anyone has any intention of using this more broadly, I'd appreciate\nit if you could open an issue or shoot me a DM so I can keep your scenario in mind.\n\n## Installation\n\n### Gallery\n\n```powershell\nInstall-Module PSLambda -Scope CurrentUser\n```\n\n### Source\n\n```powershell\ngit clone 'https://github.com/SeeminglyScience/PSLambda.git'\nSet-Location .\\PSLambda\nInvoke-Build -Task Install -Configuration Release\n```\n\n## How\n\nA custom `ICustomAstVisitor2` class visits each node in the abstract syntax tree of the ScriptBlock and\ninterprets it as a `System.Linq.Expressions.Expression`. Most of the PowerShell language features are\nrecreated from scratch as a Linq expression tree, using PowerShell API's where it makes sense to keep\nsome flavor. This is actually more or less what the PowerShell engine does as well, but it's obviously\nstill heavily reliant on the `Runspace`/`SessionState` system.\n\nThe `psdelegate` type acts as a stand in for the compiled delegate until it is interpreted. Using the\nPowerShell type conversion system, if it is passed as an argument to a method that requires a specific\ndelegate type it is interpreted and compiled at method invocation.\n\n## Usage\n\nThere's two main ways to use the module.\n\n1. The `New-PSDelegate` command - this command will take a `ScriptBlock` and optionally a target `Delegate` type. With this method the delegate will be compiled immediately and will need to be recompiled if the delegate type needs to change.\n\n1. The `psdelegate` type accelerator - you can cast a `ScriptBlock` as this type and it will retain the context until converted. This object can then be converted to a specific `Delegate` type later, either by explicittly casting the object as that type or implicitly as a method argument. Local variables are retained from when the `psdelegate` object is created. This method requires that the module be imported into the session as the type will not exist until it is.\n\n### Demonstration of variable scoping and multi-threading\n\n```powershell\nusing namespace System.Threading\nusing namespace System.Threading.Tasks\nusing namespace System.Collections.Concurrent\n\n# PowerShell variables\n$queue = [BlockingCollection[string]]::new()\n$shouldCancel = [CancellationTokenSource]::new()\n\n# Everything inside this is compiled.\n$delegate = [psdelegate]{\n    # The ThreadStart block doesn't require \"psdelegate\" because this is still within the\n    # expression tree, including the resulting nested delegate.  Scope is kept.\n    $thread = [Thread]::new([ThreadStart]{\n        try {\n            # Take from the blocking collection defined in PowerShell, and throw an\n            # OperationCancelledException if the cancellation token source defined in\n            # PowerShell has been cancelled.\n            while ($message = $queue.Take($shouldCancel.Token)) {\n                # All \"AllScope\" variables are available including $Host\n                $Host.UI.WriteLine($message)\n\n                # Implicit ScriptBlock to delegate conversion when it can determine the\n                # method argument type correctly\n                $task = [Task]::Run{\n                    [Thread]::Sleep(1000)\n                    # More Delegate nesting, still retaining variable scope.\n                    $Host.UI.WriteLine(\"Delayed: $message\")\n                }\n\n                $task.GetAwaiter().GetResult()\n            }\n        # Make sure to catch anything that could throw inside the ThreadStart delegate, uncaught\n        # exceptions will crash PowerShell. Uncaught exceptions are fine outside of the ThreadStart\n        # delegate and will throw like any other method.\n        } catch [OperationCancelledException] {\n            $Host.UI.WriteLine('Thread closed!')\n        }\n    })\n\n    $thread.Start()\n    return $thread\n}\n\n$thread = $delegate.Invoke()\n$queue.Add('This is a test message!')\n$queue.Add('Another test!')\n$queue.Add('So many tests')\n\n# To close the thread use:\n# $shouldCancel.Cancel()\n```\n\n### Create a psdelegate to pass to a method\n\n```powershell\n$a = 0\n$delegate = [psdelegate]{ $a += 1 }\n$actions = $delegate, $delegate, $delegate, $delegate\n[System.Threading.Tasks.Parallel]::Invoke($actions)\n$a\n# 4\n```\n\nCreates a delegate that increments the local `PSVariable` \"a\", and then invokes it in a different thread\nfour times. Doing the same with `ScriptBlock` objects instead would result in an error stating the\nthe thread does not have a default runspace.\n\n*Note*: Access to local variables from the `Delegate` is synced across threads for *some* thread safety,\nbut that doesn't effect anything accessing the variable directly so they are still *not* thread safe.\n\n### Access all scope variables\n\n```powershell\n$delegate = New-PSDelegate { $ExecutionContext.SessionState.InvokeProvider.Item.Get(\"\\\") }\n$delegate.Invoke()\n#     Directory:\n# Mode          LastWriteTime   Length Name\n# ----          -------------   ------ ----\n# d--hs-  3/32/2010   9:61 PM          C:\\\n```\n\n### Use alternate C# esque delegate syntax\n\n```powershell\n$timer = [System.Timers.Timer]::new(1000)\n$delegate = [psdelegate]{ ($sender, $e) =\u003e { $Host.UI.WriteLine($e.SignalTime.ToString()) }}\n$timer.Enabled = $true\n$timer.add_Elapsed($delegate)\nStart-Sleep 10\n$timer.remove_Elapsed($delegate)\n```\n\nCreate a timer that fires an event every 1000 milliseconds, then add a compiled delegated as an event\nhandler. A couple things to note here.\n\n1. Parameter type inference is done automatically during conversion. This is important because the\n  compiled delegate is *not* dynamically typed like PowerShell is.\n1. `psdelegate` objects that have been converted to a specific Delegate type are cached, so the instance\n  is the same in both `add_Elapsed` and `remove_Elapsed`.\n\n## Differences from PowerShell\n\nWhile the `ScriptBlock`'s used in the examples look like (and for the most part are) valid PowerShell\nsyntax, very little from PowerShell is actually used. The abstract syntax tree (AST) is read and\ninterpreted into a `System.Linq.Expressions.Expression` tree. The rules are a lot closer to C# than\nto normal PowerShell. There are some very PowerShell like things thrown in to make it feel a bit more\nlike PowerShell though.\n\n### Supported PowerShell features\n\n- All operators, including `like`, `match`, `split`, and all the case sensitive/insensitive comparision\n  operators. These work mostly the exact same as in PowerShell due to using `LanguagePrimitives` under\n  the hood.\n\n- PowerShell conversion rules. Explicit conversion (e.g. `[int]$myVar`) is done using `LanguagePrimitives`.\n  This allows for all of the PowerShell type conversions to be accessible from the compiled delegate.\n  However, type conversion is *not* automatic like it often is in PowerShell. Comparision operators are\n  the exception, as they also use `LanguagePrimitives`.\n\n- Access to PSVariables.  Any variable that is either `AllScope` (like `$Host` and `$ExecutionContext`)\n  is from the most local scope is available as a variable in the delegate. This works similar to\n  closures in C#, allowing the current value to be read as well as changed.  Changes to the value will\n  only be seen in the scope the delegate was created in (unless the variable is AllScope).\n\n### Unsupported PowerShell features\n\n- The extended type system and dynamic typing in genernal.  This means that things like methods, properties\n  and even the specific method overload called by an expression is all determined at compile time. If\n  a method declares a return type of `object`, you'll need to cast it to something else before you can\n  do much with it.\n\n- Commands. Yeah that's a big one I know. There's no way to run a command without a runspace, and if I\n  require a runspace to run a command then there isn't a point in any of this. If you absolutely need\n  to run a command, you can use the `$ExecutionContext` or `[powershell]` API's, but they are likely to\n  be unreliable from a thread other than the pipeline thread.\n\n- Variables need to assigned before they can be used.  If the type is not included in the assignment\n  the type will be inferred from the assignment (similar to C#'s `var` keyword, but implied).\n\n- A good amount more. I'll update this list as much as possible as I use this more. If you run into\n  something that could use explaining here, opening an issue on this repo would be very helpful.\n\n## Custom language keywords\n\nNo support for commands left some room to add some custom keywords that help with some problems\nthat aren't all that applicable to PowerShell, but may be here.\n\n### With\n\n```powershell\nusing namespace System.Management.Automation\n\n$delegate = [psdelegate]{\n    with ($ps = [powershell]::Create([RunspaceMode]::NewRunspace)) {\n        $result = $ps.AddScript('Get-Date').Invoke()\n        $result[0].Properties.Add(\n            [psnoteproperty]::new(\n                'Runspace',\n                $ps.Runspace))\n\n        return $result[0]\n    }\n}\n\n$pso = $delegate.Invoke()\n$pso\n# Sunday, April 22, 2018 6:51:04 PM\n$pso.Runspace\n# Id Name        ComputerName  Type     State   Availability\n# -- ----        ------------  ----     -----   ------------\n# 13 Runspace13  localhost     Local    Closed  None\n```\n\nThe `with` keyword works like the `using` keyword in C#. The object in the initialization expression (the\nparenthesis) will be disposed when the statement ends, even if the event of an unhandled exception.\n\n### Default\n\n```powershell\n\n$delegate = [psdelegate]{ default([ConsoleColor]) }\n$delegate.Invoke()\n# Black\n\n$delegate = [psdelegate]{ default([object]) }\n$delegate.Invoke()\n# (returns null)\n```\n\nReturns the default value for a type. This will return `null` if the type is nullable, otherwise it\nwill return the default value for the that type.\n\n### Generic\n\n```powershell\n$delegate = [psdelegate]{ generic ([array]::Empty(), [int]) }\n$delegate.Invoke().GetType()\n# IsPublic IsSerial Name       BaseType\n# -------- -------- ----       --------\n# True     True     Int32[]    System.Array\n\n$delegate = [psdelegate]{ generic ([tuple]::Create(10, 'string'), [int], [string]) }\n$delegate.Invoke()\n# Item1 Item2  Length\n# ----- -----  ------\n#    10 string      2\n```\n\nI haven't built in any automatic generic argument inference yet (PowerShell has a little bit), so\nright now this keyword is required to use any generic method. The syntax for the generic type arguments\nis similar to the `params` keyword.  The first \"parameter\" should be the the member expression, and\nany additional parameters should be types. The resulting `Expression` is just the member expression\nwith the resolved method.\n\n## Contributions Welcome!\n\nWe would love to incorporate community contributions into this project.  If you would like to\ncontribute code, documentation, tests, or bug reports, please read our [Contribution Guide](https://github.com/SeeminglyScience/PSLambda/tree/master/docs/CONTRIBUTING.md) to learn more.\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fseeminglyscience%2Fpslambda","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fseeminglyscience%2Fpslambda","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fseeminglyscience%2Fpslambda/lists"}