|
- # E02-S03: build the name-table test PRG, its fixtures, and a fresh D81, and
- # compute the exact dump the C128 must produce (build/names-test/expected.bin).
- [CmdletBinding()]
- param()
-
- $ErrorActionPreference = "Stop"
- $project = Split-Path $PSScriptRoot -Parent
- Import-Module (Join-Path $PSScriptRoot "lib\Nova128Test.psm1") -Force
- . (Join-Path $project "tests\lexer\reference-lexer.ps1")
- $out = Join-Path $project "build\names-test"
- New-Item -ItemType Directory -Force -Path $out | Out-Null
-
- $link = Invoke-Nova128Link -Name "names-test" -BuildDir $out `
- -Config (Join-Path $project "cfg\compiler.cfg") -Sources @(
- (Join-Path $project "src\compiler\mmu.s"),
- (Join-Path $project "src\compiler\diskio.s"),
- (Join-Path $project "src\compiler\source.s"),
- (Join-Path $project "src\compiler\lexer.s"),
- (Join-Path $project "src\compiler\names.s"),
- (Join-Path $project "tests\names\names-test.s")) # last: guard follows the table
- if ((Get-Nova128Label $link.Labels "test_entry") -ne 0x1C0D) { throw "test_entry is not at `$1C0D" }
- $recs = Get-Nova128Label $link.Labels "name_recs"
- $guard = Get-Nova128Label $link.Labels "guard"
- if ($guard -ne $recs + 255 * 20) { throw ("guard `${0:X4} does not directly follow the name table" -f $guard) }
-
- function Get-Lines([byte[]]$bytes) {
- # Same line split as src_next_line: $0D ends a line; a tail without $0D
- # is the last line.
- $lines = New-Object System.Collections.Generic.List[byte[]]
- $start = 0
- for ($i = 0; $i -lt $bytes.Length; $i++) {
- if ($bytes[$i] -eq 13) {
- if ($i -gt $start) { $lines.Add([byte[]]$bytes[$start..($i - 1)]) } else { $lines.Add([byte[]]@()) }
- $start = $i + 1
- }
- }
- if ($start -lt $bytes.Length) { $lines.Add([byte[]]$bytes[$start..($bytes.Length - 1)]) }
- return ,$lines
- }
-
- # ------------------------------------------------------------ name hashing
- function Get-NameKey([byte[]]$spelling) {
- return [byte[]]($spelling | ForEach-Object { if ($_ -ge 0xC1 -and $_ -le 0xDA) { $_ -band 0x7F } else { $_ } })
- }
- function Get-NameHash([byte[]]$key) {
- $h = 0
- foreach ($b in $key) { $h = ((($h -shl 1) -band 0xFF) -bor ($h -shr 7)) -bxor $b }
- return $h
- }
- function Get-AsciiHash([string]$name) { return Get-NameHash (Get-NameKey (ConvertTo-Nova128Petscii $name)) }
-
- # ------------------------------------------------------------ collide.n8
- # Pick names that share a bucket, including two with the same full hash.
- $byHash = @{}
- foreach ($a in [char[]]"abcdefghijklmnopqrstuvwxyz") {
- foreach ($b in [char[]]"abcdefghijklmnopqrstuvwxyz0123456789") {
- $n = "$a$b"; $h = Get-AsciiHash $n
- if (-not $byHash.ContainsKey($h)) { $byHash[$h] = New-Object System.Collections.Generic.List[string] }
- $byHash[$h].Add($n)
- }
- }
- $sameHash = $byHash[($byHash.Keys | Where-Object { $byHash[$_].Count -ge 2 } | Sort-Object | Select-Object -First 1)]
- $h0 = Get-AsciiHash $sameHash[0]
- $bucket = $h0 -band 63
- $sameBucket = @($byHash.Keys | Where-Object { ($_ -band 63) -eq $bucket -and $_ -ne $h0 } | Sort-Object |
- ForEach-Object { $byHash[$_] } | Select-Object -First 6)
- $group = @($sameHash[0], $sameHash[1]) + $sameBucket # up to 8 names, one bucket
- if ($group.Count -lt 5) { throw "collision chain too short: $($group -join ', ')" }
- $collide = New-Object System.Text.StringBuilder
- [void]$collide.AppendLine("// generated by build-names-test.ps1: one hash bucket ($bucket)")
- foreach ($n in $group) { [void]$collide.AppendLine("var $n as word") }
- [void]$collide.AppendLine(($group[($group.Count - 1)..0] | ForEach-Object { $_.ToUpper() }) -join " ")
- [void]$collide.AppendLine(($group | ForEach-Object { $_ + "x" }) -join " ") # near misses
- if ((Get-AsciiHash $group[0]) -ne (Get-AsciiHash $group[1])) { throw "no identical-hash pair" }
- if (@($group | ForEach-Object { (Get-AsciiHash $_) -band 63 } | Select-Object -Unique).Count -ne 1) {
- throw "collision group spans buckets"
- }
-
- # --------------------------------------------------------------- full.n8
- $full = New-Object System.Text.StringBuilder
- [void]$full.AppendLine("program Full") # ID 1
- $names = 1..259 | ForEach-Object { "n$_" } # IDs 2..255, then 4 over
- for ($i = 0; $i -lt $names.Count; $i += 8) {
- [void]$full.AppendLine(($names[$i..([Math]::Min($i + 7, $names.Count - 1))]) -join " ")
- }
- [void]$full.AppendLine("N1 full n254 N259 n258") # lookups still work when full
-
- $fixtures = [ordered]@{
- "names.n8" = ConvertTo-Nova128Petscii (Get-Content -Raw (Join-Path $project "tests\names\names.n8"))
- "collide.n8" = ConvertTo-Nova128Petscii $collide.ToString()
- "full.n8" = ConvertTo-Nova128Petscii $full.ToString()
- }
-
- # ----------------------------------------------------- expected dump bytes
- $declKinds = @("KW_CLASS", "KW_INTERFACE", "KW_PROGRAM", "KW_METHOD", "KW_VAR")
- $expected = New-Object System.Collections.Generic.List[byte]
- $index = 0
- $summary = @()
- foreach ($name in $fixtures.Keys) {
- $src = $fixtures[$name]
- $expected.AddRange([byte[]](0xFF, $index, 0))
- $lines = (Get-Lines $src)
- $table = New-Object System.Collections.Generic.List[object] # ID-1 -> record
- $ids = @{}
- $prev = "EOL"; $errors = 0
- foreach ($t in (Get-ReferenceTokens $src)) {
- $m = [regex]::Match($t, '^(\d+):(\d+) (\S+)(?: (\d+))?')
- $kind = $m.Groups[3].Value
- if ($kind -eq "EOF") { break }
- if ($kind -eq "IDENT") {
- $line = $lines[[int]$m.Groups[1].Value - 1]
- $col = [int]$m.Groups[2].Value; $len = [int]$m.Groups[4].Value
- $spelling = [byte[]]$line[($col - 1)..($col + $len - 2)]
- $key = [Convert]::ToBase64String((Get-NameKey $spelling))
- $decl = $declKinds -contains $prev
- if ($ids.ContainsKey($key)) { $id = $ids[$key] }
- elseif ($table.Count -lt 255) {
- $table.Add([pscustomobject]@{ Flags = 0; Spelling = $spelling })
- $id = $table.Count; $ids[$key] = $id
- } else { $id = 0 }
- if ($id -eq 0) { $expected.AddRange([byte[]](3, 1)); $errors++ }
- elseif ($decl) {
- $rec = $table[$id - 1]
- if ($rec.Flags -eq 0) { $rec.Flags = 1; $rec.Spelling = $spelling }
- $expected.AddRange([byte[]](2, $id))
- } else { $expected.AddRange([byte[]](1, $id)) }
- }
- $prev = $kind
- }
- $expected.AddRange([byte[]](0xFD, $table.Count, 1))
- foreach ($rec in $table) {
- $expected.AddRange([byte[]]($rec.Flags, $rec.Spelling.Length))
- $expected.AddRange($rec.Spelling)
- }
- $summary += [pscustomobject]@{ Fixture = $name; Names = $table.Count; FullErrors = $errors }
- $index++
- }
- $expected.Add(0xFE)
- [IO.File]::WriteAllBytes((Join-Path $out "expected.bin"), $expected.ToArray())
- $summary | ConvertTo-Json | Set-Content (Join-Path $out "expected-summary.json")
-
- $files = @(@{ Name = "names-test"; Path = $link.Program; Type = "prg" })
- foreach ($name in $fixtures.Keys) {
- $pet = Join-Path $out ($name -replace '\.n8$', '.pet')
- [IO.File]::WriteAllBytes($pet, $fixtures[$name])
- $files += @{ Name = $name; Path = $pet; Type = "seq" }
- }
- New-Nova128Disk -Path (Join-Path $out "names-test.d81") -Files $files
- Write-Host ("built {0}; collision bucket {1}: {2}" -f $link.Program, $bucket, ($group -join ", "))
- $summary | Format-Table -AutoSize | Out-String | Write-Host
|