# 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