functions/public/Invoke-Assembler.ps1


<#
    .SYNOPSIS
    Assembles 6502 assembler instructions to a C64 compatible .PRG file.
 
    .DESCRIPTION
    Assembles 6502 assembler instructions to a C64 compatible .PRG file.
    Supports the standard 6502 mnemonics mixed with PowerShell for optional code generating logic.
    For a list of supported assembler directives, look elsewhere...
 
    .PARAMETER SourceFile
    Specifies the name of the source file to assemble.
 
    .PARAMETER OutFile
    Specifies the name of the file the binary code is written to.
 
    .PARAMETER DumpPSFile
    Specifies the name of the file the intermediate PowerShell code is written to.
    This is mostly useful for debugging the Assembler itself.
 
    .PARAMETER ListAssembly
    Prints the assembler listing to the screen, if specified.
 
    .PARAMETER ListBinary
    Prints a Hex Dump of the binary code produced by the assembler to the screen.
 
    .INPUTS
    An array of strings to process as the assembler source code.
 
    .OUTPUTS
    An AssemblyResult object containing the properties: Success, ErrorMessage, LoadAddress, Scopes, Symbols, SymbolsFull, PSSource, Segments, SegmentInfo, Assembly, AssemblyList, Binary, BinaryList, BinaryHash, and Tokens.
 
    .EXAMPLE
    PS> $rc = Invoke-Assembler -SourceFile demo.s -OutFile demo.prg
    Pass 1... OK!
    Pass 2... OK!
 
    ✅ Assembly succeeded.
 
    Writing 'demo.prg'...File Hash: 7554E67721301AD0A4CDDA94ACA9FEC8A64A9F25A2431CF83616AE9DC619469C
 
    .EXAMPLE
    PS> '.org $1000; inc $d020; jmp *-3' | Invoke-Assembler -OutFile demo.prg
    Pass 1... OK!
    Pass 2... OK!
 
    ✅ Assembly succeeded.
 
    Writing 'demo.prg'...File Hash: 7554E67721301AD0A4CDDA94ACA9FEC8A64A9F25A2431CF83616AE9DC619469C
 
    [OK] Load=$1000 Size=$0006
#>

function Invoke-Assembler {
    [CmdletBinding()]
    param (
        [Parameter(Position=0)]
        [Alias("I","Input")]
        [string]$SourceFile,

        [Parameter(Position=1)]
        [Alias("O","Output")]
        [string]$OutFile,

        [Parameter()]
        [Alias("lbl")]
        [string]$LabelFile = $SourceFile ? "$($SourceFile -replace '(.*)([.].*)','$1').lbl" : $null,

        [Alias("ps","psfile")]
        [string]$DumpPSfile,

        [Parameter()]
        [Alias("lst")]
        [string]$ListFile = $SourceFile ? "$($SourceFile -replace '(.*)([.].*)','$1').lst" : $null,

        [Parameter(ValueFromPipeline)]
        [object]$InputObject,

        [Alias("l","list")]
        [switch]$ListAssembly,

        [Alias("h","hexdump","DumpHex","dump","hex")]
        [switch]$ListBinary,

        [switch]$NoBanner,

        [Alias("q")]
        [switch]$NoHostOutput,

        [switch]$Version
    )

    BEGIN {
        $ErrorActionPreference = 'Stop'
        function Print-Banner {
            $vTag = format-string -Text "Version $($script:ModuleFullVersion)" -Format Center -OutputStringWidth 27
            Write-Host " "
            Write-Host " _/_/_/ _/_/_/ _/_/ _/_/_/ _/ _/ "
            Write-Host " _/ _/ _/ _/ _/ _/ _/_/ _/_/ "
            Write-Host " _/_/_/ _/_/ _/_/_/_/ _/_/ _/ _/ _/ "
            Write-Host " _/ _/ _/ _/ _/ _/ _/ "
            Write-Host " _/ _/_/_/ _/ _/ _/_/_/ _/ _/ "
            Write-Host " "
            Write-Host " ---> Assembly, reimagined for PowerShell <--- "
            Write-Host " --> ©2026 by Ulf Diabelez Harries <-- "
            Write-Host " ->$vTag<- "
            Write-Host " "
        }

        $SourceFiles = @()
        $SourceLines = [System.Text.StringBuilder]::new()
    }

    PROCESS {
        if ($InputObject) {
            if ($InputObject -is [System.IO.FileInfo]) {
                $SourceFiles += $InputObject
            } elseif ($InputObject -is [string]) {
                $null = $SourceLines.AppendLine($InputObject)
            } else {
                Write-Warning -Message "Ignoring unsupported input object: $($InputObject.GetType().FullName)"
            }
        }
    }

    END {
        if (-not $NoHostOutput -and -not $NoBanner) {
            Print-Banner
        }

        if($Version) {
            if (-not $NoHostOutput) {
                Write-Host "`nBuilt on $script:ModuleBuildDate`n"
            }
            return [version]$script:ModuleVersion
        }

        if ($SourceLines.Length -eq 0 -and $SourceFiles.Count -eq 0 -and -not $SourceFile) {
            Write-Error "No source specified. Use -SourceFile or provide source via the pipeline."
            return $null
        }

        $psasm = [Assembler]::new()
        $psasm.NoHostOutput = $NoHostOutput

        # Source and SourceFile go into a LIFO buffer, so pipeline source is processed before pipeline files, before files on command line by the assembler, if more are supplied
        if ($SourceFile) {
            $psasm.LoadFile($SourceFile)
        }
        foreach ($file in $SourceFiles) {
            $psasm.LoadFile($file)
        }
        if ($SourceLines.Length -gt 0) {
            $psasm.LoadVirtualFile("<PipeLine>", $SourceLines.ToString())
        }

        try {
            $psasm.Parse()
            $psasm.Assemble()
            $asmInfo = $psasm.ToResult()
            $asmInfo.Success = $true
        } catch {
            $asmInfo = $psasm.ToResult()
            $asmInfo.Success = $false
            $asmInfo.ErrorMessage = $_.Exception.Message
            $_.Exception.Data["AssemblyResult"] = $asmInfo
        }

        if (-not $asmInfo.Success) {
            if (-not $NoHostOutput) {
                Write-Host "`n❌ Assembly failed: $($asmInfo.ErrorMessage)" -ForegroundColor Red
                Write-Host "`nℹ️ `e[3mYou can inspect `e[0m`e[33m`$Error[0].Exception.Data['AssemblyResult']`e[0m `e[3mfor the details, if you did not save the assembly result to a variable.`e[0m`n"
            }
            return $asmInfo
        } else {
            if (-not $NoHostOutput) {
                Write-Host "`n✅ Assembly succeeded.`n" -ForegroundColor Green
            }
        }

        if ($DumpPSfile) {
            $asminfo.psSource | set-content -path $DumpPSfile -Force
        }

        if ($ListFile) {
            $asmInfo.AssemblyList | set-content -path $ListFile -Force
        }

        if ($ListAssembly) {
            if (-not $NoHostOutput) {
                Write-Host "`nListing Assembly:`n"
                Write-Host $asmInfo.AssemblyList
            }
        }

        if ($ListBinary) {
            if (-not $NoHostOutput) {
                Write-Host "`nListing Binary:`n"
                Write-Host $asmInfo.BinaryList
            }
        }

        if ($OutFile) {
            if (-not $NoHostOutput) {
                Write-Host ("`nWriting '$OutFile'...") -NoNewline
            }
            if (-not(Test-Path -Path $OutFile)) {
                $null =New-Item -Path $OutFile -Force
            }
            $asmInfo.Binary | set-content -asbytestream -path $OutFile -Force
            if (-not $NoHostOutput) {
                for ($i = 0; $i -lt 30; $i++) {
                    try {
                        $hash = (Get-FileHash -Algorithm SHA256 -Path $OutFile -ErrorAction Stop).Hash
                        break
                    } catch {
                        Start-Sleep -Milliseconds 100
                    }
                }

                if (-not $hash) {
                    Write-Error "Failed to compute file hash for: $OutFile" -ErrorAction Continue
                    $hash = "N/A"
                }

                Write-Host ("File Hash: {0:x}" -f $hash)
            }
            if ($LabelFile) {
                if (-not(Test-Path -Path $LabelFile)) {
                    $null = New-Item -Path $LabelFile -Force
                }
                $asmInfo.symbols.ForEach({"al {0:x6} .{1}" -f $_.Value, $_.Name}) | set-content -path $LabelFile -Force
            }
        }

        return ($asmInfo)
    }
}