diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 68509282..8a31be54 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -10,13 +10,13 @@ jobs:
matrix:
configuration: ["Debug", "Release"]
steps:
- - uses: actions/checkout@v2
- - uses: actions/setup-node@v1
+ - uses: actions/checkout@v3
+ - uses: actions/setup-node@v3
with:
- node-version: 16.11.1
- - uses: actions/setup-dotnet@v1.7.2
+ node-version: 18
+ - uses: actions/setup-dotnet@v3
with:
- dotnet-version: 2.1.816
+ dotnet-version: '8.0.x'
- run: yarn --ci
working-directory: ./Source/SmallBasic.Client
- run: dotnet restore
diff --git a/.github/workflows/master_smallbasic-publicwebsite-code.yml b/.github/workflows/master_smallbasic-publicwebsite-code.yml
index b60c4f38..5e6dacef 100644
--- a/.github/workflows/master_smallbasic-publicwebsite-code.yml
+++ b/.github/workflows/master_smallbasic-publicwebsite-code.yml
@@ -1,53 +1,53 @@
-# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy
-# More GitHub Actions for Azure: https://github.com/Azure/actions
-
-name: Build and deploy to Caseysc`s Azure Web App - smallbasic-publicwebsite-code
-
-on:
- push:
- branches:
- - master
- workflow_dispatch:
-
-jobs:
- build:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v2
- - uses: actions/setup-node@v1
- with:
- node-version: 16.11.1
- - uses: actions/setup-dotnet@v1.7.2
- with:
- dotnet-version: 2.1.816
- - run: yarn --ci
- working-directory: ./Source/SmallBasic.Client
- - run: dotnet restore
- - run: dotnet build ./SmallBasic.sln /p:TreatWarningsAsErrors=True /p:Configuration=Release
- - run: dotnet publish ./Source/SmallBasic.Editor/SmallBasic.Editor.csproj --no-build /p:TreatWarningsAsErrors=True /p:Configuration=Release
- - uses: actions/upload-artifact@v3.1.0
- with:
- path: /home/runner/work/smallbasic-editor/smallbasic-editor/Source/SmallBasic.Editor/bin/Release/netstandard2.0/publish/
- if-no-files-found: error
-
- deploy:
- runs-on: windows-latest
- needs: build
- environment:
- name: 'Production'
- url: ${{ steps.deploy-to-webapp.outputs.webapp-url }}
-
- steps:
- - name: Download artifact from build job
- uses: actions/download-artifact@v2
- with:
- name: artifact
-
- - name: Deploy to Azure Web App
- id: deploy-to-webapp
- uses: azure/webapps-deploy@v2
- with:
- app-name: 'smallbasic-publicwebsite-code'
- slot-name: 'Production'
- publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_CC78D407C64043A1978FBF8A5FEDA312 }}
- package: .
+# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy
+# More GitHub Actions for Azure: https://github.com/Azure/actions
+
+name: Build and deploy to Caseysc`s Azure Web App - smallbasic-publicwebsite-code
+
+on:
+ push:
+ branches:
+ - master
+ workflow_dispatch:
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/setup-node@v3
+ with:
+ node-version: 18
+ - uses: actions/setup-dotnet@v3
+ with:
+ dotnet-version: '8.0.x'
+ - run: yarn --ci
+ working-directory: ./Source/SmallBasic.Client
+ - run: dotnet restore
+ - run: dotnet build ./SmallBasic.sln /p:TreatWarningsAsErrors=True /p:Configuration=Release
+ - run: dotnet publish ./Source/SmallBasic.Editor/SmallBasic.Editor.csproj --no-build /p:TreatWarningsAsErrors=True /p:Configuration=Release
+ - uses: actions/upload-artifact@v3.1.0
+ with:
+ path: /home/runner/work/smallbasic-editor/smallbasic-editor/Source/SmallBasic.Editor/bin/Release/netstandard2.0/publish/
+ if-no-files-found: error
+
+ deploy:
+ runs-on: windows-latest
+ needs: build
+ environment:
+ name: 'Production'
+ url: ${{ steps.deploy-to-webapp.outputs.webapp-url }}
+
+ steps:
+ - name: Download artifact from build job
+ uses: actions/download-artifact@v3
+ with:
+ name: artifact
+
+ - name: Deploy to Azure Web App
+ id: deploy-to-webapp
+ uses: azure/webapps-deploy@v2
+ with:
+ app-name: 'smallbasic-publicwebsite-code'
+ slot-name: 'Production'
+ publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_CC78D407C64043A1978FBF8A5FEDA312 }}
+ package: .
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
new file mode 100644
index 00000000..7e9a2607
--- /dev/null
+++ b/.github/workflows/release.yaml
@@ -0,0 +1,67 @@
+name: Release Standalone Binaries and IDE
+
+on:
+ release:
+ types: [published, created]
+ push:
+ tags:
+ - '*'
+ - 'v*'
+ workflow_dispatch:
+
+jobs:
+ build-release:
+ name: Build Standalone Release (${{ matrix.target.os }})
+ runs-on: ${{ matrix.target.runner }}
+ strategy:
+ matrix:
+ target:
+ - os: win-x64
+ runner: windows-latest
+ asset: smallbasic-win-x64.zip
+ - os: linux-x64
+ runner: ubuntu-latest
+ asset: smallbasic-linux-x64.tar.gz
+ - os: osx-x64
+ runner: macos-latest
+ asset: smallbasic-macos-x64.tar.gz
+
+ steps:
+ - name: Checkout Code
+ uses: actions/checkout@v3
+
+ - name: Setup Node.js 18
+ uses: actions/setup-node@v3
+ with:
+ node-version: 18
+
+ - name: Setup .NET 8 SDK
+ uses: actions/setup-dotnet@v3
+ with:
+ dotnet-version: '8.0.x'
+
+ - name: Restore Node Packages for Webpack
+ run: yarn --ci
+ working-directory: ./Source/SmallBasic.Client
+
+ - name: Publish Self-Contained CLI / Compiler
+ run: dotnet publish Source/SmallBasic.Cli/SmallBasic.Cli.csproj -c Release -r ${{ matrix.target.os }} --self-contained true /p:PublishSingleFile=true -o dist/${{ matrix.target.os }}
+
+ - name: Publish Editor / IDE
+ run: dotnet publish Source/SmallBasic.Editor/SmallBasic.Editor.csproj -c Release -o dist/${{ matrix.target.os }}/editor
+
+ - name: Package Release Assets (Windows)
+ if: matrix.target.runner == 'windows-latest'
+ run: Compress-Archive -Path dist/${{ matrix.target.os }}/* -DestinationPath ${{ matrix.target.asset }}
+
+ - name: Package Release Assets (Unix)
+ if: matrix.target.runner != 'windows-latest'
+ run: tar -czvf ${{ matrix.target.asset }} -C dist/${{ matrix.target.os }} .
+
+ - name: Attach Assets to GitHub Release
+ uses: softprops/action-gh-release@v1
+ with:
+ files: ${{ matrix.target.asset }}
+ tag_name: ${{ github.event.release.tag_name || github.ref_name }}
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.gitignore b/.gitignore
index fd3348bb..f4428095 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,5 +5,10 @@
# Output files
bin
obj
+dist/
node_modules
wwwroot
+*.exe
+*.dll
+*.pdb
+*.runtimeconfig.json
diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 00000000..b109b3fb
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,8 @@
+
+
+ false
+ none
+ direct
+ NU1901;NU1902;NU1903;NU1904;NETSDK1138
+
+
diff --git a/Directory.Build.targets b/Directory.Build.targets
new file mode 100644
index 00000000..f4641260
--- /dev/null
+++ b/Directory.Build.targets
@@ -0,0 +1,16 @@
+
+
+ false
+ none
+ direct
+ $(WarningsNotAsErrors);NU1901;NU1902;NU1903;NU1904;NETSDK1138
+ $(NoWarn);NU1901;NU1902;NU1903;NU1904;NETSDK1138
+
+
+
+
+ $(WarningsNotAsErrors);NU1901;NU1902;NU1903;NU1904;NETSDK1138
+ $(NoWarn);NU1901;NU1902;NU1903;NU1904;NETSDK1138
+
+
+
diff --git a/LANGUAGE_EXTENSIONS.md b/LANGUAGE_EXTENSIONS.md
new file mode 100644
index 00000000..e90fa61c
--- /dev/null
+++ b/LANGUAGE_EXTENSIONS.md
@@ -0,0 +1,128 @@
+# Small Basic Language Extensions Documentation
+
+This repository contains modern, powerful extensions to the Small Basic compiler and runtime. Below is a comprehensive guide to all newly added language features, control flow constructs, file module imports, and .NET interop libraries.
+
+---
+
+## 1. Functions with Parameters and Return Statements
+
+Small Basic now supports standard parameter-passing subroutines and functions with return values.
+
+### Keywords
+- `Function` or `Fn`
+- `EndFunction` or `EndFn`
+- `Return`
+
+### Syntax & Examples
+
+```smallbasic
+Function Add(a, b)
+ Return a + b
+EndFunction
+
+sum = Add(15, 25) ' sum = 40
+```
+
+Recursive functions are fully supported:
+
+```smallbasic
+Fn Factorial(n)
+ If n <= 1 Then
+ Return 1
+ EndIf
+ Return n * Factorial(n - 1)
+EndFn
+
+result = Factorial(5) ' result = 120
+```
+
+---
+
+## 2. File Module Import System (`Import`)
+
+Organize Small Basic code into modular files using relative or absolute path imports.
+
+### Syntax
+```smallbasic
+Import "helpers/math_utils.sb"
+
+res = Square(6)
+```
+
+Imports are recursively resolved relative to the current file's directory path. Cyclic imports are automatically detected and safely skipped.
+
+---
+
+## 3. Control Flow Constructs
+
+### A. `Select Case` Statements
+Replace long `If/ElseIf` chains with clean pattern matching:
+
+```smallbasic
+val = 2
+
+Select Case val
+ Case 1
+ TextWindow.WriteLine("One")
+ Case 2
+ TextWindow.WriteLine("Two")
+ Else
+ TextWindow.WriteLine("Other")
+EndSelect
+```
+
+### B. `Try / Catch / Finally` Error Handling
+Handle errors gracefully without terminating execution:
+
+```smallbasic
+Try
+ res = File.ReadContents("data.txt")
+Catch ex
+ res = "Default Content"
+Finally
+ TextWindow.WriteLine("Completed read attempt.")
+EndTry
+```
+
+### C. `ForEach` Loops
+Iterate over collections easily:
+
+```smallbasic
+ForEach item In list
+ TextWindow.WriteLine(item)
+EndFor
+```
+
+---
+
+## 4. .NET / C# Reflection Interop (`System` Library)
+
+Directly invoke static methods and read static properties from any available .NET type directly in Small Basic code.
+
+### Methods
+- `System.Call(typeName, methodName, argument)`
+- `System.GetProperty(typeName, propertyName)`
+- `System.Exists(filePath)`
+
+### Example Usage
+
+```smallbasic
+' Call System.Math.Abs
+absVal = System.Call("System.Math", "Abs", -42) ' returns 42
+
+' Access .NET System.Environment property
+os = System.GetProperty("System.Environment", "OSVersion")
+```
+
+---
+
+## 5. Extended Math Utilities (`Math` Library)
+
+- `Math.Clamp(value, min, max)`: Clamps `value` between `min` and `max`.
+- `Math.Lerp(start, end, amount)`: Performs linear interpolation between `start` and `end`.
+- `Math.Atan2(y, x)`: Returns the 2-argument arctangent of `y` and `x`.
+
+---
+
+## Summary of All Added Keywords
+`Function`, `Fn`, `EndFunction`, `EndFn`, `Return`, `Import`, `Select`, `Case`, `EndSelect`, `Try`, `Catch`, `Finally`, `EndTry`, `ForEach`, `In`.
diff --git a/README.md b/README.md
index 134c8c39..6f2a5bc5 100644
--- a/README.md
+++ b/README.md
@@ -12,6 +12,9 @@ This is home to the new SmallBasic editor. It has the following projects:
* **SmallBasic.Tests**: C# unit tests to test the compiler/engine.
* **SmallBasic.Utilities**: helper utilities shared between different projects.
+#### Language Extensions & New Features
+For complete details and code examples of newly added compiler features (Functions with parameters/returns, relative module `Import`, `Select Case`, `Try/Catch/Finally`, `ForEach`, `Math` extensions, and `System.Call` .NET reflection interop), see [LANGUAGE_EXTENSIONS.md](LANGUAGE_EXTENSIONS.md).
+
#### Developer Workflow
1. Open **SmallBasic.sln** in Visual Studio 2017.
diff --git a/Source/Directory.Build.props b/Source/Directory.Build.props
index 84094dd5..a78fad9d 100644
--- a/Source/Directory.Build.props
+++ b/Source/Directory.Build.props
@@ -16,6 +16,7 @@
+
diff --git a/Source/SmallBasic.Bridge/Bridge.Generated.cs b/Source/SmallBasic.Bridge/Bridge.Generated.cs
index 1e1355fc..e31df342 100644
--- a/Source/SmallBasic.Bridge/Bridge.Generated.cs
+++ b/Source/SmallBasic.Bridge/Bridge.Generated.cs
@@ -1,256 +1,256 @@
-//
-// Licensed under the MIT License. See LICENSE file in the project root for license information.
-//
-
-///
-/// This file is auto-generated by a build task. It shouldn't be edited by hand.
-///
-namespace SmallBasic.Bridge
-{
- using System.Diagnostics;
- using Newtonsoft.Json;
- using SmallBasic.Utilities;
- using SmallBasic.Utilities.Bridge;
-
- internal interface IProcessBridge
- {
- void OpenExternalLink(string url);
- }
-
- internal interface IFileBridge
- {
- FileBridgeModels.Result AppendContents(FileBridgeModels.PathAndContentsArgs args);
-
- FileBridgeModels.Result CopyFile(FileBridgeModels.SourceAndDestinationArgs args);
-
- FileBridgeModels.Result CreateDirectory(string directoryPath);
-
- FileBridgeModels.Result DeleteDirectory(string directoryPath);
-
- FileBridgeModels.Result DeleteFile(string filePath);
-
- FileBridgeModels.Result GetDirectories(string directoryPath);
-
- FileBridgeModels.Result GetFiles(string directoryPath);
-
- FileBridgeModels.Result GetTemporaryFilePath();
-
- FileBridgeModels.Result InsertLine(FileBridgeModels.PathAndLineAndContentsArgs args);
-
- FileBridgeModels.Result ReadContents(string filePath);
-
- FileBridgeModels.Result ReadLine(FileBridgeModels.PathAndLineArgs args);
-
- FileBridgeModels.Result WriteContents(FileBridgeModels.PathAndContentsArgs args);
-
- FileBridgeModels.Result WriteLine(FileBridgeModels.PathAndLineAndContentsArgs args);
- }
-
- internal interface INetworkBridge
- {
- ImageListBridgeModels.ImageData LoadImage(string fileNameOrUrl);
-
- string DownloadFile(string url);
-
- string GetWebPageContents(string url);
- }
-
- internal static class BridgeExecution
- {
- private static readonly IProcessBridge Process = new ProcessBridge();
-
- private static readonly IFileBridge File = new FileBridge();
-
- private static readonly INetworkBridge Network = new NetworkBridge();
-
- public static void Run(string[] args)
- {
- Debug.Assert(args.Length >= 2, "Only intended for bridge calls");
-
- string type = args[0];
- string method = args[1];
- string filePath = args.Length > 2 ? args[2] : null;
-
- switch (type)
- {
- case "Process":
- {
- switch (method)
- {
- case "OpenExternalLink":
- {
- string input = JsonConvert.DeserializeObject(System.IO.File.ReadAllText(filePath));
- Process.OpenExternalLink(input);
- break;
- }
-
- default:
- {
- throw ExceptionUtilities.UnexpectedValue(method);
- }
- }
-
- break;
- }
-
- case "File":
- {
- switch (method)
- {
- case "AppendContents":
- {
- FileBridgeModels.PathAndContentsArgs input = JsonConvert.DeserializeObject(System.IO.File.ReadAllText(filePath));
- FileBridgeModels.Result output = File.AppendContents(input);
- System.IO.File.WriteAllText(filePath, JsonConvert.SerializeObject(output));
- break;
- }
-
- case "CopyFile":
- {
- FileBridgeModels.SourceAndDestinationArgs input = JsonConvert.DeserializeObject(System.IO.File.ReadAllText(filePath));
- FileBridgeModels.Result output = File.CopyFile(input);
- System.IO.File.WriteAllText(filePath, JsonConvert.SerializeObject(output));
- break;
- }
-
- case "CreateDirectory":
- {
- string input = JsonConvert.DeserializeObject(System.IO.File.ReadAllText(filePath));
- FileBridgeModels.Result output = File.CreateDirectory(input);
- System.IO.File.WriteAllText(filePath, JsonConvert.SerializeObject(output));
- break;
- }
-
- case "DeleteDirectory":
- {
- string input = JsonConvert.DeserializeObject(System.IO.File.ReadAllText(filePath));
- FileBridgeModels.Result output = File.DeleteDirectory(input);
- System.IO.File.WriteAllText(filePath, JsonConvert.SerializeObject(output));
- break;
- }
-
- case "DeleteFile":
- {
- string input = JsonConvert.DeserializeObject(System.IO.File.ReadAllText(filePath));
- FileBridgeModels.Result output = File.DeleteFile(input);
- System.IO.File.WriteAllText(filePath, JsonConvert.SerializeObject(output));
- break;
- }
-
- case "GetDirectories":
- {
- string input = JsonConvert.DeserializeObject(System.IO.File.ReadAllText(filePath));
- FileBridgeModels.Result output = File.GetDirectories(input);
- System.IO.File.WriteAllText(filePath, JsonConvert.SerializeObject(output));
- break;
- }
-
- case "GetFiles":
- {
- string input = JsonConvert.DeserializeObject(System.IO.File.ReadAllText(filePath));
- FileBridgeModels.Result output = File.GetFiles(input);
- System.IO.File.WriteAllText(filePath, JsonConvert.SerializeObject(output));
- break;
- }
-
- case "GetTemporaryFilePath":
- {
- FileBridgeModels.Result output = File.GetTemporaryFilePath();
- System.IO.File.WriteAllText(filePath, JsonConvert.SerializeObject(output));
- break;
- }
-
- case "InsertLine":
- {
- FileBridgeModels.PathAndLineAndContentsArgs input = JsonConvert.DeserializeObject(System.IO.File.ReadAllText(filePath));
- FileBridgeModels.Result output = File.InsertLine(input);
- System.IO.File.WriteAllText(filePath, JsonConvert.SerializeObject(output));
- break;
- }
-
- case "ReadContents":
- {
- string input = JsonConvert.DeserializeObject(System.IO.File.ReadAllText(filePath));
- FileBridgeModels.Result output = File.ReadContents(input);
- System.IO.File.WriteAllText(filePath, JsonConvert.SerializeObject(output));
- break;
- }
-
- case "ReadLine":
- {
- FileBridgeModels.PathAndLineArgs input = JsonConvert.DeserializeObject(System.IO.File.ReadAllText(filePath));
- FileBridgeModels.Result output = File.ReadLine(input);
- System.IO.File.WriteAllText(filePath, JsonConvert.SerializeObject(output));
- break;
- }
-
- case "WriteContents":
- {
- FileBridgeModels.PathAndContentsArgs input = JsonConvert.DeserializeObject(System.IO.File.ReadAllText(filePath));
- FileBridgeModels.Result output = File.WriteContents(input);
- System.IO.File.WriteAllText(filePath, JsonConvert.SerializeObject(output));
- break;
- }
-
- case "WriteLine":
- {
- FileBridgeModels.PathAndLineAndContentsArgs input = JsonConvert.DeserializeObject(System.IO.File.ReadAllText(filePath));
- FileBridgeModels.Result output = File.WriteLine(input);
- System.IO.File.WriteAllText(filePath, JsonConvert.SerializeObject(output));
- break;
- }
-
- default:
- {
- throw ExceptionUtilities.UnexpectedValue(method);
- }
- }
-
- break;
- }
-
- case "Network":
- {
- switch (method)
- {
- case "LoadImage":
- {
- string input = JsonConvert.DeserializeObject(System.IO.File.ReadAllText(filePath));
- ImageListBridgeModels.ImageData output = Network.LoadImage(input);
- System.IO.File.WriteAllText(filePath, JsonConvert.SerializeObject(output));
- break;
- }
-
- case "DownloadFile":
- {
- string input = JsonConvert.DeserializeObject(System.IO.File.ReadAllText(filePath));
- string output = Network.DownloadFile(input);
- System.IO.File.WriteAllText(filePath, JsonConvert.SerializeObject(output));
- break;
- }
-
- case "GetWebPageContents":
- {
- string input = JsonConvert.DeserializeObject(System.IO.File.ReadAllText(filePath));
- string output = Network.GetWebPageContents(input);
- System.IO.File.WriteAllText(filePath, JsonConvert.SerializeObject(output));
- break;
- }
-
- default:
- {
- throw ExceptionUtilities.UnexpectedValue(method);
- }
- }
-
- break;
- }
-
- default:
- {
- throw ExceptionUtilities.UnexpectedValue(type);
- }
- }
- }
- }
-}
+//
+// Licensed under the MIT License. See LICENSE file in the project root for license information.
+//
+
+///
+/// This file is auto-generated by a build task. It shouldn't be edited by hand.
+///
+namespace SmallBasic.Bridge
+{
+ using System.Diagnostics;
+ using Newtonsoft.Json;
+ using SmallBasic.Utilities;
+ using SmallBasic.Utilities.Bridge;
+
+ internal interface IProcessBridge
+ {
+ void OpenExternalLink(string url);
+ }
+
+ internal interface IFileBridge
+ {
+ FileBridgeModels.Result AppendContents(FileBridgeModels.PathAndContentsArgs args);
+
+ FileBridgeModels.Result CopyFile(FileBridgeModels.SourceAndDestinationArgs args);
+
+ FileBridgeModels.Result CreateDirectory(string directoryPath);
+
+ FileBridgeModels.Result DeleteDirectory(string directoryPath);
+
+ FileBridgeModels.Result DeleteFile(string filePath);
+
+ FileBridgeModels.Result GetDirectories(string directoryPath);
+
+ FileBridgeModels.Result GetFiles(string directoryPath);
+
+ FileBridgeModels.Result GetTemporaryFilePath();
+
+ FileBridgeModels.Result InsertLine(FileBridgeModels.PathAndLineAndContentsArgs args);
+
+ FileBridgeModels.Result ReadContents(string filePath);
+
+ FileBridgeModels.Result ReadLine(FileBridgeModels.PathAndLineArgs args);
+
+ FileBridgeModels.Result WriteContents(FileBridgeModels.PathAndContentsArgs args);
+
+ FileBridgeModels.Result WriteLine(FileBridgeModels.PathAndLineAndContentsArgs args);
+ }
+
+ internal interface INetworkBridge
+ {
+ ImageListBridgeModels.ImageData LoadImage(string fileNameOrUrl);
+
+ string DownloadFile(string url);
+
+ string GetWebPageContents(string url);
+ }
+
+ internal static class BridgeExecution
+ {
+ private static readonly IProcessBridge Process = new ProcessBridge();
+
+ private static readonly IFileBridge File = new FileBridge();
+
+ private static readonly INetworkBridge Network = new NetworkBridge();
+
+ public static void Run(string[] args)
+ {
+ Debug.Assert(args.Length >= 2, "Only intended for bridge calls");
+
+ string type = args[0];
+ string method = args[1];
+ string filePath = args.Length > 2 ? args[2] : null;
+
+ switch (type)
+ {
+ case "Process":
+ {
+ switch (method)
+ {
+ case "OpenExternalLink":
+ {
+ string input = JsonConvert.DeserializeObject(System.IO.File.ReadAllText(filePath));
+ Process.OpenExternalLink(input);
+ break;
+ }
+
+ default:
+ {
+ throw ExceptionUtilities.UnexpectedValue(method);
+ }
+ }
+
+ break;
+ }
+
+ case "File":
+ {
+ switch (method)
+ {
+ case "AppendContents":
+ {
+ FileBridgeModels.PathAndContentsArgs input = JsonConvert.DeserializeObject(System.IO.File.ReadAllText(filePath));
+ FileBridgeModels.Result output = File.AppendContents(input);
+ System.IO.File.WriteAllText(filePath, JsonConvert.SerializeObject(output));
+ break;
+ }
+
+ case "CopyFile":
+ {
+ FileBridgeModels.SourceAndDestinationArgs input = JsonConvert.DeserializeObject(System.IO.File.ReadAllText(filePath));
+ FileBridgeModels.Result output = File.CopyFile(input);
+ System.IO.File.WriteAllText(filePath, JsonConvert.SerializeObject(output));
+ break;
+ }
+
+ case "CreateDirectory":
+ {
+ string input = JsonConvert.DeserializeObject(System.IO.File.ReadAllText(filePath));
+ FileBridgeModels.Result output = File.CreateDirectory(input);
+ System.IO.File.WriteAllText(filePath, JsonConvert.SerializeObject(output));
+ break;
+ }
+
+ case "DeleteDirectory":
+ {
+ string input = JsonConvert.DeserializeObject(System.IO.File.ReadAllText(filePath));
+ FileBridgeModels.Result output = File.DeleteDirectory(input);
+ System.IO.File.WriteAllText(filePath, JsonConvert.SerializeObject(output));
+ break;
+ }
+
+ case "DeleteFile":
+ {
+ string input = JsonConvert.DeserializeObject(System.IO.File.ReadAllText(filePath));
+ FileBridgeModels.Result output = File.DeleteFile(input);
+ System.IO.File.WriteAllText(filePath, JsonConvert.SerializeObject(output));
+ break;
+ }
+
+ case "GetDirectories":
+ {
+ string input = JsonConvert.DeserializeObject(System.IO.File.ReadAllText(filePath));
+ FileBridgeModels.Result output = File.GetDirectories(input);
+ System.IO.File.WriteAllText(filePath, JsonConvert.SerializeObject(output));
+ break;
+ }
+
+ case "GetFiles":
+ {
+ string input = JsonConvert.DeserializeObject(System.IO.File.ReadAllText(filePath));
+ FileBridgeModels.Result output = File.GetFiles(input);
+ System.IO.File.WriteAllText(filePath, JsonConvert.SerializeObject(output));
+ break;
+ }
+
+ case "GetTemporaryFilePath":
+ {
+ FileBridgeModels.Result output = File.GetTemporaryFilePath();
+ System.IO.File.WriteAllText(filePath, JsonConvert.SerializeObject(output));
+ break;
+ }
+
+ case "InsertLine":
+ {
+ FileBridgeModels.PathAndLineAndContentsArgs input = JsonConvert.DeserializeObject(System.IO.File.ReadAllText(filePath));
+ FileBridgeModels.Result output = File.InsertLine(input);
+ System.IO.File.WriteAllText(filePath, JsonConvert.SerializeObject(output));
+ break;
+ }
+
+ case "ReadContents":
+ {
+ string input = JsonConvert.DeserializeObject(System.IO.File.ReadAllText(filePath));
+ FileBridgeModels.Result output = File.ReadContents(input);
+ System.IO.File.WriteAllText(filePath, JsonConvert.SerializeObject(output));
+ break;
+ }
+
+ case "ReadLine":
+ {
+ FileBridgeModels.PathAndLineArgs input = JsonConvert.DeserializeObject(System.IO.File.ReadAllText(filePath));
+ FileBridgeModels.Result output = File.ReadLine(input);
+ System.IO.File.WriteAllText(filePath, JsonConvert.SerializeObject(output));
+ break;
+ }
+
+ case "WriteContents":
+ {
+ FileBridgeModels.PathAndContentsArgs input = JsonConvert.DeserializeObject(System.IO.File.ReadAllText(filePath));
+ FileBridgeModels.Result output = File.WriteContents(input);
+ System.IO.File.WriteAllText(filePath, JsonConvert.SerializeObject(output));
+ break;
+ }
+
+ case "WriteLine":
+ {
+ FileBridgeModels.PathAndLineAndContentsArgs input = JsonConvert.DeserializeObject(System.IO.File.ReadAllText(filePath));
+ FileBridgeModels.Result output = File.WriteLine(input);
+ System.IO.File.WriteAllText(filePath, JsonConvert.SerializeObject(output));
+ break;
+ }
+
+ default:
+ {
+ throw ExceptionUtilities.UnexpectedValue(method);
+ }
+ }
+
+ break;
+ }
+
+ case "Network":
+ {
+ switch (method)
+ {
+ case "LoadImage":
+ {
+ string input = JsonConvert.DeserializeObject(System.IO.File.ReadAllText(filePath));
+ ImageListBridgeModels.ImageData output = Network.LoadImage(input);
+ System.IO.File.WriteAllText(filePath, JsonConvert.SerializeObject(output));
+ break;
+ }
+
+ case "DownloadFile":
+ {
+ string input = JsonConvert.DeserializeObject(System.IO.File.ReadAllText(filePath));
+ string output = Network.DownloadFile(input);
+ System.IO.File.WriteAllText(filePath, JsonConvert.SerializeObject(output));
+ break;
+ }
+
+ case "GetWebPageContents":
+ {
+ string input = JsonConvert.DeserializeObject(System.IO.File.ReadAllText(filePath));
+ string output = Network.GetWebPageContents(input);
+ System.IO.File.WriteAllText(filePath, JsonConvert.SerializeObject(output));
+ break;
+ }
+
+ default:
+ {
+ throw ExceptionUtilities.UnexpectedValue(method);
+ }
+ }
+
+ break;
+ }
+
+ default:
+ {
+ throw ExceptionUtilities.UnexpectedValue(type);
+ }
+ }
+ }
+ }
+}
diff --git a/Source/SmallBasic.Cli/Program.cs b/Source/SmallBasic.Cli/Program.cs
new file mode 100644
index 00000000..e0b0b9c1
--- /dev/null
+++ b/Source/SmallBasic.Cli/Program.cs
@@ -0,0 +1,226 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for license information.
+//
+
+namespace SmallBasic.Cli
+{
+ using System;
+ using System.IO;
+ using System.Linq;
+ using System.Reflection;
+ using System.Threading.Tasks;
+ using Microsoft.CodeAnalysis;
+ using Microsoft.CodeAnalysis.CSharp;
+ using SmallBasic.Compiler;
+ using SmallBasic.Compiler.Runtime;
+
+ public static class Program
+ {
+ public static async Task Main(string[] args)
+ {
+ if (args.Length == 0 || args[0] == "--help" || args[0] == "-h")
+ {
+ ShowHelp();
+ return 0;
+ }
+
+ string command = args[0].ToUpperInvariant();
+
+ if (command == "RUN")
+ {
+ if (args.Length < 2)
+ {
+ Console.WriteLine("Error: Missing file path for 'run' command.");
+ return 1;
+ }
+
+ string file = args[1];
+ if (!File.Exists(file))
+ {
+ Console.WriteLine($"Error: File '{file}' not found.");
+ return 1;
+ }
+
+ string source = File.ReadAllText(file);
+ string baseDir = Path.GetDirectoryName(Path.GetFullPath(file)) ?? string.Empty;
+
+ var compilation = new SmallBasicCompilation(source, baseDirectory: baseDir);
+
+ if (compilation.Diagnostics.Any())
+ {
+ Console.WriteLine("Compilation errors:");
+ foreach (var diag in compilation.Diagnostics)
+ {
+ Console.WriteLine($" [{diag.Range.Start.Line + 1}:{diag.Range.Start.Column + 1}] {diag.ToDisplayString()}");
+ }
+
+ return 1;
+ }
+
+ var engine = new SmallBasicEngine(compilation, new BuiltInLibraries());
+ await engine.Execute().ConfigureAwait(false);
+ Console.WriteLine("\nProgram executed successfully.");
+ return 0;
+ }
+ else if (command == "BUILD")
+ {
+ if (args.Length < 2)
+ {
+ Console.WriteLine("Error: Missing file path for 'build' command.");
+ return 1;
+ }
+
+ string file = args[1];
+ if (!File.Exists(file))
+ {
+ Console.WriteLine($"Error: File '{file}' not found.");
+ return 1;
+ }
+
+ string outFile = "output.exe";
+ for (int i = 2; i < args.Length; i++)
+ {
+ if ((args[i] == "-o" || args[i] == "--output") && i + 1 < args.Length)
+ {
+ outFile = args[i + 1];
+ }
+ }
+
+ Console.WriteLine($"Building '{file}' into standalone executable '{outFile}'...");
+ string source = File.ReadAllText(file);
+ string baseDir = Path.GetDirectoryName(Path.GetFullPath(file)) ?? string.Empty;
+ var compilation = new SmallBasicCompilation(source, baseDirectory: baseDir);
+
+ if (compilation.Diagnostics.Any())
+ {
+ Console.WriteLine("Compilation failed with errors:");
+ foreach (var diag in compilation.Diagnostics)
+ {
+ Console.WriteLine($" [{diag.Range.Start.Line + 1}:{diag.Range.Start.Column + 1}] {diag.ToDisplayString()}");
+ }
+
+ return 1;
+ }
+
+ string escapedSource = SymbolDisplay.FormatLiteral(source, true);
+
+ string csharpCode = $@"
+using System;
+using System.Threading.Tasks;
+using SmallBasic.Compiler;
+using SmallBasic.Compiler.Runtime;
+
+namespace SmallBasicRunner
+{{
+ public static class Program
+ {{
+ public static async Task Main(string[] args)
+ {{
+ string code = {escapedSource};
+ var compilation = new SmallBasicCompilation(code);
+ var engine = new SmallBasicEngine(compilation, new BuiltInLibraries());
+ await engine.Execute();
+ }}
+ }}
+}}";
+
+ var parseOptions = new CSharpParseOptions(LanguageVersion.Latest);
+ var syntaxTree = CSharpSyntaxTree.ParseText(csharpCode, parseOptions);
+
+ var references = new MetadataReference[]
+ {
+ MetadataReference.CreateFromFile(GetAssemblyLocation(typeof(object))),
+ MetadataReference.CreateFromFile(GetAssemblyLocation(typeof(Console))),
+ MetadataReference.CreateFromFile(GetAssemblyLocation(typeof(Task))),
+ MetadataReference.CreateFromFile(GetAssemblyLocation(Assembly.Load("netstandard"))),
+ MetadataReference.CreateFromFile(GetAssemblyLocation(Assembly.Load("System.Runtime"))),
+ MetadataReference.CreateFromFile(GetAssemblyLocation(typeof(SmallBasicCompilation)))
+ };
+
+ var csharpCompilation = CSharpCompilation.Create(
+ Path.GetFileNameWithoutExtension(outFile),
+ syntaxTrees: new[] { syntaxTree },
+ references: references,
+ options: new CSharpCompilationOptions(OutputKind.ConsoleApplication));
+
+ var emitResult = csharpCompilation.Emit(outFile);
+
+ if (!emitResult.Success)
+ {
+ Console.WriteLine("Failed to generate executable:");
+ foreach (var diagnostic in emitResult.Diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error))
+ {
+ Console.WriteLine($" {diagnostic.GetMessage()}");
+ }
+
+ return 1;
+ }
+
+ // Copy compiler & utilities assemblies next to output so it runs out-of-the-box
+ string outDir = Path.GetDirectoryName(Path.GetFullPath(outFile)) ?? ".";
+ string compLoc = GetAssemblyLocation(typeof(SmallBasicCompilation));
+ string utilLoc = GetAssemblyLocation(typeof(SmallBasic.Utilities.ExceptionUtilities));
+ if (File.Exists(compLoc))
+ {
+ File.Copy(compLoc, Path.Combine(outDir, Path.GetFileName(compLoc)), overwrite: true);
+ }
+
+ if (File.Exists(utilLoc))
+ {
+ File.Copy(utilLoc, Path.Combine(outDir, Path.GetFileName(utilLoc)), overwrite: true);
+ }
+
+ string runtimeConfig = Path.Combine(outDir, Path.GetFileNameWithoutExtension(outFile) + ".runtimeconfig.json");
+ string jsonContent = @"{
+ ""runtimeOptions"": {
+ ""tfm"": ""net8.0"",
+ ""framework"": {
+ ""name"": ""Microsoft.NETCore.App"",
+ ""version"": ""8.0.0""
+ }
+ }
+}";
+ File.WriteAllText(runtimeConfig, jsonContent);
+
+ Console.WriteLine($"Successfully built executable at '{outFile}'.");
+ return 0;
+ }
+ else
+ {
+ ShowHelp();
+ return 1;
+ }
+ }
+
+ private static string GetAssemblyLocation(Type type)
+ {
+ string loc = type.Assembly.Location;
+ if (string.IsNullOrEmpty(loc))
+ {
+ loc = Path.Combine(AppContext.BaseDirectory, type.Assembly.GetName().Name + ".dll");
+ }
+
+ return loc;
+ }
+
+ private static string GetAssemblyLocation(Assembly assembly)
+ {
+ string loc = assembly.Location;
+ if (string.IsNullOrEmpty(loc))
+ {
+ loc = Path.Combine(AppContext.BaseDirectory, assembly.GetName().Name + ".dll");
+ }
+
+ return loc;
+ }
+
+ private static void ShowHelp()
+ {
+ Console.WriteLine("Small Basic CLI v2.0");
+ Console.WriteLine("Usage:");
+ Console.WriteLine(" sb run Run a Small Basic script");
+ Console.WriteLine(" sb build -o Build a standalone executable");
+ Console.WriteLine(" sb --help Display this help message");
+ }
+ }
+}
diff --git a/Source/SmallBasic.Cli/SmallBasic.Cli.csproj b/Source/SmallBasic.Cli/SmallBasic.Cli.csproj
new file mode 100644
index 00000000..c12aa736
--- /dev/null
+++ b/Source/SmallBasic.Cli/SmallBasic.Cli.csproj
@@ -0,0 +1,17 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ disable
+ sb
+
+
+
+
+
+
+
+
+
diff --git a/Source/SmallBasic.Client/Bridge/Bridge.Generated.ts b/Source/SmallBasic.Client/Bridge/Bridge.Generated.ts
index e48a0aea..6b1c1c77 100644
--- a/Source/SmallBasic.Client/Bridge/Bridge.Generated.ts
+++ b/Source/SmallBasic.Client/Bridge/Bridge.Generated.ts
@@ -1,107 +1,107 @@
-/*!
- * Licensed under the MIT License. See LICENSE file in the project root for license information.
- */
-
-///
-/// This file is auto-generated by a build task. It shouldn't be edited by hand.
-///
-import * as fs from "fs";
-import * as os from "os";
-import * as path from "path";
-import * as child_process from "child_process";
-
-const communicationFilePath = path.join(os.tmpdir(), `bridge-comm-${Math.random().toString().split(".")[1]}.json`);
-const bridgeBinaryPath = path.resolve(__dirname, "_bridge", "SmallBasic.Bridge.dll");
-
-(global).Bridge = {
- Process: {
- OpenExternalLink: async (url: object): Promise => {
- fs.writeFileSync(communicationFilePath, JSON.stringify(url));
- child_process.execFileSync("dotnet", [bridgeBinaryPath, "Process", "OpenExternalLink", communicationFilePath]);
- return true;
- }
- },
- File: {
- AppendContents: async (args: object): Promise