{"id":25921887,"url":"https://github.com/zgoethel/nodebuilder","last_synced_at":"2026-04-13T04:02:39.229Z","repository":{"id":280340644,"uuid":"937478963","full_name":"zgoethel/NodeBuilder","owner":"zgoethel","description":"Tool for designing grammars and generating language parsers in .NET","archived":false,"fork":false,"pushed_at":"2025-03-02T21:15:23.000Z","size":5062,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-03-02T21:28:39.235Z","etag":null,"topics":["blazor","compiler","compiler-frontend","desktop-app","dotnet","finite-state-machine","grammar","lexing","parser","parsing","regex","ui"],"latest_commit_sha":null,"homepage":"","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/zgoethel.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"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":"2025-02-23T06:31:35.000Z","updated_at":"2025-03-02T21:15:25.000Z","dependencies_parsed_at":"2025-03-02T21:38:42.742Z","dependency_job_id":null,"html_url":"https://github.com/zgoethel/NodeBuilder","commit_stats":null,"previous_names":["zgoethel/nodebuilder"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zgoethel%2FNodeBuilder","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zgoethel%2FNodeBuilder/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zgoethel%2FNodeBuilder/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zgoethel%2FNodeBuilder/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/zgoethel","download_url":"https://codeload.github.com/zgoethel/NodeBuilder/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":241696179,"owners_count":20004748,"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":["blazor","compiler","compiler-frontend","desktop-app","dotnet","finite-state-machine","grammar","lexing","parser","parsing","regex","ui"],"created_at":"2025-03-03T16:16:32.635Z","updated_at":"2025-12-31T01:03:03.878Z","avatar_url":"https://github.com/zgoethel.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Node Builder\nUI tool for creating grammars and generating parsers\n\nThis project aims to be a frontend to a set of parsing utilities. It implements common patterns used by expression trees and statements. These patterns can be composed into more complex grammars, and the resulting language parser should be reusable in any (compatible) .NET project.\n\nUI editing features allow simple interactive design of grammar elements.\n\n# `2025-03-13`\n\nThe grammar design frontend uses a blend of Windows Forms dialogs and WebView2 panes hosting Blazor components.\n\nNodes are draggable in the graph editing pane. The pane itself is scalable and pannable. Nodes are drawn as HTML content placed absolutely in the graph editing pane. SVG arcs are drawn from node handle to node handle by querying the DOM for the client positions of node handle anchors.\n\nGrammar tokens will eventually live in the \"Tokens\" form. For now, there is a regex editing and testing modal built on the included regular expression implementation.\n\n![Interactive nodes and base forms for describing token sets](https://i.imgur.com/UP4UlW3.png)\n\n_Interactive nodes and base forms for describing token sets_\n\n# `2025-03-02`\n\n## Stack Usage\n\nParsing execution is trampolined and an execution queue and stack are maintained in the heap.\n\nEach recursive call is able to produce two types of future work,\n * \"work\"\u0026mdash;additional parsing work which will be executed FIFO eventually after exiting\n   - A \"work\" task can queue nested \"work\" or \"tail\" tasks\n   - `addWork` returns a handle which contains a `Result` value\n * \"tail\"\u0026mdash;additional parsing work which will be executed FILO after all \"work\" tasks are exhausted\n   - A \"tail\" task can queue nested \"work\" or \"tail\" tasks\n   - Values returned by the last-executed `addTail` task will be propagated to as the parent task's `Result` value.\n\n and is implemented as a task of the delegate type `Trampoline.WorkUnit`. There is no guarantee the `Result` value of the handle returned by `addWork` is available unless executing within an `addTail` following that `addWork`.\n\n ```csharp\nTrampoline.WorkUnit Function(Trampoline.WorkUnit subFunction)\n{\n    return (addWork, addTail) =\u003e\n    {\n        var subValue = addWork(subFunction);\n\n        // subValue.Result is not ready yet\n\n        addWork((addWork, addTail) =\u003e\n        {\n            // Execution order of `addWork` calls is guaranteed; however,\n            // subValue.Result may not be ready yet\n\n            return null;\n        });\n\n        addTail((addWork, addTail) =\u003e\n        {\n            // subValue.Result should be ready now\n\n            // Propagate sub-value as return value of parent task\n            return subValue.Result;\n        });\n\n        return null;\n    };\n}\n ```\n\nThe `Trampoline` continuously loops and executes one task at a time,\n * from the FIFO \"work\" queue if any tasks are present\n * from the \"tail\" stack if no FIFO tasks are present but a task is on the stack; popping from this stack represents a function completing and returning a result\n\nAny object returned from the last-executed task added via `addTail` will be propagated to the nearest containing `addWork` as its `Result` value; if there is no such parent, the result will be propagated all the way and be returned by `Trampoline.Execute`.\n\nIf a `Trampoline.WorkUnit` never calls `addTail`, the returned value from the `Trampoline.WorkUnit` delegate will be used as `Result`.\n\n```csharp\nTrampoline.WorkUnit SubFunction(string text)\n{\n    return (addWork, addTail) =\u003e\n    {\n        // addWork(...);\n\n        // Result value used if `addTail` is never called\n        return text;\n    };\n}\n```\n\nCompose the two functions and execute them on the trampoline to yield `\"Hello, world!\"`.\n\n```csharp\nvar subFunction = SubFunction(\"Hello, world!\");\nvar function = Function(subFunction);\n\nvar result = await Trampoline.Execute(function, CancellationToken.None);\n\nConsole.WriteLine(result);\n```\n\n## Example of Direct Parsing Implementation\nThe following calls would be designed and assembled in the background, controlled by higher-level user input. This demo references the backend implementation directly to illustrate the parsing approach.\n\nBuild a set of tokens.\n```csharp\nvar fsa = new Fsa();\n\nfsa.Build(\"[0-9]+\", (int)_Token.Number);\nfsa.Build(\"\\\\+\", (int)_Token.Add);\nfsa.Build(\"\\\\-\\\\\u003e\", (int)_Token.Deref);\nfsa.Build(\"\\\\-\", (int)_Token.Subtract);\nfsa.Build(\"\\\\*\", (int)_Token.Multiply);\nfsa.Build(\"\\\\/\", (int)_Token.Divide);\nfsa.Build(\"\\\\.\", (int)_Token.Access);\nfsa.Build(\"\\\\,\", (int)_Token.Comma);\nfsa.Build(\"\\\\^\", (int)_Token.Exponent);\nfsa.Build(\"\\\\(\", (int)_Token.OpenParens);\nfsa.Build(\"\\\\)\", (int)_Token.CloseParens);\nfsa.Build(\"\\\\!\", (int)_Token.Not);\nfsa.Build(\"[ \\n\\r\\t]+\", 9999);\n\nfsa = fsa.ConvertToDfa().MinimizeDfa();\n```\n\nPrepare the input text.\n```csharp\nvar source = \"((1 + 2).3.4(40, 41, 42)-\u003e5) * !!6-\u003e7 / (8^9)^10.11^(12)\";\nvar stream = new TokenStream(fsa, source);\n```\n\nBuild a grammar, defined as composed anonymous functions of type `Trampoline.WorkUnit`.\n```csharp\n// Grammar top-level, which will be available later\nTrampoline.WorkUnit? _expr = null;\nobject? expr(Trampoline.WorkBuilder a, Trampoline.WorkBuilder b)\n{\n    return _expr?.Invoke(a, b);\n}\n\nvar literal = Production.Literal(\n    [\n        (int)_Token.Number\n    ]);\n\nvar parens = Production.Body(\n    startToken: (int)_Token.OpenParens,\n    endToken: (int)_Token.CloseParens,\n    content: expr);\n\nvar member = Production.FirstSet(\n    fallback: null,\n    ahead: 0,\n    ((int)_Token.OpenParens, parens),\n    ((int)_Token.Number, literal));\n\nvar invokeParameterList = Production.Body(\n    startToken: (int)_Token.OpenParens,\n    endToken: (int)_Token.CloseParens,\n    content: Production.InfixPostfixOperator(\n        [\n            ((int)_Token.Comma, true, null)\n        ],\n        expr));\n\nvar exprA = Production.InfixPostfixOperator(\n    [\n        ((int)_Token.Deref, true, null),\n        ((int)_Token.Access, true, null),\n        ((int)_Token.OpenParens, false, invokeParameterList)\n    ],\n    nextPrecedence: member);\n\nvar exprB = Production.InfixPostfixOperator(\n    [\n        ((int)_Token.Exponent, true, null)\n    ],\n    nextPrecedence: exprA,\n    assoc: SD.Associativity.Right);\n\nvar exprC = Production.PrefixOperator(\n    [\n        ((int)_Token.Not, null)\n    ],\n    nextPrecedence: exprB);\n\nvar exprD = Production.InfixPostfixOperator(\n    [\n        ((int)_Token.Multiply, true, null),\n        ((int)_Token.Divide, true, null)\n    ],\n    nextPrecedence: exprC);\n\nvar exprE = Production.InfixPostfixOperator(\n    [\n        ((int)_Token.Add, true, null),\n        ((int)_Token.Subtract, true, null)\n    ],\n    nextPrecedence: exprD);\n\n// Make grammar top-level available\n_expr = exprE;\n```\n\nThen execute the parser.\n```csharp\nvar parserOutput = await ParserContext.Begin(\n    stream,\n    async () =\u003e await Trampoline.Execute(expr, CancellationToken.None));\n```\n\nThe original input text\n```\n((1 + 2).3.4(40, 41, 42)-\u003e5) * !!6-\u003e7 / (8^9)^10.11^(12)\n```\n\nproduces the following `parserOutput` (whose tree structure mirrors the precedence rules of the built expression grammar):\n\u003cdetails\u003e \n  \u003csummary\u003e\u003cb\u003eJSON of resulting syntax tree\u003c/b\u003e\u003c/summary\u003e\n\n```json\n{\n  \"Assoc\": \"Left\",\n  \"Members\": [\n    {\n      \"OpToken\": 0,\n      \"OpText\": \"\",\n      \"Value\": {\n        \"Assoc\": \"Left\",\n        \"Members\": [\n          {\n            \"OpToken\": 0,\n            \"OpText\": \"\",\n            \"Value\": {\n              \"Assoc\": \"Left\",\n              \"Members\": [\n                {\n                  \"OpToken\": 0,\n                  \"OpText\": \"\",\n                  \"Value\": {\n                    \"Token\": 1,\n                    \"Text\": \"1\"\n                  },\n                  \"Data\": null\n                },\n                {\n                  \"OpToken\": 2,\n                  \"OpText\": \"+\",\n                  \"Value\": {\n                    \"Token\": 1,\n                    \"Text\": \"2\"\n                  },\n                  \"Data\": null\n                }\n              ]\n            },\n            \"Data\": null\n          },\n          {\n            \"OpToken\": 7,\n            \"OpText\": \".\",\n            \"Value\": {\n              \"Token\": 1,\n              \"Text\": \"3\"\n            },\n            \"Data\": null\n          },\n          {\n            \"OpToken\": 7,\n            \"OpText\": \".\",\n            \"Value\": {\n              \"Token\": 1,\n              \"Text\": \"4\"\n            },\n            \"Data\": null\n          },\n          {\n            \"OpToken\": 10,\n            \"OpText\": \"(\",\n            \"Value\": null,\n            \"Data\": {\n              \"Assoc\": \"Left\",\n              \"Members\": [\n                {\n                  \"OpToken\": 0,\n                  \"OpText\": \"\",\n                  \"Value\": {\n                    \"Token\": 1,\n                    \"Text\": \"40\"\n                  },\n                  \"Data\": null\n                },\n                {\n                  \"OpToken\": 8,\n                  \"OpText\": \",\",\n                  \"Value\": {\n                    \"Token\": 1,\n                    \"Text\": \"41\"\n                  },\n                  \"Data\": null\n                },\n                {\n                  \"OpToken\": 8,\n                  \"OpText\": \",\",\n                  \"Value\": {\n                    \"Token\": 1,\n                    \"Text\": \"42\"\n                  },\n                  \"Data\": null\n                }\n              ]\n            }\n          },\n          {\n            \"OpToken\": 6,\n            \"OpText\": \"-\u003e\",\n            \"Value\": {\n              \"Token\": 1,\n              \"Text\": \"5\"\n            },\n            \"Data\": null\n          }\n        ]\n      },\n      \"Data\": null\n    },\n    {\n      \"OpToken\": 4,\n      \"OpText\": \"*\",\n      \"Value\": {\n        \"OpToken\": 12,\n        \"OpText\": \"!\",\n        \"Value\": {\n          \"OpToken\": 12,\n          \"OpText\": \"!\",\n          \"Value\": {\n            \"Assoc\": \"Left\",\n            \"Members\": [\n              {\n                \"OpToken\": 0,\n                \"OpText\": \"\",\n                \"Value\": {\n                  \"Token\": 1,\n                  \"Text\": \"6\"\n                },\n                \"Data\": null\n              },\n              {\n                \"OpToken\": 6,\n                \"OpText\": \"-\u003e\",\n                \"Value\": {\n                  \"Token\": 1,\n                  \"Text\": \"7\"\n                },\n                \"Data\": null\n              }\n            ]\n          },\n          \"Data\": null\n        },\n        \"Data\": null\n      },\n      \"Data\": null\n    },\n    {\n      \"OpToken\": 5,\n      \"OpText\": \"/\",\n      \"Value\": {\n        \"Assoc\": \"Right\",\n        \"Members\": [\n          {\n            \"OpToken\": 0,\n            \"OpText\": \"\",\n            \"Value\": {\n              \"Assoc\": \"Right\",\n              \"Members\": [\n                {\n                  \"OpToken\": 0,\n                  \"OpText\": \"\",\n                  \"Value\": {\n                    \"Token\": 1,\n                    \"Text\": \"8\"\n                  },\n                  \"Data\": null\n                },\n                {\n                  \"OpToken\": 9,\n                  \"OpText\": \"^\",\n                  \"Value\": {\n                    \"Token\": 1,\n                    \"Text\": \"9\"\n                  },\n                  \"Data\": null\n                }\n              ]\n            },\n            \"Data\": null\n          },\n          {\n            \"OpToken\": 9,\n            \"OpText\": \"^\",\n            \"Value\": {\n              \"Assoc\": \"Left\",\n              \"Members\": [\n                {\n                  \"OpToken\": 0,\n                  \"OpText\": \"\",\n                  \"Value\": {\n                    \"Token\": 1,\n                    \"Text\": \"10\"\n                  },\n                  \"Data\": null\n                },\n                {\n                  \"OpToken\": 7,\n                  \"OpText\": \".\",\n                  \"Value\": {\n                    \"Token\": 1,\n                    \"Text\": \"11\"\n                  },\n                  \"Data\": null\n                }\n              ]\n            },\n            \"Data\": null\n          },\n          {\n            \"OpToken\": 9,\n            \"OpText\": \"^\",\n            \"Value\": {\n              \"Token\": 1,\n              \"Text\": \"12\"\n            },\n            \"Data\": null\n          }\n        ]\n      },\n      \"Data\": null\n    }\n  ]\n}\n```\n\n\u003c/details\u003e","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzgoethel%2Fnodebuilder","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fzgoethel%2Fnodebuilder","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzgoethel%2Fnodebuilder/lists"}